From f5f4f84bef73f7c865a072eeaa9593b197c8a3df Mon Sep 17 00:00:00 2001 From: igor04091968 Date: Fri, 8 May 2026 00:39:28 +0300 Subject: [PATCH] feat(installer): add standalone Windows service deployment for DLP agent --- windows/aw-standalone-service.ps1 | 88 +++++++++++ windows/install-standalone-service.ps1 | 121 +++++++++++++++ .../innosetup/AWatch-rus-InnoSetup.iss | 141 ++---------------- 3 files changed, 220 insertions(+), 130 deletions(-) create mode 100644 windows/aw-standalone-service.ps1 create mode 100644 windows/install-standalone-service.ps1 diff --git a/windows/aw-standalone-service.ps1 b/windows/aw-standalone-service.ps1 new file mode 100644 index 0000000..a2cc659 --- /dev/null +++ b/windows/aw-standalone-service.ps1 @@ -0,0 +1,88 @@ +[CmdletBinding()] +param( + [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', + [int]$LoopSeconds = 20 +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Get-Config { + param([string]$Path) + if (-not (Test-Path -LiteralPath $Path)) { + throw "Config not found: $Path" + } + Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json +} + +function Write-ServiceLog { + param([string]$Message) + try { + Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) + } + catch {} +} + +function Start-CollectorIfNeeded { + param( + [string]$ScriptPath, + [string]$ConfigPath + ) + + if ([string]::IsNullOrWhiteSpace($ScriptPath) -or -not (Test-Path -LiteralPath $ScriptPath)) { + return + } + + $escaped = [Regex]::Escape($ScriptPath) + $running = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | + Where-Object { + $_.Name -eq 'powershell.exe' -and + $_.CommandLine -match $escaped -and + $_.CommandLine -match [Regex]::Escape($ConfigPath) + } | + Select-Object -First 1 + + if ($running) { + return + } + + $args = @('-NoProfile', '-ExecutionPolicy', 'Bypass') + if ($ScriptPath -like '*dlp-endpoint-signals*') { + $args += '-STA' + } + $args += @('-File', $ScriptPath, '-ConfigPath', $ConfigPath) + Start-Process -FilePath 'powershell.exe' -ArgumentList $args -WindowStyle Hidden | Out-Null + Write-ServiceLog ("started collector: {0}" -f $ScriptPath) +} + +$cfg = Get-Config -Path $ConfigPath +$stateRoot = if ($cfg.paths -and $cfg.paths.stateRoot) { [string]$cfg.paths.stateRoot } else { 'C:\ProgramData\AWatch-rus' } +$logsRoot = Join-Path $stateRoot 'logs' +if (-not (Test-Path -LiteralPath $logsRoot)) { + New-Item -Path $logsRoot -ItemType Directory -Force | Out-Null +} +$script:LogPath = Join-Path $logsRoot 'standalone-agent-service.log' + +Write-ServiceLog ('service loop started, config={0}' -f $ConfigPath) + +while ($true) { + try { + $cfg = Get-Config -Path $ConfigPath + $paths = $cfg.paths + + Start-CollectorIfNeeded -ScriptPath ([string]$paths.collectorScript) -ConfigPath $ConfigPath + Start-CollectorIfNeeded -ScriptPath ([string]$paths.endpointCollectorScript) -ConfigPath $ConfigPath + Start-CollectorIfNeeded -ScriptPath ([string]$paths.fileCollectorScript) -ConfigPath $ConfigPath + if ($paths.PSObject.Properties.Name -contains 'emailCollectorScript') { + Start-CollectorIfNeeded -ScriptPath ([string]$paths.emailCollectorScript) -ConfigPath $ConfigPath + } + if ($paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { + Start-CollectorIfNeeded -ScriptPath ([string]$paths.sessionCollectorScript) -ConfigPath $ConfigPath + } + } + catch { + Write-ServiceLog ("loop error: {0}" -f $_.Exception.Message) + } + Start-Sleep -Seconds ([Math]::Max($LoopSeconds, 5)) +} + diff --git a/windows/install-standalone-service.ps1 b/windows/install-standalone-service.ps1 new file mode 100644 index 0000000..e7ef129 --- /dev/null +++ b/windows/install-standalone-service.ps1 @@ -0,0 +1,121 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$ServerHost, + [int]$ServerPort = 5600, + [ValidateSet('http', 'https')] + [string]$ServerScheme = 'http', + [string]$StateRoot = 'C:\ProgramData\AWatch-rus', + [string]$InstallRoot = 'C:\Program Files\AWatch-rus\bin', + [string]$ServiceName = 'AWatchRusStandaloneAgent', + [string]$AwHostname +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Assert-Admin { + $id = [Security.Principal.WindowsIdentity]::GetCurrent() + $p = [Security.Principal.WindowsPrincipal]::new($id) + if (-not $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + throw 'Run as Administrator.' + } +} + +function Ensure-Dir { + param([string]$Path) + if (-not (Test-Path -LiteralPath $Path)) { + New-Item -Path $Path -ItemType Directory -Force | Out-Null + } +} + +Assert-Admin + +$logsRoot = Join-Path $StateRoot 'logs' +Ensure-Dir -Path $StateRoot +Ensure-Dir -Path $logsRoot + +$collectorScript = Join-Path $StateRoot 'browser-domains-native-collector.ps1' +$endpointCollectorScript = Join-Path $StateRoot 'dlp-endpoint-signals-collector.ps1' +$fileCollectorScript = Join-Path $StateRoot 'file-operations-collector.ps1' +$emailCollectorScript = Join-Path $StateRoot 'email-outbound-collector.ps1' +$sessionCollectorScript = Join-Path $StateRoot 'worktime-session-collector.ps1' +$rulesPath = Join-Path $StateRoot 'web-category-rules.json' +$policyPath = Join-Path $StateRoot 'dlp-policy.json' +$configPath = Join-Path $StateRoot 'deployment-config.json' +$serviceScriptPath = Join-Path $PSScriptRoot 'aw-standalone-service.ps1' + +Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1') -Destination $collectorScript -Force +Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'dlp-endpoint-signals-collector.ps1') -Destination $endpointCollectorScript -Force +Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'file-operations-collector.ps1') -Destination $fileCollectorScript -Force +if (Test-Path -LiteralPath (Join-Path $PSScriptRoot 'email-outbound-collector.ps1')) { + Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'email-outbound-collector.ps1') -Destination $emailCollectorScript -Force +} +if (Test-Path -LiteralPath (Join-Path $PSScriptRoot 'worktime-session-collector.ps1')) { + Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'worktime-session-collector.ps1') -Destination $sessionCollectorScript -Force +} + +if (-not (Test-Path -LiteralPath $rulesPath)) { + Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'web-category-rules.example.json') -Destination $rulesPath -Force +} +if (-not (Test-Path -LiteralPath $policyPath)) { + Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'dlp-policy.example.json') -Destination $policyPath -Force +} + +$effectiveHostname = if ([string]::IsNullOrWhiteSpace($AwHostname)) { [string]$env:COMPUTERNAME } else { [string]$AwHostname } + +$config = [pscustomobject]@{ + version = 1 + generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') + awHostname = $effectiveHostname + server = [pscustomobject]@{ + host = $ServerHost + port = $ServerPort + scheme = $ServerScheme + } + paths = [pscustomobject]@{ + installRoot = $InstallRoot + stateRoot = $StateRoot + logsRoot = $logsRoot + collectorScript = $collectorScript + endpointCollectorScript = $endpointCollectorScript + fileCollectorScript = $fileCollectorScript + emailCollectorScript = $emailCollectorScript + sessionCollectorScript = $sessionCollectorScript + rulesPath = $rulesPath + policyPath = $policyPath + } + collector = [pscustomobject]@{ + pollSeconds = 5 + pulseSeconds = 30 + } + collectors = [pscustomobject]@{ + afkEnabled = $false + windowEnabled = $false + fileOpsEnabled = $true + emailEnabled = $true + } + logging = [pscustomobject]@{ + localAgentLogsEnabled = $true + } +} + +$config | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $configPath -Encoding UTF8 + +$existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue +if ($existing) { + sc.exe stop $ServiceName | Out-Null + Start-Sleep -Seconds 1 + sc.exe delete $ServiceName | Out-Null + Start-Sleep -Seconds 1 +} + +$binPath = "`"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`" -NoProfile -ExecutionPolicy Bypass -File `"$serviceScriptPath`" -ConfigPath `"$configPath`"" +sc.exe create $ServiceName binPath= "$binPath" start= auto DisplayName= "AWatch-rus Standalone Agent" | Out-Null +sc.exe description $ServiceName "Standalone AWatch-rus DLP agent service wrapper" | Out-Null +sc.exe failure $ServiceName reset= 60 actions= restart/5000/restart/5000/restart/5000 | Out-Null +sc.exe start $ServiceName | Out-Null + +Write-Output "Standalone service installed: $ServiceName" +Write-Output "Config: $configPath" +Write-Output ("Host: {0} -> {1}://{2}:{3}" -f $effectiveHostname, $ServerScheme, $ServerHost, $ServerPort) diff --git a/windows/installkit/innosetup/AWatch-rus-InnoSetup.iss b/windows/installkit/innosetup/AWatch-rus-InnoSetup.iss index cab5adc..d79a51c 100644 --- a/windows/installkit/innosetup/AWatch-rus-InnoSetup.iss +++ b/windows/installkit/innosetup/AWatch-rus-InnoSetup.iss @@ -34,6 +34,8 @@ Name: "validate"; Description: "Запустить validate-deployment (чере [Files] Source: "..\..\ActivityWatch.Windows.Common.psd1"; DestDir: "{app}\windows"; Flags: ignoreversion Source: "..\..\ActivityWatch.Windows.Common.psm1"; DestDir: "{app}\windows"; Flags: ignoreversion +Source: "..\..\install-standalone-service.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion +Source: "..\..\aw-standalone-service.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion Source: "..\..\deploy-single-user.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion Source: "..\..\deploy-domain-users.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion Source: "..\..\deploy-ensemble.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion @@ -43,6 +45,7 @@ Source: "..\..\migrate-awatch-rus-paths.ps1"; DestDir: "{app}\windows"; Flags: i Source: "..\..\worktime-session-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion Source: "..\..\browser-domains-native-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion Source: "..\..\dlp-endpoint-signals-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion +Source: "..\..\file-operations-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion Source: "..\..\email-outbound-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion Source: "..\..\web-category-rules.example.json"; DestDir: "{app}\windows"; Flags: ignoreversion Source: "..\..\dlp-policy.example.json"; DestDir: "{app}\windows"; Flags: ignoreversion @@ -51,95 +54,11 @@ Source: "payload\{#AwDefaultZipName}"; DestDir: "{app}\payload"; Flags: ignoreve Source: "innosetup-rdp-package-filelist.md"; DestDir: "{app}\windows\installkit\innosetup"; Flags: ignoreversion [Run] -Filename: "powershell.exe"; Parameters: "{code:GetDeployEnsembleParams}"; Flags: runhidden; Tasks: deploy +Filename: "powershell.exe"; Parameters: "{code:GetStandaloneInstallParams}"; Flags: runhidden; Tasks: deploy [Code] var ServerHostPage: TInputQueryWizardPage; - UsersPage: TInputQueryWizardPage; - OptionsPage: TInputOptionWizardPage; - -function NormalizeUserCsv(const UserCsv: string): string; -var - i: Integer; - s: string; - token: string; -begin - Result := ''; - s := UserCsv; - while True do - begin - i := Pos(',', s); - if i = 0 then - begin - token := Trim(s); - s := ''; - end - else - begin - token := Trim(Copy(s, 1, i - 1)); - Delete(s, 1, i); - end; - - if token <> '' then - begin - if Result <> '' then - Result := Result + ','; - Result := Result + token; - end; - - if s = '' then - Break; - end; -end; - -function BuildUsersPowerShellArg(const UserCsv: string): string; -var - i: Integer; - s: string; - token: string; - quoted: string; -begin - Result := ''; - s := UserCsv; - while True do - begin - i := Pos(',', s); - if i = 0 then - begin - token := Trim(s); - s := ''; - end - else - begin - token := Trim(Copy(s, 1, i - 1)); - Delete(s, 1, i); - end; - - if token <> '' then - begin - quoted := '"' + token + '"'; - if Result <> '' then - Result := Result + ','; - Result := Result + quoted; - end; - - if s = '' then - Break; - end; - if Result <> '' then - Result := '-Users ' + Result; -end; - -function PayloadZipPath: string; -begin - Result := ExpandConstant('{app}\payload\{#AwDefaultZipName}'); -end; - -function HasPayloadZip: Boolean; -begin - Result := FileExists(ExpandConstant('{src}\payload\{#AwDefaultZipName}')); -end; procedure InitializeWizard; begin @@ -155,62 +74,24 @@ begin ServerHostPage.Add('ServerPort', False); ServerHostPage.Values[0] := '{#AwDefaultServerHost}'; ServerHostPage.Values[1] := '{#AwDefaultServerPort}'; - - UsersPage := CreateInputQueryPage( - ServerHostPage.ID, - 'Пользователи (RDP)', - 'Перечень пользователей, для которых разворачиваем агенты.', - 'Введите список через запятую. Пример: user1,user2,user3' - ); - UsersPage.Add('Users (CSV)', False); - UsersPage.Values[0] := '{#AwDefaultUsers}'; - - OptionsPage := CreateInputOptionPage( - UsersPage.ID, - 'Опции деплоя', - 'Выберите опции для установки/валидации.', - '', - False, - False - ); - OptionsPage.Add('Использовать offline payload (встроенный ZIP)'); - OptionsPage.Add('Запустить validate-deployment после деплоя'); - OptionsPage.Values[0] := HasPayloadZip; - OptionsPage.Values[1] := True; end; -function GetDeployEnsembleParams(Param: string): string; +function GetStandaloneInstallParams(Param: string): string; var serverHost: string; serverPort: string; - usersCsv: string; - usersArg: string; - zipArg: string; - validateArg: string; begin serverHost := Trim(ServerHostPage.Values[0]); serverPort := Trim(ServerHostPage.Values[1]); - usersCsv := NormalizeUserCsv(UsersPage.Values[0]); - - usersArg := BuildUsersPowerShellArg(usersCsv); - if usersArg = '' then - RaiseException('Users list is empty.'); - - zipArg := ''; - if OptionsPage.Values[0] then - zipArg := ' -PackageZipPath "' + PayloadZipPath + '"'; - - validateArg := ''; - if OptionsPage.Values[1] and WizardIsTaskSelected('validate') then - validateArg := ' -ValidateAfterDeploy'; + if serverHost = '' then + RaiseException('ServerHost is empty.'); + if serverPort = '' then + RaiseException('ServerPort is empty.'); Result := - '-NoProfile -ExecutionPolicy Bypass -File "' + ExpandConstant('{app}\windows\deploy-ensemble.ps1') + '"' + + '-NoProfile -ExecutionPolicy Bypass -File "' + ExpandConstant('{app}\windows\install-standalone-service.ps1') + '"' + ' -ServerHost "' + serverHost + '"' + ' -ServerPort ' + serverPort + - ' ' + usersArg + - zipArg + ' -InstallRoot "{#AwDefaultInstallRoot}"' + - ' -StateRoot "{#AwDefaultStateRoot}"' + - validateArg; + ' -StateRoot "{#AwDefaultStateRoot}"'; end;