feat(installer): add standalone Windows service deployment for DLP agent
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user