diff --git a/windows/ActivityWatch.Windows.Common.psm1 b/windows/ActivityWatch.Windows.Common.psm1 index 31a20f2..5f1e7fb 100755 --- a/windows/ActivityWatch.Windows.Common.psm1 +++ b/windows/ActivityWatch.Windows.Common.psm1 @@ -1,109 +1,4 @@ -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Assert-Administrator { - $identity = [Security.Principal.WindowsIdentity]::GetCurrent() - $principal = [Security.Principal.WindowsPrincipal]::new($identity) - if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - throw 'Запустите этот скрипт из PowerShell с правами администратора.' - } -} - -function New-ActivityWatchDirectory { - param( - [Parameter(Mandatory = $true)] - [string]$Path - ) - - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Enable-ActivityWatchPrintTelemetry { - $policyPath = 'HKLM:\Software\Policies\Microsoft\Windows NT\Printers' - if (-not (Test-Path -LiteralPath $policyPath)) { - New-Item -Path $policyPath -Force | Out-Null - } - New-ItemProperty -Path $policyPath -Name 'ShowJobTitleInEventLogs' -Value 1 -PropertyType DWord -Force | Out-Null - - & wevtutil.exe sl 'Microsoft-Windows-PrintService/Operational' /e:true | Out-Null -} - - -function Get-ActivityWatchPackageUrl { - param( - [string]$Version = 'v0.13.2' - ) - - return "https://github.com/ActivityWatch/activitywatch/releases/download/$Version/activitywatch-$Version-windows-x86_64.zip" -} - -function Get-ActivityWatchArchive { - param( - [string]$PackageZipPath, - [string]$PackageUrl, - [string]$Version = 'v0.13.2', - [Parameter(Mandatory = $true)] - [string]$WorkingRoot - ) - - New-ActivityWatchDirectory -Path $WorkingRoot - - if ($PackageZipPath) { - $resolved = Resolve-Path -LiteralPath $PackageZipPath -ErrorAction Stop - return $resolved.Path - } - - if (-not $PackageUrl) { - $PackageUrl = Get-ActivityWatchPackageUrl -Version $Version - } - - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' - $suffix = ([guid]::NewGuid().Guid.Substring(0, 8)) - $archivePath = Join-Path $WorkingRoot ("activitywatch-{0}-{1}-{2}.zip" -f $Version.TrimStart('v'), $stamp, $suffix) - Invoke-WebRequest -Uri $PackageUrl -OutFile $archivePath - return $archivePath -} - -function Get-ActivityWatchPackageRoot { - param( - [Parameter(Mandatory = $true)] - [string]$ExpandedRoot - ) - - $afkBinary = Get-ChildItem -Path $ExpandedRoot -Filter 'aw-watcher-afk.exe' -File -Recurse | - Select-Object -First 1 - - if (-not $afkBinary) { - throw "Не удалось найти aw-watcher-afk.exe в $ExpandedRoot." - } - - return (Split-Path -Path (Split-Path -Path $afkBinary.FullName -Parent) -Parent) -} - -function Install-ActivityWatchPackage { - param( - [Parameter(Mandatory = $true)] - [string]$ArchivePath, - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$WorkingRoot, - [Parameter(Mandatory = $true)] - [string]$BackupRoot - ) - - New-ActivityWatchDirectory -Path $WorkingRoot - New-ActivityWatchDirectory -Path $BackupRoot - - # Ensure nothing is holding locks inside InstallRoot during upgrade. - foreach ($procName in @('aw-watcher-afk', 'aw-watcher-window', 'aw-server', 'aw-qt')) { - try { - Get-Process -Name $procName -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue - } - catch { Write-Error Set-StrictMode -Version Latest +Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' function Assert-Administrator { @@ -1316,10806 +1211,3 @@ function Start-ActivityWatchTasks { } Export-ModuleMember -Function *-ActivityWatch*, Assert-Administrator, Normalize-ActivityWatchUsers, Get-ActivityWatchPackageUrl, Remove-LegacyActivityWatchEntries -; } - } - Start-Sleep -Seconds 2 - - $extractRoot = Join-Path $WorkingRoot ('extract-' + [guid]::NewGuid().Guid) - if (Test-Path -LiteralPath $extractRoot) { - Remove-Item -LiteralPath $extractRoot -Recurse -Force - } - New-ActivityWatchDirectory -Path $extractRoot - - Expand-Archive -Path $ArchivePath -DestinationPath $extractRoot -Force - $packageRoot = Get-ActivityWatchPackageRoot -ExpandedRoot $extractRoot - - if (Test-Path -LiteralPath $InstallRoot) { - $existingItems = Get-ChildItem -LiteralPath $InstallRoot -Force -ErrorAction SilentlyContinue - if ($existingItems) { - $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' - $backupPath = Join-Path $BackupRoot ("install-$stamp") - New-ActivityWatchDirectory -Path $backupPath - Copy-Item -Path (Join-Path $InstallRoot '*') -Destination $backupPath -Recurse -Force - Get-ChildItem -LiteralPath $InstallRoot -Force | Remove-Item -Recurse -Force - } - } - else { - New-ActivityWatchDirectory -Path $InstallRoot - } - - Copy-Item -Path (Join-Path $packageRoot '*') -Destination $InstallRoot -Recurse -Force - - return [pscustomobject]@{ - PackageRoot = $packageRoot - ExtractRoot = $extractRoot - BackupRoot = $BackupRoot - } -} - -function Get-ActivityWatchExecutableMap { - param( - [Parameter(Mandatory = $true)] - [string]$InstallRoot - ) - - $map = [ordered]@{ - Afk = Join-Path $InstallRoot 'aw-watcher-afk\aw-watcher-afk.exe' - Window = Join-Path $InstallRoot 'aw-watcher-window\aw-watcher-window.exe' - } - - foreach ($entry in $map.GetEnumerator()) { - if (-not (Test-Path -LiteralPath $entry.Value)) { - throw "Не найден обязательный исполняемый файл ActivityWatch: $($entry.Value)" - } - } - - return [pscustomobject]$map -} - -function Normalize-ActivityWatchUsers { - param( - [string[]]$Users, - [string]$UserListPath, - [string]$Domain - ) - - $collected = New-Object System.Collections.Generic.List[string] - - if ($Users) { - foreach ($user in $Users) { - if (-not [string]::IsNullOrWhiteSpace($user)) { - $collected.Add($user.Trim()) - } - } - } - - if ($UserListPath) { - $resolved = Resolve-Path -LiteralPath $UserListPath -ErrorAction Stop - $extension = [IO.Path]::GetExtension($resolved.Path) - if ($extension -ieq '.csv') { - $rows = Import-Csv -LiteralPath $resolved.Path - foreach ($row in $rows) { - foreach ($column in 'User', 'Username', 'SamAccountName', 'Login') { - if ($row.PSObject.Properties.Name -contains $column) { - $value = [string]$row.$column - if (-not [string]::IsNullOrWhiteSpace($value)) { - $collected.Add($value.Trim()) - break - } - } - } - } - } - else { - Get-Content -LiteralPath $resolved.Path | ForEach-Object { - $line = $_.Trim() - if ($line -and -not $line.StartsWith('#')) { - $collected.Add($line) - } - } - } - } - - $normalized = $collected | - Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | - ForEach-Object { - if ($Domain -and ($_ -notmatch '[\\@]')) { - '{0}\{1}' -f $Domain, $_ - } - else { - $_ - } - } | - Sort-Object -Unique - - if (-not $normalized -or $normalized.Count -eq 0) { - throw 'Не удалось определить целевых пользователей. Укажите -Users или -UserListPath.' - } - - return @($normalized) -} - -function Get-ActivityWatchTaskNameToken { - param( - [Parameter(Mandatory = $true)] - [string]$UserId - ) - - $buffer = [Text.StringBuilder]::new() - foreach ($character in $UserId.ToCharArray()) { - if ([char]::IsLetterOrDigit($character)) { - [void]$buffer.Append($character) - } - else { - [void]$buffer.Append('_') - } - } - - return $buffer.ToString().Trim('_') -} - -function New-ActivityWatchUserTaskDefinitions { - param( - [Parameter(Mandatory = $true)] - [string[]]$Users - ) - - $result = foreach ($user in $Users) { - $token = Get-ActivityWatchTaskNameToken -UserId $user - [pscustomobject]@{ - UserId = $user - LaunchTaskName = "ActivityWatch Launch [$token]" - } - } - - return @($result) -} - -function Copy-ActivityWatchCollectorAssets { - param( - [Parameter(Mandatory = $true)] - [string]$CollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$EndpointCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$FileCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$SessionCollectorScriptSource, - [string]$EmailCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$ExampleRulesSource, - [Parameter(Mandatory = $true)] - [string]$ExamplePolicySource, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [string]$CustomRulesSource, - [string]$CustomPolicySource - ) - - New-ActivityWatchDirectory -Path $StateRoot - - $collectorTarget = Join-Path $StateRoot 'browser-domains-native-collector.ps1' - $endpointCollectorTarget = Join-Path $StateRoot 'dlp-endpoint-signals-collector.ps1' - $fileCollectorTarget = Join-Path $StateRoot 'file-operations-collector.ps1' - $sessionCollectorTarget = Join-Path $StateRoot 'worktime-session-collector.ps1' - $emailCollectorTarget = Join-Path $StateRoot 'email-outbound-collector.ps1' - $exampleRulesTarget = Join-Path $StateRoot 'web-category-rules.example.json' - $rulesTarget = Join-Path $StateRoot 'web-category-rules.json' - $examplePolicyTarget = Join-Path $StateRoot 'dlp-policy.example.json' - $policyTarget = Join-Path $StateRoot 'dlp-policy.json' - - Copy-Item -LiteralPath $CollectorScriptSource -Destination $collectorTarget -Force - Copy-Item -LiteralPath $EndpointCollectorScriptSource -Destination $endpointCollectorTarget -Force - Copy-Item -LiteralPath $FileCollectorScriptSource -Destination $fileCollectorTarget -Force - Copy-Item -LiteralPath $SessionCollectorScriptSource -Destination $sessionCollectorTarget -Force - if ($EmailCollectorScriptSource -and (Test-Path -LiteralPath $EmailCollectorScriptSource)) { - Copy-Item -LiteralPath $EmailCollectorScriptSource -Destination $emailCollectorTarget -Force - } - Copy-Item -LiteralPath $ExampleRulesSource -Destination $exampleRulesTarget -Force - Copy-Item -LiteralPath $ExamplePolicySource -Destination $examplePolicyTarget -Force - - if ($CustomRulesSource) { - $resolvedRules = Resolve-Path -LiteralPath $CustomRulesSource -ErrorAction Stop - Copy-Item -LiteralPath $resolvedRules.Path -Destination $rulesTarget -Force - } - else { - Copy-Item -LiteralPath $exampleRulesTarget -Destination $rulesTarget -Force - } - - if ($CustomPolicySource) { - $resolvedPolicy = Resolve-Path -LiteralPath $CustomPolicySource -ErrorAction Stop - Copy-Item -LiteralPath $resolvedPolicy.Path -Destination $policyTarget -Force - } - else { - Copy-Item -LiteralPath $examplePolicyTarget -Destination $policyTarget -Force - } - - return [pscustomobject]@{ - CollectorScript = $collectorTarget - EndpointCollectorScript = $endpointCollectorTarget - FileCollectorScript = $fileCollectorTarget - SessionCollectorScript = $sessionCollectorTarget - EmailCollectorScript = $emailCollectorTarget - ExampleRules = $exampleRulesTarget - ActiveRules = $rulesTarget - ExamplePolicy = $examplePolicyTarget - ActivePolicy = $policyTarget - } -} - -function New-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [string]$ServerHost, - [Parameter(Mandatory = $true)] - [int]$ServerPort, - [Parameter(Mandatory = $true)] - [string]$ServerScheme, - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [Parameter(Mandatory = $true)] - [string]$LogsRoot, - [Parameter(Mandatory = $true)] - [string]$CollectorScript, - [Parameter(Mandatory = $true)] - [string]$EndpointCollectorScript, - [Parameter(Mandatory = $true)] - [string]$FileCollectorScript, - [Parameter(Mandatory = $true)] - [string]$SessionCollectorScript, - [string]$EmailCollectorScript, - [Parameter(Mandatory = $true)] - [string]$RulesPath, - [Parameter(Mandatory = $true)] - [string]$PolicyPath, - [Parameter(Mandatory = $true)] - [int]$PollSeconds, - [Parameter(Mandatory = $true)] - [int]$PulseSeconds, - [Parameter(Mandatory = $true)] - [int]$RecoveryIntervalSeconds, - [bool]$AfkEnabled = $true, - [bool]$WindowEnabled = $true, - [bool]$FileOpsEnabled = $true, - [bool]$LocalAgentLogsEnabled = $true, - [bool]$IncidentCaptureEnabled = $true, - [bool]$IncidentScreenshotEnabled = $true, - [string]$IncidentArtifactsRoot, - [bool]$LogonMarkerEnabled = $true, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath, - [Parameter(Mandatory = $true)] - [string]$RecoveryScriptPath, - [Parameter(Mandatory = $true)] - [pscustomobject[]]$UserTasks, - [string]$PackageVersion = 'v0.13.2' - ) - - $effectiveIncidentArtifactsRoot = if ($IncidentArtifactsRoot) { $IncidentArtifactsRoot } else { Join-Path $StateRoot 'incident-artifacts' } - - return [pscustomobject]@{ - version = 1 - generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') - server = [pscustomobject]@{ - host = $ServerHost - port = $ServerPort - scheme = $ServerScheme - } - paths = [pscustomobject]@{ - installRoot = $InstallRoot - stateRoot = $StateRoot - logsRoot = $LogsRoot - collectorScript = $CollectorScript - endpointCollectorScript = $EndpointCollectorScript - emailCollectorScript = $EmailCollectorScript - fileCollectorScript = $FileCollectorScript - sessionCollectorScript = $SessionCollectorScript - rulesPath = $RulesPath - policyPath = $PolicyPath - launchScript = $LaunchScriptPath - recoveryScript = $RecoveryScriptPath - } - collector = [pscustomobject]@{ - pollSeconds = $PollSeconds - pulseSeconds = $PulseSeconds - } - collectors = [pscustomobject]@{ - afkEnabled = $AfkEnabled - windowEnabled = $WindowEnabled - fileOpsEnabled = $FileOpsEnabled - emailEnabled = ($null -ne $EmailCollectorScript -and $EmailCollectorScript -ne '') - } - logging = [pscustomobject]@{ - localAgentLogsEnabled = $LocalAgentLogsEnabled - } - incidentCapture = [pscustomobject]@{ - enabled = $IncidentCaptureEnabled - screenshotEnabled = $IncidentScreenshotEnabled - artifactsRoot = $effectiveIncidentArtifactsRoot - } - sessionEvents = [pscustomobject]@{ - logonEnabled = $LogonMarkerEnabled - bucketPrefix = 'aw-session-events' - } - recovery = [pscustomobject]@{ - intervalSeconds = $RecoveryIntervalSeconds - taskName = 'ActivityWatch Recovery' - } - dlp = [pscustomobject]@{ - incidentBucketPrefix = 'aw-dlp-incidents' - enabled = $true - } - package = [pscustomobject]@{ - version = $PackageVersion - } - userTasks = @($UserTasks) - } -} - -function Write-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [pscustomobject]$Config, - [Parameter(Mandatory = $true)] - [string]$Path - ) - - $directory = Split-Path -Path $Path -Parent - if ($directory) { - New-ActivityWatchDirectory -Path $directory - } - - $json = $Config | ConvertTo-Json -Depth 8 - Set-Content -LiteralPath $Path -Value $json -Encoding UTF8 -} - -function Read-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [string]$Path - ) - - if (-not (Test-Path -LiteralPath $Path)) { - throw "Конфигурация развёртывания не найдена: $Path" - } - - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json -} - -function Write-ActivityWatchLaunchScript { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $content = @" -param( - [string]`$ConfigPath = '$ConfigPath' -) - -Set-StrictMode -Version Latest -`$ErrorActionPreference = 'Stop' - -[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -Add-Type -AssemblyName System.Net.Http -`$script:MaxCollectorPowerShellProcesses = 24 - -function Get-DeploymentConfig { - param([string]`$Path) - return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json -} - -function Test-ProcessInSession { - param( - [string]`$Name, - [int]`$SessionId - ) - - return [bool](Get-Process -Name `$Name -ErrorAction SilentlyContinue | Where-Object { `$_.SessionId -eq `$SessionId } | Select-Object -First 1) -} - -function Test-CollectorRunning { - param( - [string]`$ScriptPath, - [int]`$SessionId - ) - - `$escapedCollector = [Regex]::Escape(`$ScriptPath) - `$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - (`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and - `$_.SessionId -eq `$SessionId -and - `$_.CommandLine -match `$escapedCollector - } - - return [bool](`$processes | Select-Object -First 1) -} - -function Get-CollectorPowerShellProcessCount { - `$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - (`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and - `$_.CommandLine -match 'AWatch-rus' -and - `$_.CommandLine -match '\.ps1' - } - - return @(`$processes).Count -} - -function New-LaunchLock { - param([string]`$StateRoot, [int]`$SessionId) - - `$lockPath = Join-Path `$env:TEMP ("launch-watchers-session-{0}.lock" -f `$SessionId) - if (Test-Path -LiteralPath `$lockPath) { - try { - `$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json - `$existingPid = [int]`$lockData.pid - if (`$existingPid -gt 0 -and (Get-Process -Id `$existingPid -ErrorAction SilentlyContinue)) { - return `$null - } - } - catch { Write-Error Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Assert-Administrator { - $identity = [Security.Principal.WindowsIdentity]::GetCurrent() - $principal = [Security.Principal.WindowsPrincipal]::new($identity) - if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - throw 'Запустите этот скрипт из PowerShell с правами администратора.' - } -} - -function New-ActivityWatchDirectory { - param( - [Parameter(Mandatory = $true)] - [string]$Path - ) - - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Enable-ActivityWatchPrintTelemetry { - $policyPath = 'HKLM:\Software\Policies\Microsoft\Windows NT\Printers' - if (-not (Test-Path -LiteralPath $policyPath)) { - New-Item -Path $policyPath -Force | Out-Null - } - New-ItemProperty -Path $policyPath -Name 'ShowJobTitleInEventLogs' -Value 1 -PropertyType DWord -Force | Out-Null - - & wevtutil.exe sl 'Microsoft-Windows-PrintService/Operational' /e:true | Out-Null -} - - -function Get-ActivityWatchPackageUrl { - param( - [string]$Version = 'v0.13.2' - ) - - return "https://github.com/ActivityWatch/activitywatch/releases/download/$Version/activitywatch-$Version-windows-x86_64.zip" -} - -function Get-ActivityWatchArchive { - param( - [string]$PackageZipPath, - [string]$PackageUrl, - [string]$Version = 'v0.13.2', - [Parameter(Mandatory = $true)] - [string]$WorkingRoot - ) - - New-ActivityWatchDirectory -Path $WorkingRoot - - if ($PackageZipPath) { - $resolved = Resolve-Path -LiteralPath $PackageZipPath -ErrorAction Stop - return $resolved.Path - } - - if (-not $PackageUrl) { - $PackageUrl = Get-ActivityWatchPackageUrl -Version $Version - } - - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' - $suffix = ([guid]::NewGuid().Guid.Substring(0, 8)) - $archivePath = Join-Path $WorkingRoot ("activitywatch-{0}-{1}-{2}.zip" -f $Version.TrimStart('v'), $stamp, $suffix) - Invoke-WebRequest -Uri $PackageUrl -OutFile $archivePath - return $archivePath -} - -function Get-ActivityWatchPackageRoot { - param( - [Parameter(Mandatory = $true)] - [string]$ExpandedRoot - ) - - $afkBinary = Get-ChildItem -Path $ExpandedRoot -Filter 'aw-watcher-afk.exe' -File -Recurse | - Select-Object -First 1 - - if (-not $afkBinary) { - throw "Не удалось найти aw-watcher-afk.exe в $ExpandedRoot." - } - - return (Split-Path -Path (Split-Path -Path $afkBinary.FullName -Parent) -Parent) -} - -function Install-ActivityWatchPackage { - param( - [Parameter(Mandatory = $true)] - [string]$ArchivePath, - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$WorkingRoot, - [Parameter(Mandatory = $true)] - [string]$BackupRoot - ) - - New-ActivityWatchDirectory -Path $WorkingRoot - New-ActivityWatchDirectory -Path $BackupRoot - - # Ensure nothing is holding locks inside InstallRoot during upgrade. - foreach ($procName in @('aw-watcher-afk', 'aw-watcher-window', 'aw-server', 'aw-qt')) { - try { - Get-Process -Name $procName -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue - } - catch { - } - } - Start-Sleep -Seconds 2 - - $extractRoot = Join-Path $WorkingRoot ('extract-' + [guid]::NewGuid().Guid) - if (Test-Path -LiteralPath $extractRoot) { - Remove-Item -LiteralPath $extractRoot -Recurse -Force - } - New-ActivityWatchDirectory -Path $extractRoot - - Expand-Archive -Path $ArchivePath -DestinationPath $extractRoot -Force - $packageRoot = Get-ActivityWatchPackageRoot -ExpandedRoot $extractRoot - - if (Test-Path -LiteralPath $InstallRoot) { - $existingItems = Get-ChildItem -LiteralPath $InstallRoot -Force -ErrorAction SilentlyContinue - if ($existingItems) { - $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' - $backupPath = Join-Path $BackupRoot ("install-$stamp") - New-ActivityWatchDirectory -Path $backupPath - Copy-Item -Path (Join-Path $InstallRoot '*') -Destination $backupPath -Recurse -Force - Get-ChildItem -LiteralPath $InstallRoot -Force | Remove-Item -Recurse -Force - } - } - else { - New-ActivityWatchDirectory -Path $InstallRoot - } - - Copy-Item -Path (Join-Path $packageRoot '*') -Destination $InstallRoot -Recurse -Force - - return [pscustomobject]@{ - PackageRoot = $packageRoot - ExtractRoot = $extractRoot - BackupRoot = $BackupRoot - } -} - -function Get-ActivityWatchExecutableMap { - param( - [Parameter(Mandatory = $true)] - [string]$InstallRoot - ) - - $map = [ordered]@{ - Afk = Join-Path $InstallRoot 'aw-watcher-afk\aw-watcher-afk.exe' - Window = Join-Path $InstallRoot 'aw-watcher-window\aw-watcher-window.exe' - } - - foreach ($entry in $map.GetEnumerator()) { - if (-not (Test-Path -LiteralPath $entry.Value)) { - throw "Не найден обязательный исполняемый файл ActivityWatch: $($entry.Value)" - } - } - - return [pscustomobject]$map -} - -function Normalize-ActivityWatchUsers { - param( - [string[]]$Users, - [string]$UserListPath, - [string]$Domain - ) - - $collected = New-Object System.Collections.Generic.List[string] - - if ($Users) { - foreach ($user in $Users) { - if (-not [string]::IsNullOrWhiteSpace($user)) { - $collected.Add($user.Trim()) - } - } - } - - if ($UserListPath) { - $resolved = Resolve-Path -LiteralPath $UserListPath -ErrorAction Stop - $extension = [IO.Path]::GetExtension($resolved.Path) - if ($extension -ieq '.csv') { - $rows = Import-Csv -LiteralPath $resolved.Path - foreach ($row in $rows) { - foreach ($column in 'User', 'Username', 'SamAccountName', 'Login') { - if ($row.PSObject.Properties.Name -contains $column) { - $value = [string]$row.$column - if (-not [string]::IsNullOrWhiteSpace($value)) { - $collected.Add($value.Trim()) - break - } - } - } - } - } - else { - Get-Content -LiteralPath $resolved.Path | ForEach-Object { - $line = $_.Trim() - if ($line -and -not $line.StartsWith('#')) { - $collected.Add($line) - } - } - } - } - - $normalized = $collected | - Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | - ForEach-Object { - if ($Domain -and ($_ -notmatch '[\\@]')) { - '{0}\{1}' -f $Domain, $_ - } - else { - $_ - } - } | - Sort-Object -Unique - - if (-not $normalized -or $normalized.Count -eq 0) { - throw 'Не удалось определить целевых пользователей. Укажите -Users или -UserListPath.' - } - - return @($normalized) -} - -function Get-ActivityWatchTaskNameToken { - param( - [Parameter(Mandatory = $true)] - [string]$UserId - ) - - $buffer = [Text.StringBuilder]::new() - foreach ($character in $UserId.ToCharArray()) { - if ([char]::IsLetterOrDigit($character)) { - [void]$buffer.Append($character) - } - else { - [void]$buffer.Append('_') - } - } - - return $buffer.ToString().Trim('_') -} - -function New-ActivityWatchUserTaskDefinitions { - param( - [Parameter(Mandatory = $true)] - [string[]]$Users - ) - - $result = foreach ($user in $Users) { - $token = Get-ActivityWatchTaskNameToken -UserId $user - [pscustomobject]@{ - UserId = $user - LaunchTaskName = "ActivityWatch Launch [$token]" - } - } - - return @($result) -} - -function Copy-ActivityWatchCollectorAssets { - param( - [Parameter(Mandatory = $true)] - [string]$CollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$EndpointCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$FileCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$SessionCollectorScriptSource, - [string]$EmailCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$ExampleRulesSource, - [Parameter(Mandatory = $true)] - [string]$ExamplePolicySource, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [string]$CustomRulesSource, - [string]$CustomPolicySource - ) - - New-ActivityWatchDirectory -Path $StateRoot - - $collectorTarget = Join-Path $StateRoot 'browser-domains-native-collector.ps1' - $endpointCollectorTarget = Join-Path $StateRoot 'dlp-endpoint-signals-collector.ps1' - $fileCollectorTarget = Join-Path $StateRoot 'file-operations-collector.ps1' - $sessionCollectorTarget = Join-Path $StateRoot 'worktime-session-collector.ps1' - $emailCollectorTarget = Join-Path $StateRoot 'email-outbound-collector.ps1' - $exampleRulesTarget = Join-Path $StateRoot 'web-category-rules.example.json' - $rulesTarget = Join-Path $StateRoot 'web-category-rules.json' - $examplePolicyTarget = Join-Path $StateRoot 'dlp-policy.example.json' - $policyTarget = Join-Path $StateRoot 'dlp-policy.json' - - Copy-Item -LiteralPath $CollectorScriptSource -Destination $collectorTarget -Force - Copy-Item -LiteralPath $EndpointCollectorScriptSource -Destination $endpointCollectorTarget -Force - Copy-Item -LiteralPath $FileCollectorScriptSource -Destination $fileCollectorTarget -Force - Copy-Item -LiteralPath $SessionCollectorScriptSource -Destination $sessionCollectorTarget -Force - if ($EmailCollectorScriptSource -and (Test-Path -LiteralPath $EmailCollectorScriptSource)) { - Copy-Item -LiteralPath $EmailCollectorScriptSource -Destination $emailCollectorTarget -Force - } - Copy-Item -LiteralPath $ExampleRulesSource -Destination $exampleRulesTarget -Force - Copy-Item -LiteralPath $ExamplePolicySource -Destination $examplePolicyTarget -Force - - if ($CustomRulesSource) { - $resolvedRules = Resolve-Path -LiteralPath $CustomRulesSource -ErrorAction Stop - Copy-Item -LiteralPath $resolvedRules.Path -Destination $rulesTarget -Force - } - else { - Copy-Item -LiteralPath $exampleRulesTarget -Destination $rulesTarget -Force - } - - if ($CustomPolicySource) { - $resolvedPolicy = Resolve-Path -LiteralPath $CustomPolicySource -ErrorAction Stop - Copy-Item -LiteralPath $resolvedPolicy.Path -Destination $policyTarget -Force - } - else { - Copy-Item -LiteralPath $examplePolicyTarget -Destination $policyTarget -Force - } - - return [pscustomobject]@{ - CollectorScript = $collectorTarget - EndpointCollectorScript = $endpointCollectorTarget - FileCollectorScript = $fileCollectorTarget - SessionCollectorScript = $sessionCollectorTarget - EmailCollectorScript = $emailCollectorTarget - ExampleRules = $exampleRulesTarget - ActiveRules = $rulesTarget - ExamplePolicy = $examplePolicyTarget - ActivePolicy = $policyTarget - } -} - -function New-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [string]$ServerHost, - [Parameter(Mandatory = $true)] - [int]$ServerPort, - [Parameter(Mandatory = $true)] - [string]$ServerScheme, - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [Parameter(Mandatory = $true)] - [string]$LogsRoot, - [Parameter(Mandatory = $true)] - [string]$CollectorScript, - [Parameter(Mandatory = $true)] - [string]$EndpointCollectorScript, - [Parameter(Mandatory = $true)] - [string]$FileCollectorScript, - [Parameter(Mandatory = $true)] - [string]$SessionCollectorScript, - [string]$EmailCollectorScript, - [Parameter(Mandatory = $true)] - [string]$RulesPath, - [Parameter(Mandatory = $true)] - [string]$PolicyPath, - [Parameter(Mandatory = $true)] - [int]$PollSeconds, - [Parameter(Mandatory = $true)] - [int]$PulseSeconds, - [Parameter(Mandatory = $true)] - [int]$RecoveryIntervalSeconds, - [bool]$AfkEnabled = $true, - [bool]$WindowEnabled = $true, - [bool]$FileOpsEnabled = $true, - [bool]$LocalAgentLogsEnabled = $true, - [bool]$IncidentCaptureEnabled = $true, - [bool]$IncidentScreenshotEnabled = $true, - [string]$IncidentArtifactsRoot, - [bool]$LogonMarkerEnabled = $true, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath, - [Parameter(Mandatory = $true)] - [string]$RecoveryScriptPath, - [Parameter(Mandatory = $true)] - [pscustomobject[]]$UserTasks, - [string]$PackageVersion = 'v0.13.2' - ) - - $effectiveIncidentArtifactsRoot = if ($IncidentArtifactsRoot) { $IncidentArtifactsRoot } else { Join-Path $StateRoot 'incident-artifacts' } - - return [pscustomobject]@{ - version = 1 - generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') - server = [pscustomobject]@{ - host = $ServerHost - port = $ServerPort - scheme = $ServerScheme - } - paths = [pscustomobject]@{ - installRoot = $InstallRoot - stateRoot = $StateRoot - logsRoot = $LogsRoot - collectorScript = $CollectorScript - endpointCollectorScript = $EndpointCollectorScript - emailCollectorScript = $EmailCollectorScript - fileCollectorScript = $FileCollectorScript - sessionCollectorScript = $SessionCollectorScript - rulesPath = $RulesPath - policyPath = $PolicyPath - launchScript = $LaunchScriptPath - recoveryScript = $RecoveryScriptPath - } - collector = [pscustomobject]@{ - pollSeconds = $PollSeconds - pulseSeconds = $PulseSeconds - } - collectors = [pscustomobject]@{ - afkEnabled = $AfkEnabled - windowEnabled = $WindowEnabled - fileOpsEnabled = $FileOpsEnabled - emailEnabled = ($null -ne $EmailCollectorScript -and $EmailCollectorScript -ne '') - } - logging = [pscustomobject]@{ - localAgentLogsEnabled = $LocalAgentLogsEnabled - } - incidentCapture = [pscustomobject]@{ - enabled = $IncidentCaptureEnabled - screenshotEnabled = $IncidentScreenshotEnabled - artifactsRoot = $effectiveIncidentArtifactsRoot - } - sessionEvents = [pscustomobject]@{ - logonEnabled = $LogonMarkerEnabled - bucketPrefix = 'aw-session-events' - } - recovery = [pscustomobject]@{ - intervalSeconds = $RecoveryIntervalSeconds - taskName = 'ActivityWatch Recovery' - } - dlp = [pscustomobject]@{ - incidentBucketPrefix = 'aw-dlp-incidents' - enabled = $true - } - package = [pscustomobject]@{ - version = $PackageVersion - } - userTasks = @($UserTasks) - } -} - -function Write-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [pscustomobject]$Config, - [Parameter(Mandatory = $true)] - [string]$Path - ) - - $directory = Split-Path -Path $Path -Parent - if ($directory) { - New-ActivityWatchDirectory -Path $directory - } - - $json = $Config | ConvertTo-Json -Depth 8 - Set-Content -LiteralPath $Path -Value $json -Encoding UTF8 -} - -function Read-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [string]$Path - ) - - if (-not (Test-Path -LiteralPath $Path)) { - throw "Конфигурация развёртывания не найдена: $Path" - } - - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json -} - -function Write-ActivityWatchLaunchScript { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $content = @" -param( - [string]`$ConfigPath = '$ConfigPath' -) - -Set-StrictMode -Version Latest -`$ErrorActionPreference = 'Stop' - -[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -Add-Type -AssemblyName System.Net.Http -`$script:MaxCollectorPowerShellProcesses = 24 - -function Get-DeploymentConfig { - param([string]`$Path) - return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json -} - -function Test-ProcessInSession { - param( - [string]`$Name, - [int]`$SessionId - ) - - return [bool](Get-Process -Name `$Name -ErrorAction SilentlyContinue | Where-Object { `$_.SessionId -eq `$SessionId } | Select-Object -First 1) -} - -function Test-CollectorRunning { - param( - [string]`$ScriptPath, - [int]`$SessionId - ) - - `$escapedCollector = [Regex]::Escape(`$ScriptPath) - `$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - (`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and - `$_.SessionId -eq `$SessionId -and - `$_.CommandLine -match `$escapedCollector - } - - return [bool](`$processes | Select-Object -First 1) -} - -function Get-CollectorPowerShellProcessCount { - `$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - (`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and - `$_.CommandLine -match 'AWatch-rus' -and - `$_.CommandLine -match '\.ps1' - } - - return @(`$processes).Count -} - -function New-LaunchLock { - param([string]`$StateRoot, [int]`$SessionId) - - `$lockPath = Join-Path `$env:TEMP ("launch-watchers-session-{0}.lock" -f `$SessionId) - if (Test-Path -LiteralPath `$lockPath) { - try { - `$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json - `$existingPid = [int]`$lockData.pid - if (`$existingPid -gt 0 -and (Get-Process -Id `$existingPid -ErrorAction SilentlyContinue)) { - return `$null - } - } - catch { - } - } - - `$payload = @{ - pid = `$PID - sessionId = `$SessionId - createdAt = (Get-Date).ToUniversalTime().ToString('o') - } | ConvertTo-Json -Compress - Set-Content -LiteralPath `$lockPath -Value `$payload -Encoding UTF8 - return `$lockPath -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = `$true)][string]`$Uri, - [Parameter(Mandatory = `$true)][string]`$Json - ) - - `$httpClient = New-Object System.Net.Http.HttpClient - try { - `$content = New-Object System.Net.Http.StringContent(`$Json, [System.Text.Encoding]::UTF8, 'application/json') - `$response = `$httpClient.PostAsync(`$Uri, `$content).Result - if (-not `$response.IsSuccessStatusCode) { - return `$false - } - return `$true - } - catch { - return `$false - } - finally { - `$httpClient.Dispose() - } -} - -function Ensure-Bucket { - param( - [string]`$BucketId, - [string]`$ClientName, - [string]`$BucketType - ) - - if (`$script:KnownBuckets.ContainsKey(`$BucketId)) { - return - } - - try { - Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null - `$script:KnownBuckets[`$BucketId] = `$true - return - } - catch { - } - - `$body = @{ - client = `$ClientName - type = `$BucketType - hostname = `$script:Hostname - } | ConvertTo-Json -Compress - - try { - if (-not (Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" -Json `$body)) { - return - } - } - catch { - try { - Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null - } - catch { - return - } - } - - `$script:KnownBuckets[`$BucketId] = `$true -} - -function Send-LogonMarkerIfNeeded { - param( - [pscustomobject]`$Config, - [int]`$SessionId - ) - - `$sessionEvents = if (`$Config.PSObject.Properties.Name -contains 'sessionEvents') { `$Config.sessionEvents } else { `$null } - `$logging = if (`$Config.PSObject.Properties.Name -contains 'logging') { `$Config.logging } else { `$null } - `$logonEnabled = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'logonEnabled') { [bool]`$sessionEvents.logonEnabled } else { `$false } - if (-not `$logonEnabled) { - return - } - - `$bucketPrefix = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'bucketPrefix' -and -not [string]::IsNullOrWhiteSpace([string]`$sessionEvents.bucketPrefix)) { - [string]`$sessionEvents.bucketPrefix - } - else { - 'aw-session-events' - } - - `$stateRoot = [string]`$Config.paths.stateRoot - `$markerRoots = New-Object System.Collections.Generic.List[string] - if (-not [string]::IsNullOrWhiteSpace(`$env:LOCALAPPDATA)) { - `$markerRoots.Add((Join-Path `$env:LOCALAPPDATA 'AWatch-rus\markers')) - } - if (-not [string]::IsNullOrWhiteSpace(`$stateRoot)) { - `$markerRoots.Add((Join-Path `$stateRoot 'markers')) - } - - `$markerDir = `$null - foreach (`$candidate in `$markerRoots) { - try { - if (-not (Test-Path -LiteralPath `$candidate)) { - New-Item -Path `$candidate -ItemType Directory -Force | Out-Null - } - - `$probePath = Join-Path `$candidate 'write-test.tmp' - Set-Content -LiteralPath `$probePath -Value 'ok' -Encoding ASCII - Remove-Item -LiteralPath `$probePath -Force -ErrorAction SilentlyContinue - `$markerDir = `$candidate - break - } - catch { - } - } - - if (-not `$markerDir) { - return - } - - `$markerFile = Join-Path `$markerDir ("logon-{0}-{1}.marker" -f `$env:USERNAME, `$SessionId) - if (Test-Path -LiteralPath `$markerFile) { - return - } - - Set-Content -LiteralPath `$markerFile -Value ((Get-Date).ToUniversalTime().ToString('o')) -Encoding UTF8 - - `$bucketId = ('{0}_{1}' -f `$bucketPrefix, `$script:Hostname) - Ensure-Bucket -BucketId `$bucketId -ClientName 'aw-session-events' -BucketType 'aw.session.event' - - `$payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - eventType = 'logon' - username = `$env:USERNAME - userId = "`$(`$env:USERDOMAIN)\`$(`$env:USERNAME)" - sessionId = `$SessionId - hostname = `$script:Hostname - source = 'launch-watchers-awatch-rus' - } - } | ConvertTo-Json -Depth 5 -Compress - - try { - Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$bucketId/heartbeat?pulsetime=1" -Json `$payload - } - catch { - Remove-Item -LiteralPath `$markerFile -Force -ErrorAction SilentlyContinue - throw - } -} - -function Start-CollectorScriptIfNeeded { - param( - [string]`$ScriptPath, - [string]`$ConfigPath, - [string]`$PowerShellExe, - [int]`$SessionId - ) - - if ([string]::IsNullOrWhiteSpace(`$ScriptPath)) { - return - } - - if (-not (Test-Path -LiteralPath `$ScriptPath)) { - return - } - - if (Test-CollectorRunning -ScriptPath `$ScriptPath -SessionId `$SessionId) { - return - } - - if ((Get-CollectorPowerShellProcessCount) -ge `$script:MaxCollectorPowerShellProcesses) { - return - } - - `$staParam = if (`$ScriptPath -like "*endpoint-signals*") { "-STA" } else { `$null } - `$argumentList = @('-NoProfile', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass') - if (`$staParam) { `$argumentList += `$staParam } - `$argumentList += @('-File', `$ScriptPath, '-ConfigPath', `$ConfigPath) - Start-Process -FilePath `$PowerShellExe -ArgumentList `$argumentList -WindowStyle Hidden -} - -`$config = Get-DeploymentConfig -Path `$ConfigPath -`$sessionId = (Get-Process -Id `$PID).SessionId -`$installRoot = [string]`$config.paths.installRoot -`$stateRoot = [string]`$config.paths.stateRoot -`$script:ApiBase = '{0}://{1}:{2}/api/0' -f [string]`$config.server.scheme, [string]`$config.server.host, [string]`$config.server.port -`$script:Hostname = `$env:COMPUTERNAME -`$script:KnownBuckets = @{} -`$collectorScript = [string]`$config.paths.collectorScript -`$endpointCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]`$config.paths.endpointCollectorScript } else { Join-Path `$stateRoot 'dlp-endpoint-signals-collector.ps1' } -`$fileCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'fileCollectorScript') { [string]`$config.paths.fileCollectorScript } else { Join-Path `$stateRoot 'file-operations-collector.ps1' } -`$sessionCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]`$config.paths.sessionCollectorScript } else { Join-Path `$stateRoot 'worktime-session-collector.ps1' } -`$afkExe = Join-Path `$installRoot 'aw-watcher-afk\aw-watcher-afk.exe' -`$windowExe = Join-Path `$installRoot 'aw-watcher-window\aw-watcher-window.exe' -`$serverArgs = @('--host', [string]`$config.server.host, '--port', [string]`$config.server.port) -`$powershellExe = Join-Path `$env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' -`$afkEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]`$config.collectors.afkEnabled } else { `$true } -`$windowEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]`$config.collectors.windowEnabled } else { `$true } -`$fileOpsEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { [bool]`$config.collectors.fileOpsEnabled } else { `$true } -`$emailEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'emailEnabled') { [bool]`$config.collectors.emailEnabled } else { `$false } -`$emailCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'emailCollectorScript') { [string]`$config.paths.emailCollectorScript } else { Join-Path `$stateRoot 'email-outbound-collector.ps1' } -`$launchLockPath = New-LaunchLock -StateRoot `$stateRoot -SessionId `$sessionId -if (-not `$launchLockPath) { - return -} - -try { - if (`$afkEnabled -and -not (Test-Path -LiteralPath `$afkExe)) { - throw "Не найден aw-watcher-afk.exe: `$afkExe" - } - - if (`$windowEnabled -and -not (Test-Path -LiteralPath `$windowExe)) { - throw "Не найден aw-watcher-window.exe: `$windowExe" - } - - if (`$afkEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-afk' -SessionId `$sessionId)) { - Start-Process -FilePath `$afkExe -ArgumentList `$serverArgs -WindowStyle Hidden - } - - if (`$windowEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-window' -SessionId `$sessionId)) { - Start-Process -FilePath `$windowExe -ArgumentList `$serverArgs -WindowStyle Hidden - } - - try { - Send-LogonMarkerIfNeeded -Config `$config -SessionId `$sessionId - } - catch { - } - Start-CollectorScriptIfNeeded -ScriptPath `$collectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - Start-CollectorScriptIfNeeded -ScriptPath `$endpointCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - if (`$fileOpsEnabled) { - Start-CollectorScriptIfNeeded -ScriptPath `$fileCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - } - Start-CollectorScriptIfNeeded -ScriptPath `$sessionCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - if (`$emailEnabled -and (Test-Path -LiteralPath `$emailCollectorScript)) { - Start-CollectorScriptIfNeeded -ScriptPath `$emailCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - } -} -finally { - if (`$launchLockPath -and (Test-Path -LiteralPath `$launchLockPath)) { - Remove-Item -LiteralPath `$launchLockPath -Force -ErrorAction SilentlyContinue - } -} -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding UTF8 -} - -function Write-ActivityWatchRecoveryScript { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $content = @" -param( - [string]`$ConfigPath = '$ConfigPath' -) - -Set-StrictMode -Version Latest -`$ErrorActionPreference = 'Continue' - -function Get-DeploymentConfig { - param([string]`$Path) - return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json -} - -function Get-RecoveryConfigPaths { - param([string]`$PrimaryConfigPath) - - `$paths = New-Object System.Collections.Generic.List[string] - if (`$PrimaryConfigPath -and (Test-Path -LiteralPath `$PrimaryConfigPath)) { - `$paths.Add((Resolve-Path -LiteralPath `$PrimaryConfigPath).Path) - } - - `$searchRoot = `$env:ProgramData - if (`$PrimaryConfigPath) { - `$stateRoot = Split-Path -Path `$PrimaryConfigPath -Parent - `$candidateRoot = Split-Path -Path `$stateRoot -Parent - if (`$candidateRoot -and (Test-Path -LiteralPath `$candidateRoot)) { - `$searchRoot = `$candidateRoot - } - } - - if (Test-Path -LiteralPath `$searchRoot) { - Get-ChildItem -LiteralPath `$searchRoot -Directory -ErrorAction SilentlyContinue | - Where-Object { `$_.Name -like 'ActivityWatch*' } | - ForEach-Object { - `$candidate = Join-Path `$_.FullName 'deployment-config.json' - if (Test-Path -LiteralPath `$candidate) { - `$paths.Add(`$candidate) - } - } - } - - return @(`$paths | Sort-Object -Unique) -} - -function Get-RecoveryTaskNames { - param([string[]]`$ConfigPaths) - - `$taskNames = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) - foreach (`$candidatePath in @(`$ConfigPaths)) { - try { - `$config = Get-DeploymentConfig -Path `$candidatePath - foreach (`$task in @(`$config.userTasks)) { - `$taskName = [string]`$task.launchTaskName - if (-not [string]::IsNullOrWhiteSpace(`$taskName)) { - [void]`$taskNames.Add(`$taskName) - } - } - } - catch { - } - } - - return @(`$taskNames) -} - -function New-RecoveryLock { - param([string]`$PrimaryConfigPath) - - `$stateRoot = if (`$PrimaryConfigPath) { Split-Path -Path `$PrimaryConfigPath -Parent } else { Join-Path `$env:ProgramData 'AWatch-rus' } - if (-not (Test-Path -LiteralPath `$stateRoot)) { - New-Item -Path `$stateRoot -ItemType Directory -Force | Out-Null - } - - `$lockPath = Join-Path `$stateRoot 'recovery-loop.lock' - if (Test-Path -LiteralPath `$lockPath) { - try { - `$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json - `$existingPid = [int]`$lockData.pid - if (`$existingPid -gt 0 -and (Get-Process -Id `$existingPid -ErrorAction SilentlyContinue)) { - return `$null - } - } - catch { - } - } - - `$payload = @{ - pid = `$PID - createdAt = (Get-Date).ToUniversalTime().ToString('o') - } | ConvertTo-Json -Compress - Set-Content -LiteralPath `$lockPath -Value `$payload -Encoding UTF8 - return `$lockPath -} - -function Start-TaskIfNotRunning { - param([string]`$TaskName) - if ([string]::IsNullOrWhiteSpace(`$TaskName)) { - return - } - - try { - `$task = Get-ScheduledTask -TaskName `$TaskName -ErrorAction SilentlyContinue - if (-not `$task) { - return - } - if ([string]`$task.State -eq 'Running') { - return - } - Start-ScheduledTask -TaskName `$TaskName -ErrorAction SilentlyContinue - } - catch { - } -} - -`$recoveryLockPath = New-RecoveryLock -PrimaryConfigPath `$ConfigPath -if (-not `$recoveryLockPath) { - return -} - -try { - while (`$true) { - `$sleepSeconds = 180 - try { - `$configPaths = Get-RecoveryConfigPaths -PrimaryConfigPath `$ConfigPath - foreach (`$taskName in Get-RecoveryTaskNames -ConfigPaths `$configPaths) { - Start-TaskIfNotRunning -TaskName `$taskName - } - - `$config = Get-DeploymentConfig -Path `$ConfigPath - if (`$config -and `$config.recovery -and `$config.recovery.intervalSeconds) { - `$sleepSeconds = [Math]::Max([int]`$config.recovery.intervalSeconds, 30) - } - } - catch { - } - - Start-Sleep -Seconds `$sleepSeconds - } -} -finally { - if (`$recoveryLockPath -and (Test-Path -LiteralPath `$recoveryLockPath)) { - Remove-Item -LiteralPath `$recoveryLockPath -Force -ErrorAction SilentlyContinue - } -} -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding UTF8 -} - -function Get-ActivityWatchHiddenLauncherPath { - param( - [Parameter(Mandatory = $true)] - [string]$ScriptPath - ) - - $directory = Split-Path -Path $ScriptPath -Parent - $baseName = [IO.Path]::GetFileNameWithoutExtension($ScriptPath) - return Join-Path $directory ("{0}-hidden.vbs" -f $baseName) -} - -function Write-ActivityWatchHiddenPowerShellWrapper { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $directory = Split-Path -Path $Path -Parent - if ($directory) { - New-ActivityWatchDirectory -Path $directory - } - - $powershellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' - $escapedPowerShellExe = $powershellExe.Replace('"', '""') - $escapedScriptPath = $ScriptPath.Replace('"', '""') - $escapedConfigPath = $ConfigPath.Replace('"', '""') - - $content = @" -Set shell = CreateObject("WScript.Shell") -shell.Run """$escapedPowerShellExe"" -NoProfile -ExecutionPolicy Bypass -File ""$escapedScriptPath"" -ConfigPath ""$escapedConfigPath""", 0, False -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding ASCII -} - -function Remove-LegacyActivityWatchEntries { - $legacyTaskNames = @( - 'ActivityWatch Watchers', - 'ActivityWatch Guard', - 'ActivityWatch Heal' - ) - - foreach ($taskName in $legacyTaskNames) { - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue - } - - $runKey = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run' - foreach ($name in 'ActivityWatchAFK', 'ActivityWatchWindow', 'ActivityWatchBrowserCollector') { - Remove-ItemProperty -Path $runKey -Name $name -ErrorAction SilentlyContinue - } -} - -function Remove-ActivityWatchScheduledTask { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName - ) - - Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue - & cmd.exe /c "schtasks /Delete /TN `"$TaskName`" /F >nul 2>&1" | Out-Null - - for ($attempt = 0; $attempt -lt 10; $attempt++) { - $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue - if (-not $task) { - return - } - - Start-Sleep -Milliseconds 300 - } -} - -function Set-ActivityWatchScheduledTaskAction { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [Parameter(Mandatory = $true)] - [string]$Execute, - [Parameter(Mandatory = $true)] - [string]$Arguments - ) - - $taskCommand = ('"{0}" {1}' -f $Execute, $Arguments) - & schtasks.exe /Change /TN $TaskName /TR $taskCommand | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "schtasks.exe /Change завершился с ошибкой для $TaskName" - } -} - -function Get-ActivityWatchScheduledTaskByCommand { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [string]$CommandMatch - ) - - $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue - if ($task) { - return $task - } - - if ([string]::IsNullOrWhiteSpace($CommandMatch)) { - return $null - } - - foreach ($candidate in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch*' })) { - foreach ($action in @($candidate.Actions)) { - if ([string]$action.Arguments -like "*$CommandMatch*") { - return $candidate - } - } - } - - return $null -} - -function Remove-StaleActivityWatchUserTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath - ) - - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath - $desiredTaskNames = @($TaskDefinitions | ForEach-Object { [string]$_.LaunchTaskName }) - - foreach ($candidate in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch*' })) { - $taskName = [string]$candidate.TaskName - if ($desiredTaskNames -contains $taskName) { - continue - } - - $usesCurrentLauncher = $false - foreach ($action in @($candidate.Actions)) { - if ([string]$action.Arguments -like "*$launcherPath*") { - $usesCurrentLauncher = $true - break - } - } - - if ($usesCurrentLauncher) { - Remove-ActivityWatchScheduledTask -TaskName $taskName - } - } -} - -function Register-ActivityWatchUserTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe' - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath - Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $LaunchScriptPath -ConfigPath $ConfigPath - Remove-StaleActivityWatchUserTasks -TaskDefinitions $TaskDefinitions -LaunchScriptPath $LaunchScriptPath - - foreach ($definition in $TaskDefinitions) { - $action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`"" - $trigger = New-ScheduledTaskTrigger -AtLogOn -User $definition.UserId - $principal = New-ScheduledTaskPrincipal -UserId $definition.UserId -LogonType Interactive -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0) - $existingTask = Get-ActivityWatchScheduledTaskByCommand -TaskName $definition.LaunchTaskName -CommandMatch $ConfigPath - - if ($existingTask) { - Set-ActivityWatchScheduledTaskAction -TaskName $existingTask.TaskName -Execute $wscriptExe -Arguments $action.Arguments - continue - } - - Remove-ActivityWatchScheduledTask -TaskName $definition.LaunchTaskName - Register-ScheduledTask -TaskName $definition.LaunchTaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null - } -} - -function Register-ActivityWatchRecoveryTask { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [Parameter(Mandatory = $true)] - [string]$RecoveryScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - Remove-ActivityWatchScheduledTask -TaskName $TaskName - - $wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe' - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $RecoveryScriptPath - Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $RecoveryScriptPath -ConfigPath $ConfigPath - $action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`"" - $trigger = New-ScheduledTaskTrigger -AtStartup - $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -Hidden -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0) - - Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null -} - -function Set-ActivityWatchAcl { - param( - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [Parameter(Mandatory = $true)] - [string]$LogsRoot - ) - - foreach ($path in $InstallRoot, $StateRoot, $LogsRoot) { - New-ActivityWatchDirectory -Path $path - } - - & icacls $InstallRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $InstallRoot" - } - - & icacls $StateRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $StateRoot" - } - - & icacls $LogsRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(M)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $LogsRoot" - } -} - -function Start-ActivityWatchTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [string]$RecoveryTaskName = 'ActivityWatch Recovery' - ) - - foreach ($definition in $TaskDefinitions) { - Start-ScheduledTask -TaskName $definition.LaunchTaskName -ErrorAction SilentlyContinue - } - - Start-ScheduledTask -TaskName $RecoveryTaskName -ErrorAction SilentlyContinue -} - -Export-ModuleMember -Function *-ActivityWatch*, Assert-Administrator, Normalize-ActivityWatchUsers, Get-ActivityWatchPackageUrl, Remove-LegacyActivityWatchEntries -; } - } - - `$payload = @{ - pid = `$PID - sessionId = `$SessionId - createdAt = (Get-Date).ToUniversalTime().ToString('o') - } | ConvertTo-Json -Compress - Set-Content -LiteralPath `$lockPath -Value `$payload -Encoding UTF8 - return `$lockPath -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = `$true)][string]`$Uri, - [Parameter(Mandatory = `$true)][string]`$Json - ) - - `$httpClient = New-Object System.Net.Http.HttpClient - try { - `$content = New-Object System.Net.Http.StringContent(`$Json, [System.Text.Encoding]::UTF8, 'application/json') - `$response = `$httpClient.PostAsync(`$Uri, `$content).Result - if (-not `$response.IsSuccessStatusCode) { - return `$false - } - return `$true - } - catch { - return `$false - } - finally { - `$httpClient.Dispose() - } -} - -function Ensure-Bucket { - param( - [string]`$BucketId, - [string]`$ClientName, - [string]`$BucketType - ) - - if (`$script:KnownBuckets.ContainsKey(`$BucketId)) { - return - } - - try { - Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null - `$script:KnownBuckets[`$BucketId] = `$true - return - } - catch { Write-Error Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Assert-Administrator { - $identity = [Security.Principal.WindowsIdentity]::GetCurrent() - $principal = [Security.Principal.WindowsPrincipal]::new($identity) - if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - throw 'Запустите этот скрипт из PowerShell с правами администратора.' - } -} - -function New-ActivityWatchDirectory { - param( - [Parameter(Mandatory = $true)] - [string]$Path - ) - - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Enable-ActivityWatchPrintTelemetry { - $policyPath = 'HKLM:\Software\Policies\Microsoft\Windows NT\Printers' - if (-not (Test-Path -LiteralPath $policyPath)) { - New-Item -Path $policyPath -Force | Out-Null - } - New-ItemProperty -Path $policyPath -Name 'ShowJobTitleInEventLogs' -Value 1 -PropertyType DWord -Force | Out-Null - - & wevtutil.exe sl 'Microsoft-Windows-PrintService/Operational' /e:true | Out-Null -} - - -function Get-ActivityWatchPackageUrl { - param( - [string]$Version = 'v0.13.2' - ) - - return "https://github.com/ActivityWatch/activitywatch/releases/download/$Version/activitywatch-$Version-windows-x86_64.zip" -} - -function Get-ActivityWatchArchive { - param( - [string]$PackageZipPath, - [string]$PackageUrl, - [string]$Version = 'v0.13.2', - [Parameter(Mandatory = $true)] - [string]$WorkingRoot - ) - - New-ActivityWatchDirectory -Path $WorkingRoot - - if ($PackageZipPath) { - $resolved = Resolve-Path -LiteralPath $PackageZipPath -ErrorAction Stop - return $resolved.Path - } - - if (-not $PackageUrl) { - $PackageUrl = Get-ActivityWatchPackageUrl -Version $Version - } - - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' - $suffix = ([guid]::NewGuid().Guid.Substring(0, 8)) - $archivePath = Join-Path $WorkingRoot ("activitywatch-{0}-{1}-{2}.zip" -f $Version.TrimStart('v'), $stamp, $suffix) - Invoke-WebRequest -Uri $PackageUrl -OutFile $archivePath - return $archivePath -} - -function Get-ActivityWatchPackageRoot { - param( - [Parameter(Mandatory = $true)] - [string]$ExpandedRoot - ) - - $afkBinary = Get-ChildItem -Path $ExpandedRoot -Filter 'aw-watcher-afk.exe' -File -Recurse | - Select-Object -First 1 - - if (-not $afkBinary) { - throw "Не удалось найти aw-watcher-afk.exe в $ExpandedRoot." - } - - return (Split-Path -Path (Split-Path -Path $afkBinary.FullName -Parent) -Parent) -} - -function Install-ActivityWatchPackage { - param( - [Parameter(Mandatory = $true)] - [string]$ArchivePath, - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$WorkingRoot, - [Parameter(Mandatory = $true)] - [string]$BackupRoot - ) - - New-ActivityWatchDirectory -Path $WorkingRoot - New-ActivityWatchDirectory -Path $BackupRoot - - # Ensure nothing is holding locks inside InstallRoot during upgrade. - foreach ($procName in @('aw-watcher-afk', 'aw-watcher-window', 'aw-server', 'aw-qt')) { - try { - Get-Process -Name $procName -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue - } - catch { - } - } - Start-Sleep -Seconds 2 - - $extractRoot = Join-Path $WorkingRoot ('extract-' + [guid]::NewGuid().Guid) - if (Test-Path -LiteralPath $extractRoot) { - Remove-Item -LiteralPath $extractRoot -Recurse -Force - } - New-ActivityWatchDirectory -Path $extractRoot - - Expand-Archive -Path $ArchivePath -DestinationPath $extractRoot -Force - $packageRoot = Get-ActivityWatchPackageRoot -ExpandedRoot $extractRoot - - if (Test-Path -LiteralPath $InstallRoot) { - $existingItems = Get-ChildItem -LiteralPath $InstallRoot -Force -ErrorAction SilentlyContinue - if ($existingItems) { - $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' - $backupPath = Join-Path $BackupRoot ("install-$stamp") - New-ActivityWatchDirectory -Path $backupPath - Copy-Item -Path (Join-Path $InstallRoot '*') -Destination $backupPath -Recurse -Force - Get-ChildItem -LiteralPath $InstallRoot -Force | Remove-Item -Recurse -Force - } - } - else { - New-ActivityWatchDirectory -Path $InstallRoot - } - - Copy-Item -Path (Join-Path $packageRoot '*') -Destination $InstallRoot -Recurse -Force - - return [pscustomobject]@{ - PackageRoot = $packageRoot - ExtractRoot = $extractRoot - BackupRoot = $BackupRoot - } -} - -function Get-ActivityWatchExecutableMap { - param( - [Parameter(Mandatory = $true)] - [string]$InstallRoot - ) - - $map = [ordered]@{ - Afk = Join-Path $InstallRoot 'aw-watcher-afk\aw-watcher-afk.exe' - Window = Join-Path $InstallRoot 'aw-watcher-window\aw-watcher-window.exe' - } - - foreach ($entry in $map.GetEnumerator()) { - if (-not (Test-Path -LiteralPath $entry.Value)) { - throw "Не найден обязательный исполняемый файл ActivityWatch: $($entry.Value)" - } - } - - return [pscustomobject]$map -} - -function Normalize-ActivityWatchUsers { - param( - [string[]]$Users, - [string]$UserListPath, - [string]$Domain - ) - - $collected = New-Object System.Collections.Generic.List[string] - - if ($Users) { - foreach ($user in $Users) { - if (-not [string]::IsNullOrWhiteSpace($user)) { - $collected.Add($user.Trim()) - } - } - } - - if ($UserListPath) { - $resolved = Resolve-Path -LiteralPath $UserListPath -ErrorAction Stop - $extension = [IO.Path]::GetExtension($resolved.Path) - if ($extension -ieq '.csv') { - $rows = Import-Csv -LiteralPath $resolved.Path - foreach ($row in $rows) { - foreach ($column in 'User', 'Username', 'SamAccountName', 'Login') { - if ($row.PSObject.Properties.Name -contains $column) { - $value = [string]$row.$column - if (-not [string]::IsNullOrWhiteSpace($value)) { - $collected.Add($value.Trim()) - break - } - } - } - } - } - else { - Get-Content -LiteralPath $resolved.Path | ForEach-Object { - $line = $_.Trim() - if ($line -and -not $line.StartsWith('#')) { - $collected.Add($line) - } - } - } - } - - $normalized = $collected | - Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | - ForEach-Object { - if ($Domain -and ($_ -notmatch '[\\@]')) { - '{0}\{1}' -f $Domain, $_ - } - else { - $_ - } - } | - Sort-Object -Unique - - if (-not $normalized -or $normalized.Count -eq 0) { - throw 'Не удалось определить целевых пользователей. Укажите -Users или -UserListPath.' - } - - return @($normalized) -} - -function Get-ActivityWatchTaskNameToken { - param( - [Parameter(Mandatory = $true)] - [string]$UserId - ) - - $buffer = [Text.StringBuilder]::new() - foreach ($character in $UserId.ToCharArray()) { - if ([char]::IsLetterOrDigit($character)) { - [void]$buffer.Append($character) - } - else { - [void]$buffer.Append('_') - } - } - - return $buffer.ToString().Trim('_') -} - -function New-ActivityWatchUserTaskDefinitions { - param( - [Parameter(Mandatory = $true)] - [string[]]$Users - ) - - $result = foreach ($user in $Users) { - $token = Get-ActivityWatchTaskNameToken -UserId $user - [pscustomobject]@{ - UserId = $user - LaunchTaskName = "ActivityWatch Launch [$token]" - } - } - - return @($result) -} - -function Copy-ActivityWatchCollectorAssets { - param( - [Parameter(Mandatory = $true)] - [string]$CollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$EndpointCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$FileCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$SessionCollectorScriptSource, - [string]$EmailCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$ExampleRulesSource, - [Parameter(Mandatory = $true)] - [string]$ExamplePolicySource, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [string]$CustomRulesSource, - [string]$CustomPolicySource - ) - - New-ActivityWatchDirectory -Path $StateRoot - - $collectorTarget = Join-Path $StateRoot 'browser-domains-native-collector.ps1' - $endpointCollectorTarget = Join-Path $StateRoot 'dlp-endpoint-signals-collector.ps1' - $fileCollectorTarget = Join-Path $StateRoot 'file-operations-collector.ps1' - $sessionCollectorTarget = Join-Path $StateRoot 'worktime-session-collector.ps1' - $emailCollectorTarget = Join-Path $StateRoot 'email-outbound-collector.ps1' - $exampleRulesTarget = Join-Path $StateRoot 'web-category-rules.example.json' - $rulesTarget = Join-Path $StateRoot 'web-category-rules.json' - $examplePolicyTarget = Join-Path $StateRoot 'dlp-policy.example.json' - $policyTarget = Join-Path $StateRoot 'dlp-policy.json' - - Copy-Item -LiteralPath $CollectorScriptSource -Destination $collectorTarget -Force - Copy-Item -LiteralPath $EndpointCollectorScriptSource -Destination $endpointCollectorTarget -Force - Copy-Item -LiteralPath $FileCollectorScriptSource -Destination $fileCollectorTarget -Force - Copy-Item -LiteralPath $SessionCollectorScriptSource -Destination $sessionCollectorTarget -Force - if ($EmailCollectorScriptSource -and (Test-Path -LiteralPath $EmailCollectorScriptSource)) { - Copy-Item -LiteralPath $EmailCollectorScriptSource -Destination $emailCollectorTarget -Force - } - Copy-Item -LiteralPath $ExampleRulesSource -Destination $exampleRulesTarget -Force - Copy-Item -LiteralPath $ExamplePolicySource -Destination $examplePolicyTarget -Force - - if ($CustomRulesSource) { - $resolvedRules = Resolve-Path -LiteralPath $CustomRulesSource -ErrorAction Stop - Copy-Item -LiteralPath $resolvedRules.Path -Destination $rulesTarget -Force - } - else { - Copy-Item -LiteralPath $exampleRulesTarget -Destination $rulesTarget -Force - } - - if ($CustomPolicySource) { - $resolvedPolicy = Resolve-Path -LiteralPath $CustomPolicySource -ErrorAction Stop - Copy-Item -LiteralPath $resolvedPolicy.Path -Destination $policyTarget -Force - } - else { - Copy-Item -LiteralPath $examplePolicyTarget -Destination $policyTarget -Force - } - - return [pscustomobject]@{ - CollectorScript = $collectorTarget - EndpointCollectorScript = $endpointCollectorTarget - FileCollectorScript = $fileCollectorTarget - SessionCollectorScript = $sessionCollectorTarget - EmailCollectorScript = $emailCollectorTarget - ExampleRules = $exampleRulesTarget - ActiveRules = $rulesTarget - ExamplePolicy = $examplePolicyTarget - ActivePolicy = $policyTarget - } -} - -function New-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [string]$ServerHost, - [Parameter(Mandatory = $true)] - [int]$ServerPort, - [Parameter(Mandatory = $true)] - [string]$ServerScheme, - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [Parameter(Mandatory = $true)] - [string]$LogsRoot, - [Parameter(Mandatory = $true)] - [string]$CollectorScript, - [Parameter(Mandatory = $true)] - [string]$EndpointCollectorScript, - [Parameter(Mandatory = $true)] - [string]$FileCollectorScript, - [Parameter(Mandatory = $true)] - [string]$SessionCollectorScript, - [string]$EmailCollectorScript, - [Parameter(Mandatory = $true)] - [string]$RulesPath, - [Parameter(Mandatory = $true)] - [string]$PolicyPath, - [Parameter(Mandatory = $true)] - [int]$PollSeconds, - [Parameter(Mandatory = $true)] - [int]$PulseSeconds, - [Parameter(Mandatory = $true)] - [int]$RecoveryIntervalSeconds, - [bool]$AfkEnabled = $true, - [bool]$WindowEnabled = $true, - [bool]$FileOpsEnabled = $true, - [bool]$LocalAgentLogsEnabled = $true, - [bool]$IncidentCaptureEnabled = $true, - [bool]$IncidentScreenshotEnabled = $true, - [string]$IncidentArtifactsRoot, - [bool]$LogonMarkerEnabled = $true, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath, - [Parameter(Mandatory = $true)] - [string]$RecoveryScriptPath, - [Parameter(Mandatory = $true)] - [pscustomobject[]]$UserTasks, - [string]$PackageVersion = 'v0.13.2' - ) - - $effectiveIncidentArtifactsRoot = if ($IncidentArtifactsRoot) { $IncidentArtifactsRoot } else { Join-Path $StateRoot 'incident-artifacts' } - - return [pscustomobject]@{ - version = 1 - generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') - server = [pscustomobject]@{ - host = $ServerHost - port = $ServerPort - scheme = $ServerScheme - } - paths = [pscustomobject]@{ - installRoot = $InstallRoot - stateRoot = $StateRoot - logsRoot = $LogsRoot - collectorScript = $CollectorScript - endpointCollectorScript = $EndpointCollectorScript - emailCollectorScript = $EmailCollectorScript - fileCollectorScript = $FileCollectorScript - sessionCollectorScript = $SessionCollectorScript - rulesPath = $RulesPath - policyPath = $PolicyPath - launchScript = $LaunchScriptPath - recoveryScript = $RecoveryScriptPath - } - collector = [pscustomobject]@{ - pollSeconds = $PollSeconds - pulseSeconds = $PulseSeconds - } - collectors = [pscustomobject]@{ - afkEnabled = $AfkEnabled - windowEnabled = $WindowEnabled - fileOpsEnabled = $FileOpsEnabled - emailEnabled = ($null -ne $EmailCollectorScript -and $EmailCollectorScript -ne '') - } - logging = [pscustomobject]@{ - localAgentLogsEnabled = $LocalAgentLogsEnabled - } - incidentCapture = [pscustomobject]@{ - enabled = $IncidentCaptureEnabled - screenshotEnabled = $IncidentScreenshotEnabled - artifactsRoot = $effectiveIncidentArtifactsRoot - } - sessionEvents = [pscustomobject]@{ - logonEnabled = $LogonMarkerEnabled - bucketPrefix = 'aw-session-events' - } - recovery = [pscustomobject]@{ - intervalSeconds = $RecoveryIntervalSeconds - taskName = 'ActivityWatch Recovery' - } - dlp = [pscustomobject]@{ - incidentBucketPrefix = 'aw-dlp-incidents' - enabled = $true - } - package = [pscustomobject]@{ - version = $PackageVersion - } - userTasks = @($UserTasks) - } -} - -function Write-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [pscustomobject]$Config, - [Parameter(Mandatory = $true)] - [string]$Path - ) - - $directory = Split-Path -Path $Path -Parent - if ($directory) { - New-ActivityWatchDirectory -Path $directory - } - - $json = $Config | ConvertTo-Json -Depth 8 - Set-Content -LiteralPath $Path -Value $json -Encoding UTF8 -} - -function Read-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [string]$Path - ) - - if (-not (Test-Path -LiteralPath $Path)) { - throw "Конфигурация развёртывания не найдена: $Path" - } - - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json -} - -function Write-ActivityWatchLaunchScript { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $content = @" -param( - [string]`$ConfigPath = '$ConfigPath' -) - -Set-StrictMode -Version Latest -`$ErrorActionPreference = 'Stop' - -[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -Add-Type -AssemblyName System.Net.Http -`$script:MaxCollectorPowerShellProcesses = 24 - -function Get-DeploymentConfig { - param([string]`$Path) - return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json -} - -function Test-ProcessInSession { - param( - [string]`$Name, - [int]`$SessionId - ) - - return [bool](Get-Process -Name `$Name -ErrorAction SilentlyContinue | Where-Object { `$_.SessionId -eq `$SessionId } | Select-Object -First 1) -} - -function Test-CollectorRunning { - param( - [string]`$ScriptPath, - [int]`$SessionId - ) - - `$escapedCollector = [Regex]::Escape(`$ScriptPath) - `$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - (`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and - `$_.SessionId -eq `$SessionId -and - `$_.CommandLine -match `$escapedCollector - } - - return [bool](`$processes | Select-Object -First 1) -} - -function Get-CollectorPowerShellProcessCount { - `$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - (`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and - `$_.CommandLine -match 'AWatch-rus' -and - `$_.CommandLine -match '\.ps1' - } - - return @(`$processes).Count -} - -function New-LaunchLock { - param([string]`$StateRoot, [int]`$SessionId) - - `$lockPath = Join-Path `$env:TEMP ("launch-watchers-session-{0}.lock" -f `$SessionId) - if (Test-Path -LiteralPath `$lockPath) { - try { - `$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json - `$existingPid = [int]`$lockData.pid - if (`$existingPid -gt 0 -and (Get-Process -Id `$existingPid -ErrorAction SilentlyContinue)) { - return `$null - } - } - catch { - } - } - - `$payload = @{ - pid = `$PID - sessionId = `$SessionId - createdAt = (Get-Date).ToUniversalTime().ToString('o') - } | ConvertTo-Json -Compress - Set-Content -LiteralPath `$lockPath -Value `$payload -Encoding UTF8 - return `$lockPath -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = `$true)][string]`$Uri, - [Parameter(Mandatory = `$true)][string]`$Json - ) - - `$httpClient = New-Object System.Net.Http.HttpClient - try { - `$content = New-Object System.Net.Http.StringContent(`$Json, [System.Text.Encoding]::UTF8, 'application/json') - `$response = `$httpClient.PostAsync(`$Uri, `$content).Result - if (-not `$response.IsSuccessStatusCode) { - return `$false - } - return `$true - } - catch { - return `$false - } - finally { - `$httpClient.Dispose() - } -} - -function Ensure-Bucket { - param( - [string]`$BucketId, - [string]`$ClientName, - [string]`$BucketType - ) - - if (`$script:KnownBuckets.ContainsKey(`$BucketId)) { - return - } - - try { - Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null - `$script:KnownBuckets[`$BucketId] = `$true - return - } - catch { - } - - `$body = @{ - client = `$ClientName - type = `$BucketType - hostname = `$script:Hostname - } | ConvertTo-Json -Compress - - try { - if (-not (Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" -Json `$body)) { - return - } - } - catch { - try { - Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null - } - catch { - return - } - } - - `$script:KnownBuckets[`$BucketId] = `$true -} - -function Send-LogonMarkerIfNeeded { - param( - [pscustomobject]`$Config, - [int]`$SessionId - ) - - `$sessionEvents = if (`$Config.PSObject.Properties.Name -contains 'sessionEvents') { `$Config.sessionEvents } else { `$null } - `$logging = if (`$Config.PSObject.Properties.Name -contains 'logging') { `$Config.logging } else { `$null } - `$logonEnabled = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'logonEnabled') { [bool]`$sessionEvents.logonEnabled } else { `$false } - if (-not `$logonEnabled) { - return - } - - `$bucketPrefix = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'bucketPrefix' -and -not [string]::IsNullOrWhiteSpace([string]`$sessionEvents.bucketPrefix)) { - [string]`$sessionEvents.bucketPrefix - } - else { - 'aw-session-events' - } - - `$stateRoot = [string]`$Config.paths.stateRoot - `$markerRoots = New-Object System.Collections.Generic.List[string] - if (-not [string]::IsNullOrWhiteSpace(`$env:LOCALAPPDATA)) { - `$markerRoots.Add((Join-Path `$env:LOCALAPPDATA 'AWatch-rus\markers')) - } - if (-not [string]::IsNullOrWhiteSpace(`$stateRoot)) { - `$markerRoots.Add((Join-Path `$stateRoot 'markers')) - } - - `$markerDir = `$null - foreach (`$candidate in `$markerRoots) { - try { - if (-not (Test-Path -LiteralPath `$candidate)) { - New-Item -Path `$candidate -ItemType Directory -Force | Out-Null - } - - `$probePath = Join-Path `$candidate 'write-test.tmp' - Set-Content -LiteralPath `$probePath -Value 'ok' -Encoding ASCII - Remove-Item -LiteralPath `$probePath -Force -ErrorAction SilentlyContinue - `$markerDir = `$candidate - break - } - catch { - } - } - - if (-not `$markerDir) { - return - } - - `$markerFile = Join-Path `$markerDir ("logon-{0}-{1}.marker" -f `$env:USERNAME, `$SessionId) - if (Test-Path -LiteralPath `$markerFile) { - return - } - - Set-Content -LiteralPath `$markerFile -Value ((Get-Date).ToUniversalTime().ToString('o')) -Encoding UTF8 - - `$bucketId = ('{0}_{1}' -f `$bucketPrefix, `$script:Hostname) - Ensure-Bucket -BucketId `$bucketId -ClientName 'aw-session-events' -BucketType 'aw.session.event' - - `$payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - eventType = 'logon' - username = `$env:USERNAME - userId = "`$(`$env:USERDOMAIN)\`$(`$env:USERNAME)" - sessionId = `$SessionId - hostname = `$script:Hostname - source = 'launch-watchers-awatch-rus' - } - } | ConvertTo-Json -Depth 5 -Compress - - try { - Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$bucketId/heartbeat?pulsetime=1" -Json `$payload - } - catch { - Remove-Item -LiteralPath `$markerFile -Force -ErrorAction SilentlyContinue - throw - } -} - -function Start-CollectorScriptIfNeeded { - param( - [string]`$ScriptPath, - [string]`$ConfigPath, - [string]`$PowerShellExe, - [int]`$SessionId - ) - - if ([string]::IsNullOrWhiteSpace(`$ScriptPath)) { - return - } - - if (-not (Test-Path -LiteralPath `$ScriptPath)) { - return - } - - if (Test-CollectorRunning -ScriptPath `$ScriptPath -SessionId `$SessionId) { - return - } - - if ((Get-CollectorPowerShellProcessCount) -ge `$script:MaxCollectorPowerShellProcesses) { - return - } - - `$staParam = if (`$ScriptPath -like "*endpoint-signals*") { "-STA" } else { `$null } - `$argumentList = @('-NoProfile', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass') - if (`$staParam) { `$argumentList += `$staParam } - `$argumentList += @('-File', `$ScriptPath, '-ConfigPath', `$ConfigPath) - Start-Process -FilePath `$PowerShellExe -ArgumentList `$argumentList -WindowStyle Hidden -} - -`$config = Get-DeploymentConfig -Path `$ConfigPath -`$sessionId = (Get-Process -Id `$PID).SessionId -`$installRoot = [string]`$config.paths.installRoot -`$stateRoot = [string]`$config.paths.stateRoot -`$script:ApiBase = '{0}://{1}:{2}/api/0' -f [string]`$config.server.scheme, [string]`$config.server.host, [string]`$config.server.port -`$script:Hostname = `$env:COMPUTERNAME -`$script:KnownBuckets = @{} -`$collectorScript = [string]`$config.paths.collectorScript -`$endpointCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]`$config.paths.endpointCollectorScript } else { Join-Path `$stateRoot 'dlp-endpoint-signals-collector.ps1' } -`$fileCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'fileCollectorScript') { [string]`$config.paths.fileCollectorScript } else { Join-Path `$stateRoot 'file-operations-collector.ps1' } -`$sessionCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]`$config.paths.sessionCollectorScript } else { Join-Path `$stateRoot 'worktime-session-collector.ps1' } -`$afkExe = Join-Path `$installRoot 'aw-watcher-afk\aw-watcher-afk.exe' -`$windowExe = Join-Path `$installRoot 'aw-watcher-window\aw-watcher-window.exe' -`$serverArgs = @('--host', [string]`$config.server.host, '--port', [string]`$config.server.port) -`$powershellExe = Join-Path `$env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' -`$afkEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]`$config.collectors.afkEnabled } else { `$true } -`$windowEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]`$config.collectors.windowEnabled } else { `$true } -`$fileOpsEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { [bool]`$config.collectors.fileOpsEnabled } else { `$true } -`$emailEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'emailEnabled') { [bool]`$config.collectors.emailEnabled } else { `$false } -`$emailCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'emailCollectorScript') { [string]`$config.paths.emailCollectorScript } else { Join-Path `$stateRoot 'email-outbound-collector.ps1' } -`$launchLockPath = New-LaunchLock -StateRoot `$stateRoot -SessionId `$sessionId -if (-not `$launchLockPath) { - return -} - -try { - if (`$afkEnabled -and -not (Test-Path -LiteralPath `$afkExe)) { - throw "Не найден aw-watcher-afk.exe: `$afkExe" - } - - if (`$windowEnabled -and -not (Test-Path -LiteralPath `$windowExe)) { - throw "Не найден aw-watcher-window.exe: `$windowExe" - } - - if (`$afkEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-afk' -SessionId `$sessionId)) { - Start-Process -FilePath `$afkExe -ArgumentList `$serverArgs -WindowStyle Hidden - } - - if (`$windowEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-window' -SessionId `$sessionId)) { - Start-Process -FilePath `$windowExe -ArgumentList `$serverArgs -WindowStyle Hidden - } - - try { - Send-LogonMarkerIfNeeded -Config `$config -SessionId `$sessionId - } - catch { - } - Start-CollectorScriptIfNeeded -ScriptPath `$collectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - Start-CollectorScriptIfNeeded -ScriptPath `$endpointCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - if (`$fileOpsEnabled) { - Start-CollectorScriptIfNeeded -ScriptPath `$fileCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - } - Start-CollectorScriptIfNeeded -ScriptPath `$sessionCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - if (`$emailEnabled -and (Test-Path -LiteralPath `$emailCollectorScript)) { - Start-CollectorScriptIfNeeded -ScriptPath `$emailCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - } -} -finally { - if (`$launchLockPath -and (Test-Path -LiteralPath `$launchLockPath)) { - Remove-Item -LiteralPath `$launchLockPath -Force -ErrorAction SilentlyContinue - } -} -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding UTF8 -} - -function Write-ActivityWatchRecoveryScript { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $content = @" -param( - [string]`$ConfigPath = '$ConfigPath' -) - -Set-StrictMode -Version Latest -`$ErrorActionPreference = 'Continue' - -function Get-DeploymentConfig { - param([string]`$Path) - return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json -} - -function Get-RecoveryConfigPaths { - param([string]`$PrimaryConfigPath) - - `$paths = New-Object System.Collections.Generic.List[string] - if (`$PrimaryConfigPath -and (Test-Path -LiteralPath `$PrimaryConfigPath)) { - `$paths.Add((Resolve-Path -LiteralPath `$PrimaryConfigPath).Path) - } - - `$searchRoot = `$env:ProgramData - if (`$PrimaryConfigPath) { - `$stateRoot = Split-Path -Path `$PrimaryConfigPath -Parent - `$candidateRoot = Split-Path -Path `$stateRoot -Parent - if (`$candidateRoot -and (Test-Path -LiteralPath `$candidateRoot)) { - `$searchRoot = `$candidateRoot - } - } - - if (Test-Path -LiteralPath `$searchRoot) { - Get-ChildItem -LiteralPath `$searchRoot -Directory -ErrorAction SilentlyContinue | - Where-Object { `$_.Name -like 'ActivityWatch*' } | - ForEach-Object { - `$candidate = Join-Path `$_.FullName 'deployment-config.json' - if (Test-Path -LiteralPath `$candidate) { - `$paths.Add(`$candidate) - } - } - } - - return @(`$paths | Sort-Object -Unique) -} - -function Get-RecoveryTaskNames { - param([string[]]`$ConfigPaths) - - `$taskNames = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) - foreach (`$candidatePath in @(`$ConfigPaths)) { - try { - `$config = Get-DeploymentConfig -Path `$candidatePath - foreach (`$task in @(`$config.userTasks)) { - `$taskName = [string]`$task.launchTaskName - if (-not [string]::IsNullOrWhiteSpace(`$taskName)) { - [void]`$taskNames.Add(`$taskName) - } - } - } - catch { - } - } - - return @(`$taskNames) -} - -function New-RecoveryLock { - param([string]`$PrimaryConfigPath) - - `$stateRoot = if (`$PrimaryConfigPath) { Split-Path -Path `$PrimaryConfigPath -Parent } else { Join-Path `$env:ProgramData 'AWatch-rus' } - if (-not (Test-Path -LiteralPath `$stateRoot)) { - New-Item -Path `$stateRoot -ItemType Directory -Force | Out-Null - } - - `$lockPath = Join-Path `$stateRoot 'recovery-loop.lock' - if (Test-Path -LiteralPath `$lockPath) { - try { - `$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json - `$existingPid = [int]`$lockData.pid - if (`$existingPid -gt 0 -and (Get-Process -Id `$existingPid -ErrorAction SilentlyContinue)) { - return `$null - } - } - catch { - } - } - - `$payload = @{ - pid = `$PID - createdAt = (Get-Date).ToUniversalTime().ToString('o') - } | ConvertTo-Json -Compress - Set-Content -LiteralPath `$lockPath -Value `$payload -Encoding UTF8 - return `$lockPath -} - -function Start-TaskIfNotRunning { - param([string]`$TaskName) - if ([string]::IsNullOrWhiteSpace(`$TaskName)) { - return - } - - try { - `$task = Get-ScheduledTask -TaskName `$TaskName -ErrorAction SilentlyContinue - if (-not `$task) { - return - } - if ([string]`$task.State -eq 'Running') { - return - } - Start-ScheduledTask -TaskName `$TaskName -ErrorAction SilentlyContinue - } - catch { - } -} - -`$recoveryLockPath = New-RecoveryLock -PrimaryConfigPath `$ConfigPath -if (-not `$recoveryLockPath) { - return -} - -try { - while (`$true) { - `$sleepSeconds = 180 - try { - `$configPaths = Get-RecoveryConfigPaths -PrimaryConfigPath `$ConfigPath - foreach (`$taskName in Get-RecoveryTaskNames -ConfigPaths `$configPaths) { - Start-TaskIfNotRunning -TaskName `$taskName - } - - `$config = Get-DeploymentConfig -Path `$ConfigPath - if (`$config -and `$config.recovery -and `$config.recovery.intervalSeconds) { - `$sleepSeconds = [Math]::Max([int]`$config.recovery.intervalSeconds, 30) - } - } - catch { - } - - Start-Sleep -Seconds `$sleepSeconds - } -} -finally { - if (`$recoveryLockPath -and (Test-Path -LiteralPath `$recoveryLockPath)) { - Remove-Item -LiteralPath `$recoveryLockPath -Force -ErrorAction SilentlyContinue - } -} -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding UTF8 -} - -function Get-ActivityWatchHiddenLauncherPath { - param( - [Parameter(Mandatory = $true)] - [string]$ScriptPath - ) - - $directory = Split-Path -Path $ScriptPath -Parent - $baseName = [IO.Path]::GetFileNameWithoutExtension($ScriptPath) - return Join-Path $directory ("{0}-hidden.vbs" -f $baseName) -} - -function Write-ActivityWatchHiddenPowerShellWrapper { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $directory = Split-Path -Path $Path -Parent - if ($directory) { - New-ActivityWatchDirectory -Path $directory - } - - $powershellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' - $escapedPowerShellExe = $powershellExe.Replace('"', '""') - $escapedScriptPath = $ScriptPath.Replace('"', '""') - $escapedConfigPath = $ConfigPath.Replace('"', '""') - - $content = @" -Set shell = CreateObject("WScript.Shell") -shell.Run """$escapedPowerShellExe"" -NoProfile -ExecutionPolicy Bypass -File ""$escapedScriptPath"" -ConfigPath ""$escapedConfigPath""", 0, False -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding ASCII -} - -function Remove-LegacyActivityWatchEntries { - $legacyTaskNames = @( - 'ActivityWatch Watchers', - 'ActivityWatch Guard', - 'ActivityWatch Heal' - ) - - foreach ($taskName in $legacyTaskNames) { - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue - } - - $runKey = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run' - foreach ($name in 'ActivityWatchAFK', 'ActivityWatchWindow', 'ActivityWatchBrowserCollector') { - Remove-ItemProperty -Path $runKey -Name $name -ErrorAction SilentlyContinue - } -} - -function Remove-ActivityWatchScheduledTask { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName - ) - - Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue - & cmd.exe /c "schtasks /Delete /TN `"$TaskName`" /F >nul 2>&1" | Out-Null - - for ($attempt = 0; $attempt -lt 10; $attempt++) { - $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue - if (-not $task) { - return - } - - Start-Sleep -Milliseconds 300 - } -} - -function Set-ActivityWatchScheduledTaskAction { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [Parameter(Mandatory = $true)] - [string]$Execute, - [Parameter(Mandatory = $true)] - [string]$Arguments - ) - - $taskCommand = ('"{0}" {1}' -f $Execute, $Arguments) - & schtasks.exe /Change /TN $TaskName /TR $taskCommand | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "schtasks.exe /Change завершился с ошибкой для $TaskName" - } -} - -function Get-ActivityWatchScheduledTaskByCommand { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [string]$CommandMatch - ) - - $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue - if ($task) { - return $task - } - - if ([string]::IsNullOrWhiteSpace($CommandMatch)) { - return $null - } - - foreach ($candidate in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch*' })) { - foreach ($action in @($candidate.Actions)) { - if ([string]$action.Arguments -like "*$CommandMatch*") { - return $candidate - } - } - } - - return $null -} - -function Remove-StaleActivityWatchUserTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath - ) - - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath - $desiredTaskNames = @($TaskDefinitions | ForEach-Object { [string]$_.LaunchTaskName }) - - foreach ($candidate in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch*' })) { - $taskName = [string]$candidate.TaskName - if ($desiredTaskNames -contains $taskName) { - continue - } - - $usesCurrentLauncher = $false - foreach ($action in @($candidate.Actions)) { - if ([string]$action.Arguments -like "*$launcherPath*") { - $usesCurrentLauncher = $true - break - } - } - - if ($usesCurrentLauncher) { - Remove-ActivityWatchScheduledTask -TaskName $taskName - } - } -} - -function Register-ActivityWatchUserTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe' - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath - Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $LaunchScriptPath -ConfigPath $ConfigPath - Remove-StaleActivityWatchUserTasks -TaskDefinitions $TaskDefinitions -LaunchScriptPath $LaunchScriptPath - - foreach ($definition in $TaskDefinitions) { - $action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`"" - $trigger = New-ScheduledTaskTrigger -AtLogOn -User $definition.UserId - $principal = New-ScheduledTaskPrincipal -UserId $definition.UserId -LogonType Interactive -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0) - $existingTask = Get-ActivityWatchScheduledTaskByCommand -TaskName $definition.LaunchTaskName -CommandMatch $ConfigPath - - if ($existingTask) { - Set-ActivityWatchScheduledTaskAction -TaskName $existingTask.TaskName -Execute $wscriptExe -Arguments $action.Arguments - continue - } - - Remove-ActivityWatchScheduledTask -TaskName $definition.LaunchTaskName - Register-ScheduledTask -TaskName $definition.LaunchTaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null - } -} - -function Register-ActivityWatchRecoveryTask { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [Parameter(Mandatory = $true)] - [string]$RecoveryScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - Remove-ActivityWatchScheduledTask -TaskName $TaskName - - $wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe' - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $RecoveryScriptPath - Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $RecoveryScriptPath -ConfigPath $ConfigPath - $action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`"" - $trigger = New-ScheduledTaskTrigger -AtStartup - $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -Hidden -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0) - - Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null -} - -function Set-ActivityWatchAcl { - param( - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [Parameter(Mandatory = $true)] - [string]$LogsRoot - ) - - foreach ($path in $InstallRoot, $StateRoot, $LogsRoot) { - New-ActivityWatchDirectory -Path $path - } - - & icacls $InstallRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $InstallRoot" - } - - & icacls $StateRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $StateRoot" - } - - & icacls $LogsRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(M)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $LogsRoot" - } -} - -function Start-ActivityWatchTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [string]$RecoveryTaskName = 'ActivityWatch Recovery' - ) - - foreach ($definition in $TaskDefinitions) { - Start-ScheduledTask -TaskName $definition.LaunchTaskName -ErrorAction SilentlyContinue - } - - Start-ScheduledTask -TaskName $RecoveryTaskName -ErrorAction SilentlyContinue -} - -Export-ModuleMember -Function *-ActivityWatch*, Assert-Administrator, Normalize-ActivityWatchUsers, Get-ActivityWatchPackageUrl, Remove-LegacyActivityWatchEntries -; } - - `$body = @{ - client = `$ClientName - type = `$BucketType - hostname = `$script:Hostname - } | ConvertTo-Json -Compress - - try { - if (-not (Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" -Json `$body)) { - return - } - } - catch { - try { - Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null - } - catch { - return - } - } - - `$script:KnownBuckets[`$BucketId] = `$true -} - -function Send-LogonMarkerIfNeeded { - param( - [pscustomobject]`$Config, - [int]`$SessionId - ) - - `$sessionEvents = if (`$Config.PSObject.Properties.Name -contains 'sessionEvents') { `$Config.sessionEvents } else { `$null } - `$logging = if (`$Config.PSObject.Properties.Name -contains 'logging') { `$Config.logging } else { `$null } - `$logonEnabled = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'logonEnabled') { [bool]`$sessionEvents.logonEnabled } else { `$false } - if (-not `$logonEnabled) { - return - } - - `$bucketPrefix = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'bucketPrefix' -and -not [string]::IsNullOrWhiteSpace([string]`$sessionEvents.bucketPrefix)) { - [string]`$sessionEvents.bucketPrefix - } - else { - 'aw-session-events' - } - - `$stateRoot = [string]`$Config.paths.stateRoot - `$markerRoots = New-Object System.Collections.Generic.List[string] - if (-not [string]::IsNullOrWhiteSpace(`$env:LOCALAPPDATA)) { - `$markerRoots.Add((Join-Path `$env:LOCALAPPDATA 'AWatch-rus\markers')) - } - if (-not [string]::IsNullOrWhiteSpace(`$stateRoot)) { - `$markerRoots.Add((Join-Path `$stateRoot 'markers')) - } - - `$markerDir = `$null - foreach (`$candidate in `$markerRoots) { - try { - if (-not (Test-Path -LiteralPath `$candidate)) { - New-Item -Path `$candidate -ItemType Directory -Force | Out-Null - } - - `$probePath = Join-Path `$candidate 'write-test.tmp' - Set-Content -LiteralPath `$probePath -Value 'ok' -Encoding ASCII - Remove-Item -LiteralPath `$probePath -Force -ErrorAction SilentlyContinue - `$markerDir = `$candidate - break - } - catch { Write-Error Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Assert-Administrator { - $identity = [Security.Principal.WindowsIdentity]::GetCurrent() - $principal = [Security.Principal.WindowsPrincipal]::new($identity) - if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - throw 'Запустите этот скрипт из PowerShell с правами администратора.' - } -} - -function New-ActivityWatchDirectory { - param( - [Parameter(Mandatory = $true)] - [string]$Path - ) - - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Enable-ActivityWatchPrintTelemetry { - $policyPath = 'HKLM:\Software\Policies\Microsoft\Windows NT\Printers' - if (-not (Test-Path -LiteralPath $policyPath)) { - New-Item -Path $policyPath -Force | Out-Null - } - New-ItemProperty -Path $policyPath -Name 'ShowJobTitleInEventLogs' -Value 1 -PropertyType DWord -Force | Out-Null - - & wevtutil.exe sl 'Microsoft-Windows-PrintService/Operational' /e:true | Out-Null -} - - -function Get-ActivityWatchPackageUrl { - param( - [string]$Version = 'v0.13.2' - ) - - return "https://github.com/ActivityWatch/activitywatch/releases/download/$Version/activitywatch-$Version-windows-x86_64.zip" -} - -function Get-ActivityWatchArchive { - param( - [string]$PackageZipPath, - [string]$PackageUrl, - [string]$Version = 'v0.13.2', - [Parameter(Mandatory = $true)] - [string]$WorkingRoot - ) - - New-ActivityWatchDirectory -Path $WorkingRoot - - if ($PackageZipPath) { - $resolved = Resolve-Path -LiteralPath $PackageZipPath -ErrorAction Stop - return $resolved.Path - } - - if (-not $PackageUrl) { - $PackageUrl = Get-ActivityWatchPackageUrl -Version $Version - } - - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' - $suffix = ([guid]::NewGuid().Guid.Substring(0, 8)) - $archivePath = Join-Path $WorkingRoot ("activitywatch-{0}-{1}-{2}.zip" -f $Version.TrimStart('v'), $stamp, $suffix) - Invoke-WebRequest -Uri $PackageUrl -OutFile $archivePath - return $archivePath -} - -function Get-ActivityWatchPackageRoot { - param( - [Parameter(Mandatory = $true)] - [string]$ExpandedRoot - ) - - $afkBinary = Get-ChildItem -Path $ExpandedRoot -Filter 'aw-watcher-afk.exe' -File -Recurse | - Select-Object -First 1 - - if (-not $afkBinary) { - throw "Не удалось найти aw-watcher-afk.exe в $ExpandedRoot." - } - - return (Split-Path -Path (Split-Path -Path $afkBinary.FullName -Parent) -Parent) -} - -function Install-ActivityWatchPackage { - param( - [Parameter(Mandatory = $true)] - [string]$ArchivePath, - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$WorkingRoot, - [Parameter(Mandatory = $true)] - [string]$BackupRoot - ) - - New-ActivityWatchDirectory -Path $WorkingRoot - New-ActivityWatchDirectory -Path $BackupRoot - - # Ensure nothing is holding locks inside InstallRoot during upgrade. - foreach ($procName in @('aw-watcher-afk', 'aw-watcher-window', 'aw-server', 'aw-qt')) { - try { - Get-Process -Name $procName -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue - } - catch { - } - } - Start-Sleep -Seconds 2 - - $extractRoot = Join-Path $WorkingRoot ('extract-' + [guid]::NewGuid().Guid) - if (Test-Path -LiteralPath $extractRoot) { - Remove-Item -LiteralPath $extractRoot -Recurse -Force - } - New-ActivityWatchDirectory -Path $extractRoot - - Expand-Archive -Path $ArchivePath -DestinationPath $extractRoot -Force - $packageRoot = Get-ActivityWatchPackageRoot -ExpandedRoot $extractRoot - - if (Test-Path -LiteralPath $InstallRoot) { - $existingItems = Get-ChildItem -LiteralPath $InstallRoot -Force -ErrorAction SilentlyContinue - if ($existingItems) { - $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' - $backupPath = Join-Path $BackupRoot ("install-$stamp") - New-ActivityWatchDirectory -Path $backupPath - Copy-Item -Path (Join-Path $InstallRoot '*') -Destination $backupPath -Recurse -Force - Get-ChildItem -LiteralPath $InstallRoot -Force | Remove-Item -Recurse -Force - } - } - else { - New-ActivityWatchDirectory -Path $InstallRoot - } - - Copy-Item -Path (Join-Path $packageRoot '*') -Destination $InstallRoot -Recurse -Force - - return [pscustomobject]@{ - PackageRoot = $packageRoot - ExtractRoot = $extractRoot - BackupRoot = $BackupRoot - } -} - -function Get-ActivityWatchExecutableMap { - param( - [Parameter(Mandatory = $true)] - [string]$InstallRoot - ) - - $map = [ordered]@{ - Afk = Join-Path $InstallRoot 'aw-watcher-afk\aw-watcher-afk.exe' - Window = Join-Path $InstallRoot 'aw-watcher-window\aw-watcher-window.exe' - } - - foreach ($entry in $map.GetEnumerator()) { - if (-not (Test-Path -LiteralPath $entry.Value)) { - throw "Не найден обязательный исполняемый файл ActivityWatch: $($entry.Value)" - } - } - - return [pscustomobject]$map -} - -function Normalize-ActivityWatchUsers { - param( - [string[]]$Users, - [string]$UserListPath, - [string]$Domain - ) - - $collected = New-Object System.Collections.Generic.List[string] - - if ($Users) { - foreach ($user in $Users) { - if (-not [string]::IsNullOrWhiteSpace($user)) { - $collected.Add($user.Trim()) - } - } - } - - if ($UserListPath) { - $resolved = Resolve-Path -LiteralPath $UserListPath -ErrorAction Stop - $extension = [IO.Path]::GetExtension($resolved.Path) - if ($extension -ieq '.csv') { - $rows = Import-Csv -LiteralPath $resolved.Path - foreach ($row in $rows) { - foreach ($column in 'User', 'Username', 'SamAccountName', 'Login') { - if ($row.PSObject.Properties.Name -contains $column) { - $value = [string]$row.$column - if (-not [string]::IsNullOrWhiteSpace($value)) { - $collected.Add($value.Trim()) - break - } - } - } - } - } - else { - Get-Content -LiteralPath $resolved.Path | ForEach-Object { - $line = $_.Trim() - if ($line -and -not $line.StartsWith('#')) { - $collected.Add($line) - } - } - } - } - - $normalized = $collected | - Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | - ForEach-Object { - if ($Domain -and ($_ -notmatch '[\\@]')) { - '{0}\{1}' -f $Domain, $_ - } - else { - $_ - } - } | - Sort-Object -Unique - - if (-not $normalized -or $normalized.Count -eq 0) { - throw 'Не удалось определить целевых пользователей. Укажите -Users или -UserListPath.' - } - - return @($normalized) -} - -function Get-ActivityWatchTaskNameToken { - param( - [Parameter(Mandatory = $true)] - [string]$UserId - ) - - $buffer = [Text.StringBuilder]::new() - foreach ($character in $UserId.ToCharArray()) { - if ([char]::IsLetterOrDigit($character)) { - [void]$buffer.Append($character) - } - else { - [void]$buffer.Append('_') - } - } - - return $buffer.ToString().Trim('_') -} - -function New-ActivityWatchUserTaskDefinitions { - param( - [Parameter(Mandatory = $true)] - [string[]]$Users - ) - - $result = foreach ($user in $Users) { - $token = Get-ActivityWatchTaskNameToken -UserId $user - [pscustomobject]@{ - UserId = $user - LaunchTaskName = "ActivityWatch Launch [$token]" - } - } - - return @($result) -} - -function Copy-ActivityWatchCollectorAssets { - param( - [Parameter(Mandatory = $true)] - [string]$CollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$EndpointCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$FileCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$SessionCollectorScriptSource, - [string]$EmailCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$ExampleRulesSource, - [Parameter(Mandatory = $true)] - [string]$ExamplePolicySource, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [string]$CustomRulesSource, - [string]$CustomPolicySource - ) - - New-ActivityWatchDirectory -Path $StateRoot - - $collectorTarget = Join-Path $StateRoot 'browser-domains-native-collector.ps1' - $endpointCollectorTarget = Join-Path $StateRoot 'dlp-endpoint-signals-collector.ps1' - $fileCollectorTarget = Join-Path $StateRoot 'file-operations-collector.ps1' - $sessionCollectorTarget = Join-Path $StateRoot 'worktime-session-collector.ps1' - $emailCollectorTarget = Join-Path $StateRoot 'email-outbound-collector.ps1' - $exampleRulesTarget = Join-Path $StateRoot 'web-category-rules.example.json' - $rulesTarget = Join-Path $StateRoot 'web-category-rules.json' - $examplePolicyTarget = Join-Path $StateRoot 'dlp-policy.example.json' - $policyTarget = Join-Path $StateRoot 'dlp-policy.json' - - Copy-Item -LiteralPath $CollectorScriptSource -Destination $collectorTarget -Force - Copy-Item -LiteralPath $EndpointCollectorScriptSource -Destination $endpointCollectorTarget -Force - Copy-Item -LiteralPath $FileCollectorScriptSource -Destination $fileCollectorTarget -Force - Copy-Item -LiteralPath $SessionCollectorScriptSource -Destination $sessionCollectorTarget -Force - if ($EmailCollectorScriptSource -and (Test-Path -LiteralPath $EmailCollectorScriptSource)) { - Copy-Item -LiteralPath $EmailCollectorScriptSource -Destination $emailCollectorTarget -Force - } - Copy-Item -LiteralPath $ExampleRulesSource -Destination $exampleRulesTarget -Force - Copy-Item -LiteralPath $ExamplePolicySource -Destination $examplePolicyTarget -Force - - if ($CustomRulesSource) { - $resolvedRules = Resolve-Path -LiteralPath $CustomRulesSource -ErrorAction Stop - Copy-Item -LiteralPath $resolvedRules.Path -Destination $rulesTarget -Force - } - else { - Copy-Item -LiteralPath $exampleRulesTarget -Destination $rulesTarget -Force - } - - if ($CustomPolicySource) { - $resolvedPolicy = Resolve-Path -LiteralPath $CustomPolicySource -ErrorAction Stop - Copy-Item -LiteralPath $resolvedPolicy.Path -Destination $policyTarget -Force - } - else { - Copy-Item -LiteralPath $examplePolicyTarget -Destination $policyTarget -Force - } - - return [pscustomobject]@{ - CollectorScript = $collectorTarget - EndpointCollectorScript = $endpointCollectorTarget - FileCollectorScript = $fileCollectorTarget - SessionCollectorScript = $sessionCollectorTarget - EmailCollectorScript = $emailCollectorTarget - ExampleRules = $exampleRulesTarget - ActiveRules = $rulesTarget - ExamplePolicy = $examplePolicyTarget - ActivePolicy = $policyTarget - } -} - -function New-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [string]$ServerHost, - [Parameter(Mandatory = $true)] - [int]$ServerPort, - [Parameter(Mandatory = $true)] - [string]$ServerScheme, - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [Parameter(Mandatory = $true)] - [string]$LogsRoot, - [Parameter(Mandatory = $true)] - [string]$CollectorScript, - [Parameter(Mandatory = $true)] - [string]$EndpointCollectorScript, - [Parameter(Mandatory = $true)] - [string]$FileCollectorScript, - [Parameter(Mandatory = $true)] - [string]$SessionCollectorScript, - [string]$EmailCollectorScript, - [Parameter(Mandatory = $true)] - [string]$RulesPath, - [Parameter(Mandatory = $true)] - [string]$PolicyPath, - [Parameter(Mandatory = $true)] - [int]$PollSeconds, - [Parameter(Mandatory = $true)] - [int]$PulseSeconds, - [Parameter(Mandatory = $true)] - [int]$RecoveryIntervalSeconds, - [bool]$AfkEnabled = $true, - [bool]$WindowEnabled = $true, - [bool]$FileOpsEnabled = $true, - [bool]$LocalAgentLogsEnabled = $true, - [bool]$IncidentCaptureEnabled = $true, - [bool]$IncidentScreenshotEnabled = $true, - [string]$IncidentArtifactsRoot, - [bool]$LogonMarkerEnabled = $true, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath, - [Parameter(Mandatory = $true)] - [string]$RecoveryScriptPath, - [Parameter(Mandatory = $true)] - [pscustomobject[]]$UserTasks, - [string]$PackageVersion = 'v0.13.2' - ) - - $effectiveIncidentArtifactsRoot = if ($IncidentArtifactsRoot) { $IncidentArtifactsRoot } else { Join-Path $StateRoot 'incident-artifacts' } - - return [pscustomobject]@{ - version = 1 - generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') - server = [pscustomobject]@{ - host = $ServerHost - port = $ServerPort - scheme = $ServerScheme - } - paths = [pscustomobject]@{ - installRoot = $InstallRoot - stateRoot = $StateRoot - logsRoot = $LogsRoot - collectorScript = $CollectorScript - endpointCollectorScript = $EndpointCollectorScript - emailCollectorScript = $EmailCollectorScript - fileCollectorScript = $FileCollectorScript - sessionCollectorScript = $SessionCollectorScript - rulesPath = $RulesPath - policyPath = $PolicyPath - launchScript = $LaunchScriptPath - recoveryScript = $RecoveryScriptPath - } - collector = [pscustomobject]@{ - pollSeconds = $PollSeconds - pulseSeconds = $PulseSeconds - } - collectors = [pscustomobject]@{ - afkEnabled = $AfkEnabled - windowEnabled = $WindowEnabled - fileOpsEnabled = $FileOpsEnabled - emailEnabled = ($null -ne $EmailCollectorScript -and $EmailCollectorScript -ne '') - } - logging = [pscustomobject]@{ - localAgentLogsEnabled = $LocalAgentLogsEnabled - } - incidentCapture = [pscustomobject]@{ - enabled = $IncidentCaptureEnabled - screenshotEnabled = $IncidentScreenshotEnabled - artifactsRoot = $effectiveIncidentArtifactsRoot - } - sessionEvents = [pscustomobject]@{ - logonEnabled = $LogonMarkerEnabled - bucketPrefix = 'aw-session-events' - } - recovery = [pscustomobject]@{ - intervalSeconds = $RecoveryIntervalSeconds - taskName = 'ActivityWatch Recovery' - } - dlp = [pscustomobject]@{ - incidentBucketPrefix = 'aw-dlp-incidents' - enabled = $true - } - package = [pscustomobject]@{ - version = $PackageVersion - } - userTasks = @($UserTasks) - } -} - -function Write-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [pscustomobject]$Config, - [Parameter(Mandatory = $true)] - [string]$Path - ) - - $directory = Split-Path -Path $Path -Parent - if ($directory) { - New-ActivityWatchDirectory -Path $directory - } - - $json = $Config | ConvertTo-Json -Depth 8 - Set-Content -LiteralPath $Path -Value $json -Encoding UTF8 -} - -function Read-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [string]$Path - ) - - if (-not (Test-Path -LiteralPath $Path)) { - throw "Конфигурация развёртывания не найдена: $Path" - } - - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json -} - -function Write-ActivityWatchLaunchScript { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $content = @" -param( - [string]`$ConfigPath = '$ConfigPath' -) - -Set-StrictMode -Version Latest -`$ErrorActionPreference = 'Stop' - -[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -Add-Type -AssemblyName System.Net.Http -`$script:MaxCollectorPowerShellProcesses = 24 - -function Get-DeploymentConfig { - param([string]`$Path) - return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json -} - -function Test-ProcessInSession { - param( - [string]`$Name, - [int]`$SessionId - ) - - return [bool](Get-Process -Name `$Name -ErrorAction SilentlyContinue | Where-Object { `$_.SessionId -eq `$SessionId } | Select-Object -First 1) -} - -function Test-CollectorRunning { - param( - [string]`$ScriptPath, - [int]`$SessionId - ) - - `$escapedCollector = [Regex]::Escape(`$ScriptPath) - `$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - (`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and - `$_.SessionId -eq `$SessionId -and - `$_.CommandLine -match `$escapedCollector - } - - return [bool](`$processes | Select-Object -First 1) -} - -function Get-CollectorPowerShellProcessCount { - `$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - (`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and - `$_.CommandLine -match 'AWatch-rus' -and - `$_.CommandLine -match '\.ps1' - } - - return @(`$processes).Count -} - -function New-LaunchLock { - param([string]`$StateRoot, [int]`$SessionId) - - `$lockPath = Join-Path `$env:TEMP ("launch-watchers-session-{0}.lock" -f `$SessionId) - if (Test-Path -LiteralPath `$lockPath) { - try { - `$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json - `$existingPid = [int]`$lockData.pid - if (`$existingPid -gt 0 -and (Get-Process -Id `$existingPid -ErrorAction SilentlyContinue)) { - return `$null - } - } - catch { - } - } - - `$payload = @{ - pid = `$PID - sessionId = `$SessionId - createdAt = (Get-Date).ToUniversalTime().ToString('o') - } | ConvertTo-Json -Compress - Set-Content -LiteralPath `$lockPath -Value `$payload -Encoding UTF8 - return `$lockPath -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = `$true)][string]`$Uri, - [Parameter(Mandatory = `$true)][string]`$Json - ) - - `$httpClient = New-Object System.Net.Http.HttpClient - try { - `$content = New-Object System.Net.Http.StringContent(`$Json, [System.Text.Encoding]::UTF8, 'application/json') - `$response = `$httpClient.PostAsync(`$Uri, `$content).Result - if (-not `$response.IsSuccessStatusCode) { - return `$false - } - return `$true - } - catch { - return `$false - } - finally { - `$httpClient.Dispose() - } -} - -function Ensure-Bucket { - param( - [string]`$BucketId, - [string]`$ClientName, - [string]`$BucketType - ) - - if (`$script:KnownBuckets.ContainsKey(`$BucketId)) { - return - } - - try { - Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null - `$script:KnownBuckets[`$BucketId] = `$true - return - } - catch { - } - - `$body = @{ - client = `$ClientName - type = `$BucketType - hostname = `$script:Hostname - } | ConvertTo-Json -Compress - - try { - if (-not (Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" -Json `$body)) { - return - } - } - catch { - try { - Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null - } - catch { - return - } - } - - `$script:KnownBuckets[`$BucketId] = `$true -} - -function Send-LogonMarkerIfNeeded { - param( - [pscustomobject]`$Config, - [int]`$SessionId - ) - - `$sessionEvents = if (`$Config.PSObject.Properties.Name -contains 'sessionEvents') { `$Config.sessionEvents } else { `$null } - `$logging = if (`$Config.PSObject.Properties.Name -contains 'logging') { `$Config.logging } else { `$null } - `$logonEnabled = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'logonEnabled') { [bool]`$sessionEvents.logonEnabled } else { `$false } - if (-not `$logonEnabled) { - return - } - - `$bucketPrefix = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'bucketPrefix' -and -not [string]::IsNullOrWhiteSpace([string]`$sessionEvents.bucketPrefix)) { - [string]`$sessionEvents.bucketPrefix - } - else { - 'aw-session-events' - } - - `$stateRoot = [string]`$Config.paths.stateRoot - `$markerRoots = New-Object System.Collections.Generic.List[string] - if (-not [string]::IsNullOrWhiteSpace(`$env:LOCALAPPDATA)) { - `$markerRoots.Add((Join-Path `$env:LOCALAPPDATA 'AWatch-rus\markers')) - } - if (-not [string]::IsNullOrWhiteSpace(`$stateRoot)) { - `$markerRoots.Add((Join-Path `$stateRoot 'markers')) - } - - `$markerDir = `$null - foreach (`$candidate in `$markerRoots) { - try { - if (-not (Test-Path -LiteralPath `$candidate)) { - New-Item -Path `$candidate -ItemType Directory -Force | Out-Null - } - - `$probePath = Join-Path `$candidate 'write-test.tmp' - Set-Content -LiteralPath `$probePath -Value 'ok' -Encoding ASCII - Remove-Item -LiteralPath `$probePath -Force -ErrorAction SilentlyContinue - `$markerDir = `$candidate - break - } - catch { - } - } - - if (-not `$markerDir) { - return - } - - `$markerFile = Join-Path `$markerDir ("logon-{0}-{1}.marker" -f `$env:USERNAME, `$SessionId) - if (Test-Path -LiteralPath `$markerFile) { - return - } - - Set-Content -LiteralPath `$markerFile -Value ((Get-Date).ToUniversalTime().ToString('o')) -Encoding UTF8 - - `$bucketId = ('{0}_{1}' -f `$bucketPrefix, `$script:Hostname) - Ensure-Bucket -BucketId `$bucketId -ClientName 'aw-session-events' -BucketType 'aw.session.event' - - `$payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - eventType = 'logon' - username = `$env:USERNAME - userId = "`$(`$env:USERDOMAIN)\`$(`$env:USERNAME)" - sessionId = `$SessionId - hostname = `$script:Hostname - source = 'launch-watchers-awatch-rus' - } - } | ConvertTo-Json -Depth 5 -Compress - - try { - Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$bucketId/heartbeat?pulsetime=1" -Json `$payload - } - catch { - Remove-Item -LiteralPath `$markerFile -Force -ErrorAction SilentlyContinue - throw - } -} - -function Start-CollectorScriptIfNeeded { - param( - [string]`$ScriptPath, - [string]`$ConfigPath, - [string]`$PowerShellExe, - [int]`$SessionId - ) - - if ([string]::IsNullOrWhiteSpace(`$ScriptPath)) { - return - } - - if (-not (Test-Path -LiteralPath `$ScriptPath)) { - return - } - - if (Test-CollectorRunning -ScriptPath `$ScriptPath -SessionId `$SessionId) { - return - } - - if ((Get-CollectorPowerShellProcessCount) -ge `$script:MaxCollectorPowerShellProcesses) { - return - } - - `$staParam = if (`$ScriptPath -like "*endpoint-signals*") { "-STA" } else { `$null } - `$argumentList = @('-NoProfile', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass') - if (`$staParam) { `$argumentList += `$staParam } - `$argumentList += @('-File', `$ScriptPath, '-ConfigPath', `$ConfigPath) - Start-Process -FilePath `$PowerShellExe -ArgumentList `$argumentList -WindowStyle Hidden -} - -`$config = Get-DeploymentConfig -Path `$ConfigPath -`$sessionId = (Get-Process -Id `$PID).SessionId -`$installRoot = [string]`$config.paths.installRoot -`$stateRoot = [string]`$config.paths.stateRoot -`$script:ApiBase = '{0}://{1}:{2}/api/0' -f [string]`$config.server.scheme, [string]`$config.server.host, [string]`$config.server.port -`$script:Hostname = `$env:COMPUTERNAME -`$script:KnownBuckets = @{} -`$collectorScript = [string]`$config.paths.collectorScript -`$endpointCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]`$config.paths.endpointCollectorScript } else { Join-Path `$stateRoot 'dlp-endpoint-signals-collector.ps1' } -`$fileCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'fileCollectorScript') { [string]`$config.paths.fileCollectorScript } else { Join-Path `$stateRoot 'file-operations-collector.ps1' } -`$sessionCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]`$config.paths.sessionCollectorScript } else { Join-Path `$stateRoot 'worktime-session-collector.ps1' } -`$afkExe = Join-Path `$installRoot 'aw-watcher-afk\aw-watcher-afk.exe' -`$windowExe = Join-Path `$installRoot 'aw-watcher-window\aw-watcher-window.exe' -`$serverArgs = @('--host', [string]`$config.server.host, '--port', [string]`$config.server.port) -`$powershellExe = Join-Path `$env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' -`$afkEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]`$config.collectors.afkEnabled } else { `$true } -`$windowEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]`$config.collectors.windowEnabled } else { `$true } -`$fileOpsEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { [bool]`$config.collectors.fileOpsEnabled } else { `$true } -`$emailEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'emailEnabled') { [bool]`$config.collectors.emailEnabled } else { `$false } -`$emailCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'emailCollectorScript') { [string]`$config.paths.emailCollectorScript } else { Join-Path `$stateRoot 'email-outbound-collector.ps1' } -`$launchLockPath = New-LaunchLock -StateRoot `$stateRoot -SessionId `$sessionId -if (-not `$launchLockPath) { - return -} - -try { - if (`$afkEnabled -and -not (Test-Path -LiteralPath `$afkExe)) { - throw "Не найден aw-watcher-afk.exe: `$afkExe" - } - - if (`$windowEnabled -and -not (Test-Path -LiteralPath `$windowExe)) { - throw "Не найден aw-watcher-window.exe: `$windowExe" - } - - if (`$afkEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-afk' -SessionId `$sessionId)) { - Start-Process -FilePath `$afkExe -ArgumentList `$serverArgs -WindowStyle Hidden - } - - if (`$windowEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-window' -SessionId `$sessionId)) { - Start-Process -FilePath `$windowExe -ArgumentList `$serverArgs -WindowStyle Hidden - } - - try { - Send-LogonMarkerIfNeeded -Config `$config -SessionId `$sessionId - } - catch { - } - Start-CollectorScriptIfNeeded -ScriptPath `$collectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - Start-CollectorScriptIfNeeded -ScriptPath `$endpointCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - if (`$fileOpsEnabled) { - Start-CollectorScriptIfNeeded -ScriptPath `$fileCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - } - Start-CollectorScriptIfNeeded -ScriptPath `$sessionCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - if (`$emailEnabled -and (Test-Path -LiteralPath `$emailCollectorScript)) { - Start-CollectorScriptIfNeeded -ScriptPath `$emailCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - } -} -finally { - if (`$launchLockPath -and (Test-Path -LiteralPath `$launchLockPath)) { - Remove-Item -LiteralPath `$launchLockPath -Force -ErrorAction SilentlyContinue - } -} -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding UTF8 -} - -function Write-ActivityWatchRecoveryScript { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $content = @" -param( - [string]`$ConfigPath = '$ConfigPath' -) - -Set-StrictMode -Version Latest -`$ErrorActionPreference = 'Continue' - -function Get-DeploymentConfig { - param([string]`$Path) - return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json -} - -function Get-RecoveryConfigPaths { - param([string]`$PrimaryConfigPath) - - `$paths = New-Object System.Collections.Generic.List[string] - if (`$PrimaryConfigPath -and (Test-Path -LiteralPath `$PrimaryConfigPath)) { - `$paths.Add((Resolve-Path -LiteralPath `$PrimaryConfigPath).Path) - } - - `$searchRoot = `$env:ProgramData - if (`$PrimaryConfigPath) { - `$stateRoot = Split-Path -Path `$PrimaryConfigPath -Parent - `$candidateRoot = Split-Path -Path `$stateRoot -Parent - if (`$candidateRoot -and (Test-Path -LiteralPath `$candidateRoot)) { - `$searchRoot = `$candidateRoot - } - } - - if (Test-Path -LiteralPath `$searchRoot) { - Get-ChildItem -LiteralPath `$searchRoot -Directory -ErrorAction SilentlyContinue | - Where-Object { `$_.Name -like 'ActivityWatch*' } | - ForEach-Object { - `$candidate = Join-Path `$_.FullName 'deployment-config.json' - if (Test-Path -LiteralPath `$candidate) { - `$paths.Add(`$candidate) - } - } - } - - return @(`$paths | Sort-Object -Unique) -} - -function Get-RecoveryTaskNames { - param([string[]]`$ConfigPaths) - - `$taskNames = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) - foreach (`$candidatePath in @(`$ConfigPaths)) { - try { - `$config = Get-DeploymentConfig -Path `$candidatePath - foreach (`$task in @(`$config.userTasks)) { - `$taskName = [string]`$task.launchTaskName - if (-not [string]::IsNullOrWhiteSpace(`$taskName)) { - [void]`$taskNames.Add(`$taskName) - } - } - } - catch { - } - } - - return @(`$taskNames) -} - -function New-RecoveryLock { - param([string]`$PrimaryConfigPath) - - `$stateRoot = if (`$PrimaryConfigPath) { Split-Path -Path `$PrimaryConfigPath -Parent } else { Join-Path `$env:ProgramData 'AWatch-rus' } - if (-not (Test-Path -LiteralPath `$stateRoot)) { - New-Item -Path `$stateRoot -ItemType Directory -Force | Out-Null - } - - `$lockPath = Join-Path `$stateRoot 'recovery-loop.lock' - if (Test-Path -LiteralPath `$lockPath) { - try { - `$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json - `$existingPid = [int]`$lockData.pid - if (`$existingPid -gt 0 -and (Get-Process -Id `$existingPid -ErrorAction SilentlyContinue)) { - return `$null - } - } - catch { - } - } - - `$payload = @{ - pid = `$PID - createdAt = (Get-Date).ToUniversalTime().ToString('o') - } | ConvertTo-Json -Compress - Set-Content -LiteralPath `$lockPath -Value `$payload -Encoding UTF8 - return `$lockPath -} - -function Start-TaskIfNotRunning { - param([string]`$TaskName) - if ([string]::IsNullOrWhiteSpace(`$TaskName)) { - return - } - - try { - `$task = Get-ScheduledTask -TaskName `$TaskName -ErrorAction SilentlyContinue - if (-not `$task) { - return - } - if ([string]`$task.State -eq 'Running') { - return - } - Start-ScheduledTask -TaskName `$TaskName -ErrorAction SilentlyContinue - } - catch { - } -} - -`$recoveryLockPath = New-RecoveryLock -PrimaryConfigPath `$ConfigPath -if (-not `$recoveryLockPath) { - return -} - -try { - while (`$true) { - `$sleepSeconds = 180 - try { - `$configPaths = Get-RecoveryConfigPaths -PrimaryConfigPath `$ConfigPath - foreach (`$taskName in Get-RecoveryTaskNames -ConfigPaths `$configPaths) { - Start-TaskIfNotRunning -TaskName `$taskName - } - - `$config = Get-DeploymentConfig -Path `$ConfigPath - if (`$config -and `$config.recovery -and `$config.recovery.intervalSeconds) { - `$sleepSeconds = [Math]::Max([int]`$config.recovery.intervalSeconds, 30) - } - } - catch { - } - - Start-Sleep -Seconds `$sleepSeconds - } -} -finally { - if (`$recoveryLockPath -and (Test-Path -LiteralPath `$recoveryLockPath)) { - Remove-Item -LiteralPath `$recoveryLockPath -Force -ErrorAction SilentlyContinue - } -} -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding UTF8 -} - -function Get-ActivityWatchHiddenLauncherPath { - param( - [Parameter(Mandatory = $true)] - [string]$ScriptPath - ) - - $directory = Split-Path -Path $ScriptPath -Parent - $baseName = [IO.Path]::GetFileNameWithoutExtension($ScriptPath) - return Join-Path $directory ("{0}-hidden.vbs" -f $baseName) -} - -function Write-ActivityWatchHiddenPowerShellWrapper { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $directory = Split-Path -Path $Path -Parent - if ($directory) { - New-ActivityWatchDirectory -Path $directory - } - - $powershellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' - $escapedPowerShellExe = $powershellExe.Replace('"', '""') - $escapedScriptPath = $ScriptPath.Replace('"', '""') - $escapedConfigPath = $ConfigPath.Replace('"', '""') - - $content = @" -Set shell = CreateObject("WScript.Shell") -shell.Run """$escapedPowerShellExe"" -NoProfile -ExecutionPolicy Bypass -File ""$escapedScriptPath"" -ConfigPath ""$escapedConfigPath""", 0, False -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding ASCII -} - -function Remove-LegacyActivityWatchEntries { - $legacyTaskNames = @( - 'ActivityWatch Watchers', - 'ActivityWatch Guard', - 'ActivityWatch Heal' - ) - - foreach ($taskName in $legacyTaskNames) { - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue - } - - $runKey = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run' - foreach ($name in 'ActivityWatchAFK', 'ActivityWatchWindow', 'ActivityWatchBrowserCollector') { - Remove-ItemProperty -Path $runKey -Name $name -ErrorAction SilentlyContinue - } -} - -function Remove-ActivityWatchScheduledTask { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName - ) - - Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue - & cmd.exe /c "schtasks /Delete /TN `"$TaskName`" /F >nul 2>&1" | Out-Null - - for ($attempt = 0; $attempt -lt 10; $attempt++) { - $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue - if (-not $task) { - return - } - - Start-Sleep -Milliseconds 300 - } -} - -function Set-ActivityWatchScheduledTaskAction { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [Parameter(Mandatory = $true)] - [string]$Execute, - [Parameter(Mandatory = $true)] - [string]$Arguments - ) - - $taskCommand = ('"{0}" {1}' -f $Execute, $Arguments) - & schtasks.exe /Change /TN $TaskName /TR $taskCommand | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "schtasks.exe /Change завершился с ошибкой для $TaskName" - } -} - -function Get-ActivityWatchScheduledTaskByCommand { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [string]$CommandMatch - ) - - $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue - if ($task) { - return $task - } - - if ([string]::IsNullOrWhiteSpace($CommandMatch)) { - return $null - } - - foreach ($candidate in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch*' })) { - foreach ($action in @($candidate.Actions)) { - if ([string]$action.Arguments -like "*$CommandMatch*") { - return $candidate - } - } - } - - return $null -} - -function Remove-StaleActivityWatchUserTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath - ) - - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath - $desiredTaskNames = @($TaskDefinitions | ForEach-Object { [string]$_.LaunchTaskName }) - - foreach ($candidate in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch*' })) { - $taskName = [string]$candidate.TaskName - if ($desiredTaskNames -contains $taskName) { - continue - } - - $usesCurrentLauncher = $false - foreach ($action in @($candidate.Actions)) { - if ([string]$action.Arguments -like "*$launcherPath*") { - $usesCurrentLauncher = $true - break - } - } - - if ($usesCurrentLauncher) { - Remove-ActivityWatchScheduledTask -TaskName $taskName - } - } -} - -function Register-ActivityWatchUserTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe' - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath - Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $LaunchScriptPath -ConfigPath $ConfigPath - Remove-StaleActivityWatchUserTasks -TaskDefinitions $TaskDefinitions -LaunchScriptPath $LaunchScriptPath - - foreach ($definition in $TaskDefinitions) { - $action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`"" - $trigger = New-ScheduledTaskTrigger -AtLogOn -User $definition.UserId - $principal = New-ScheduledTaskPrincipal -UserId $definition.UserId -LogonType Interactive -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0) - $existingTask = Get-ActivityWatchScheduledTaskByCommand -TaskName $definition.LaunchTaskName -CommandMatch $ConfigPath - - if ($existingTask) { - Set-ActivityWatchScheduledTaskAction -TaskName $existingTask.TaskName -Execute $wscriptExe -Arguments $action.Arguments - continue - } - - Remove-ActivityWatchScheduledTask -TaskName $definition.LaunchTaskName - Register-ScheduledTask -TaskName $definition.LaunchTaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null - } -} - -function Register-ActivityWatchRecoveryTask { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [Parameter(Mandatory = $true)] - [string]$RecoveryScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - Remove-ActivityWatchScheduledTask -TaskName $TaskName - - $wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe' - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $RecoveryScriptPath - Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $RecoveryScriptPath -ConfigPath $ConfigPath - $action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`"" - $trigger = New-ScheduledTaskTrigger -AtStartup - $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -Hidden -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0) - - Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null -} - -function Set-ActivityWatchAcl { - param( - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [Parameter(Mandatory = $true)] - [string]$LogsRoot - ) - - foreach ($path in $InstallRoot, $StateRoot, $LogsRoot) { - New-ActivityWatchDirectory -Path $path - } - - & icacls $InstallRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $InstallRoot" - } - - & icacls $StateRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $StateRoot" - } - - & icacls $LogsRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(M)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $LogsRoot" - } -} - -function Start-ActivityWatchTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [string]$RecoveryTaskName = 'ActivityWatch Recovery' - ) - - foreach ($definition in $TaskDefinitions) { - Start-ScheduledTask -TaskName $definition.LaunchTaskName -ErrorAction SilentlyContinue - } - - Start-ScheduledTask -TaskName $RecoveryTaskName -ErrorAction SilentlyContinue -} - -Export-ModuleMember -Function *-ActivityWatch*, Assert-Administrator, Normalize-ActivityWatchUsers, Get-ActivityWatchPackageUrl, Remove-LegacyActivityWatchEntries -; } - } - - if (-not `$markerDir) { - return - } - - `$markerFile = Join-Path `$markerDir ("logon-{0}-{1}.marker" -f `$env:USERNAME, `$SessionId) - if (Test-Path -LiteralPath `$markerFile) { - return - } - - Set-Content -LiteralPath `$markerFile -Value ((Get-Date).ToUniversalTime().ToString('o')) -Encoding UTF8 - - `$bucketId = ('{0}_{1}' -f `$bucketPrefix, `$script:Hostname) - Ensure-Bucket -BucketId `$bucketId -ClientName 'aw-session-events' -BucketType 'aw.session.event' - - `$payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - eventType = 'logon' - username = `$env:USERNAME - userId = "`$(`$env:USERDOMAIN)\`$(`$env:USERNAME)" - sessionId = `$SessionId - hostname = `$script:Hostname - source = 'launch-watchers-awatch-rus' - } - } | ConvertTo-Json -Depth 5 -Compress - - try { - Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$bucketId/heartbeat?pulsetime=1" -Json `$payload - } - catch { - Remove-Item -LiteralPath `$markerFile -Force -ErrorAction SilentlyContinue - throw - } -} - -function Start-CollectorScriptIfNeeded { - param( - [string]`$ScriptPath, - [string]`$ConfigPath, - [string]`$PowerShellExe, - [int]`$SessionId - ) - - if ([string]::IsNullOrWhiteSpace(`$ScriptPath)) { - return - } - - if (-not (Test-Path -LiteralPath `$ScriptPath)) { - return - } - - if (Test-CollectorRunning -ScriptPath `$ScriptPath -SessionId `$SessionId) { - return - } - - if ((Get-CollectorPowerShellProcessCount) -ge `$script:MaxCollectorPowerShellProcesses) { - return - } - - `$staParam = if (`$ScriptPath -like "*endpoint-signals*") { "-STA" } else { `$null } - `$argumentList = @('-NoProfile', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass') - if (`$staParam) { `$argumentList += `$staParam } - `$argumentList += @('-File', `$ScriptPath, '-ConfigPath', `$ConfigPath) - Start-Process -FilePath `$PowerShellExe -ArgumentList `$argumentList -WindowStyle Hidden -} - -`$config = Get-DeploymentConfig -Path `$ConfigPath -`$sessionId = (Get-Process -Id `$PID).SessionId -`$installRoot = [string]`$config.paths.installRoot -`$stateRoot = [string]`$config.paths.stateRoot -`$script:ApiBase = '{0}://{1}:{2}/api/0' -f [string]`$config.server.scheme, [string]`$config.server.host, [string]`$config.server.port -`$script:Hostname = `$env:COMPUTERNAME -`$script:KnownBuckets = @{} -`$collectorScript = [string]`$config.paths.collectorScript -`$endpointCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]`$config.paths.endpointCollectorScript } else { Join-Path `$stateRoot 'dlp-endpoint-signals-collector.ps1' } -`$fileCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'fileCollectorScript') { [string]`$config.paths.fileCollectorScript } else { Join-Path `$stateRoot 'file-operations-collector.ps1' } -`$sessionCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]`$config.paths.sessionCollectorScript } else { Join-Path `$stateRoot 'worktime-session-collector.ps1' } -`$afkExe = Join-Path `$installRoot 'aw-watcher-afk\aw-watcher-afk.exe' -`$windowExe = Join-Path `$installRoot 'aw-watcher-window\aw-watcher-window.exe' -`$serverArgs = @('--host', [string]`$config.server.host, '--port', [string]`$config.server.port) -`$powershellExe = Join-Path `$env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' -`$afkEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]`$config.collectors.afkEnabled } else { `$true } -`$windowEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]`$config.collectors.windowEnabled } else { `$true } -`$fileOpsEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { [bool]`$config.collectors.fileOpsEnabled } else { `$true } -`$emailEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'emailEnabled') { [bool]`$config.collectors.emailEnabled } else { `$false } -`$emailCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'emailCollectorScript') { [string]`$config.paths.emailCollectorScript } else { Join-Path `$stateRoot 'email-outbound-collector.ps1' } -`$launchLockPath = New-LaunchLock -StateRoot `$stateRoot -SessionId `$sessionId -if (-not `$launchLockPath) { - return -} - -try { - if (`$afkEnabled -and -not (Test-Path -LiteralPath `$afkExe)) { - throw "Не найден aw-watcher-afk.exe: `$afkExe" - } - - if (`$windowEnabled -and -not (Test-Path -LiteralPath `$windowExe)) { - throw "Не найден aw-watcher-window.exe: `$windowExe" - } - - if (`$afkEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-afk' -SessionId `$sessionId)) { - Start-Process -FilePath `$afkExe -ArgumentList `$serverArgs -WindowStyle Hidden - } - - if (`$windowEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-window' -SessionId `$sessionId)) { - Start-Process -FilePath `$windowExe -ArgumentList `$serverArgs -WindowStyle Hidden - } - - try { - Send-LogonMarkerIfNeeded -Config `$config -SessionId `$sessionId - } - catch { Write-Error Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Assert-Administrator { - $identity = [Security.Principal.WindowsIdentity]::GetCurrent() - $principal = [Security.Principal.WindowsPrincipal]::new($identity) - if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - throw 'Запустите этот скрипт из PowerShell с правами администратора.' - } -} - -function New-ActivityWatchDirectory { - param( - [Parameter(Mandatory = $true)] - [string]$Path - ) - - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Enable-ActivityWatchPrintTelemetry { - $policyPath = 'HKLM:\Software\Policies\Microsoft\Windows NT\Printers' - if (-not (Test-Path -LiteralPath $policyPath)) { - New-Item -Path $policyPath -Force | Out-Null - } - New-ItemProperty -Path $policyPath -Name 'ShowJobTitleInEventLogs' -Value 1 -PropertyType DWord -Force | Out-Null - - & wevtutil.exe sl 'Microsoft-Windows-PrintService/Operational' /e:true | Out-Null -} - - -function Get-ActivityWatchPackageUrl { - param( - [string]$Version = 'v0.13.2' - ) - - return "https://github.com/ActivityWatch/activitywatch/releases/download/$Version/activitywatch-$Version-windows-x86_64.zip" -} - -function Get-ActivityWatchArchive { - param( - [string]$PackageZipPath, - [string]$PackageUrl, - [string]$Version = 'v0.13.2', - [Parameter(Mandatory = $true)] - [string]$WorkingRoot - ) - - New-ActivityWatchDirectory -Path $WorkingRoot - - if ($PackageZipPath) { - $resolved = Resolve-Path -LiteralPath $PackageZipPath -ErrorAction Stop - return $resolved.Path - } - - if (-not $PackageUrl) { - $PackageUrl = Get-ActivityWatchPackageUrl -Version $Version - } - - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' - $suffix = ([guid]::NewGuid().Guid.Substring(0, 8)) - $archivePath = Join-Path $WorkingRoot ("activitywatch-{0}-{1}-{2}.zip" -f $Version.TrimStart('v'), $stamp, $suffix) - Invoke-WebRequest -Uri $PackageUrl -OutFile $archivePath - return $archivePath -} - -function Get-ActivityWatchPackageRoot { - param( - [Parameter(Mandatory = $true)] - [string]$ExpandedRoot - ) - - $afkBinary = Get-ChildItem -Path $ExpandedRoot -Filter 'aw-watcher-afk.exe' -File -Recurse | - Select-Object -First 1 - - if (-not $afkBinary) { - throw "Не удалось найти aw-watcher-afk.exe в $ExpandedRoot." - } - - return (Split-Path -Path (Split-Path -Path $afkBinary.FullName -Parent) -Parent) -} - -function Install-ActivityWatchPackage { - param( - [Parameter(Mandatory = $true)] - [string]$ArchivePath, - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$WorkingRoot, - [Parameter(Mandatory = $true)] - [string]$BackupRoot - ) - - New-ActivityWatchDirectory -Path $WorkingRoot - New-ActivityWatchDirectory -Path $BackupRoot - - # Ensure nothing is holding locks inside InstallRoot during upgrade. - foreach ($procName in @('aw-watcher-afk', 'aw-watcher-window', 'aw-server', 'aw-qt')) { - try { - Get-Process -Name $procName -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue - } - catch { - } - } - Start-Sleep -Seconds 2 - - $extractRoot = Join-Path $WorkingRoot ('extract-' + [guid]::NewGuid().Guid) - if (Test-Path -LiteralPath $extractRoot) { - Remove-Item -LiteralPath $extractRoot -Recurse -Force - } - New-ActivityWatchDirectory -Path $extractRoot - - Expand-Archive -Path $ArchivePath -DestinationPath $extractRoot -Force - $packageRoot = Get-ActivityWatchPackageRoot -ExpandedRoot $extractRoot - - if (Test-Path -LiteralPath $InstallRoot) { - $existingItems = Get-ChildItem -LiteralPath $InstallRoot -Force -ErrorAction SilentlyContinue - if ($existingItems) { - $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' - $backupPath = Join-Path $BackupRoot ("install-$stamp") - New-ActivityWatchDirectory -Path $backupPath - Copy-Item -Path (Join-Path $InstallRoot '*') -Destination $backupPath -Recurse -Force - Get-ChildItem -LiteralPath $InstallRoot -Force | Remove-Item -Recurse -Force - } - } - else { - New-ActivityWatchDirectory -Path $InstallRoot - } - - Copy-Item -Path (Join-Path $packageRoot '*') -Destination $InstallRoot -Recurse -Force - - return [pscustomobject]@{ - PackageRoot = $packageRoot - ExtractRoot = $extractRoot - BackupRoot = $BackupRoot - } -} - -function Get-ActivityWatchExecutableMap { - param( - [Parameter(Mandatory = $true)] - [string]$InstallRoot - ) - - $map = [ordered]@{ - Afk = Join-Path $InstallRoot 'aw-watcher-afk\aw-watcher-afk.exe' - Window = Join-Path $InstallRoot 'aw-watcher-window\aw-watcher-window.exe' - } - - foreach ($entry in $map.GetEnumerator()) { - if (-not (Test-Path -LiteralPath $entry.Value)) { - throw "Не найден обязательный исполняемый файл ActivityWatch: $($entry.Value)" - } - } - - return [pscustomobject]$map -} - -function Normalize-ActivityWatchUsers { - param( - [string[]]$Users, - [string]$UserListPath, - [string]$Domain - ) - - $collected = New-Object System.Collections.Generic.List[string] - - if ($Users) { - foreach ($user in $Users) { - if (-not [string]::IsNullOrWhiteSpace($user)) { - $collected.Add($user.Trim()) - } - } - } - - if ($UserListPath) { - $resolved = Resolve-Path -LiteralPath $UserListPath -ErrorAction Stop - $extension = [IO.Path]::GetExtension($resolved.Path) - if ($extension -ieq '.csv') { - $rows = Import-Csv -LiteralPath $resolved.Path - foreach ($row in $rows) { - foreach ($column in 'User', 'Username', 'SamAccountName', 'Login') { - if ($row.PSObject.Properties.Name -contains $column) { - $value = [string]$row.$column - if (-not [string]::IsNullOrWhiteSpace($value)) { - $collected.Add($value.Trim()) - break - } - } - } - } - } - else { - Get-Content -LiteralPath $resolved.Path | ForEach-Object { - $line = $_.Trim() - if ($line -and -not $line.StartsWith('#')) { - $collected.Add($line) - } - } - } - } - - $normalized = $collected | - Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | - ForEach-Object { - if ($Domain -and ($_ -notmatch '[\\@]')) { - '{0}\{1}' -f $Domain, $_ - } - else { - $_ - } - } | - Sort-Object -Unique - - if (-not $normalized -or $normalized.Count -eq 0) { - throw 'Не удалось определить целевых пользователей. Укажите -Users или -UserListPath.' - } - - return @($normalized) -} - -function Get-ActivityWatchTaskNameToken { - param( - [Parameter(Mandatory = $true)] - [string]$UserId - ) - - $buffer = [Text.StringBuilder]::new() - foreach ($character in $UserId.ToCharArray()) { - if ([char]::IsLetterOrDigit($character)) { - [void]$buffer.Append($character) - } - else { - [void]$buffer.Append('_') - } - } - - return $buffer.ToString().Trim('_') -} - -function New-ActivityWatchUserTaskDefinitions { - param( - [Parameter(Mandatory = $true)] - [string[]]$Users - ) - - $result = foreach ($user in $Users) { - $token = Get-ActivityWatchTaskNameToken -UserId $user - [pscustomobject]@{ - UserId = $user - LaunchTaskName = "ActivityWatch Launch [$token]" - } - } - - return @($result) -} - -function Copy-ActivityWatchCollectorAssets { - param( - [Parameter(Mandatory = $true)] - [string]$CollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$EndpointCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$FileCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$SessionCollectorScriptSource, - [string]$EmailCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$ExampleRulesSource, - [Parameter(Mandatory = $true)] - [string]$ExamplePolicySource, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [string]$CustomRulesSource, - [string]$CustomPolicySource - ) - - New-ActivityWatchDirectory -Path $StateRoot - - $collectorTarget = Join-Path $StateRoot 'browser-domains-native-collector.ps1' - $endpointCollectorTarget = Join-Path $StateRoot 'dlp-endpoint-signals-collector.ps1' - $fileCollectorTarget = Join-Path $StateRoot 'file-operations-collector.ps1' - $sessionCollectorTarget = Join-Path $StateRoot 'worktime-session-collector.ps1' - $emailCollectorTarget = Join-Path $StateRoot 'email-outbound-collector.ps1' - $exampleRulesTarget = Join-Path $StateRoot 'web-category-rules.example.json' - $rulesTarget = Join-Path $StateRoot 'web-category-rules.json' - $examplePolicyTarget = Join-Path $StateRoot 'dlp-policy.example.json' - $policyTarget = Join-Path $StateRoot 'dlp-policy.json' - - Copy-Item -LiteralPath $CollectorScriptSource -Destination $collectorTarget -Force - Copy-Item -LiteralPath $EndpointCollectorScriptSource -Destination $endpointCollectorTarget -Force - Copy-Item -LiteralPath $FileCollectorScriptSource -Destination $fileCollectorTarget -Force - Copy-Item -LiteralPath $SessionCollectorScriptSource -Destination $sessionCollectorTarget -Force - if ($EmailCollectorScriptSource -and (Test-Path -LiteralPath $EmailCollectorScriptSource)) { - Copy-Item -LiteralPath $EmailCollectorScriptSource -Destination $emailCollectorTarget -Force - } - Copy-Item -LiteralPath $ExampleRulesSource -Destination $exampleRulesTarget -Force - Copy-Item -LiteralPath $ExamplePolicySource -Destination $examplePolicyTarget -Force - - if ($CustomRulesSource) { - $resolvedRules = Resolve-Path -LiteralPath $CustomRulesSource -ErrorAction Stop - Copy-Item -LiteralPath $resolvedRules.Path -Destination $rulesTarget -Force - } - else { - Copy-Item -LiteralPath $exampleRulesTarget -Destination $rulesTarget -Force - } - - if ($CustomPolicySource) { - $resolvedPolicy = Resolve-Path -LiteralPath $CustomPolicySource -ErrorAction Stop - Copy-Item -LiteralPath $resolvedPolicy.Path -Destination $policyTarget -Force - } - else { - Copy-Item -LiteralPath $examplePolicyTarget -Destination $policyTarget -Force - } - - return [pscustomobject]@{ - CollectorScript = $collectorTarget - EndpointCollectorScript = $endpointCollectorTarget - FileCollectorScript = $fileCollectorTarget - SessionCollectorScript = $sessionCollectorTarget - EmailCollectorScript = $emailCollectorTarget - ExampleRules = $exampleRulesTarget - ActiveRules = $rulesTarget - ExamplePolicy = $examplePolicyTarget - ActivePolicy = $policyTarget - } -} - -function New-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [string]$ServerHost, - [Parameter(Mandatory = $true)] - [int]$ServerPort, - [Parameter(Mandatory = $true)] - [string]$ServerScheme, - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [Parameter(Mandatory = $true)] - [string]$LogsRoot, - [Parameter(Mandatory = $true)] - [string]$CollectorScript, - [Parameter(Mandatory = $true)] - [string]$EndpointCollectorScript, - [Parameter(Mandatory = $true)] - [string]$FileCollectorScript, - [Parameter(Mandatory = $true)] - [string]$SessionCollectorScript, - [string]$EmailCollectorScript, - [Parameter(Mandatory = $true)] - [string]$RulesPath, - [Parameter(Mandatory = $true)] - [string]$PolicyPath, - [Parameter(Mandatory = $true)] - [int]$PollSeconds, - [Parameter(Mandatory = $true)] - [int]$PulseSeconds, - [Parameter(Mandatory = $true)] - [int]$RecoveryIntervalSeconds, - [bool]$AfkEnabled = $true, - [bool]$WindowEnabled = $true, - [bool]$FileOpsEnabled = $true, - [bool]$LocalAgentLogsEnabled = $true, - [bool]$IncidentCaptureEnabled = $true, - [bool]$IncidentScreenshotEnabled = $true, - [string]$IncidentArtifactsRoot, - [bool]$LogonMarkerEnabled = $true, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath, - [Parameter(Mandatory = $true)] - [string]$RecoveryScriptPath, - [Parameter(Mandatory = $true)] - [pscustomobject[]]$UserTasks, - [string]$PackageVersion = 'v0.13.2' - ) - - $effectiveIncidentArtifactsRoot = if ($IncidentArtifactsRoot) { $IncidentArtifactsRoot } else { Join-Path $StateRoot 'incident-artifacts' } - - return [pscustomobject]@{ - version = 1 - generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') - server = [pscustomobject]@{ - host = $ServerHost - port = $ServerPort - scheme = $ServerScheme - } - paths = [pscustomobject]@{ - installRoot = $InstallRoot - stateRoot = $StateRoot - logsRoot = $LogsRoot - collectorScript = $CollectorScript - endpointCollectorScript = $EndpointCollectorScript - emailCollectorScript = $EmailCollectorScript - fileCollectorScript = $FileCollectorScript - sessionCollectorScript = $SessionCollectorScript - rulesPath = $RulesPath - policyPath = $PolicyPath - launchScript = $LaunchScriptPath - recoveryScript = $RecoveryScriptPath - } - collector = [pscustomobject]@{ - pollSeconds = $PollSeconds - pulseSeconds = $PulseSeconds - } - collectors = [pscustomobject]@{ - afkEnabled = $AfkEnabled - windowEnabled = $WindowEnabled - fileOpsEnabled = $FileOpsEnabled - emailEnabled = ($null -ne $EmailCollectorScript -and $EmailCollectorScript -ne '') - } - logging = [pscustomobject]@{ - localAgentLogsEnabled = $LocalAgentLogsEnabled - } - incidentCapture = [pscustomobject]@{ - enabled = $IncidentCaptureEnabled - screenshotEnabled = $IncidentScreenshotEnabled - artifactsRoot = $effectiveIncidentArtifactsRoot - } - sessionEvents = [pscustomobject]@{ - logonEnabled = $LogonMarkerEnabled - bucketPrefix = 'aw-session-events' - } - recovery = [pscustomobject]@{ - intervalSeconds = $RecoveryIntervalSeconds - taskName = 'ActivityWatch Recovery' - } - dlp = [pscustomobject]@{ - incidentBucketPrefix = 'aw-dlp-incidents' - enabled = $true - } - package = [pscustomobject]@{ - version = $PackageVersion - } - userTasks = @($UserTasks) - } -} - -function Write-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [pscustomobject]$Config, - [Parameter(Mandatory = $true)] - [string]$Path - ) - - $directory = Split-Path -Path $Path -Parent - if ($directory) { - New-ActivityWatchDirectory -Path $directory - } - - $json = $Config | ConvertTo-Json -Depth 8 - Set-Content -LiteralPath $Path -Value $json -Encoding UTF8 -} - -function Read-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [string]$Path - ) - - if (-not (Test-Path -LiteralPath $Path)) { - throw "Конфигурация развёртывания не найдена: $Path" - } - - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json -} - -function Write-ActivityWatchLaunchScript { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $content = @" -param( - [string]`$ConfigPath = '$ConfigPath' -) - -Set-StrictMode -Version Latest -`$ErrorActionPreference = 'Stop' - -[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -Add-Type -AssemblyName System.Net.Http -`$script:MaxCollectorPowerShellProcesses = 24 - -function Get-DeploymentConfig { - param([string]`$Path) - return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json -} - -function Test-ProcessInSession { - param( - [string]`$Name, - [int]`$SessionId - ) - - return [bool](Get-Process -Name `$Name -ErrorAction SilentlyContinue | Where-Object { `$_.SessionId -eq `$SessionId } | Select-Object -First 1) -} - -function Test-CollectorRunning { - param( - [string]`$ScriptPath, - [int]`$SessionId - ) - - `$escapedCollector = [Regex]::Escape(`$ScriptPath) - `$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - (`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and - `$_.SessionId -eq `$SessionId -and - `$_.CommandLine -match `$escapedCollector - } - - return [bool](`$processes | Select-Object -First 1) -} - -function Get-CollectorPowerShellProcessCount { - `$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - (`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and - `$_.CommandLine -match 'AWatch-rus' -and - `$_.CommandLine -match '\.ps1' - } - - return @(`$processes).Count -} - -function New-LaunchLock { - param([string]`$StateRoot, [int]`$SessionId) - - `$lockPath = Join-Path `$env:TEMP ("launch-watchers-session-{0}.lock" -f `$SessionId) - if (Test-Path -LiteralPath `$lockPath) { - try { - `$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json - `$existingPid = [int]`$lockData.pid - if (`$existingPid -gt 0 -and (Get-Process -Id `$existingPid -ErrorAction SilentlyContinue)) { - return `$null - } - } - catch { - } - } - - `$payload = @{ - pid = `$PID - sessionId = `$SessionId - createdAt = (Get-Date).ToUniversalTime().ToString('o') - } | ConvertTo-Json -Compress - Set-Content -LiteralPath `$lockPath -Value `$payload -Encoding UTF8 - return `$lockPath -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = `$true)][string]`$Uri, - [Parameter(Mandatory = `$true)][string]`$Json - ) - - `$httpClient = New-Object System.Net.Http.HttpClient - try { - `$content = New-Object System.Net.Http.StringContent(`$Json, [System.Text.Encoding]::UTF8, 'application/json') - `$response = `$httpClient.PostAsync(`$Uri, `$content).Result - if (-not `$response.IsSuccessStatusCode) { - return `$false - } - return `$true - } - catch { - return `$false - } - finally { - `$httpClient.Dispose() - } -} - -function Ensure-Bucket { - param( - [string]`$BucketId, - [string]`$ClientName, - [string]`$BucketType - ) - - if (`$script:KnownBuckets.ContainsKey(`$BucketId)) { - return - } - - try { - Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null - `$script:KnownBuckets[`$BucketId] = `$true - return - } - catch { - } - - `$body = @{ - client = `$ClientName - type = `$BucketType - hostname = `$script:Hostname - } | ConvertTo-Json -Compress - - try { - if (-not (Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" -Json `$body)) { - return - } - } - catch { - try { - Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null - } - catch { - return - } - } - - `$script:KnownBuckets[`$BucketId] = `$true -} - -function Send-LogonMarkerIfNeeded { - param( - [pscustomobject]`$Config, - [int]`$SessionId - ) - - `$sessionEvents = if (`$Config.PSObject.Properties.Name -contains 'sessionEvents') { `$Config.sessionEvents } else { `$null } - `$logging = if (`$Config.PSObject.Properties.Name -contains 'logging') { `$Config.logging } else { `$null } - `$logonEnabled = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'logonEnabled') { [bool]`$sessionEvents.logonEnabled } else { `$false } - if (-not `$logonEnabled) { - return - } - - `$bucketPrefix = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'bucketPrefix' -and -not [string]::IsNullOrWhiteSpace([string]`$sessionEvents.bucketPrefix)) { - [string]`$sessionEvents.bucketPrefix - } - else { - 'aw-session-events' - } - - `$stateRoot = [string]`$Config.paths.stateRoot - `$markerRoots = New-Object System.Collections.Generic.List[string] - if (-not [string]::IsNullOrWhiteSpace(`$env:LOCALAPPDATA)) { - `$markerRoots.Add((Join-Path `$env:LOCALAPPDATA 'AWatch-rus\markers')) - } - if (-not [string]::IsNullOrWhiteSpace(`$stateRoot)) { - `$markerRoots.Add((Join-Path `$stateRoot 'markers')) - } - - `$markerDir = `$null - foreach (`$candidate in `$markerRoots) { - try { - if (-not (Test-Path -LiteralPath `$candidate)) { - New-Item -Path `$candidate -ItemType Directory -Force | Out-Null - } - - `$probePath = Join-Path `$candidate 'write-test.tmp' - Set-Content -LiteralPath `$probePath -Value 'ok' -Encoding ASCII - Remove-Item -LiteralPath `$probePath -Force -ErrorAction SilentlyContinue - `$markerDir = `$candidate - break - } - catch { - } - } - - if (-not `$markerDir) { - return - } - - `$markerFile = Join-Path `$markerDir ("logon-{0}-{1}.marker" -f `$env:USERNAME, `$SessionId) - if (Test-Path -LiteralPath `$markerFile) { - return - } - - Set-Content -LiteralPath `$markerFile -Value ((Get-Date).ToUniversalTime().ToString('o')) -Encoding UTF8 - - `$bucketId = ('{0}_{1}' -f `$bucketPrefix, `$script:Hostname) - Ensure-Bucket -BucketId `$bucketId -ClientName 'aw-session-events' -BucketType 'aw.session.event' - - `$payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - eventType = 'logon' - username = `$env:USERNAME - userId = "`$(`$env:USERDOMAIN)\`$(`$env:USERNAME)" - sessionId = `$SessionId - hostname = `$script:Hostname - source = 'launch-watchers-awatch-rus' - } - } | ConvertTo-Json -Depth 5 -Compress - - try { - Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$bucketId/heartbeat?pulsetime=1" -Json `$payload - } - catch { - Remove-Item -LiteralPath `$markerFile -Force -ErrorAction SilentlyContinue - throw - } -} - -function Start-CollectorScriptIfNeeded { - param( - [string]`$ScriptPath, - [string]`$ConfigPath, - [string]`$PowerShellExe, - [int]`$SessionId - ) - - if ([string]::IsNullOrWhiteSpace(`$ScriptPath)) { - return - } - - if (-not (Test-Path -LiteralPath `$ScriptPath)) { - return - } - - if (Test-CollectorRunning -ScriptPath `$ScriptPath -SessionId `$SessionId) { - return - } - - if ((Get-CollectorPowerShellProcessCount) -ge `$script:MaxCollectorPowerShellProcesses) { - return - } - - `$staParam = if (`$ScriptPath -like "*endpoint-signals*") { "-STA" } else { `$null } - `$argumentList = @('-NoProfile', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass') - if (`$staParam) { `$argumentList += `$staParam } - `$argumentList += @('-File', `$ScriptPath, '-ConfigPath', `$ConfigPath) - Start-Process -FilePath `$PowerShellExe -ArgumentList `$argumentList -WindowStyle Hidden -} - -`$config = Get-DeploymentConfig -Path `$ConfigPath -`$sessionId = (Get-Process -Id `$PID).SessionId -`$installRoot = [string]`$config.paths.installRoot -`$stateRoot = [string]`$config.paths.stateRoot -`$script:ApiBase = '{0}://{1}:{2}/api/0' -f [string]`$config.server.scheme, [string]`$config.server.host, [string]`$config.server.port -`$script:Hostname = `$env:COMPUTERNAME -`$script:KnownBuckets = @{} -`$collectorScript = [string]`$config.paths.collectorScript -`$endpointCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]`$config.paths.endpointCollectorScript } else { Join-Path `$stateRoot 'dlp-endpoint-signals-collector.ps1' } -`$fileCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'fileCollectorScript') { [string]`$config.paths.fileCollectorScript } else { Join-Path `$stateRoot 'file-operations-collector.ps1' } -`$sessionCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]`$config.paths.sessionCollectorScript } else { Join-Path `$stateRoot 'worktime-session-collector.ps1' } -`$afkExe = Join-Path `$installRoot 'aw-watcher-afk\aw-watcher-afk.exe' -`$windowExe = Join-Path `$installRoot 'aw-watcher-window\aw-watcher-window.exe' -`$serverArgs = @('--host', [string]`$config.server.host, '--port', [string]`$config.server.port) -`$powershellExe = Join-Path `$env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' -`$afkEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]`$config.collectors.afkEnabled } else { `$true } -`$windowEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]`$config.collectors.windowEnabled } else { `$true } -`$fileOpsEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { [bool]`$config.collectors.fileOpsEnabled } else { `$true } -`$emailEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'emailEnabled') { [bool]`$config.collectors.emailEnabled } else { `$false } -`$emailCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'emailCollectorScript') { [string]`$config.paths.emailCollectorScript } else { Join-Path `$stateRoot 'email-outbound-collector.ps1' } -`$launchLockPath = New-LaunchLock -StateRoot `$stateRoot -SessionId `$sessionId -if (-not `$launchLockPath) { - return -} - -try { - if (`$afkEnabled -and -not (Test-Path -LiteralPath `$afkExe)) { - throw "Не найден aw-watcher-afk.exe: `$afkExe" - } - - if (`$windowEnabled -and -not (Test-Path -LiteralPath `$windowExe)) { - throw "Не найден aw-watcher-window.exe: `$windowExe" - } - - if (`$afkEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-afk' -SessionId `$sessionId)) { - Start-Process -FilePath `$afkExe -ArgumentList `$serverArgs -WindowStyle Hidden - } - - if (`$windowEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-window' -SessionId `$sessionId)) { - Start-Process -FilePath `$windowExe -ArgumentList `$serverArgs -WindowStyle Hidden - } - - try { - Send-LogonMarkerIfNeeded -Config `$config -SessionId `$sessionId - } - catch { - } - Start-CollectorScriptIfNeeded -ScriptPath `$collectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - Start-CollectorScriptIfNeeded -ScriptPath `$endpointCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - if (`$fileOpsEnabled) { - Start-CollectorScriptIfNeeded -ScriptPath `$fileCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - } - Start-CollectorScriptIfNeeded -ScriptPath `$sessionCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - if (`$emailEnabled -and (Test-Path -LiteralPath `$emailCollectorScript)) { - Start-CollectorScriptIfNeeded -ScriptPath `$emailCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - } -} -finally { - if (`$launchLockPath -and (Test-Path -LiteralPath `$launchLockPath)) { - Remove-Item -LiteralPath `$launchLockPath -Force -ErrorAction SilentlyContinue - } -} -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding UTF8 -} - -function Write-ActivityWatchRecoveryScript { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $content = @" -param( - [string]`$ConfigPath = '$ConfigPath' -) - -Set-StrictMode -Version Latest -`$ErrorActionPreference = 'Continue' - -function Get-DeploymentConfig { - param([string]`$Path) - return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json -} - -function Get-RecoveryConfigPaths { - param([string]`$PrimaryConfigPath) - - `$paths = New-Object System.Collections.Generic.List[string] - if (`$PrimaryConfigPath -and (Test-Path -LiteralPath `$PrimaryConfigPath)) { - `$paths.Add((Resolve-Path -LiteralPath `$PrimaryConfigPath).Path) - } - - `$searchRoot = `$env:ProgramData - if (`$PrimaryConfigPath) { - `$stateRoot = Split-Path -Path `$PrimaryConfigPath -Parent - `$candidateRoot = Split-Path -Path `$stateRoot -Parent - if (`$candidateRoot -and (Test-Path -LiteralPath `$candidateRoot)) { - `$searchRoot = `$candidateRoot - } - } - - if (Test-Path -LiteralPath `$searchRoot) { - Get-ChildItem -LiteralPath `$searchRoot -Directory -ErrorAction SilentlyContinue | - Where-Object { `$_.Name -like 'ActivityWatch*' } | - ForEach-Object { - `$candidate = Join-Path `$_.FullName 'deployment-config.json' - if (Test-Path -LiteralPath `$candidate) { - `$paths.Add(`$candidate) - } - } - } - - return @(`$paths | Sort-Object -Unique) -} - -function Get-RecoveryTaskNames { - param([string[]]`$ConfigPaths) - - `$taskNames = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) - foreach (`$candidatePath in @(`$ConfigPaths)) { - try { - `$config = Get-DeploymentConfig -Path `$candidatePath - foreach (`$task in @(`$config.userTasks)) { - `$taskName = [string]`$task.launchTaskName - if (-not [string]::IsNullOrWhiteSpace(`$taskName)) { - [void]`$taskNames.Add(`$taskName) - } - } - } - catch { - } - } - - return @(`$taskNames) -} - -function New-RecoveryLock { - param([string]`$PrimaryConfigPath) - - `$stateRoot = if (`$PrimaryConfigPath) { Split-Path -Path `$PrimaryConfigPath -Parent } else { Join-Path `$env:ProgramData 'AWatch-rus' } - if (-not (Test-Path -LiteralPath `$stateRoot)) { - New-Item -Path `$stateRoot -ItemType Directory -Force | Out-Null - } - - `$lockPath = Join-Path `$stateRoot 'recovery-loop.lock' - if (Test-Path -LiteralPath `$lockPath) { - try { - `$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json - `$existingPid = [int]`$lockData.pid - if (`$existingPid -gt 0 -and (Get-Process -Id `$existingPid -ErrorAction SilentlyContinue)) { - return `$null - } - } - catch { - } - } - - `$payload = @{ - pid = `$PID - createdAt = (Get-Date).ToUniversalTime().ToString('o') - } | ConvertTo-Json -Compress - Set-Content -LiteralPath `$lockPath -Value `$payload -Encoding UTF8 - return `$lockPath -} - -function Start-TaskIfNotRunning { - param([string]`$TaskName) - if ([string]::IsNullOrWhiteSpace(`$TaskName)) { - return - } - - try { - `$task = Get-ScheduledTask -TaskName `$TaskName -ErrorAction SilentlyContinue - if (-not `$task) { - return - } - if ([string]`$task.State -eq 'Running') { - return - } - Start-ScheduledTask -TaskName `$TaskName -ErrorAction SilentlyContinue - } - catch { - } -} - -`$recoveryLockPath = New-RecoveryLock -PrimaryConfigPath `$ConfigPath -if (-not `$recoveryLockPath) { - return -} - -try { - while (`$true) { - `$sleepSeconds = 180 - try { - `$configPaths = Get-RecoveryConfigPaths -PrimaryConfigPath `$ConfigPath - foreach (`$taskName in Get-RecoveryTaskNames -ConfigPaths `$configPaths) { - Start-TaskIfNotRunning -TaskName `$taskName - } - - `$config = Get-DeploymentConfig -Path `$ConfigPath - if (`$config -and `$config.recovery -and `$config.recovery.intervalSeconds) { - `$sleepSeconds = [Math]::Max([int]`$config.recovery.intervalSeconds, 30) - } - } - catch { - } - - Start-Sleep -Seconds `$sleepSeconds - } -} -finally { - if (`$recoveryLockPath -and (Test-Path -LiteralPath `$recoveryLockPath)) { - Remove-Item -LiteralPath `$recoveryLockPath -Force -ErrorAction SilentlyContinue - } -} -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding UTF8 -} - -function Get-ActivityWatchHiddenLauncherPath { - param( - [Parameter(Mandatory = $true)] - [string]$ScriptPath - ) - - $directory = Split-Path -Path $ScriptPath -Parent - $baseName = [IO.Path]::GetFileNameWithoutExtension($ScriptPath) - return Join-Path $directory ("{0}-hidden.vbs" -f $baseName) -} - -function Write-ActivityWatchHiddenPowerShellWrapper { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $directory = Split-Path -Path $Path -Parent - if ($directory) { - New-ActivityWatchDirectory -Path $directory - } - - $powershellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' - $escapedPowerShellExe = $powershellExe.Replace('"', '""') - $escapedScriptPath = $ScriptPath.Replace('"', '""') - $escapedConfigPath = $ConfigPath.Replace('"', '""') - - $content = @" -Set shell = CreateObject("WScript.Shell") -shell.Run """$escapedPowerShellExe"" -NoProfile -ExecutionPolicy Bypass -File ""$escapedScriptPath"" -ConfigPath ""$escapedConfigPath""", 0, False -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding ASCII -} - -function Remove-LegacyActivityWatchEntries { - $legacyTaskNames = @( - 'ActivityWatch Watchers', - 'ActivityWatch Guard', - 'ActivityWatch Heal' - ) - - foreach ($taskName in $legacyTaskNames) { - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue - } - - $runKey = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run' - foreach ($name in 'ActivityWatchAFK', 'ActivityWatchWindow', 'ActivityWatchBrowserCollector') { - Remove-ItemProperty -Path $runKey -Name $name -ErrorAction SilentlyContinue - } -} - -function Remove-ActivityWatchScheduledTask { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName - ) - - Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue - & cmd.exe /c "schtasks /Delete /TN `"$TaskName`" /F >nul 2>&1" | Out-Null - - for ($attempt = 0; $attempt -lt 10; $attempt++) { - $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue - if (-not $task) { - return - } - - Start-Sleep -Milliseconds 300 - } -} - -function Set-ActivityWatchScheduledTaskAction { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [Parameter(Mandatory = $true)] - [string]$Execute, - [Parameter(Mandatory = $true)] - [string]$Arguments - ) - - $taskCommand = ('"{0}" {1}' -f $Execute, $Arguments) - & schtasks.exe /Change /TN $TaskName /TR $taskCommand | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "schtasks.exe /Change завершился с ошибкой для $TaskName" - } -} - -function Get-ActivityWatchScheduledTaskByCommand { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [string]$CommandMatch - ) - - $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue - if ($task) { - return $task - } - - if ([string]::IsNullOrWhiteSpace($CommandMatch)) { - return $null - } - - foreach ($candidate in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch*' })) { - foreach ($action in @($candidate.Actions)) { - if ([string]$action.Arguments -like "*$CommandMatch*") { - return $candidate - } - } - } - - return $null -} - -function Remove-StaleActivityWatchUserTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath - ) - - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath - $desiredTaskNames = @($TaskDefinitions | ForEach-Object { [string]$_.LaunchTaskName }) - - foreach ($candidate in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch*' })) { - $taskName = [string]$candidate.TaskName - if ($desiredTaskNames -contains $taskName) { - continue - } - - $usesCurrentLauncher = $false - foreach ($action in @($candidate.Actions)) { - if ([string]$action.Arguments -like "*$launcherPath*") { - $usesCurrentLauncher = $true - break - } - } - - if ($usesCurrentLauncher) { - Remove-ActivityWatchScheduledTask -TaskName $taskName - } - } -} - -function Register-ActivityWatchUserTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe' - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath - Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $LaunchScriptPath -ConfigPath $ConfigPath - Remove-StaleActivityWatchUserTasks -TaskDefinitions $TaskDefinitions -LaunchScriptPath $LaunchScriptPath - - foreach ($definition in $TaskDefinitions) { - $action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`"" - $trigger = New-ScheduledTaskTrigger -AtLogOn -User $definition.UserId - $principal = New-ScheduledTaskPrincipal -UserId $definition.UserId -LogonType Interactive -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0) - $existingTask = Get-ActivityWatchScheduledTaskByCommand -TaskName $definition.LaunchTaskName -CommandMatch $ConfigPath - - if ($existingTask) { - Set-ActivityWatchScheduledTaskAction -TaskName $existingTask.TaskName -Execute $wscriptExe -Arguments $action.Arguments - continue - } - - Remove-ActivityWatchScheduledTask -TaskName $definition.LaunchTaskName - Register-ScheduledTask -TaskName $definition.LaunchTaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null - } -} - -function Register-ActivityWatchRecoveryTask { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [Parameter(Mandatory = $true)] - [string]$RecoveryScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - Remove-ActivityWatchScheduledTask -TaskName $TaskName - - $wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe' - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $RecoveryScriptPath - Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $RecoveryScriptPath -ConfigPath $ConfigPath - $action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`"" - $trigger = New-ScheduledTaskTrigger -AtStartup - $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -Hidden -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0) - - Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null -} - -function Set-ActivityWatchAcl { - param( - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [Parameter(Mandatory = $true)] - [string]$LogsRoot - ) - - foreach ($path in $InstallRoot, $StateRoot, $LogsRoot) { - New-ActivityWatchDirectory -Path $path - } - - & icacls $InstallRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $InstallRoot" - } - - & icacls $StateRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $StateRoot" - } - - & icacls $LogsRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(M)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $LogsRoot" - } -} - -function Start-ActivityWatchTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [string]$RecoveryTaskName = 'ActivityWatch Recovery' - ) - - foreach ($definition in $TaskDefinitions) { - Start-ScheduledTask -TaskName $definition.LaunchTaskName -ErrorAction SilentlyContinue - } - - Start-ScheduledTask -TaskName $RecoveryTaskName -ErrorAction SilentlyContinue -} - -Export-ModuleMember -Function *-ActivityWatch*, Assert-Administrator, Normalize-ActivityWatchUsers, Get-ActivityWatchPackageUrl, Remove-LegacyActivityWatchEntries -; } - Start-CollectorScriptIfNeeded -ScriptPath `$collectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - Start-CollectorScriptIfNeeded -ScriptPath `$endpointCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - if (`$fileOpsEnabled) { - Start-CollectorScriptIfNeeded -ScriptPath `$fileCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - } - Start-CollectorScriptIfNeeded -ScriptPath `$sessionCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - if (`$emailEnabled -and (Test-Path -LiteralPath `$emailCollectorScript)) { - Start-CollectorScriptIfNeeded -ScriptPath `$emailCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - } -} -finally { - if (`$launchLockPath -and (Test-Path -LiteralPath `$launchLockPath)) { - Remove-Item -LiteralPath `$launchLockPath -Force -ErrorAction SilentlyContinue - } -} -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding UTF8 -} - -function Write-ActivityWatchRecoveryScript { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $content = @" -param( - [string]`$ConfigPath = '$ConfigPath' -) - -Set-StrictMode -Version Latest -`$ErrorActionPreference = 'Continue' - -function Get-DeploymentConfig { - param([string]`$Path) - return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json -} - -function Get-RecoveryConfigPaths { - param([string]`$PrimaryConfigPath) - - `$paths = New-Object System.Collections.Generic.List[string] - if (`$PrimaryConfigPath -and (Test-Path -LiteralPath `$PrimaryConfigPath)) { - `$paths.Add((Resolve-Path -LiteralPath `$PrimaryConfigPath).Path) - } - - `$searchRoot = `$env:ProgramData - if (`$PrimaryConfigPath) { - `$stateRoot = Split-Path -Path `$PrimaryConfigPath -Parent - `$candidateRoot = Split-Path -Path `$stateRoot -Parent - if (`$candidateRoot -and (Test-Path -LiteralPath `$candidateRoot)) { - `$searchRoot = `$candidateRoot - } - } - - if (Test-Path -LiteralPath `$searchRoot) { - Get-ChildItem -LiteralPath `$searchRoot -Directory -ErrorAction SilentlyContinue | - Where-Object { `$_.Name -like 'ActivityWatch*' } | - ForEach-Object { - `$candidate = Join-Path `$_.FullName 'deployment-config.json' - if (Test-Path -LiteralPath `$candidate) { - `$paths.Add(`$candidate) - } - } - } - - return @(`$paths | Sort-Object -Unique) -} - -function Get-RecoveryTaskNames { - param([string[]]`$ConfigPaths) - - `$taskNames = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) - foreach (`$candidatePath in @(`$ConfigPaths)) { - try { - `$config = Get-DeploymentConfig -Path `$candidatePath - foreach (`$task in @(`$config.userTasks)) { - `$taskName = [string]`$task.launchTaskName - if (-not [string]::IsNullOrWhiteSpace(`$taskName)) { - [void]`$taskNames.Add(`$taskName) - } - } - } - catch { Write-Error Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Assert-Administrator { - $identity = [Security.Principal.WindowsIdentity]::GetCurrent() - $principal = [Security.Principal.WindowsPrincipal]::new($identity) - if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - throw 'Запустите этот скрипт из PowerShell с правами администратора.' - } -} - -function New-ActivityWatchDirectory { - param( - [Parameter(Mandatory = $true)] - [string]$Path - ) - - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Enable-ActivityWatchPrintTelemetry { - $policyPath = 'HKLM:\Software\Policies\Microsoft\Windows NT\Printers' - if (-not (Test-Path -LiteralPath $policyPath)) { - New-Item -Path $policyPath -Force | Out-Null - } - New-ItemProperty -Path $policyPath -Name 'ShowJobTitleInEventLogs' -Value 1 -PropertyType DWord -Force | Out-Null - - & wevtutil.exe sl 'Microsoft-Windows-PrintService/Operational' /e:true | Out-Null -} - - -function Get-ActivityWatchPackageUrl { - param( - [string]$Version = 'v0.13.2' - ) - - return "https://github.com/ActivityWatch/activitywatch/releases/download/$Version/activitywatch-$Version-windows-x86_64.zip" -} - -function Get-ActivityWatchArchive { - param( - [string]$PackageZipPath, - [string]$PackageUrl, - [string]$Version = 'v0.13.2', - [Parameter(Mandatory = $true)] - [string]$WorkingRoot - ) - - New-ActivityWatchDirectory -Path $WorkingRoot - - if ($PackageZipPath) { - $resolved = Resolve-Path -LiteralPath $PackageZipPath -ErrorAction Stop - return $resolved.Path - } - - if (-not $PackageUrl) { - $PackageUrl = Get-ActivityWatchPackageUrl -Version $Version - } - - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' - $suffix = ([guid]::NewGuid().Guid.Substring(0, 8)) - $archivePath = Join-Path $WorkingRoot ("activitywatch-{0}-{1}-{2}.zip" -f $Version.TrimStart('v'), $stamp, $suffix) - Invoke-WebRequest -Uri $PackageUrl -OutFile $archivePath - return $archivePath -} - -function Get-ActivityWatchPackageRoot { - param( - [Parameter(Mandatory = $true)] - [string]$ExpandedRoot - ) - - $afkBinary = Get-ChildItem -Path $ExpandedRoot -Filter 'aw-watcher-afk.exe' -File -Recurse | - Select-Object -First 1 - - if (-not $afkBinary) { - throw "Не удалось найти aw-watcher-afk.exe в $ExpandedRoot." - } - - return (Split-Path -Path (Split-Path -Path $afkBinary.FullName -Parent) -Parent) -} - -function Install-ActivityWatchPackage { - param( - [Parameter(Mandatory = $true)] - [string]$ArchivePath, - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$WorkingRoot, - [Parameter(Mandatory = $true)] - [string]$BackupRoot - ) - - New-ActivityWatchDirectory -Path $WorkingRoot - New-ActivityWatchDirectory -Path $BackupRoot - - # Ensure nothing is holding locks inside InstallRoot during upgrade. - foreach ($procName in @('aw-watcher-afk', 'aw-watcher-window', 'aw-server', 'aw-qt')) { - try { - Get-Process -Name $procName -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue - } - catch { - } - } - Start-Sleep -Seconds 2 - - $extractRoot = Join-Path $WorkingRoot ('extract-' + [guid]::NewGuid().Guid) - if (Test-Path -LiteralPath $extractRoot) { - Remove-Item -LiteralPath $extractRoot -Recurse -Force - } - New-ActivityWatchDirectory -Path $extractRoot - - Expand-Archive -Path $ArchivePath -DestinationPath $extractRoot -Force - $packageRoot = Get-ActivityWatchPackageRoot -ExpandedRoot $extractRoot - - if (Test-Path -LiteralPath $InstallRoot) { - $existingItems = Get-ChildItem -LiteralPath $InstallRoot -Force -ErrorAction SilentlyContinue - if ($existingItems) { - $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' - $backupPath = Join-Path $BackupRoot ("install-$stamp") - New-ActivityWatchDirectory -Path $backupPath - Copy-Item -Path (Join-Path $InstallRoot '*') -Destination $backupPath -Recurse -Force - Get-ChildItem -LiteralPath $InstallRoot -Force | Remove-Item -Recurse -Force - } - } - else { - New-ActivityWatchDirectory -Path $InstallRoot - } - - Copy-Item -Path (Join-Path $packageRoot '*') -Destination $InstallRoot -Recurse -Force - - return [pscustomobject]@{ - PackageRoot = $packageRoot - ExtractRoot = $extractRoot - BackupRoot = $BackupRoot - } -} - -function Get-ActivityWatchExecutableMap { - param( - [Parameter(Mandatory = $true)] - [string]$InstallRoot - ) - - $map = [ordered]@{ - Afk = Join-Path $InstallRoot 'aw-watcher-afk\aw-watcher-afk.exe' - Window = Join-Path $InstallRoot 'aw-watcher-window\aw-watcher-window.exe' - } - - foreach ($entry in $map.GetEnumerator()) { - if (-not (Test-Path -LiteralPath $entry.Value)) { - throw "Не найден обязательный исполняемый файл ActivityWatch: $($entry.Value)" - } - } - - return [pscustomobject]$map -} - -function Normalize-ActivityWatchUsers { - param( - [string[]]$Users, - [string]$UserListPath, - [string]$Domain - ) - - $collected = New-Object System.Collections.Generic.List[string] - - if ($Users) { - foreach ($user in $Users) { - if (-not [string]::IsNullOrWhiteSpace($user)) { - $collected.Add($user.Trim()) - } - } - } - - if ($UserListPath) { - $resolved = Resolve-Path -LiteralPath $UserListPath -ErrorAction Stop - $extension = [IO.Path]::GetExtension($resolved.Path) - if ($extension -ieq '.csv') { - $rows = Import-Csv -LiteralPath $resolved.Path - foreach ($row in $rows) { - foreach ($column in 'User', 'Username', 'SamAccountName', 'Login') { - if ($row.PSObject.Properties.Name -contains $column) { - $value = [string]$row.$column - if (-not [string]::IsNullOrWhiteSpace($value)) { - $collected.Add($value.Trim()) - break - } - } - } - } - } - else { - Get-Content -LiteralPath $resolved.Path | ForEach-Object { - $line = $_.Trim() - if ($line -and -not $line.StartsWith('#')) { - $collected.Add($line) - } - } - } - } - - $normalized = $collected | - Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | - ForEach-Object { - if ($Domain -and ($_ -notmatch '[\\@]')) { - '{0}\{1}' -f $Domain, $_ - } - else { - $_ - } - } | - Sort-Object -Unique - - if (-not $normalized -or $normalized.Count -eq 0) { - throw 'Не удалось определить целевых пользователей. Укажите -Users или -UserListPath.' - } - - return @($normalized) -} - -function Get-ActivityWatchTaskNameToken { - param( - [Parameter(Mandatory = $true)] - [string]$UserId - ) - - $buffer = [Text.StringBuilder]::new() - foreach ($character in $UserId.ToCharArray()) { - if ([char]::IsLetterOrDigit($character)) { - [void]$buffer.Append($character) - } - else { - [void]$buffer.Append('_') - } - } - - return $buffer.ToString().Trim('_') -} - -function New-ActivityWatchUserTaskDefinitions { - param( - [Parameter(Mandatory = $true)] - [string[]]$Users - ) - - $result = foreach ($user in $Users) { - $token = Get-ActivityWatchTaskNameToken -UserId $user - [pscustomobject]@{ - UserId = $user - LaunchTaskName = "ActivityWatch Launch [$token]" - } - } - - return @($result) -} - -function Copy-ActivityWatchCollectorAssets { - param( - [Parameter(Mandatory = $true)] - [string]$CollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$EndpointCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$FileCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$SessionCollectorScriptSource, - [string]$EmailCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$ExampleRulesSource, - [Parameter(Mandatory = $true)] - [string]$ExamplePolicySource, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [string]$CustomRulesSource, - [string]$CustomPolicySource - ) - - New-ActivityWatchDirectory -Path $StateRoot - - $collectorTarget = Join-Path $StateRoot 'browser-domains-native-collector.ps1' - $endpointCollectorTarget = Join-Path $StateRoot 'dlp-endpoint-signals-collector.ps1' - $fileCollectorTarget = Join-Path $StateRoot 'file-operations-collector.ps1' - $sessionCollectorTarget = Join-Path $StateRoot 'worktime-session-collector.ps1' - $emailCollectorTarget = Join-Path $StateRoot 'email-outbound-collector.ps1' - $exampleRulesTarget = Join-Path $StateRoot 'web-category-rules.example.json' - $rulesTarget = Join-Path $StateRoot 'web-category-rules.json' - $examplePolicyTarget = Join-Path $StateRoot 'dlp-policy.example.json' - $policyTarget = Join-Path $StateRoot 'dlp-policy.json' - - Copy-Item -LiteralPath $CollectorScriptSource -Destination $collectorTarget -Force - Copy-Item -LiteralPath $EndpointCollectorScriptSource -Destination $endpointCollectorTarget -Force - Copy-Item -LiteralPath $FileCollectorScriptSource -Destination $fileCollectorTarget -Force - Copy-Item -LiteralPath $SessionCollectorScriptSource -Destination $sessionCollectorTarget -Force - if ($EmailCollectorScriptSource -and (Test-Path -LiteralPath $EmailCollectorScriptSource)) { - Copy-Item -LiteralPath $EmailCollectorScriptSource -Destination $emailCollectorTarget -Force - } - Copy-Item -LiteralPath $ExampleRulesSource -Destination $exampleRulesTarget -Force - Copy-Item -LiteralPath $ExamplePolicySource -Destination $examplePolicyTarget -Force - - if ($CustomRulesSource) { - $resolvedRules = Resolve-Path -LiteralPath $CustomRulesSource -ErrorAction Stop - Copy-Item -LiteralPath $resolvedRules.Path -Destination $rulesTarget -Force - } - else { - Copy-Item -LiteralPath $exampleRulesTarget -Destination $rulesTarget -Force - } - - if ($CustomPolicySource) { - $resolvedPolicy = Resolve-Path -LiteralPath $CustomPolicySource -ErrorAction Stop - Copy-Item -LiteralPath $resolvedPolicy.Path -Destination $policyTarget -Force - } - else { - Copy-Item -LiteralPath $examplePolicyTarget -Destination $policyTarget -Force - } - - return [pscustomobject]@{ - CollectorScript = $collectorTarget - EndpointCollectorScript = $endpointCollectorTarget - FileCollectorScript = $fileCollectorTarget - SessionCollectorScript = $sessionCollectorTarget - EmailCollectorScript = $emailCollectorTarget - ExampleRules = $exampleRulesTarget - ActiveRules = $rulesTarget - ExamplePolicy = $examplePolicyTarget - ActivePolicy = $policyTarget - } -} - -function New-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [string]$ServerHost, - [Parameter(Mandatory = $true)] - [int]$ServerPort, - [Parameter(Mandatory = $true)] - [string]$ServerScheme, - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [Parameter(Mandatory = $true)] - [string]$LogsRoot, - [Parameter(Mandatory = $true)] - [string]$CollectorScript, - [Parameter(Mandatory = $true)] - [string]$EndpointCollectorScript, - [Parameter(Mandatory = $true)] - [string]$FileCollectorScript, - [Parameter(Mandatory = $true)] - [string]$SessionCollectorScript, - [string]$EmailCollectorScript, - [Parameter(Mandatory = $true)] - [string]$RulesPath, - [Parameter(Mandatory = $true)] - [string]$PolicyPath, - [Parameter(Mandatory = $true)] - [int]$PollSeconds, - [Parameter(Mandatory = $true)] - [int]$PulseSeconds, - [Parameter(Mandatory = $true)] - [int]$RecoveryIntervalSeconds, - [bool]$AfkEnabled = $true, - [bool]$WindowEnabled = $true, - [bool]$FileOpsEnabled = $true, - [bool]$LocalAgentLogsEnabled = $true, - [bool]$IncidentCaptureEnabled = $true, - [bool]$IncidentScreenshotEnabled = $true, - [string]$IncidentArtifactsRoot, - [bool]$LogonMarkerEnabled = $true, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath, - [Parameter(Mandatory = $true)] - [string]$RecoveryScriptPath, - [Parameter(Mandatory = $true)] - [pscustomobject[]]$UserTasks, - [string]$PackageVersion = 'v0.13.2' - ) - - $effectiveIncidentArtifactsRoot = if ($IncidentArtifactsRoot) { $IncidentArtifactsRoot } else { Join-Path $StateRoot 'incident-artifacts' } - - return [pscustomobject]@{ - version = 1 - generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') - server = [pscustomobject]@{ - host = $ServerHost - port = $ServerPort - scheme = $ServerScheme - } - paths = [pscustomobject]@{ - installRoot = $InstallRoot - stateRoot = $StateRoot - logsRoot = $LogsRoot - collectorScript = $CollectorScript - endpointCollectorScript = $EndpointCollectorScript - emailCollectorScript = $EmailCollectorScript - fileCollectorScript = $FileCollectorScript - sessionCollectorScript = $SessionCollectorScript - rulesPath = $RulesPath - policyPath = $PolicyPath - launchScript = $LaunchScriptPath - recoveryScript = $RecoveryScriptPath - } - collector = [pscustomobject]@{ - pollSeconds = $PollSeconds - pulseSeconds = $PulseSeconds - } - collectors = [pscustomobject]@{ - afkEnabled = $AfkEnabled - windowEnabled = $WindowEnabled - fileOpsEnabled = $FileOpsEnabled - emailEnabled = ($null -ne $EmailCollectorScript -and $EmailCollectorScript -ne '') - } - logging = [pscustomobject]@{ - localAgentLogsEnabled = $LocalAgentLogsEnabled - } - incidentCapture = [pscustomobject]@{ - enabled = $IncidentCaptureEnabled - screenshotEnabled = $IncidentScreenshotEnabled - artifactsRoot = $effectiveIncidentArtifactsRoot - } - sessionEvents = [pscustomobject]@{ - logonEnabled = $LogonMarkerEnabled - bucketPrefix = 'aw-session-events' - } - recovery = [pscustomobject]@{ - intervalSeconds = $RecoveryIntervalSeconds - taskName = 'ActivityWatch Recovery' - } - dlp = [pscustomobject]@{ - incidentBucketPrefix = 'aw-dlp-incidents' - enabled = $true - } - package = [pscustomobject]@{ - version = $PackageVersion - } - userTasks = @($UserTasks) - } -} - -function Write-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [pscustomobject]$Config, - [Parameter(Mandatory = $true)] - [string]$Path - ) - - $directory = Split-Path -Path $Path -Parent - if ($directory) { - New-ActivityWatchDirectory -Path $directory - } - - $json = $Config | ConvertTo-Json -Depth 8 - Set-Content -LiteralPath $Path -Value $json -Encoding UTF8 -} - -function Read-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [string]$Path - ) - - if (-not (Test-Path -LiteralPath $Path)) { - throw "Конфигурация развёртывания не найдена: $Path" - } - - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json -} - -function Write-ActivityWatchLaunchScript { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $content = @" -param( - [string]`$ConfigPath = '$ConfigPath' -) - -Set-StrictMode -Version Latest -`$ErrorActionPreference = 'Stop' - -[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -Add-Type -AssemblyName System.Net.Http -`$script:MaxCollectorPowerShellProcesses = 24 - -function Get-DeploymentConfig { - param([string]`$Path) - return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json -} - -function Test-ProcessInSession { - param( - [string]`$Name, - [int]`$SessionId - ) - - return [bool](Get-Process -Name `$Name -ErrorAction SilentlyContinue | Where-Object { `$_.SessionId -eq `$SessionId } | Select-Object -First 1) -} - -function Test-CollectorRunning { - param( - [string]`$ScriptPath, - [int]`$SessionId - ) - - `$escapedCollector = [Regex]::Escape(`$ScriptPath) - `$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - (`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and - `$_.SessionId -eq `$SessionId -and - `$_.CommandLine -match `$escapedCollector - } - - return [bool](`$processes | Select-Object -First 1) -} - -function Get-CollectorPowerShellProcessCount { - `$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - (`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and - `$_.CommandLine -match 'AWatch-rus' -and - `$_.CommandLine -match '\.ps1' - } - - return @(`$processes).Count -} - -function New-LaunchLock { - param([string]`$StateRoot, [int]`$SessionId) - - `$lockPath = Join-Path `$env:TEMP ("launch-watchers-session-{0}.lock" -f `$SessionId) - if (Test-Path -LiteralPath `$lockPath) { - try { - `$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json - `$existingPid = [int]`$lockData.pid - if (`$existingPid -gt 0 -and (Get-Process -Id `$existingPid -ErrorAction SilentlyContinue)) { - return `$null - } - } - catch { - } - } - - `$payload = @{ - pid = `$PID - sessionId = `$SessionId - createdAt = (Get-Date).ToUniversalTime().ToString('o') - } | ConvertTo-Json -Compress - Set-Content -LiteralPath `$lockPath -Value `$payload -Encoding UTF8 - return `$lockPath -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = `$true)][string]`$Uri, - [Parameter(Mandatory = `$true)][string]`$Json - ) - - `$httpClient = New-Object System.Net.Http.HttpClient - try { - `$content = New-Object System.Net.Http.StringContent(`$Json, [System.Text.Encoding]::UTF8, 'application/json') - `$response = `$httpClient.PostAsync(`$Uri, `$content).Result - if (-not `$response.IsSuccessStatusCode) { - return `$false - } - return `$true - } - catch { - return `$false - } - finally { - `$httpClient.Dispose() - } -} - -function Ensure-Bucket { - param( - [string]`$BucketId, - [string]`$ClientName, - [string]`$BucketType - ) - - if (`$script:KnownBuckets.ContainsKey(`$BucketId)) { - return - } - - try { - Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null - `$script:KnownBuckets[`$BucketId] = `$true - return - } - catch { - } - - `$body = @{ - client = `$ClientName - type = `$BucketType - hostname = `$script:Hostname - } | ConvertTo-Json -Compress - - try { - if (-not (Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" -Json `$body)) { - return - } - } - catch { - try { - Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null - } - catch { - return - } - } - - `$script:KnownBuckets[`$BucketId] = `$true -} - -function Send-LogonMarkerIfNeeded { - param( - [pscustomobject]`$Config, - [int]`$SessionId - ) - - `$sessionEvents = if (`$Config.PSObject.Properties.Name -contains 'sessionEvents') { `$Config.sessionEvents } else { `$null } - `$logging = if (`$Config.PSObject.Properties.Name -contains 'logging') { `$Config.logging } else { `$null } - `$logonEnabled = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'logonEnabled') { [bool]`$sessionEvents.logonEnabled } else { `$false } - if (-not `$logonEnabled) { - return - } - - `$bucketPrefix = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'bucketPrefix' -and -not [string]::IsNullOrWhiteSpace([string]`$sessionEvents.bucketPrefix)) { - [string]`$sessionEvents.bucketPrefix - } - else { - 'aw-session-events' - } - - `$stateRoot = [string]`$Config.paths.stateRoot - `$markerRoots = New-Object System.Collections.Generic.List[string] - if (-not [string]::IsNullOrWhiteSpace(`$env:LOCALAPPDATA)) { - `$markerRoots.Add((Join-Path `$env:LOCALAPPDATA 'AWatch-rus\markers')) - } - if (-not [string]::IsNullOrWhiteSpace(`$stateRoot)) { - `$markerRoots.Add((Join-Path `$stateRoot 'markers')) - } - - `$markerDir = `$null - foreach (`$candidate in `$markerRoots) { - try { - if (-not (Test-Path -LiteralPath `$candidate)) { - New-Item -Path `$candidate -ItemType Directory -Force | Out-Null - } - - `$probePath = Join-Path `$candidate 'write-test.tmp' - Set-Content -LiteralPath `$probePath -Value 'ok' -Encoding ASCII - Remove-Item -LiteralPath `$probePath -Force -ErrorAction SilentlyContinue - `$markerDir = `$candidate - break - } - catch { - } - } - - if (-not `$markerDir) { - return - } - - `$markerFile = Join-Path `$markerDir ("logon-{0}-{1}.marker" -f `$env:USERNAME, `$SessionId) - if (Test-Path -LiteralPath `$markerFile) { - return - } - - Set-Content -LiteralPath `$markerFile -Value ((Get-Date).ToUniversalTime().ToString('o')) -Encoding UTF8 - - `$bucketId = ('{0}_{1}' -f `$bucketPrefix, `$script:Hostname) - Ensure-Bucket -BucketId `$bucketId -ClientName 'aw-session-events' -BucketType 'aw.session.event' - - `$payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - eventType = 'logon' - username = `$env:USERNAME - userId = "`$(`$env:USERDOMAIN)\`$(`$env:USERNAME)" - sessionId = `$SessionId - hostname = `$script:Hostname - source = 'launch-watchers-awatch-rus' - } - } | ConvertTo-Json -Depth 5 -Compress - - try { - Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$bucketId/heartbeat?pulsetime=1" -Json `$payload - } - catch { - Remove-Item -LiteralPath `$markerFile -Force -ErrorAction SilentlyContinue - throw - } -} - -function Start-CollectorScriptIfNeeded { - param( - [string]`$ScriptPath, - [string]`$ConfigPath, - [string]`$PowerShellExe, - [int]`$SessionId - ) - - if ([string]::IsNullOrWhiteSpace(`$ScriptPath)) { - return - } - - if (-not (Test-Path -LiteralPath `$ScriptPath)) { - return - } - - if (Test-CollectorRunning -ScriptPath `$ScriptPath -SessionId `$SessionId) { - return - } - - if ((Get-CollectorPowerShellProcessCount) -ge `$script:MaxCollectorPowerShellProcesses) { - return - } - - `$staParam = if (`$ScriptPath -like "*endpoint-signals*") { "-STA" } else { `$null } - `$argumentList = @('-NoProfile', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass') - if (`$staParam) { `$argumentList += `$staParam } - `$argumentList += @('-File', `$ScriptPath, '-ConfigPath', `$ConfigPath) - Start-Process -FilePath `$PowerShellExe -ArgumentList `$argumentList -WindowStyle Hidden -} - -`$config = Get-DeploymentConfig -Path `$ConfigPath -`$sessionId = (Get-Process -Id `$PID).SessionId -`$installRoot = [string]`$config.paths.installRoot -`$stateRoot = [string]`$config.paths.stateRoot -`$script:ApiBase = '{0}://{1}:{2}/api/0' -f [string]`$config.server.scheme, [string]`$config.server.host, [string]`$config.server.port -`$script:Hostname = `$env:COMPUTERNAME -`$script:KnownBuckets = @{} -`$collectorScript = [string]`$config.paths.collectorScript -`$endpointCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]`$config.paths.endpointCollectorScript } else { Join-Path `$stateRoot 'dlp-endpoint-signals-collector.ps1' } -`$fileCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'fileCollectorScript') { [string]`$config.paths.fileCollectorScript } else { Join-Path `$stateRoot 'file-operations-collector.ps1' } -`$sessionCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]`$config.paths.sessionCollectorScript } else { Join-Path `$stateRoot 'worktime-session-collector.ps1' } -`$afkExe = Join-Path `$installRoot 'aw-watcher-afk\aw-watcher-afk.exe' -`$windowExe = Join-Path `$installRoot 'aw-watcher-window\aw-watcher-window.exe' -`$serverArgs = @('--host', [string]`$config.server.host, '--port', [string]`$config.server.port) -`$powershellExe = Join-Path `$env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' -`$afkEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]`$config.collectors.afkEnabled } else { `$true } -`$windowEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]`$config.collectors.windowEnabled } else { `$true } -`$fileOpsEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { [bool]`$config.collectors.fileOpsEnabled } else { `$true } -`$emailEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'emailEnabled') { [bool]`$config.collectors.emailEnabled } else { `$false } -`$emailCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'emailCollectorScript') { [string]`$config.paths.emailCollectorScript } else { Join-Path `$stateRoot 'email-outbound-collector.ps1' } -`$launchLockPath = New-LaunchLock -StateRoot `$stateRoot -SessionId `$sessionId -if (-not `$launchLockPath) { - return -} - -try { - if (`$afkEnabled -and -not (Test-Path -LiteralPath `$afkExe)) { - throw "Не найден aw-watcher-afk.exe: `$afkExe" - } - - if (`$windowEnabled -and -not (Test-Path -LiteralPath `$windowExe)) { - throw "Не найден aw-watcher-window.exe: `$windowExe" - } - - if (`$afkEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-afk' -SessionId `$sessionId)) { - Start-Process -FilePath `$afkExe -ArgumentList `$serverArgs -WindowStyle Hidden - } - - if (`$windowEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-window' -SessionId `$sessionId)) { - Start-Process -FilePath `$windowExe -ArgumentList `$serverArgs -WindowStyle Hidden - } - - try { - Send-LogonMarkerIfNeeded -Config `$config -SessionId `$sessionId - } - catch { - } - Start-CollectorScriptIfNeeded -ScriptPath `$collectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - Start-CollectorScriptIfNeeded -ScriptPath `$endpointCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - if (`$fileOpsEnabled) { - Start-CollectorScriptIfNeeded -ScriptPath `$fileCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - } - Start-CollectorScriptIfNeeded -ScriptPath `$sessionCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - if (`$emailEnabled -and (Test-Path -LiteralPath `$emailCollectorScript)) { - Start-CollectorScriptIfNeeded -ScriptPath `$emailCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - } -} -finally { - if (`$launchLockPath -and (Test-Path -LiteralPath `$launchLockPath)) { - Remove-Item -LiteralPath `$launchLockPath -Force -ErrorAction SilentlyContinue - } -} -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding UTF8 -} - -function Write-ActivityWatchRecoveryScript { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $content = @" -param( - [string]`$ConfigPath = '$ConfigPath' -) - -Set-StrictMode -Version Latest -`$ErrorActionPreference = 'Continue' - -function Get-DeploymentConfig { - param([string]`$Path) - return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json -} - -function Get-RecoveryConfigPaths { - param([string]`$PrimaryConfigPath) - - `$paths = New-Object System.Collections.Generic.List[string] - if (`$PrimaryConfigPath -and (Test-Path -LiteralPath `$PrimaryConfigPath)) { - `$paths.Add((Resolve-Path -LiteralPath `$PrimaryConfigPath).Path) - } - - `$searchRoot = `$env:ProgramData - if (`$PrimaryConfigPath) { - `$stateRoot = Split-Path -Path `$PrimaryConfigPath -Parent - `$candidateRoot = Split-Path -Path `$stateRoot -Parent - if (`$candidateRoot -and (Test-Path -LiteralPath `$candidateRoot)) { - `$searchRoot = `$candidateRoot - } - } - - if (Test-Path -LiteralPath `$searchRoot) { - Get-ChildItem -LiteralPath `$searchRoot -Directory -ErrorAction SilentlyContinue | - Where-Object { `$_.Name -like 'ActivityWatch*' } | - ForEach-Object { - `$candidate = Join-Path `$_.FullName 'deployment-config.json' - if (Test-Path -LiteralPath `$candidate) { - `$paths.Add(`$candidate) - } - } - } - - return @(`$paths | Sort-Object -Unique) -} - -function Get-RecoveryTaskNames { - param([string[]]`$ConfigPaths) - - `$taskNames = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) - foreach (`$candidatePath in @(`$ConfigPaths)) { - try { - `$config = Get-DeploymentConfig -Path `$candidatePath - foreach (`$task in @(`$config.userTasks)) { - `$taskName = [string]`$task.launchTaskName - if (-not [string]::IsNullOrWhiteSpace(`$taskName)) { - [void]`$taskNames.Add(`$taskName) - } - } - } - catch { - } - } - - return @(`$taskNames) -} - -function New-RecoveryLock { - param([string]`$PrimaryConfigPath) - - `$stateRoot = if (`$PrimaryConfigPath) { Split-Path -Path `$PrimaryConfigPath -Parent } else { Join-Path `$env:ProgramData 'AWatch-rus' } - if (-not (Test-Path -LiteralPath `$stateRoot)) { - New-Item -Path `$stateRoot -ItemType Directory -Force | Out-Null - } - - `$lockPath = Join-Path `$stateRoot 'recovery-loop.lock' - if (Test-Path -LiteralPath `$lockPath) { - try { - `$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json - `$existingPid = [int]`$lockData.pid - if (`$existingPid -gt 0 -and (Get-Process -Id `$existingPid -ErrorAction SilentlyContinue)) { - return `$null - } - } - catch { - } - } - - `$payload = @{ - pid = `$PID - createdAt = (Get-Date).ToUniversalTime().ToString('o') - } | ConvertTo-Json -Compress - Set-Content -LiteralPath `$lockPath -Value `$payload -Encoding UTF8 - return `$lockPath -} - -function Start-TaskIfNotRunning { - param([string]`$TaskName) - if ([string]::IsNullOrWhiteSpace(`$TaskName)) { - return - } - - try { - `$task = Get-ScheduledTask -TaskName `$TaskName -ErrorAction SilentlyContinue - if (-not `$task) { - return - } - if ([string]`$task.State -eq 'Running') { - return - } - Start-ScheduledTask -TaskName `$TaskName -ErrorAction SilentlyContinue - } - catch { - } -} - -`$recoveryLockPath = New-RecoveryLock -PrimaryConfigPath `$ConfigPath -if (-not `$recoveryLockPath) { - return -} - -try { - while (`$true) { - `$sleepSeconds = 180 - try { - `$configPaths = Get-RecoveryConfigPaths -PrimaryConfigPath `$ConfigPath - foreach (`$taskName in Get-RecoveryTaskNames -ConfigPaths `$configPaths) { - Start-TaskIfNotRunning -TaskName `$taskName - } - - `$config = Get-DeploymentConfig -Path `$ConfigPath - if (`$config -and `$config.recovery -and `$config.recovery.intervalSeconds) { - `$sleepSeconds = [Math]::Max([int]`$config.recovery.intervalSeconds, 30) - } - } - catch { - } - - Start-Sleep -Seconds `$sleepSeconds - } -} -finally { - if (`$recoveryLockPath -and (Test-Path -LiteralPath `$recoveryLockPath)) { - Remove-Item -LiteralPath `$recoveryLockPath -Force -ErrorAction SilentlyContinue - } -} -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding UTF8 -} - -function Get-ActivityWatchHiddenLauncherPath { - param( - [Parameter(Mandatory = $true)] - [string]$ScriptPath - ) - - $directory = Split-Path -Path $ScriptPath -Parent - $baseName = [IO.Path]::GetFileNameWithoutExtension($ScriptPath) - return Join-Path $directory ("{0}-hidden.vbs" -f $baseName) -} - -function Write-ActivityWatchHiddenPowerShellWrapper { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $directory = Split-Path -Path $Path -Parent - if ($directory) { - New-ActivityWatchDirectory -Path $directory - } - - $powershellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' - $escapedPowerShellExe = $powershellExe.Replace('"', '""') - $escapedScriptPath = $ScriptPath.Replace('"', '""') - $escapedConfigPath = $ConfigPath.Replace('"', '""') - - $content = @" -Set shell = CreateObject("WScript.Shell") -shell.Run """$escapedPowerShellExe"" -NoProfile -ExecutionPolicy Bypass -File ""$escapedScriptPath"" -ConfigPath ""$escapedConfigPath""", 0, False -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding ASCII -} - -function Remove-LegacyActivityWatchEntries { - $legacyTaskNames = @( - 'ActivityWatch Watchers', - 'ActivityWatch Guard', - 'ActivityWatch Heal' - ) - - foreach ($taskName in $legacyTaskNames) { - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue - } - - $runKey = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run' - foreach ($name in 'ActivityWatchAFK', 'ActivityWatchWindow', 'ActivityWatchBrowserCollector') { - Remove-ItemProperty -Path $runKey -Name $name -ErrorAction SilentlyContinue - } -} - -function Remove-ActivityWatchScheduledTask { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName - ) - - Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue - & cmd.exe /c "schtasks /Delete /TN `"$TaskName`" /F >nul 2>&1" | Out-Null - - for ($attempt = 0; $attempt -lt 10; $attempt++) { - $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue - if (-not $task) { - return - } - - Start-Sleep -Milliseconds 300 - } -} - -function Set-ActivityWatchScheduledTaskAction { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [Parameter(Mandatory = $true)] - [string]$Execute, - [Parameter(Mandatory = $true)] - [string]$Arguments - ) - - $taskCommand = ('"{0}" {1}' -f $Execute, $Arguments) - & schtasks.exe /Change /TN $TaskName /TR $taskCommand | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "schtasks.exe /Change завершился с ошибкой для $TaskName" - } -} - -function Get-ActivityWatchScheduledTaskByCommand { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [string]$CommandMatch - ) - - $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue - if ($task) { - return $task - } - - if ([string]::IsNullOrWhiteSpace($CommandMatch)) { - return $null - } - - foreach ($candidate in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch*' })) { - foreach ($action in @($candidate.Actions)) { - if ([string]$action.Arguments -like "*$CommandMatch*") { - return $candidate - } - } - } - - return $null -} - -function Remove-StaleActivityWatchUserTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath - ) - - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath - $desiredTaskNames = @($TaskDefinitions | ForEach-Object { [string]$_.LaunchTaskName }) - - foreach ($candidate in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch*' })) { - $taskName = [string]$candidate.TaskName - if ($desiredTaskNames -contains $taskName) { - continue - } - - $usesCurrentLauncher = $false - foreach ($action in @($candidate.Actions)) { - if ([string]$action.Arguments -like "*$launcherPath*") { - $usesCurrentLauncher = $true - break - } - } - - if ($usesCurrentLauncher) { - Remove-ActivityWatchScheduledTask -TaskName $taskName - } - } -} - -function Register-ActivityWatchUserTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe' - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath - Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $LaunchScriptPath -ConfigPath $ConfigPath - Remove-StaleActivityWatchUserTasks -TaskDefinitions $TaskDefinitions -LaunchScriptPath $LaunchScriptPath - - foreach ($definition in $TaskDefinitions) { - $action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`"" - $trigger = New-ScheduledTaskTrigger -AtLogOn -User $definition.UserId - $principal = New-ScheduledTaskPrincipal -UserId $definition.UserId -LogonType Interactive -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0) - $existingTask = Get-ActivityWatchScheduledTaskByCommand -TaskName $definition.LaunchTaskName -CommandMatch $ConfigPath - - if ($existingTask) { - Set-ActivityWatchScheduledTaskAction -TaskName $existingTask.TaskName -Execute $wscriptExe -Arguments $action.Arguments - continue - } - - Remove-ActivityWatchScheduledTask -TaskName $definition.LaunchTaskName - Register-ScheduledTask -TaskName $definition.LaunchTaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null - } -} - -function Register-ActivityWatchRecoveryTask { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [Parameter(Mandatory = $true)] - [string]$RecoveryScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - Remove-ActivityWatchScheduledTask -TaskName $TaskName - - $wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe' - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $RecoveryScriptPath - Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $RecoveryScriptPath -ConfigPath $ConfigPath - $action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`"" - $trigger = New-ScheduledTaskTrigger -AtStartup - $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -Hidden -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0) - - Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null -} - -function Set-ActivityWatchAcl { - param( - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [Parameter(Mandatory = $true)] - [string]$LogsRoot - ) - - foreach ($path in $InstallRoot, $StateRoot, $LogsRoot) { - New-ActivityWatchDirectory -Path $path - } - - & icacls $InstallRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $InstallRoot" - } - - & icacls $StateRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $StateRoot" - } - - & icacls $LogsRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(M)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $LogsRoot" - } -} - -function Start-ActivityWatchTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [string]$RecoveryTaskName = 'ActivityWatch Recovery' - ) - - foreach ($definition in $TaskDefinitions) { - Start-ScheduledTask -TaskName $definition.LaunchTaskName -ErrorAction SilentlyContinue - } - - Start-ScheduledTask -TaskName $RecoveryTaskName -ErrorAction SilentlyContinue -} - -Export-ModuleMember -Function *-ActivityWatch*, Assert-Administrator, Normalize-ActivityWatchUsers, Get-ActivityWatchPackageUrl, Remove-LegacyActivityWatchEntries -; } - } - - return @(`$taskNames) -} - -function New-RecoveryLock { - param([string]`$PrimaryConfigPath) - - `$stateRoot = if (`$PrimaryConfigPath) { Split-Path -Path `$PrimaryConfigPath -Parent } else { Join-Path `$env:ProgramData 'AWatch-rus' } - if (-not (Test-Path -LiteralPath `$stateRoot)) { - New-Item -Path `$stateRoot -ItemType Directory -Force | Out-Null - } - - `$lockPath = Join-Path `$stateRoot 'recovery-loop.lock' - if (Test-Path -LiteralPath `$lockPath) { - try { - `$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json - `$existingPid = [int]`$lockData.pid - if (`$existingPid -gt 0 -and (Get-Process -Id `$existingPid -ErrorAction SilentlyContinue)) { - return `$null - } - } - catch { Write-Error Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Assert-Administrator { - $identity = [Security.Principal.WindowsIdentity]::GetCurrent() - $principal = [Security.Principal.WindowsPrincipal]::new($identity) - if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - throw 'Запустите этот скрипт из PowerShell с правами администратора.' - } -} - -function New-ActivityWatchDirectory { - param( - [Parameter(Mandatory = $true)] - [string]$Path - ) - - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Enable-ActivityWatchPrintTelemetry { - $policyPath = 'HKLM:\Software\Policies\Microsoft\Windows NT\Printers' - if (-not (Test-Path -LiteralPath $policyPath)) { - New-Item -Path $policyPath -Force | Out-Null - } - New-ItemProperty -Path $policyPath -Name 'ShowJobTitleInEventLogs' -Value 1 -PropertyType DWord -Force | Out-Null - - & wevtutil.exe sl 'Microsoft-Windows-PrintService/Operational' /e:true | Out-Null -} - - -function Get-ActivityWatchPackageUrl { - param( - [string]$Version = 'v0.13.2' - ) - - return "https://github.com/ActivityWatch/activitywatch/releases/download/$Version/activitywatch-$Version-windows-x86_64.zip" -} - -function Get-ActivityWatchArchive { - param( - [string]$PackageZipPath, - [string]$PackageUrl, - [string]$Version = 'v0.13.2', - [Parameter(Mandatory = $true)] - [string]$WorkingRoot - ) - - New-ActivityWatchDirectory -Path $WorkingRoot - - if ($PackageZipPath) { - $resolved = Resolve-Path -LiteralPath $PackageZipPath -ErrorAction Stop - return $resolved.Path - } - - if (-not $PackageUrl) { - $PackageUrl = Get-ActivityWatchPackageUrl -Version $Version - } - - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' - $suffix = ([guid]::NewGuid().Guid.Substring(0, 8)) - $archivePath = Join-Path $WorkingRoot ("activitywatch-{0}-{1}-{2}.zip" -f $Version.TrimStart('v'), $stamp, $suffix) - Invoke-WebRequest -Uri $PackageUrl -OutFile $archivePath - return $archivePath -} - -function Get-ActivityWatchPackageRoot { - param( - [Parameter(Mandatory = $true)] - [string]$ExpandedRoot - ) - - $afkBinary = Get-ChildItem -Path $ExpandedRoot -Filter 'aw-watcher-afk.exe' -File -Recurse | - Select-Object -First 1 - - if (-not $afkBinary) { - throw "Не удалось найти aw-watcher-afk.exe в $ExpandedRoot." - } - - return (Split-Path -Path (Split-Path -Path $afkBinary.FullName -Parent) -Parent) -} - -function Install-ActivityWatchPackage { - param( - [Parameter(Mandatory = $true)] - [string]$ArchivePath, - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$WorkingRoot, - [Parameter(Mandatory = $true)] - [string]$BackupRoot - ) - - New-ActivityWatchDirectory -Path $WorkingRoot - New-ActivityWatchDirectory -Path $BackupRoot - - # Ensure nothing is holding locks inside InstallRoot during upgrade. - foreach ($procName in @('aw-watcher-afk', 'aw-watcher-window', 'aw-server', 'aw-qt')) { - try { - Get-Process -Name $procName -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue - } - catch { - } - } - Start-Sleep -Seconds 2 - - $extractRoot = Join-Path $WorkingRoot ('extract-' + [guid]::NewGuid().Guid) - if (Test-Path -LiteralPath $extractRoot) { - Remove-Item -LiteralPath $extractRoot -Recurse -Force - } - New-ActivityWatchDirectory -Path $extractRoot - - Expand-Archive -Path $ArchivePath -DestinationPath $extractRoot -Force - $packageRoot = Get-ActivityWatchPackageRoot -ExpandedRoot $extractRoot - - if (Test-Path -LiteralPath $InstallRoot) { - $existingItems = Get-ChildItem -LiteralPath $InstallRoot -Force -ErrorAction SilentlyContinue - if ($existingItems) { - $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' - $backupPath = Join-Path $BackupRoot ("install-$stamp") - New-ActivityWatchDirectory -Path $backupPath - Copy-Item -Path (Join-Path $InstallRoot '*') -Destination $backupPath -Recurse -Force - Get-ChildItem -LiteralPath $InstallRoot -Force | Remove-Item -Recurse -Force - } - } - else { - New-ActivityWatchDirectory -Path $InstallRoot - } - - Copy-Item -Path (Join-Path $packageRoot '*') -Destination $InstallRoot -Recurse -Force - - return [pscustomobject]@{ - PackageRoot = $packageRoot - ExtractRoot = $extractRoot - BackupRoot = $BackupRoot - } -} - -function Get-ActivityWatchExecutableMap { - param( - [Parameter(Mandatory = $true)] - [string]$InstallRoot - ) - - $map = [ordered]@{ - Afk = Join-Path $InstallRoot 'aw-watcher-afk\aw-watcher-afk.exe' - Window = Join-Path $InstallRoot 'aw-watcher-window\aw-watcher-window.exe' - } - - foreach ($entry in $map.GetEnumerator()) { - if (-not (Test-Path -LiteralPath $entry.Value)) { - throw "Не найден обязательный исполняемый файл ActivityWatch: $($entry.Value)" - } - } - - return [pscustomobject]$map -} - -function Normalize-ActivityWatchUsers { - param( - [string[]]$Users, - [string]$UserListPath, - [string]$Domain - ) - - $collected = New-Object System.Collections.Generic.List[string] - - if ($Users) { - foreach ($user in $Users) { - if (-not [string]::IsNullOrWhiteSpace($user)) { - $collected.Add($user.Trim()) - } - } - } - - if ($UserListPath) { - $resolved = Resolve-Path -LiteralPath $UserListPath -ErrorAction Stop - $extension = [IO.Path]::GetExtension($resolved.Path) - if ($extension -ieq '.csv') { - $rows = Import-Csv -LiteralPath $resolved.Path - foreach ($row in $rows) { - foreach ($column in 'User', 'Username', 'SamAccountName', 'Login') { - if ($row.PSObject.Properties.Name -contains $column) { - $value = [string]$row.$column - if (-not [string]::IsNullOrWhiteSpace($value)) { - $collected.Add($value.Trim()) - break - } - } - } - } - } - else { - Get-Content -LiteralPath $resolved.Path | ForEach-Object { - $line = $_.Trim() - if ($line -and -not $line.StartsWith('#')) { - $collected.Add($line) - } - } - } - } - - $normalized = $collected | - Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | - ForEach-Object { - if ($Domain -and ($_ -notmatch '[\\@]')) { - '{0}\{1}' -f $Domain, $_ - } - else { - $_ - } - } | - Sort-Object -Unique - - if (-not $normalized -or $normalized.Count -eq 0) { - throw 'Не удалось определить целевых пользователей. Укажите -Users или -UserListPath.' - } - - return @($normalized) -} - -function Get-ActivityWatchTaskNameToken { - param( - [Parameter(Mandatory = $true)] - [string]$UserId - ) - - $buffer = [Text.StringBuilder]::new() - foreach ($character in $UserId.ToCharArray()) { - if ([char]::IsLetterOrDigit($character)) { - [void]$buffer.Append($character) - } - else { - [void]$buffer.Append('_') - } - } - - return $buffer.ToString().Trim('_') -} - -function New-ActivityWatchUserTaskDefinitions { - param( - [Parameter(Mandatory = $true)] - [string[]]$Users - ) - - $result = foreach ($user in $Users) { - $token = Get-ActivityWatchTaskNameToken -UserId $user - [pscustomobject]@{ - UserId = $user - LaunchTaskName = "ActivityWatch Launch [$token]" - } - } - - return @($result) -} - -function Copy-ActivityWatchCollectorAssets { - param( - [Parameter(Mandatory = $true)] - [string]$CollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$EndpointCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$FileCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$SessionCollectorScriptSource, - [string]$EmailCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$ExampleRulesSource, - [Parameter(Mandatory = $true)] - [string]$ExamplePolicySource, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [string]$CustomRulesSource, - [string]$CustomPolicySource - ) - - New-ActivityWatchDirectory -Path $StateRoot - - $collectorTarget = Join-Path $StateRoot 'browser-domains-native-collector.ps1' - $endpointCollectorTarget = Join-Path $StateRoot 'dlp-endpoint-signals-collector.ps1' - $fileCollectorTarget = Join-Path $StateRoot 'file-operations-collector.ps1' - $sessionCollectorTarget = Join-Path $StateRoot 'worktime-session-collector.ps1' - $emailCollectorTarget = Join-Path $StateRoot 'email-outbound-collector.ps1' - $exampleRulesTarget = Join-Path $StateRoot 'web-category-rules.example.json' - $rulesTarget = Join-Path $StateRoot 'web-category-rules.json' - $examplePolicyTarget = Join-Path $StateRoot 'dlp-policy.example.json' - $policyTarget = Join-Path $StateRoot 'dlp-policy.json' - - Copy-Item -LiteralPath $CollectorScriptSource -Destination $collectorTarget -Force - Copy-Item -LiteralPath $EndpointCollectorScriptSource -Destination $endpointCollectorTarget -Force - Copy-Item -LiteralPath $FileCollectorScriptSource -Destination $fileCollectorTarget -Force - Copy-Item -LiteralPath $SessionCollectorScriptSource -Destination $sessionCollectorTarget -Force - if ($EmailCollectorScriptSource -and (Test-Path -LiteralPath $EmailCollectorScriptSource)) { - Copy-Item -LiteralPath $EmailCollectorScriptSource -Destination $emailCollectorTarget -Force - } - Copy-Item -LiteralPath $ExampleRulesSource -Destination $exampleRulesTarget -Force - Copy-Item -LiteralPath $ExamplePolicySource -Destination $examplePolicyTarget -Force - - if ($CustomRulesSource) { - $resolvedRules = Resolve-Path -LiteralPath $CustomRulesSource -ErrorAction Stop - Copy-Item -LiteralPath $resolvedRules.Path -Destination $rulesTarget -Force - } - else { - Copy-Item -LiteralPath $exampleRulesTarget -Destination $rulesTarget -Force - } - - if ($CustomPolicySource) { - $resolvedPolicy = Resolve-Path -LiteralPath $CustomPolicySource -ErrorAction Stop - Copy-Item -LiteralPath $resolvedPolicy.Path -Destination $policyTarget -Force - } - else { - Copy-Item -LiteralPath $examplePolicyTarget -Destination $policyTarget -Force - } - - return [pscustomobject]@{ - CollectorScript = $collectorTarget - EndpointCollectorScript = $endpointCollectorTarget - FileCollectorScript = $fileCollectorTarget - SessionCollectorScript = $sessionCollectorTarget - EmailCollectorScript = $emailCollectorTarget - ExampleRules = $exampleRulesTarget - ActiveRules = $rulesTarget - ExamplePolicy = $examplePolicyTarget - ActivePolicy = $policyTarget - } -} - -function New-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [string]$ServerHost, - [Parameter(Mandatory = $true)] - [int]$ServerPort, - [Parameter(Mandatory = $true)] - [string]$ServerScheme, - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [Parameter(Mandatory = $true)] - [string]$LogsRoot, - [Parameter(Mandatory = $true)] - [string]$CollectorScript, - [Parameter(Mandatory = $true)] - [string]$EndpointCollectorScript, - [Parameter(Mandatory = $true)] - [string]$FileCollectorScript, - [Parameter(Mandatory = $true)] - [string]$SessionCollectorScript, - [string]$EmailCollectorScript, - [Parameter(Mandatory = $true)] - [string]$RulesPath, - [Parameter(Mandatory = $true)] - [string]$PolicyPath, - [Parameter(Mandatory = $true)] - [int]$PollSeconds, - [Parameter(Mandatory = $true)] - [int]$PulseSeconds, - [Parameter(Mandatory = $true)] - [int]$RecoveryIntervalSeconds, - [bool]$AfkEnabled = $true, - [bool]$WindowEnabled = $true, - [bool]$FileOpsEnabled = $true, - [bool]$LocalAgentLogsEnabled = $true, - [bool]$IncidentCaptureEnabled = $true, - [bool]$IncidentScreenshotEnabled = $true, - [string]$IncidentArtifactsRoot, - [bool]$LogonMarkerEnabled = $true, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath, - [Parameter(Mandatory = $true)] - [string]$RecoveryScriptPath, - [Parameter(Mandatory = $true)] - [pscustomobject[]]$UserTasks, - [string]$PackageVersion = 'v0.13.2' - ) - - $effectiveIncidentArtifactsRoot = if ($IncidentArtifactsRoot) { $IncidentArtifactsRoot } else { Join-Path $StateRoot 'incident-artifacts' } - - return [pscustomobject]@{ - version = 1 - generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') - server = [pscustomobject]@{ - host = $ServerHost - port = $ServerPort - scheme = $ServerScheme - } - paths = [pscustomobject]@{ - installRoot = $InstallRoot - stateRoot = $StateRoot - logsRoot = $LogsRoot - collectorScript = $CollectorScript - endpointCollectorScript = $EndpointCollectorScript - emailCollectorScript = $EmailCollectorScript - fileCollectorScript = $FileCollectorScript - sessionCollectorScript = $SessionCollectorScript - rulesPath = $RulesPath - policyPath = $PolicyPath - launchScript = $LaunchScriptPath - recoveryScript = $RecoveryScriptPath - } - collector = [pscustomobject]@{ - pollSeconds = $PollSeconds - pulseSeconds = $PulseSeconds - } - collectors = [pscustomobject]@{ - afkEnabled = $AfkEnabled - windowEnabled = $WindowEnabled - fileOpsEnabled = $FileOpsEnabled - emailEnabled = ($null -ne $EmailCollectorScript -and $EmailCollectorScript -ne '') - } - logging = [pscustomobject]@{ - localAgentLogsEnabled = $LocalAgentLogsEnabled - } - incidentCapture = [pscustomobject]@{ - enabled = $IncidentCaptureEnabled - screenshotEnabled = $IncidentScreenshotEnabled - artifactsRoot = $effectiveIncidentArtifactsRoot - } - sessionEvents = [pscustomobject]@{ - logonEnabled = $LogonMarkerEnabled - bucketPrefix = 'aw-session-events' - } - recovery = [pscustomobject]@{ - intervalSeconds = $RecoveryIntervalSeconds - taskName = 'ActivityWatch Recovery' - } - dlp = [pscustomobject]@{ - incidentBucketPrefix = 'aw-dlp-incidents' - enabled = $true - } - package = [pscustomobject]@{ - version = $PackageVersion - } - userTasks = @($UserTasks) - } -} - -function Write-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [pscustomobject]$Config, - [Parameter(Mandatory = $true)] - [string]$Path - ) - - $directory = Split-Path -Path $Path -Parent - if ($directory) { - New-ActivityWatchDirectory -Path $directory - } - - $json = $Config | ConvertTo-Json -Depth 8 - Set-Content -LiteralPath $Path -Value $json -Encoding UTF8 -} - -function Read-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [string]$Path - ) - - if (-not (Test-Path -LiteralPath $Path)) { - throw "Конфигурация развёртывания не найдена: $Path" - } - - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json -} - -function Write-ActivityWatchLaunchScript { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $content = @" -param( - [string]`$ConfigPath = '$ConfigPath' -) - -Set-StrictMode -Version Latest -`$ErrorActionPreference = 'Stop' - -[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -Add-Type -AssemblyName System.Net.Http -`$script:MaxCollectorPowerShellProcesses = 24 - -function Get-DeploymentConfig { - param([string]`$Path) - return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json -} - -function Test-ProcessInSession { - param( - [string]`$Name, - [int]`$SessionId - ) - - return [bool](Get-Process -Name `$Name -ErrorAction SilentlyContinue | Where-Object { `$_.SessionId -eq `$SessionId } | Select-Object -First 1) -} - -function Test-CollectorRunning { - param( - [string]`$ScriptPath, - [int]`$SessionId - ) - - `$escapedCollector = [Regex]::Escape(`$ScriptPath) - `$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - (`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and - `$_.SessionId -eq `$SessionId -and - `$_.CommandLine -match `$escapedCollector - } - - return [bool](`$processes | Select-Object -First 1) -} - -function Get-CollectorPowerShellProcessCount { - `$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - (`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and - `$_.CommandLine -match 'AWatch-rus' -and - `$_.CommandLine -match '\.ps1' - } - - return @(`$processes).Count -} - -function New-LaunchLock { - param([string]`$StateRoot, [int]`$SessionId) - - `$lockPath = Join-Path `$env:TEMP ("launch-watchers-session-{0}.lock" -f `$SessionId) - if (Test-Path -LiteralPath `$lockPath) { - try { - `$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json - `$existingPid = [int]`$lockData.pid - if (`$existingPid -gt 0 -and (Get-Process -Id `$existingPid -ErrorAction SilentlyContinue)) { - return `$null - } - } - catch { - } - } - - `$payload = @{ - pid = `$PID - sessionId = `$SessionId - createdAt = (Get-Date).ToUniversalTime().ToString('o') - } | ConvertTo-Json -Compress - Set-Content -LiteralPath `$lockPath -Value `$payload -Encoding UTF8 - return `$lockPath -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = `$true)][string]`$Uri, - [Parameter(Mandatory = `$true)][string]`$Json - ) - - `$httpClient = New-Object System.Net.Http.HttpClient - try { - `$content = New-Object System.Net.Http.StringContent(`$Json, [System.Text.Encoding]::UTF8, 'application/json') - `$response = `$httpClient.PostAsync(`$Uri, `$content).Result - if (-not `$response.IsSuccessStatusCode) { - return `$false - } - return `$true - } - catch { - return `$false - } - finally { - `$httpClient.Dispose() - } -} - -function Ensure-Bucket { - param( - [string]`$BucketId, - [string]`$ClientName, - [string]`$BucketType - ) - - if (`$script:KnownBuckets.ContainsKey(`$BucketId)) { - return - } - - try { - Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null - `$script:KnownBuckets[`$BucketId] = `$true - return - } - catch { - } - - `$body = @{ - client = `$ClientName - type = `$BucketType - hostname = `$script:Hostname - } | ConvertTo-Json -Compress - - try { - if (-not (Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" -Json `$body)) { - return - } - } - catch { - try { - Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null - } - catch { - return - } - } - - `$script:KnownBuckets[`$BucketId] = `$true -} - -function Send-LogonMarkerIfNeeded { - param( - [pscustomobject]`$Config, - [int]`$SessionId - ) - - `$sessionEvents = if (`$Config.PSObject.Properties.Name -contains 'sessionEvents') { `$Config.sessionEvents } else { `$null } - `$logging = if (`$Config.PSObject.Properties.Name -contains 'logging') { `$Config.logging } else { `$null } - `$logonEnabled = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'logonEnabled') { [bool]`$sessionEvents.logonEnabled } else { `$false } - if (-not `$logonEnabled) { - return - } - - `$bucketPrefix = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'bucketPrefix' -and -not [string]::IsNullOrWhiteSpace([string]`$sessionEvents.bucketPrefix)) { - [string]`$sessionEvents.bucketPrefix - } - else { - 'aw-session-events' - } - - `$stateRoot = [string]`$Config.paths.stateRoot - `$markerRoots = New-Object System.Collections.Generic.List[string] - if (-not [string]::IsNullOrWhiteSpace(`$env:LOCALAPPDATA)) { - `$markerRoots.Add((Join-Path `$env:LOCALAPPDATA 'AWatch-rus\markers')) - } - if (-not [string]::IsNullOrWhiteSpace(`$stateRoot)) { - `$markerRoots.Add((Join-Path `$stateRoot 'markers')) - } - - `$markerDir = `$null - foreach (`$candidate in `$markerRoots) { - try { - if (-not (Test-Path -LiteralPath `$candidate)) { - New-Item -Path `$candidate -ItemType Directory -Force | Out-Null - } - - `$probePath = Join-Path `$candidate 'write-test.tmp' - Set-Content -LiteralPath `$probePath -Value 'ok' -Encoding ASCII - Remove-Item -LiteralPath `$probePath -Force -ErrorAction SilentlyContinue - `$markerDir = `$candidate - break - } - catch { - } - } - - if (-not `$markerDir) { - return - } - - `$markerFile = Join-Path `$markerDir ("logon-{0}-{1}.marker" -f `$env:USERNAME, `$SessionId) - if (Test-Path -LiteralPath `$markerFile) { - return - } - - Set-Content -LiteralPath `$markerFile -Value ((Get-Date).ToUniversalTime().ToString('o')) -Encoding UTF8 - - `$bucketId = ('{0}_{1}' -f `$bucketPrefix, `$script:Hostname) - Ensure-Bucket -BucketId `$bucketId -ClientName 'aw-session-events' -BucketType 'aw.session.event' - - `$payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - eventType = 'logon' - username = `$env:USERNAME - userId = "`$(`$env:USERDOMAIN)\`$(`$env:USERNAME)" - sessionId = `$SessionId - hostname = `$script:Hostname - source = 'launch-watchers-awatch-rus' - } - } | ConvertTo-Json -Depth 5 -Compress - - try { - Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$bucketId/heartbeat?pulsetime=1" -Json `$payload - } - catch { - Remove-Item -LiteralPath `$markerFile -Force -ErrorAction SilentlyContinue - throw - } -} - -function Start-CollectorScriptIfNeeded { - param( - [string]`$ScriptPath, - [string]`$ConfigPath, - [string]`$PowerShellExe, - [int]`$SessionId - ) - - if ([string]::IsNullOrWhiteSpace(`$ScriptPath)) { - return - } - - if (-not (Test-Path -LiteralPath `$ScriptPath)) { - return - } - - if (Test-CollectorRunning -ScriptPath `$ScriptPath -SessionId `$SessionId) { - return - } - - if ((Get-CollectorPowerShellProcessCount) -ge `$script:MaxCollectorPowerShellProcesses) { - return - } - - `$staParam = if (`$ScriptPath -like "*endpoint-signals*") { "-STA" } else { `$null } - `$argumentList = @('-NoProfile', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass') - if (`$staParam) { `$argumentList += `$staParam } - `$argumentList += @('-File', `$ScriptPath, '-ConfigPath', `$ConfigPath) - Start-Process -FilePath `$PowerShellExe -ArgumentList `$argumentList -WindowStyle Hidden -} - -`$config = Get-DeploymentConfig -Path `$ConfigPath -`$sessionId = (Get-Process -Id `$PID).SessionId -`$installRoot = [string]`$config.paths.installRoot -`$stateRoot = [string]`$config.paths.stateRoot -`$script:ApiBase = '{0}://{1}:{2}/api/0' -f [string]`$config.server.scheme, [string]`$config.server.host, [string]`$config.server.port -`$script:Hostname = `$env:COMPUTERNAME -`$script:KnownBuckets = @{} -`$collectorScript = [string]`$config.paths.collectorScript -`$endpointCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]`$config.paths.endpointCollectorScript } else { Join-Path `$stateRoot 'dlp-endpoint-signals-collector.ps1' } -`$fileCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'fileCollectorScript') { [string]`$config.paths.fileCollectorScript } else { Join-Path `$stateRoot 'file-operations-collector.ps1' } -`$sessionCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]`$config.paths.sessionCollectorScript } else { Join-Path `$stateRoot 'worktime-session-collector.ps1' } -`$afkExe = Join-Path `$installRoot 'aw-watcher-afk\aw-watcher-afk.exe' -`$windowExe = Join-Path `$installRoot 'aw-watcher-window\aw-watcher-window.exe' -`$serverArgs = @('--host', [string]`$config.server.host, '--port', [string]`$config.server.port) -`$powershellExe = Join-Path `$env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' -`$afkEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]`$config.collectors.afkEnabled } else { `$true } -`$windowEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]`$config.collectors.windowEnabled } else { `$true } -`$fileOpsEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { [bool]`$config.collectors.fileOpsEnabled } else { `$true } -`$emailEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'emailEnabled') { [bool]`$config.collectors.emailEnabled } else { `$false } -`$emailCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'emailCollectorScript') { [string]`$config.paths.emailCollectorScript } else { Join-Path `$stateRoot 'email-outbound-collector.ps1' } -`$launchLockPath = New-LaunchLock -StateRoot `$stateRoot -SessionId `$sessionId -if (-not `$launchLockPath) { - return -} - -try { - if (`$afkEnabled -and -not (Test-Path -LiteralPath `$afkExe)) { - throw "Не найден aw-watcher-afk.exe: `$afkExe" - } - - if (`$windowEnabled -and -not (Test-Path -LiteralPath `$windowExe)) { - throw "Не найден aw-watcher-window.exe: `$windowExe" - } - - if (`$afkEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-afk' -SessionId `$sessionId)) { - Start-Process -FilePath `$afkExe -ArgumentList `$serverArgs -WindowStyle Hidden - } - - if (`$windowEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-window' -SessionId `$sessionId)) { - Start-Process -FilePath `$windowExe -ArgumentList `$serverArgs -WindowStyle Hidden - } - - try { - Send-LogonMarkerIfNeeded -Config `$config -SessionId `$sessionId - } - catch { - } - Start-CollectorScriptIfNeeded -ScriptPath `$collectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - Start-CollectorScriptIfNeeded -ScriptPath `$endpointCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - if (`$fileOpsEnabled) { - Start-CollectorScriptIfNeeded -ScriptPath `$fileCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - } - Start-CollectorScriptIfNeeded -ScriptPath `$sessionCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - if (`$emailEnabled -and (Test-Path -LiteralPath `$emailCollectorScript)) { - Start-CollectorScriptIfNeeded -ScriptPath `$emailCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - } -} -finally { - if (`$launchLockPath -and (Test-Path -LiteralPath `$launchLockPath)) { - Remove-Item -LiteralPath `$launchLockPath -Force -ErrorAction SilentlyContinue - } -} -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding UTF8 -} - -function Write-ActivityWatchRecoveryScript { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $content = @" -param( - [string]`$ConfigPath = '$ConfigPath' -) - -Set-StrictMode -Version Latest -`$ErrorActionPreference = 'Continue' - -function Get-DeploymentConfig { - param([string]`$Path) - return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json -} - -function Get-RecoveryConfigPaths { - param([string]`$PrimaryConfigPath) - - `$paths = New-Object System.Collections.Generic.List[string] - if (`$PrimaryConfigPath -and (Test-Path -LiteralPath `$PrimaryConfigPath)) { - `$paths.Add((Resolve-Path -LiteralPath `$PrimaryConfigPath).Path) - } - - `$searchRoot = `$env:ProgramData - if (`$PrimaryConfigPath) { - `$stateRoot = Split-Path -Path `$PrimaryConfigPath -Parent - `$candidateRoot = Split-Path -Path `$stateRoot -Parent - if (`$candidateRoot -and (Test-Path -LiteralPath `$candidateRoot)) { - `$searchRoot = `$candidateRoot - } - } - - if (Test-Path -LiteralPath `$searchRoot) { - Get-ChildItem -LiteralPath `$searchRoot -Directory -ErrorAction SilentlyContinue | - Where-Object { `$_.Name -like 'ActivityWatch*' } | - ForEach-Object { - `$candidate = Join-Path `$_.FullName 'deployment-config.json' - if (Test-Path -LiteralPath `$candidate) { - `$paths.Add(`$candidate) - } - } - } - - return @(`$paths | Sort-Object -Unique) -} - -function Get-RecoveryTaskNames { - param([string[]]`$ConfigPaths) - - `$taskNames = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) - foreach (`$candidatePath in @(`$ConfigPaths)) { - try { - `$config = Get-DeploymentConfig -Path `$candidatePath - foreach (`$task in @(`$config.userTasks)) { - `$taskName = [string]`$task.launchTaskName - if (-not [string]::IsNullOrWhiteSpace(`$taskName)) { - [void]`$taskNames.Add(`$taskName) - } - } - } - catch { - } - } - - return @(`$taskNames) -} - -function New-RecoveryLock { - param([string]`$PrimaryConfigPath) - - `$stateRoot = if (`$PrimaryConfigPath) { Split-Path -Path `$PrimaryConfigPath -Parent } else { Join-Path `$env:ProgramData 'AWatch-rus' } - if (-not (Test-Path -LiteralPath `$stateRoot)) { - New-Item -Path `$stateRoot -ItemType Directory -Force | Out-Null - } - - `$lockPath = Join-Path `$stateRoot 'recovery-loop.lock' - if (Test-Path -LiteralPath `$lockPath) { - try { - `$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json - `$existingPid = [int]`$lockData.pid - if (`$existingPid -gt 0 -and (Get-Process -Id `$existingPid -ErrorAction SilentlyContinue)) { - return `$null - } - } - catch { - } - } - - `$payload = @{ - pid = `$PID - createdAt = (Get-Date).ToUniversalTime().ToString('o') - } | ConvertTo-Json -Compress - Set-Content -LiteralPath `$lockPath -Value `$payload -Encoding UTF8 - return `$lockPath -} - -function Start-TaskIfNotRunning { - param([string]`$TaskName) - if ([string]::IsNullOrWhiteSpace(`$TaskName)) { - return - } - - try { - `$task = Get-ScheduledTask -TaskName `$TaskName -ErrorAction SilentlyContinue - if (-not `$task) { - return - } - if ([string]`$task.State -eq 'Running') { - return - } - Start-ScheduledTask -TaskName `$TaskName -ErrorAction SilentlyContinue - } - catch { - } -} - -`$recoveryLockPath = New-RecoveryLock -PrimaryConfigPath `$ConfigPath -if (-not `$recoveryLockPath) { - return -} - -try { - while (`$true) { - `$sleepSeconds = 180 - try { - `$configPaths = Get-RecoveryConfigPaths -PrimaryConfigPath `$ConfigPath - foreach (`$taskName in Get-RecoveryTaskNames -ConfigPaths `$configPaths) { - Start-TaskIfNotRunning -TaskName `$taskName - } - - `$config = Get-DeploymentConfig -Path `$ConfigPath - if (`$config -and `$config.recovery -and `$config.recovery.intervalSeconds) { - `$sleepSeconds = [Math]::Max([int]`$config.recovery.intervalSeconds, 30) - } - } - catch { - } - - Start-Sleep -Seconds `$sleepSeconds - } -} -finally { - if (`$recoveryLockPath -and (Test-Path -LiteralPath `$recoveryLockPath)) { - Remove-Item -LiteralPath `$recoveryLockPath -Force -ErrorAction SilentlyContinue - } -} -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding UTF8 -} - -function Get-ActivityWatchHiddenLauncherPath { - param( - [Parameter(Mandatory = $true)] - [string]$ScriptPath - ) - - $directory = Split-Path -Path $ScriptPath -Parent - $baseName = [IO.Path]::GetFileNameWithoutExtension($ScriptPath) - return Join-Path $directory ("{0}-hidden.vbs" -f $baseName) -} - -function Write-ActivityWatchHiddenPowerShellWrapper { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $directory = Split-Path -Path $Path -Parent - if ($directory) { - New-ActivityWatchDirectory -Path $directory - } - - $powershellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' - $escapedPowerShellExe = $powershellExe.Replace('"', '""') - $escapedScriptPath = $ScriptPath.Replace('"', '""') - $escapedConfigPath = $ConfigPath.Replace('"', '""') - - $content = @" -Set shell = CreateObject("WScript.Shell") -shell.Run """$escapedPowerShellExe"" -NoProfile -ExecutionPolicy Bypass -File ""$escapedScriptPath"" -ConfigPath ""$escapedConfigPath""", 0, False -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding ASCII -} - -function Remove-LegacyActivityWatchEntries { - $legacyTaskNames = @( - 'ActivityWatch Watchers', - 'ActivityWatch Guard', - 'ActivityWatch Heal' - ) - - foreach ($taskName in $legacyTaskNames) { - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue - } - - $runKey = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run' - foreach ($name in 'ActivityWatchAFK', 'ActivityWatchWindow', 'ActivityWatchBrowserCollector') { - Remove-ItemProperty -Path $runKey -Name $name -ErrorAction SilentlyContinue - } -} - -function Remove-ActivityWatchScheduledTask { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName - ) - - Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue - & cmd.exe /c "schtasks /Delete /TN `"$TaskName`" /F >nul 2>&1" | Out-Null - - for ($attempt = 0; $attempt -lt 10; $attempt++) { - $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue - if (-not $task) { - return - } - - Start-Sleep -Milliseconds 300 - } -} - -function Set-ActivityWatchScheduledTaskAction { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [Parameter(Mandatory = $true)] - [string]$Execute, - [Parameter(Mandatory = $true)] - [string]$Arguments - ) - - $taskCommand = ('"{0}" {1}' -f $Execute, $Arguments) - & schtasks.exe /Change /TN $TaskName /TR $taskCommand | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "schtasks.exe /Change завершился с ошибкой для $TaskName" - } -} - -function Get-ActivityWatchScheduledTaskByCommand { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [string]$CommandMatch - ) - - $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue - if ($task) { - return $task - } - - if ([string]::IsNullOrWhiteSpace($CommandMatch)) { - return $null - } - - foreach ($candidate in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch*' })) { - foreach ($action in @($candidate.Actions)) { - if ([string]$action.Arguments -like "*$CommandMatch*") { - return $candidate - } - } - } - - return $null -} - -function Remove-StaleActivityWatchUserTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath - ) - - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath - $desiredTaskNames = @($TaskDefinitions | ForEach-Object { [string]$_.LaunchTaskName }) - - foreach ($candidate in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch*' })) { - $taskName = [string]$candidate.TaskName - if ($desiredTaskNames -contains $taskName) { - continue - } - - $usesCurrentLauncher = $false - foreach ($action in @($candidate.Actions)) { - if ([string]$action.Arguments -like "*$launcherPath*") { - $usesCurrentLauncher = $true - break - } - } - - if ($usesCurrentLauncher) { - Remove-ActivityWatchScheduledTask -TaskName $taskName - } - } -} - -function Register-ActivityWatchUserTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe' - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath - Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $LaunchScriptPath -ConfigPath $ConfigPath - Remove-StaleActivityWatchUserTasks -TaskDefinitions $TaskDefinitions -LaunchScriptPath $LaunchScriptPath - - foreach ($definition in $TaskDefinitions) { - $action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`"" - $trigger = New-ScheduledTaskTrigger -AtLogOn -User $definition.UserId - $principal = New-ScheduledTaskPrincipal -UserId $definition.UserId -LogonType Interactive -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0) - $existingTask = Get-ActivityWatchScheduledTaskByCommand -TaskName $definition.LaunchTaskName -CommandMatch $ConfigPath - - if ($existingTask) { - Set-ActivityWatchScheduledTaskAction -TaskName $existingTask.TaskName -Execute $wscriptExe -Arguments $action.Arguments - continue - } - - Remove-ActivityWatchScheduledTask -TaskName $definition.LaunchTaskName - Register-ScheduledTask -TaskName $definition.LaunchTaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null - } -} - -function Register-ActivityWatchRecoveryTask { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [Parameter(Mandatory = $true)] - [string]$RecoveryScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - Remove-ActivityWatchScheduledTask -TaskName $TaskName - - $wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe' - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $RecoveryScriptPath - Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $RecoveryScriptPath -ConfigPath $ConfigPath - $action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`"" - $trigger = New-ScheduledTaskTrigger -AtStartup - $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -Hidden -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0) - - Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null -} - -function Set-ActivityWatchAcl { - param( - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [Parameter(Mandatory = $true)] - [string]$LogsRoot - ) - - foreach ($path in $InstallRoot, $StateRoot, $LogsRoot) { - New-ActivityWatchDirectory -Path $path - } - - & icacls $InstallRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $InstallRoot" - } - - & icacls $StateRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $StateRoot" - } - - & icacls $LogsRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(M)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $LogsRoot" - } -} - -function Start-ActivityWatchTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [string]$RecoveryTaskName = 'ActivityWatch Recovery' - ) - - foreach ($definition in $TaskDefinitions) { - Start-ScheduledTask -TaskName $definition.LaunchTaskName -ErrorAction SilentlyContinue - } - - Start-ScheduledTask -TaskName $RecoveryTaskName -ErrorAction SilentlyContinue -} - -Export-ModuleMember -Function *-ActivityWatch*, Assert-Administrator, Normalize-ActivityWatchUsers, Get-ActivityWatchPackageUrl, Remove-LegacyActivityWatchEntries -; } - } - - `$payload = @{ - pid = `$PID - createdAt = (Get-Date).ToUniversalTime().ToString('o') - } | ConvertTo-Json -Compress - Set-Content -LiteralPath `$lockPath -Value `$payload -Encoding UTF8 - return `$lockPath -} - -function Start-TaskIfNotRunning { - param([string]`$TaskName) - if ([string]::IsNullOrWhiteSpace(`$TaskName)) { - return - } - - try { - `$task = Get-ScheduledTask -TaskName `$TaskName -ErrorAction SilentlyContinue - if (-not `$task) { - return - } - if ([string]`$task.State -eq 'Running') { - return - } - Start-ScheduledTask -TaskName `$TaskName -ErrorAction SilentlyContinue - } - catch { Write-Error Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Assert-Administrator { - $identity = [Security.Principal.WindowsIdentity]::GetCurrent() - $principal = [Security.Principal.WindowsPrincipal]::new($identity) - if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - throw 'Запустите этот скрипт из PowerShell с правами администратора.' - } -} - -function New-ActivityWatchDirectory { - param( - [Parameter(Mandatory = $true)] - [string]$Path - ) - - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Enable-ActivityWatchPrintTelemetry { - $policyPath = 'HKLM:\Software\Policies\Microsoft\Windows NT\Printers' - if (-not (Test-Path -LiteralPath $policyPath)) { - New-Item -Path $policyPath -Force | Out-Null - } - New-ItemProperty -Path $policyPath -Name 'ShowJobTitleInEventLogs' -Value 1 -PropertyType DWord -Force | Out-Null - - & wevtutil.exe sl 'Microsoft-Windows-PrintService/Operational' /e:true | Out-Null -} - - -function Get-ActivityWatchPackageUrl { - param( - [string]$Version = 'v0.13.2' - ) - - return "https://github.com/ActivityWatch/activitywatch/releases/download/$Version/activitywatch-$Version-windows-x86_64.zip" -} - -function Get-ActivityWatchArchive { - param( - [string]$PackageZipPath, - [string]$PackageUrl, - [string]$Version = 'v0.13.2', - [Parameter(Mandatory = $true)] - [string]$WorkingRoot - ) - - New-ActivityWatchDirectory -Path $WorkingRoot - - if ($PackageZipPath) { - $resolved = Resolve-Path -LiteralPath $PackageZipPath -ErrorAction Stop - return $resolved.Path - } - - if (-not $PackageUrl) { - $PackageUrl = Get-ActivityWatchPackageUrl -Version $Version - } - - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' - $suffix = ([guid]::NewGuid().Guid.Substring(0, 8)) - $archivePath = Join-Path $WorkingRoot ("activitywatch-{0}-{1}-{2}.zip" -f $Version.TrimStart('v'), $stamp, $suffix) - Invoke-WebRequest -Uri $PackageUrl -OutFile $archivePath - return $archivePath -} - -function Get-ActivityWatchPackageRoot { - param( - [Parameter(Mandatory = $true)] - [string]$ExpandedRoot - ) - - $afkBinary = Get-ChildItem -Path $ExpandedRoot -Filter 'aw-watcher-afk.exe' -File -Recurse | - Select-Object -First 1 - - if (-not $afkBinary) { - throw "Не удалось найти aw-watcher-afk.exe в $ExpandedRoot." - } - - return (Split-Path -Path (Split-Path -Path $afkBinary.FullName -Parent) -Parent) -} - -function Install-ActivityWatchPackage { - param( - [Parameter(Mandatory = $true)] - [string]$ArchivePath, - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$WorkingRoot, - [Parameter(Mandatory = $true)] - [string]$BackupRoot - ) - - New-ActivityWatchDirectory -Path $WorkingRoot - New-ActivityWatchDirectory -Path $BackupRoot - - # Ensure nothing is holding locks inside InstallRoot during upgrade. - foreach ($procName in @('aw-watcher-afk', 'aw-watcher-window', 'aw-server', 'aw-qt')) { - try { - Get-Process -Name $procName -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue - } - catch { - } - } - Start-Sleep -Seconds 2 - - $extractRoot = Join-Path $WorkingRoot ('extract-' + [guid]::NewGuid().Guid) - if (Test-Path -LiteralPath $extractRoot) { - Remove-Item -LiteralPath $extractRoot -Recurse -Force - } - New-ActivityWatchDirectory -Path $extractRoot - - Expand-Archive -Path $ArchivePath -DestinationPath $extractRoot -Force - $packageRoot = Get-ActivityWatchPackageRoot -ExpandedRoot $extractRoot - - if (Test-Path -LiteralPath $InstallRoot) { - $existingItems = Get-ChildItem -LiteralPath $InstallRoot -Force -ErrorAction SilentlyContinue - if ($existingItems) { - $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' - $backupPath = Join-Path $BackupRoot ("install-$stamp") - New-ActivityWatchDirectory -Path $backupPath - Copy-Item -Path (Join-Path $InstallRoot '*') -Destination $backupPath -Recurse -Force - Get-ChildItem -LiteralPath $InstallRoot -Force | Remove-Item -Recurse -Force - } - } - else { - New-ActivityWatchDirectory -Path $InstallRoot - } - - Copy-Item -Path (Join-Path $packageRoot '*') -Destination $InstallRoot -Recurse -Force - - return [pscustomobject]@{ - PackageRoot = $packageRoot - ExtractRoot = $extractRoot - BackupRoot = $BackupRoot - } -} - -function Get-ActivityWatchExecutableMap { - param( - [Parameter(Mandatory = $true)] - [string]$InstallRoot - ) - - $map = [ordered]@{ - Afk = Join-Path $InstallRoot 'aw-watcher-afk\aw-watcher-afk.exe' - Window = Join-Path $InstallRoot 'aw-watcher-window\aw-watcher-window.exe' - } - - foreach ($entry in $map.GetEnumerator()) { - if (-not (Test-Path -LiteralPath $entry.Value)) { - throw "Не найден обязательный исполняемый файл ActivityWatch: $($entry.Value)" - } - } - - return [pscustomobject]$map -} - -function Normalize-ActivityWatchUsers { - param( - [string[]]$Users, - [string]$UserListPath, - [string]$Domain - ) - - $collected = New-Object System.Collections.Generic.List[string] - - if ($Users) { - foreach ($user in $Users) { - if (-not [string]::IsNullOrWhiteSpace($user)) { - $collected.Add($user.Trim()) - } - } - } - - if ($UserListPath) { - $resolved = Resolve-Path -LiteralPath $UserListPath -ErrorAction Stop - $extension = [IO.Path]::GetExtension($resolved.Path) - if ($extension -ieq '.csv') { - $rows = Import-Csv -LiteralPath $resolved.Path - foreach ($row in $rows) { - foreach ($column in 'User', 'Username', 'SamAccountName', 'Login') { - if ($row.PSObject.Properties.Name -contains $column) { - $value = [string]$row.$column - if (-not [string]::IsNullOrWhiteSpace($value)) { - $collected.Add($value.Trim()) - break - } - } - } - } - } - else { - Get-Content -LiteralPath $resolved.Path | ForEach-Object { - $line = $_.Trim() - if ($line -and -not $line.StartsWith('#')) { - $collected.Add($line) - } - } - } - } - - $normalized = $collected | - Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | - ForEach-Object { - if ($Domain -and ($_ -notmatch '[\\@]')) { - '{0}\{1}' -f $Domain, $_ - } - else { - $_ - } - } | - Sort-Object -Unique - - if (-not $normalized -or $normalized.Count -eq 0) { - throw 'Не удалось определить целевых пользователей. Укажите -Users или -UserListPath.' - } - - return @($normalized) -} - -function Get-ActivityWatchTaskNameToken { - param( - [Parameter(Mandatory = $true)] - [string]$UserId - ) - - $buffer = [Text.StringBuilder]::new() - foreach ($character in $UserId.ToCharArray()) { - if ([char]::IsLetterOrDigit($character)) { - [void]$buffer.Append($character) - } - else { - [void]$buffer.Append('_') - } - } - - return $buffer.ToString().Trim('_') -} - -function New-ActivityWatchUserTaskDefinitions { - param( - [Parameter(Mandatory = $true)] - [string[]]$Users - ) - - $result = foreach ($user in $Users) { - $token = Get-ActivityWatchTaskNameToken -UserId $user - [pscustomobject]@{ - UserId = $user - LaunchTaskName = "ActivityWatch Launch [$token]" - } - } - - return @($result) -} - -function Copy-ActivityWatchCollectorAssets { - param( - [Parameter(Mandatory = $true)] - [string]$CollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$EndpointCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$FileCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$SessionCollectorScriptSource, - [string]$EmailCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$ExampleRulesSource, - [Parameter(Mandatory = $true)] - [string]$ExamplePolicySource, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [string]$CustomRulesSource, - [string]$CustomPolicySource - ) - - New-ActivityWatchDirectory -Path $StateRoot - - $collectorTarget = Join-Path $StateRoot 'browser-domains-native-collector.ps1' - $endpointCollectorTarget = Join-Path $StateRoot 'dlp-endpoint-signals-collector.ps1' - $fileCollectorTarget = Join-Path $StateRoot 'file-operations-collector.ps1' - $sessionCollectorTarget = Join-Path $StateRoot 'worktime-session-collector.ps1' - $emailCollectorTarget = Join-Path $StateRoot 'email-outbound-collector.ps1' - $exampleRulesTarget = Join-Path $StateRoot 'web-category-rules.example.json' - $rulesTarget = Join-Path $StateRoot 'web-category-rules.json' - $examplePolicyTarget = Join-Path $StateRoot 'dlp-policy.example.json' - $policyTarget = Join-Path $StateRoot 'dlp-policy.json' - - Copy-Item -LiteralPath $CollectorScriptSource -Destination $collectorTarget -Force - Copy-Item -LiteralPath $EndpointCollectorScriptSource -Destination $endpointCollectorTarget -Force - Copy-Item -LiteralPath $FileCollectorScriptSource -Destination $fileCollectorTarget -Force - Copy-Item -LiteralPath $SessionCollectorScriptSource -Destination $sessionCollectorTarget -Force - if ($EmailCollectorScriptSource -and (Test-Path -LiteralPath $EmailCollectorScriptSource)) { - Copy-Item -LiteralPath $EmailCollectorScriptSource -Destination $emailCollectorTarget -Force - } - Copy-Item -LiteralPath $ExampleRulesSource -Destination $exampleRulesTarget -Force - Copy-Item -LiteralPath $ExamplePolicySource -Destination $examplePolicyTarget -Force - - if ($CustomRulesSource) { - $resolvedRules = Resolve-Path -LiteralPath $CustomRulesSource -ErrorAction Stop - Copy-Item -LiteralPath $resolvedRules.Path -Destination $rulesTarget -Force - } - else { - Copy-Item -LiteralPath $exampleRulesTarget -Destination $rulesTarget -Force - } - - if ($CustomPolicySource) { - $resolvedPolicy = Resolve-Path -LiteralPath $CustomPolicySource -ErrorAction Stop - Copy-Item -LiteralPath $resolvedPolicy.Path -Destination $policyTarget -Force - } - else { - Copy-Item -LiteralPath $examplePolicyTarget -Destination $policyTarget -Force - } - - return [pscustomobject]@{ - CollectorScript = $collectorTarget - EndpointCollectorScript = $endpointCollectorTarget - FileCollectorScript = $fileCollectorTarget - SessionCollectorScript = $sessionCollectorTarget - EmailCollectorScript = $emailCollectorTarget - ExampleRules = $exampleRulesTarget - ActiveRules = $rulesTarget - ExamplePolicy = $examplePolicyTarget - ActivePolicy = $policyTarget - } -} - -function New-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [string]$ServerHost, - [Parameter(Mandatory = $true)] - [int]$ServerPort, - [Parameter(Mandatory = $true)] - [string]$ServerScheme, - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [Parameter(Mandatory = $true)] - [string]$LogsRoot, - [Parameter(Mandatory = $true)] - [string]$CollectorScript, - [Parameter(Mandatory = $true)] - [string]$EndpointCollectorScript, - [Parameter(Mandatory = $true)] - [string]$FileCollectorScript, - [Parameter(Mandatory = $true)] - [string]$SessionCollectorScript, - [string]$EmailCollectorScript, - [Parameter(Mandatory = $true)] - [string]$RulesPath, - [Parameter(Mandatory = $true)] - [string]$PolicyPath, - [Parameter(Mandatory = $true)] - [int]$PollSeconds, - [Parameter(Mandatory = $true)] - [int]$PulseSeconds, - [Parameter(Mandatory = $true)] - [int]$RecoveryIntervalSeconds, - [bool]$AfkEnabled = $true, - [bool]$WindowEnabled = $true, - [bool]$FileOpsEnabled = $true, - [bool]$LocalAgentLogsEnabled = $true, - [bool]$IncidentCaptureEnabled = $true, - [bool]$IncidentScreenshotEnabled = $true, - [string]$IncidentArtifactsRoot, - [bool]$LogonMarkerEnabled = $true, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath, - [Parameter(Mandatory = $true)] - [string]$RecoveryScriptPath, - [Parameter(Mandatory = $true)] - [pscustomobject[]]$UserTasks, - [string]$PackageVersion = 'v0.13.2' - ) - - $effectiveIncidentArtifactsRoot = if ($IncidentArtifactsRoot) { $IncidentArtifactsRoot } else { Join-Path $StateRoot 'incident-artifacts' } - - return [pscustomobject]@{ - version = 1 - generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') - server = [pscustomobject]@{ - host = $ServerHost - port = $ServerPort - scheme = $ServerScheme - } - paths = [pscustomobject]@{ - installRoot = $InstallRoot - stateRoot = $StateRoot - logsRoot = $LogsRoot - collectorScript = $CollectorScript - endpointCollectorScript = $EndpointCollectorScript - emailCollectorScript = $EmailCollectorScript - fileCollectorScript = $FileCollectorScript - sessionCollectorScript = $SessionCollectorScript - rulesPath = $RulesPath - policyPath = $PolicyPath - launchScript = $LaunchScriptPath - recoveryScript = $RecoveryScriptPath - } - collector = [pscustomobject]@{ - pollSeconds = $PollSeconds - pulseSeconds = $PulseSeconds - } - collectors = [pscustomobject]@{ - afkEnabled = $AfkEnabled - windowEnabled = $WindowEnabled - fileOpsEnabled = $FileOpsEnabled - emailEnabled = ($null -ne $EmailCollectorScript -and $EmailCollectorScript -ne '') - } - logging = [pscustomobject]@{ - localAgentLogsEnabled = $LocalAgentLogsEnabled - } - incidentCapture = [pscustomobject]@{ - enabled = $IncidentCaptureEnabled - screenshotEnabled = $IncidentScreenshotEnabled - artifactsRoot = $effectiveIncidentArtifactsRoot - } - sessionEvents = [pscustomobject]@{ - logonEnabled = $LogonMarkerEnabled - bucketPrefix = 'aw-session-events' - } - recovery = [pscustomobject]@{ - intervalSeconds = $RecoveryIntervalSeconds - taskName = 'ActivityWatch Recovery' - } - dlp = [pscustomobject]@{ - incidentBucketPrefix = 'aw-dlp-incidents' - enabled = $true - } - package = [pscustomobject]@{ - version = $PackageVersion - } - userTasks = @($UserTasks) - } -} - -function Write-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [pscustomobject]$Config, - [Parameter(Mandatory = $true)] - [string]$Path - ) - - $directory = Split-Path -Path $Path -Parent - if ($directory) { - New-ActivityWatchDirectory -Path $directory - } - - $json = $Config | ConvertTo-Json -Depth 8 - Set-Content -LiteralPath $Path -Value $json -Encoding UTF8 -} - -function Read-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [string]$Path - ) - - if (-not (Test-Path -LiteralPath $Path)) { - throw "Конфигурация развёртывания не найдена: $Path" - } - - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json -} - -function Write-ActivityWatchLaunchScript { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $content = @" -param( - [string]`$ConfigPath = '$ConfigPath' -) - -Set-StrictMode -Version Latest -`$ErrorActionPreference = 'Stop' - -[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -Add-Type -AssemblyName System.Net.Http -`$script:MaxCollectorPowerShellProcesses = 24 - -function Get-DeploymentConfig { - param([string]`$Path) - return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json -} - -function Test-ProcessInSession { - param( - [string]`$Name, - [int]`$SessionId - ) - - return [bool](Get-Process -Name `$Name -ErrorAction SilentlyContinue | Where-Object { `$_.SessionId -eq `$SessionId } | Select-Object -First 1) -} - -function Test-CollectorRunning { - param( - [string]`$ScriptPath, - [int]`$SessionId - ) - - `$escapedCollector = [Regex]::Escape(`$ScriptPath) - `$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - (`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and - `$_.SessionId -eq `$SessionId -and - `$_.CommandLine -match `$escapedCollector - } - - return [bool](`$processes | Select-Object -First 1) -} - -function Get-CollectorPowerShellProcessCount { - `$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - (`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and - `$_.CommandLine -match 'AWatch-rus' -and - `$_.CommandLine -match '\.ps1' - } - - return @(`$processes).Count -} - -function New-LaunchLock { - param([string]`$StateRoot, [int]`$SessionId) - - `$lockPath = Join-Path `$env:TEMP ("launch-watchers-session-{0}.lock" -f `$SessionId) - if (Test-Path -LiteralPath `$lockPath) { - try { - `$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json - `$existingPid = [int]`$lockData.pid - if (`$existingPid -gt 0 -and (Get-Process -Id `$existingPid -ErrorAction SilentlyContinue)) { - return `$null - } - } - catch { - } - } - - `$payload = @{ - pid = `$PID - sessionId = `$SessionId - createdAt = (Get-Date).ToUniversalTime().ToString('o') - } | ConvertTo-Json -Compress - Set-Content -LiteralPath `$lockPath -Value `$payload -Encoding UTF8 - return `$lockPath -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = `$true)][string]`$Uri, - [Parameter(Mandatory = `$true)][string]`$Json - ) - - `$httpClient = New-Object System.Net.Http.HttpClient - try { - `$content = New-Object System.Net.Http.StringContent(`$Json, [System.Text.Encoding]::UTF8, 'application/json') - `$response = `$httpClient.PostAsync(`$Uri, `$content).Result - if (-not `$response.IsSuccessStatusCode) { - return `$false - } - return `$true - } - catch { - return `$false - } - finally { - `$httpClient.Dispose() - } -} - -function Ensure-Bucket { - param( - [string]`$BucketId, - [string]`$ClientName, - [string]`$BucketType - ) - - if (`$script:KnownBuckets.ContainsKey(`$BucketId)) { - return - } - - try { - Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null - `$script:KnownBuckets[`$BucketId] = `$true - return - } - catch { - } - - `$body = @{ - client = `$ClientName - type = `$BucketType - hostname = `$script:Hostname - } | ConvertTo-Json -Compress - - try { - if (-not (Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" -Json `$body)) { - return - } - } - catch { - try { - Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null - } - catch { - return - } - } - - `$script:KnownBuckets[`$BucketId] = `$true -} - -function Send-LogonMarkerIfNeeded { - param( - [pscustomobject]`$Config, - [int]`$SessionId - ) - - `$sessionEvents = if (`$Config.PSObject.Properties.Name -contains 'sessionEvents') { `$Config.sessionEvents } else { `$null } - `$logging = if (`$Config.PSObject.Properties.Name -contains 'logging') { `$Config.logging } else { `$null } - `$logonEnabled = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'logonEnabled') { [bool]`$sessionEvents.logonEnabled } else { `$false } - if (-not `$logonEnabled) { - return - } - - `$bucketPrefix = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'bucketPrefix' -and -not [string]::IsNullOrWhiteSpace([string]`$sessionEvents.bucketPrefix)) { - [string]`$sessionEvents.bucketPrefix - } - else { - 'aw-session-events' - } - - `$stateRoot = [string]`$Config.paths.stateRoot - `$markerRoots = New-Object System.Collections.Generic.List[string] - if (-not [string]::IsNullOrWhiteSpace(`$env:LOCALAPPDATA)) { - `$markerRoots.Add((Join-Path `$env:LOCALAPPDATA 'AWatch-rus\markers')) - } - if (-not [string]::IsNullOrWhiteSpace(`$stateRoot)) { - `$markerRoots.Add((Join-Path `$stateRoot 'markers')) - } - - `$markerDir = `$null - foreach (`$candidate in `$markerRoots) { - try { - if (-not (Test-Path -LiteralPath `$candidate)) { - New-Item -Path `$candidate -ItemType Directory -Force | Out-Null - } - - `$probePath = Join-Path `$candidate 'write-test.tmp' - Set-Content -LiteralPath `$probePath -Value 'ok' -Encoding ASCII - Remove-Item -LiteralPath `$probePath -Force -ErrorAction SilentlyContinue - `$markerDir = `$candidate - break - } - catch { - } - } - - if (-not `$markerDir) { - return - } - - `$markerFile = Join-Path `$markerDir ("logon-{0}-{1}.marker" -f `$env:USERNAME, `$SessionId) - if (Test-Path -LiteralPath `$markerFile) { - return - } - - Set-Content -LiteralPath `$markerFile -Value ((Get-Date).ToUniversalTime().ToString('o')) -Encoding UTF8 - - `$bucketId = ('{0}_{1}' -f `$bucketPrefix, `$script:Hostname) - Ensure-Bucket -BucketId `$bucketId -ClientName 'aw-session-events' -BucketType 'aw.session.event' - - `$payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - eventType = 'logon' - username = `$env:USERNAME - userId = "`$(`$env:USERDOMAIN)\`$(`$env:USERNAME)" - sessionId = `$SessionId - hostname = `$script:Hostname - source = 'launch-watchers-awatch-rus' - } - } | ConvertTo-Json -Depth 5 -Compress - - try { - Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$bucketId/heartbeat?pulsetime=1" -Json `$payload - } - catch { - Remove-Item -LiteralPath `$markerFile -Force -ErrorAction SilentlyContinue - throw - } -} - -function Start-CollectorScriptIfNeeded { - param( - [string]`$ScriptPath, - [string]`$ConfigPath, - [string]`$PowerShellExe, - [int]`$SessionId - ) - - if ([string]::IsNullOrWhiteSpace(`$ScriptPath)) { - return - } - - if (-not (Test-Path -LiteralPath `$ScriptPath)) { - return - } - - if (Test-CollectorRunning -ScriptPath `$ScriptPath -SessionId `$SessionId) { - return - } - - if ((Get-CollectorPowerShellProcessCount) -ge `$script:MaxCollectorPowerShellProcesses) { - return - } - - `$staParam = if (`$ScriptPath -like "*endpoint-signals*") { "-STA" } else { `$null } - `$argumentList = @('-NoProfile', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass') - if (`$staParam) { `$argumentList += `$staParam } - `$argumentList += @('-File', `$ScriptPath, '-ConfigPath', `$ConfigPath) - Start-Process -FilePath `$PowerShellExe -ArgumentList `$argumentList -WindowStyle Hidden -} - -`$config = Get-DeploymentConfig -Path `$ConfigPath -`$sessionId = (Get-Process -Id `$PID).SessionId -`$installRoot = [string]`$config.paths.installRoot -`$stateRoot = [string]`$config.paths.stateRoot -`$script:ApiBase = '{0}://{1}:{2}/api/0' -f [string]`$config.server.scheme, [string]`$config.server.host, [string]`$config.server.port -`$script:Hostname = `$env:COMPUTERNAME -`$script:KnownBuckets = @{} -`$collectorScript = [string]`$config.paths.collectorScript -`$endpointCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]`$config.paths.endpointCollectorScript } else { Join-Path `$stateRoot 'dlp-endpoint-signals-collector.ps1' } -`$fileCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'fileCollectorScript') { [string]`$config.paths.fileCollectorScript } else { Join-Path `$stateRoot 'file-operations-collector.ps1' } -`$sessionCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]`$config.paths.sessionCollectorScript } else { Join-Path `$stateRoot 'worktime-session-collector.ps1' } -`$afkExe = Join-Path `$installRoot 'aw-watcher-afk\aw-watcher-afk.exe' -`$windowExe = Join-Path `$installRoot 'aw-watcher-window\aw-watcher-window.exe' -`$serverArgs = @('--host', [string]`$config.server.host, '--port', [string]`$config.server.port) -`$powershellExe = Join-Path `$env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' -`$afkEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]`$config.collectors.afkEnabled } else { `$true } -`$windowEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]`$config.collectors.windowEnabled } else { `$true } -`$fileOpsEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { [bool]`$config.collectors.fileOpsEnabled } else { `$true } -`$emailEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'emailEnabled') { [bool]`$config.collectors.emailEnabled } else { `$false } -`$emailCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'emailCollectorScript') { [string]`$config.paths.emailCollectorScript } else { Join-Path `$stateRoot 'email-outbound-collector.ps1' } -`$launchLockPath = New-LaunchLock -StateRoot `$stateRoot -SessionId `$sessionId -if (-not `$launchLockPath) { - return -} - -try { - if (`$afkEnabled -and -not (Test-Path -LiteralPath `$afkExe)) { - throw "Не найден aw-watcher-afk.exe: `$afkExe" - } - - if (`$windowEnabled -and -not (Test-Path -LiteralPath `$windowExe)) { - throw "Не найден aw-watcher-window.exe: `$windowExe" - } - - if (`$afkEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-afk' -SessionId `$sessionId)) { - Start-Process -FilePath `$afkExe -ArgumentList `$serverArgs -WindowStyle Hidden - } - - if (`$windowEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-window' -SessionId `$sessionId)) { - Start-Process -FilePath `$windowExe -ArgumentList `$serverArgs -WindowStyle Hidden - } - - try { - Send-LogonMarkerIfNeeded -Config `$config -SessionId `$sessionId - } - catch { - } - Start-CollectorScriptIfNeeded -ScriptPath `$collectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - Start-CollectorScriptIfNeeded -ScriptPath `$endpointCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - if (`$fileOpsEnabled) { - Start-CollectorScriptIfNeeded -ScriptPath `$fileCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - } - Start-CollectorScriptIfNeeded -ScriptPath `$sessionCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - if (`$emailEnabled -and (Test-Path -LiteralPath `$emailCollectorScript)) { - Start-CollectorScriptIfNeeded -ScriptPath `$emailCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - } -} -finally { - if (`$launchLockPath -and (Test-Path -LiteralPath `$launchLockPath)) { - Remove-Item -LiteralPath `$launchLockPath -Force -ErrorAction SilentlyContinue - } -} -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding UTF8 -} - -function Write-ActivityWatchRecoveryScript { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $content = @" -param( - [string]`$ConfigPath = '$ConfigPath' -) - -Set-StrictMode -Version Latest -`$ErrorActionPreference = 'Continue' - -function Get-DeploymentConfig { - param([string]`$Path) - return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json -} - -function Get-RecoveryConfigPaths { - param([string]`$PrimaryConfigPath) - - `$paths = New-Object System.Collections.Generic.List[string] - if (`$PrimaryConfigPath -and (Test-Path -LiteralPath `$PrimaryConfigPath)) { - `$paths.Add((Resolve-Path -LiteralPath `$PrimaryConfigPath).Path) - } - - `$searchRoot = `$env:ProgramData - if (`$PrimaryConfigPath) { - `$stateRoot = Split-Path -Path `$PrimaryConfigPath -Parent - `$candidateRoot = Split-Path -Path `$stateRoot -Parent - if (`$candidateRoot -and (Test-Path -LiteralPath `$candidateRoot)) { - `$searchRoot = `$candidateRoot - } - } - - if (Test-Path -LiteralPath `$searchRoot) { - Get-ChildItem -LiteralPath `$searchRoot -Directory -ErrorAction SilentlyContinue | - Where-Object { `$_.Name -like 'ActivityWatch*' } | - ForEach-Object { - `$candidate = Join-Path `$_.FullName 'deployment-config.json' - if (Test-Path -LiteralPath `$candidate) { - `$paths.Add(`$candidate) - } - } - } - - return @(`$paths | Sort-Object -Unique) -} - -function Get-RecoveryTaskNames { - param([string[]]`$ConfigPaths) - - `$taskNames = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) - foreach (`$candidatePath in @(`$ConfigPaths)) { - try { - `$config = Get-DeploymentConfig -Path `$candidatePath - foreach (`$task in @(`$config.userTasks)) { - `$taskName = [string]`$task.launchTaskName - if (-not [string]::IsNullOrWhiteSpace(`$taskName)) { - [void]`$taskNames.Add(`$taskName) - } - } - } - catch { - } - } - - return @(`$taskNames) -} - -function New-RecoveryLock { - param([string]`$PrimaryConfigPath) - - `$stateRoot = if (`$PrimaryConfigPath) { Split-Path -Path `$PrimaryConfigPath -Parent } else { Join-Path `$env:ProgramData 'AWatch-rus' } - if (-not (Test-Path -LiteralPath `$stateRoot)) { - New-Item -Path `$stateRoot -ItemType Directory -Force | Out-Null - } - - `$lockPath = Join-Path `$stateRoot 'recovery-loop.lock' - if (Test-Path -LiteralPath `$lockPath) { - try { - `$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json - `$existingPid = [int]`$lockData.pid - if (`$existingPid -gt 0 -and (Get-Process -Id `$existingPid -ErrorAction SilentlyContinue)) { - return `$null - } - } - catch { - } - } - - `$payload = @{ - pid = `$PID - createdAt = (Get-Date).ToUniversalTime().ToString('o') - } | ConvertTo-Json -Compress - Set-Content -LiteralPath `$lockPath -Value `$payload -Encoding UTF8 - return `$lockPath -} - -function Start-TaskIfNotRunning { - param([string]`$TaskName) - if ([string]::IsNullOrWhiteSpace(`$TaskName)) { - return - } - - try { - `$task = Get-ScheduledTask -TaskName `$TaskName -ErrorAction SilentlyContinue - if (-not `$task) { - return - } - if ([string]`$task.State -eq 'Running') { - return - } - Start-ScheduledTask -TaskName `$TaskName -ErrorAction SilentlyContinue - } - catch { - } -} - -`$recoveryLockPath = New-RecoveryLock -PrimaryConfigPath `$ConfigPath -if (-not `$recoveryLockPath) { - return -} - -try { - while (`$true) { - `$sleepSeconds = 180 - try { - `$configPaths = Get-RecoveryConfigPaths -PrimaryConfigPath `$ConfigPath - foreach (`$taskName in Get-RecoveryTaskNames -ConfigPaths `$configPaths) { - Start-TaskIfNotRunning -TaskName `$taskName - } - - `$config = Get-DeploymentConfig -Path `$ConfigPath - if (`$config -and `$config.recovery -and `$config.recovery.intervalSeconds) { - `$sleepSeconds = [Math]::Max([int]`$config.recovery.intervalSeconds, 30) - } - } - catch { - } - - Start-Sleep -Seconds `$sleepSeconds - } -} -finally { - if (`$recoveryLockPath -and (Test-Path -LiteralPath `$recoveryLockPath)) { - Remove-Item -LiteralPath `$recoveryLockPath -Force -ErrorAction SilentlyContinue - } -} -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding UTF8 -} - -function Get-ActivityWatchHiddenLauncherPath { - param( - [Parameter(Mandatory = $true)] - [string]$ScriptPath - ) - - $directory = Split-Path -Path $ScriptPath -Parent - $baseName = [IO.Path]::GetFileNameWithoutExtension($ScriptPath) - return Join-Path $directory ("{0}-hidden.vbs" -f $baseName) -} - -function Write-ActivityWatchHiddenPowerShellWrapper { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $directory = Split-Path -Path $Path -Parent - if ($directory) { - New-ActivityWatchDirectory -Path $directory - } - - $powershellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' - $escapedPowerShellExe = $powershellExe.Replace('"', '""') - $escapedScriptPath = $ScriptPath.Replace('"', '""') - $escapedConfigPath = $ConfigPath.Replace('"', '""') - - $content = @" -Set shell = CreateObject("WScript.Shell") -shell.Run """$escapedPowerShellExe"" -NoProfile -ExecutionPolicy Bypass -File ""$escapedScriptPath"" -ConfigPath ""$escapedConfigPath""", 0, False -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding ASCII -} - -function Remove-LegacyActivityWatchEntries { - $legacyTaskNames = @( - 'ActivityWatch Watchers', - 'ActivityWatch Guard', - 'ActivityWatch Heal' - ) - - foreach ($taskName in $legacyTaskNames) { - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue - } - - $runKey = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run' - foreach ($name in 'ActivityWatchAFK', 'ActivityWatchWindow', 'ActivityWatchBrowserCollector') { - Remove-ItemProperty -Path $runKey -Name $name -ErrorAction SilentlyContinue - } -} - -function Remove-ActivityWatchScheduledTask { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName - ) - - Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue - & cmd.exe /c "schtasks /Delete /TN `"$TaskName`" /F >nul 2>&1" | Out-Null - - for ($attempt = 0; $attempt -lt 10; $attempt++) { - $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue - if (-not $task) { - return - } - - Start-Sleep -Milliseconds 300 - } -} - -function Set-ActivityWatchScheduledTaskAction { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [Parameter(Mandatory = $true)] - [string]$Execute, - [Parameter(Mandatory = $true)] - [string]$Arguments - ) - - $taskCommand = ('"{0}" {1}' -f $Execute, $Arguments) - & schtasks.exe /Change /TN $TaskName /TR $taskCommand | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "schtasks.exe /Change завершился с ошибкой для $TaskName" - } -} - -function Get-ActivityWatchScheduledTaskByCommand { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [string]$CommandMatch - ) - - $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue - if ($task) { - return $task - } - - if ([string]::IsNullOrWhiteSpace($CommandMatch)) { - return $null - } - - foreach ($candidate in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch*' })) { - foreach ($action in @($candidate.Actions)) { - if ([string]$action.Arguments -like "*$CommandMatch*") { - return $candidate - } - } - } - - return $null -} - -function Remove-StaleActivityWatchUserTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath - ) - - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath - $desiredTaskNames = @($TaskDefinitions | ForEach-Object { [string]$_.LaunchTaskName }) - - foreach ($candidate in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch*' })) { - $taskName = [string]$candidate.TaskName - if ($desiredTaskNames -contains $taskName) { - continue - } - - $usesCurrentLauncher = $false - foreach ($action in @($candidate.Actions)) { - if ([string]$action.Arguments -like "*$launcherPath*") { - $usesCurrentLauncher = $true - break - } - } - - if ($usesCurrentLauncher) { - Remove-ActivityWatchScheduledTask -TaskName $taskName - } - } -} - -function Register-ActivityWatchUserTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe' - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath - Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $LaunchScriptPath -ConfigPath $ConfigPath - Remove-StaleActivityWatchUserTasks -TaskDefinitions $TaskDefinitions -LaunchScriptPath $LaunchScriptPath - - foreach ($definition in $TaskDefinitions) { - $action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`"" - $trigger = New-ScheduledTaskTrigger -AtLogOn -User $definition.UserId - $principal = New-ScheduledTaskPrincipal -UserId $definition.UserId -LogonType Interactive -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0) - $existingTask = Get-ActivityWatchScheduledTaskByCommand -TaskName $definition.LaunchTaskName -CommandMatch $ConfigPath - - if ($existingTask) { - Set-ActivityWatchScheduledTaskAction -TaskName $existingTask.TaskName -Execute $wscriptExe -Arguments $action.Arguments - continue - } - - Remove-ActivityWatchScheduledTask -TaskName $definition.LaunchTaskName - Register-ScheduledTask -TaskName $definition.LaunchTaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null - } -} - -function Register-ActivityWatchRecoveryTask { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [Parameter(Mandatory = $true)] - [string]$RecoveryScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - Remove-ActivityWatchScheduledTask -TaskName $TaskName - - $wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe' - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $RecoveryScriptPath - Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $RecoveryScriptPath -ConfigPath $ConfigPath - $action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`"" - $trigger = New-ScheduledTaskTrigger -AtStartup - $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -Hidden -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0) - - Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null -} - -function Set-ActivityWatchAcl { - param( - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [Parameter(Mandatory = $true)] - [string]$LogsRoot - ) - - foreach ($path in $InstallRoot, $StateRoot, $LogsRoot) { - New-ActivityWatchDirectory -Path $path - } - - & icacls $InstallRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $InstallRoot" - } - - & icacls $StateRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $StateRoot" - } - - & icacls $LogsRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(M)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $LogsRoot" - } -} - -function Start-ActivityWatchTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [string]$RecoveryTaskName = 'ActivityWatch Recovery' - ) - - foreach ($definition in $TaskDefinitions) { - Start-ScheduledTask -TaskName $definition.LaunchTaskName -ErrorAction SilentlyContinue - } - - Start-ScheduledTask -TaskName $RecoveryTaskName -ErrorAction SilentlyContinue -} - -Export-ModuleMember -Function *-ActivityWatch*, Assert-Administrator, Normalize-ActivityWatchUsers, Get-ActivityWatchPackageUrl, Remove-LegacyActivityWatchEntries -; } -} - -`$recoveryLockPath = New-RecoveryLock -PrimaryConfigPath `$ConfigPath -if (-not `$recoveryLockPath) { - return -} - -try { - while (`$true) { - `$sleepSeconds = 180 - try { - `$configPaths = Get-RecoveryConfigPaths -PrimaryConfigPath `$ConfigPath - foreach (`$taskName in Get-RecoveryTaskNames -ConfigPaths `$configPaths) { - Start-TaskIfNotRunning -TaskName `$taskName - } - - `$config = Get-DeploymentConfig -Path `$ConfigPath - if (`$config -and `$config.recovery -and `$config.recovery.intervalSeconds) { - `$sleepSeconds = [Math]::Max([int]`$config.recovery.intervalSeconds, 30) - } - } - catch { Write-Error Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Assert-Administrator { - $identity = [Security.Principal.WindowsIdentity]::GetCurrent() - $principal = [Security.Principal.WindowsPrincipal]::new($identity) - if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - throw 'Запустите этот скрипт из PowerShell с правами администратора.' - } -} - -function New-ActivityWatchDirectory { - param( - [Parameter(Mandatory = $true)] - [string]$Path - ) - - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Enable-ActivityWatchPrintTelemetry { - $policyPath = 'HKLM:\Software\Policies\Microsoft\Windows NT\Printers' - if (-not (Test-Path -LiteralPath $policyPath)) { - New-Item -Path $policyPath -Force | Out-Null - } - New-ItemProperty -Path $policyPath -Name 'ShowJobTitleInEventLogs' -Value 1 -PropertyType DWord -Force | Out-Null - - & wevtutil.exe sl 'Microsoft-Windows-PrintService/Operational' /e:true | Out-Null -} - - -function Get-ActivityWatchPackageUrl { - param( - [string]$Version = 'v0.13.2' - ) - - return "https://github.com/ActivityWatch/activitywatch/releases/download/$Version/activitywatch-$Version-windows-x86_64.zip" -} - -function Get-ActivityWatchArchive { - param( - [string]$PackageZipPath, - [string]$PackageUrl, - [string]$Version = 'v0.13.2', - [Parameter(Mandatory = $true)] - [string]$WorkingRoot - ) - - New-ActivityWatchDirectory -Path $WorkingRoot - - if ($PackageZipPath) { - $resolved = Resolve-Path -LiteralPath $PackageZipPath -ErrorAction Stop - return $resolved.Path - } - - if (-not $PackageUrl) { - $PackageUrl = Get-ActivityWatchPackageUrl -Version $Version - } - - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' - $suffix = ([guid]::NewGuid().Guid.Substring(0, 8)) - $archivePath = Join-Path $WorkingRoot ("activitywatch-{0}-{1}-{2}.zip" -f $Version.TrimStart('v'), $stamp, $suffix) - Invoke-WebRequest -Uri $PackageUrl -OutFile $archivePath - return $archivePath -} - -function Get-ActivityWatchPackageRoot { - param( - [Parameter(Mandatory = $true)] - [string]$ExpandedRoot - ) - - $afkBinary = Get-ChildItem -Path $ExpandedRoot -Filter 'aw-watcher-afk.exe' -File -Recurse | - Select-Object -First 1 - - if (-not $afkBinary) { - throw "Не удалось найти aw-watcher-afk.exe в $ExpandedRoot." - } - - return (Split-Path -Path (Split-Path -Path $afkBinary.FullName -Parent) -Parent) -} - -function Install-ActivityWatchPackage { - param( - [Parameter(Mandatory = $true)] - [string]$ArchivePath, - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$WorkingRoot, - [Parameter(Mandatory = $true)] - [string]$BackupRoot - ) - - New-ActivityWatchDirectory -Path $WorkingRoot - New-ActivityWatchDirectory -Path $BackupRoot - - # Ensure nothing is holding locks inside InstallRoot during upgrade. - foreach ($procName in @('aw-watcher-afk', 'aw-watcher-window', 'aw-server', 'aw-qt')) { - try { - Get-Process -Name $procName -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue - } - catch { - } - } - Start-Sleep -Seconds 2 - - $extractRoot = Join-Path $WorkingRoot ('extract-' + [guid]::NewGuid().Guid) - if (Test-Path -LiteralPath $extractRoot) { - Remove-Item -LiteralPath $extractRoot -Recurse -Force - } - New-ActivityWatchDirectory -Path $extractRoot - - Expand-Archive -Path $ArchivePath -DestinationPath $extractRoot -Force - $packageRoot = Get-ActivityWatchPackageRoot -ExpandedRoot $extractRoot - - if (Test-Path -LiteralPath $InstallRoot) { - $existingItems = Get-ChildItem -LiteralPath $InstallRoot -Force -ErrorAction SilentlyContinue - if ($existingItems) { - $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' - $backupPath = Join-Path $BackupRoot ("install-$stamp") - New-ActivityWatchDirectory -Path $backupPath - Copy-Item -Path (Join-Path $InstallRoot '*') -Destination $backupPath -Recurse -Force - Get-ChildItem -LiteralPath $InstallRoot -Force | Remove-Item -Recurse -Force - } - } - else { - New-ActivityWatchDirectory -Path $InstallRoot - } - - Copy-Item -Path (Join-Path $packageRoot '*') -Destination $InstallRoot -Recurse -Force - - return [pscustomobject]@{ - PackageRoot = $packageRoot - ExtractRoot = $extractRoot - BackupRoot = $BackupRoot - } -} - -function Get-ActivityWatchExecutableMap { - param( - [Parameter(Mandatory = $true)] - [string]$InstallRoot - ) - - $map = [ordered]@{ - Afk = Join-Path $InstallRoot 'aw-watcher-afk\aw-watcher-afk.exe' - Window = Join-Path $InstallRoot 'aw-watcher-window\aw-watcher-window.exe' - } - - foreach ($entry in $map.GetEnumerator()) { - if (-not (Test-Path -LiteralPath $entry.Value)) { - throw "Не найден обязательный исполняемый файл ActivityWatch: $($entry.Value)" - } - } - - return [pscustomobject]$map -} - -function Normalize-ActivityWatchUsers { - param( - [string[]]$Users, - [string]$UserListPath, - [string]$Domain - ) - - $collected = New-Object System.Collections.Generic.List[string] - - if ($Users) { - foreach ($user in $Users) { - if (-not [string]::IsNullOrWhiteSpace($user)) { - $collected.Add($user.Trim()) - } - } - } - - if ($UserListPath) { - $resolved = Resolve-Path -LiteralPath $UserListPath -ErrorAction Stop - $extension = [IO.Path]::GetExtension($resolved.Path) - if ($extension -ieq '.csv') { - $rows = Import-Csv -LiteralPath $resolved.Path - foreach ($row in $rows) { - foreach ($column in 'User', 'Username', 'SamAccountName', 'Login') { - if ($row.PSObject.Properties.Name -contains $column) { - $value = [string]$row.$column - if (-not [string]::IsNullOrWhiteSpace($value)) { - $collected.Add($value.Trim()) - break - } - } - } - } - } - else { - Get-Content -LiteralPath $resolved.Path | ForEach-Object { - $line = $_.Trim() - if ($line -and -not $line.StartsWith('#')) { - $collected.Add($line) - } - } - } - } - - $normalized = $collected | - Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | - ForEach-Object { - if ($Domain -and ($_ -notmatch '[\\@]')) { - '{0}\{1}' -f $Domain, $_ - } - else { - $_ - } - } | - Sort-Object -Unique - - if (-not $normalized -or $normalized.Count -eq 0) { - throw 'Не удалось определить целевых пользователей. Укажите -Users или -UserListPath.' - } - - return @($normalized) -} - -function Get-ActivityWatchTaskNameToken { - param( - [Parameter(Mandatory = $true)] - [string]$UserId - ) - - $buffer = [Text.StringBuilder]::new() - foreach ($character in $UserId.ToCharArray()) { - if ([char]::IsLetterOrDigit($character)) { - [void]$buffer.Append($character) - } - else { - [void]$buffer.Append('_') - } - } - - return $buffer.ToString().Trim('_') -} - -function New-ActivityWatchUserTaskDefinitions { - param( - [Parameter(Mandatory = $true)] - [string[]]$Users - ) - - $result = foreach ($user in $Users) { - $token = Get-ActivityWatchTaskNameToken -UserId $user - [pscustomobject]@{ - UserId = $user - LaunchTaskName = "ActivityWatch Launch [$token]" - } - } - - return @($result) -} - -function Copy-ActivityWatchCollectorAssets { - param( - [Parameter(Mandatory = $true)] - [string]$CollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$EndpointCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$FileCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$SessionCollectorScriptSource, - [string]$EmailCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$ExampleRulesSource, - [Parameter(Mandatory = $true)] - [string]$ExamplePolicySource, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [string]$CustomRulesSource, - [string]$CustomPolicySource - ) - - New-ActivityWatchDirectory -Path $StateRoot - - $collectorTarget = Join-Path $StateRoot 'browser-domains-native-collector.ps1' - $endpointCollectorTarget = Join-Path $StateRoot 'dlp-endpoint-signals-collector.ps1' - $fileCollectorTarget = Join-Path $StateRoot 'file-operations-collector.ps1' - $sessionCollectorTarget = Join-Path $StateRoot 'worktime-session-collector.ps1' - $emailCollectorTarget = Join-Path $StateRoot 'email-outbound-collector.ps1' - $exampleRulesTarget = Join-Path $StateRoot 'web-category-rules.example.json' - $rulesTarget = Join-Path $StateRoot 'web-category-rules.json' - $examplePolicyTarget = Join-Path $StateRoot 'dlp-policy.example.json' - $policyTarget = Join-Path $StateRoot 'dlp-policy.json' - - Copy-Item -LiteralPath $CollectorScriptSource -Destination $collectorTarget -Force - Copy-Item -LiteralPath $EndpointCollectorScriptSource -Destination $endpointCollectorTarget -Force - Copy-Item -LiteralPath $FileCollectorScriptSource -Destination $fileCollectorTarget -Force - Copy-Item -LiteralPath $SessionCollectorScriptSource -Destination $sessionCollectorTarget -Force - if ($EmailCollectorScriptSource -and (Test-Path -LiteralPath $EmailCollectorScriptSource)) { - Copy-Item -LiteralPath $EmailCollectorScriptSource -Destination $emailCollectorTarget -Force - } - Copy-Item -LiteralPath $ExampleRulesSource -Destination $exampleRulesTarget -Force - Copy-Item -LiteralPath $ExamplePolicySource -Destination $examplePolicyTarget -Force - - if ($CustomRulesSource) { - $resolvedRules = Resolve-Path -LiteralPath $CustomRulesSource -ErrorAction Stop - Copy-Item -LiteralPath $resolvedRules.Path -Destination $rulesTarget -Force - } - else { - Copy-Item -LiteralPath $exampleRulesTarget -Destination $rulesTarget -Force - } - - if ($CustomPolicySource) { - $resolvedPolicy = Resolve-Path -LiteralPath $CustomPolicySource -ErrorAction Stop - Copy-Item -LiteralPath $resolvedPolicy.Path -Destination $policyTarget -Force - } - else { - Copy-Item -LiteralPath $examplePolicyTarget -Destination $policyTarget -Force - } - - return [pscustomobject]@{ - CollectorScript = $collectorTarget - EndpointCollectorScript = $endpointCollectorTarget - FileCollectorScript = $fileCollectorTarget - SessionCollectorScript = $sessionCollectorTarget - EmailCollectorScript = $emailCollectorTarget - ExampleRules = $exampleRulesTarget - ActiveRules = $rulesTarget - ExamplePolicy = $examplePolicyTarget - ActivePolicy = $policyTarget - } -} - -function New-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [string]$ServerHost, - [Parameter(Mandatory = $true)] - [int]$ServerPort, - [Parameter(Mandatory = $true)] - [string]$ServerScheme, - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [Parameter(Mandatory = $true)] - [string]$LogsRoot, - [Parameter(Mandatory = $true)] - [string]$CollectorScript, - [Parameter(Mandatory = $true)] - [string]$EndpointCollectorScript, - [Parameter(Mandatory = $true)] - [string]$FileCollectorScript, - [Parameter(Mandatory = $true)] - [string]$SessionCollectorScript, - [string]$EmailCollectorScript, - [Parameter(Mandatory = $true)] - [string]$RulesPath, - [Parameter(Mandatory = $true)] - [string]$PolicyPath, - [Parameter(Mandatory = $true)] - [int]$PollSeconds, - [Parameter(Mandatory = $true)] - [int]$PulseSeconds, - [Parameter(Mandatory = $true)] - [int]$RecoveryIntervalSeconds, - [bool]$AfkEnabled = $true, - [bool]$WindowEnabled = $true, - [bool]$FileOpsEnabled = $true, - [bool]$LocalAgentLogsEnabled = $true, - [bool]$IncidentCaptureEnabled = $true, - [bool]$IncidentScreenshotEnabled = $true, - [string]$IncidentArtifactsRoot, - [bool]$LogonMarkerEnabled = $true, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath, - [Parameter(Mandatory = $true)] - [string]$RecoveryScriptPath, - [Parameter(Mandatory = $true)] - [pscustomobject[]]$UserTasks, - [string]$PackageVersion = 'v0.13.2' - ) - - $effectiveIncidentArtifactsRoot = if ($IncidentArtifactsRoot) { $IncidentArtifactsRoot } else { Join-Path $StateRoot 'incident-artifacts' } - - return [pscustomobject]@{ - version = 1 - generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') - server = [pscustomobject]@{ - host = $ServerHost - port = $ServerPort - scheme = $ServerScheme - } - paths = [pscustomobject]@{ - installRoot = $InstallRoot - stateRoot = $StateRoot - logsRoot = $LogsRoot - collectorScript = $CollectorScript - endpointCollectorScript = $EndpointCollectorScript - emailCollectorScript = $EmailCollectorScript - fileCollectorScript = $FileCollectorScript - sessionCollectorScript = $SessionCollectorScript - rulesPath = $RulesPath - policyPath = $PolicyPath - launchScript = $LaunchScriptPath - recoveryScript = $RecoveryScriptPath - } - collector = [pscustomobject]@{ - pollSeconds = $PollSeconds - pulseSeconds = $PulseSeconds - } - collectors = [pscustomobject]@{ - afkEnabled = $AfkEnabled - windowEnabled = $WindowEnabled - fileOpsEnabled = $FileOpsEnabled - emailEnabled = ($null -ne $EmailCollectorScript -and $EmailCollectorScript -ne '') - } - logging = [pscustomobject]@{ - localAgentLogsEnabled = $LocalAgentLogsEnabled - } - incidentCapture = [pscustomobject]@{ - enabled = $IncidentCaptureEnabled - screenshotEnabled = $IncidentScreenshotEnabled - artifactsRoot = $effectiveIncidentArtifactsRoot - } - sessionEvents = [pscustomobject]@{ - logonEnabled = $LogonMarkerEnabled - bucketPrefix = 'aw-session-events' - } - recovery = [pscustomobject]@{ - intervalSeconds = $RecoveryIntervalSeconds - taskName = 'ActivityWatch Recovery' - } - dlp = [pscustomobject]@{ - incidentBucketPrefix = 'aw-dlp-incidents' - enabled = $true - } - package = [pscustomobject]@{ - version = $PackageVersion - } - userTasks = @($UserTasks) - } -} - -function Write-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [pscustomobject]$Config, - [Parameter(Mandatory = $true)] - [string]$Path - ) - - $directory = Split-Path -Path $Path -Parent - if ($directory) { - New-ActivityWatchDirectory -Path $directory - } - - $json = $Config | ConvertTo-Json -Depth 8 - Set-Content -LiteralPath $Path -Value $json -Encoding UTF8 -} - -function Read-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [string]$Path - ) - - if (-not (Test-Path -LiteralPath $Path)) { - throw "Конфигурация развёртывания не найдена: $Path" - } - - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json -} - -function Write-ActivityWatchLaunchScript { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $content = @" -param( - [string]`$ConfigPath = '$ConfigPath' -) - -Set-StrictMode -Version Latest -`$ErrorActionPreference = 'Stop' - -[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -Add-Type -AssemblyName System.Net.Http -`$script:MaxCollectorPowerShellProcesses = 24 - -function Get-DeploymentConfig { - param([string]`$Path) - return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json -} - -function Test-ProcessInSession { - param( - [string]`$Name, - [int]`$SessionId - ) - - return [bool](Get-Process -Name `$Name -ErrorAction SilentlyContinue | Where-Object { `$_.SessionId -eq `$SessionId } | Select-Object -First 1) -} - -function Test-CollectorRunning { - param( - [string]`$ScriptPath, - [int]`$SessionId - ) - - `$escapedCollector = [Regex]::Escape(`$ScriptPath) - `$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - (`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and - `$_.SessionId -eq `$SessionId -and - `$_.CommandLine -match `$escapedCollector - } - - return [bool](`$processes | Select-Object -First 1) -} - -function Get-CollectorPowerShellProcessCount { - `$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - (`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and - `$_.CommandLine -match 'AWatch-rus' -and - `$_.CommandLine -match '\.ps1' - } - - return @(`$processes).Count -} - -function New-LaunchLock { - param([string]`$StateRoot, [int]`$SessionId) - - `$lockPath = Join-Path `$env:TEMP ("launch-watchers-session-{0}.lock" -f `$SessionId) - if (Test-Path -LiteralPath `$lockPath) { - try { - `$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json - `$existingPid = [int]`$lockData.pid - if (`$existingPid -gt 0 -and (Get-Process -Id `$existingPid -ErrorAction SilentlyContinue)) { - return `$null - } - } - catch { - } - } - - `$payload = @{ - pid = `$PID - sessionId = `$SessionId - createdAt = (Get-Date).ToUniversalTime().ToString('o') - } | ConvertTo-Json -Compress - Set-Content -LiteralPath `$lockPath -Value `$payload -Encoding UTF8 - return `$lockPath -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = `$true)][string]`$Uri, - [Parameter(Mandatory = `$true)][string]`$Json - ) - - `$httpClient = New-Object System.Net.Http.HttpClient - try { - `$content = New-Object System.Net.Http.StringContent(`$Json, [System.Text.Encoding]::UTF8, 'application/json') - `$response = `$httpClient.PostAsync(`$Uri, `$content).Result - if (-not `$response.IsSuccessStatusCode) { - return `$false - } - return `$true - } - catch { - return `$false - } - finally { - `$httpClient.Dispose() - } -} - -function Ensure-Bucket { - param( - [string]`$BucketId, - [string]`$ClientName, - [string]`$BucketType - ) - - if (`$script:KnownBuckets.ContainsKey(`$BucketId)) { - return - } - - try { - Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null - `$script:KnownBuckets[`$BucketId] = `$true - return - } - catch { - } - - `$body = @{ - client = `$ClientName - type = `$BucketType - hostname = `$script:Hostname - } | ConvertTo-Json -Compress - - try { - if (-not (Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" -Json `$body)) { - return - } - } - catch { - try { - Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null - } - catch { - return - } - } - - `$script:KnownBuckets[`$BucketId] = `$true -} - -function Send-LogonMarkerIfNeeded { - param( - [pscustomobject]`$Config, - [int]`$SessionId - ) - - `$sessionEvents = if (`$Config.PSObject.Properties.Name -contains 'sessionEvents') { `$Config.sessionEvents } else { `$null } - `$logging = if (`$Config.PSObject.Properties.Name -contains 'logging') { `$Config.logging } else { `$null } - `$logonEnabled = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'logonEnabled') { [bool]`$sessionEvents.logonEnabled } else { `$false } - if (-not `$logonEnabled) { - return - } - - `$bucketPrefix = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'bucketPrefix' -and -not [string]::IsNullOrWhiteSpace([string]`$sessionEvents.bucketPrefix)) { - [string]`$sessionEvents.bucketPrefix - } - else { - 'aw-session-events' - } - - `$stateRoot = [string]`$Config.paths.stateRoot - `$markerRoots = New-Object System.Collections.Generic.List[string] - if (-not [string]::IsNullOrWhiteSpace(`$env:LOCALAPPDATA)) { - `$markerRoots.Add((Join-Path `$env:LOCALAPPDATA 'AWatch-rus\markers')) - } - if (-not [string]::IsNullOrWhiteSpace(`$stateRoot)) { - `$markerRoots.Add((Join-Path `$stateRoot 'markers')) - } - - `$markerDir = `$null - foreach (`$candidate in `$markerRoots) { - try { - if (-not (Test-Path -LiteralPath `$candidate)) { - New-Item -Path `$candidate -ItemType Directory -Force | Out-Null - } - - `$probePath = Join-Path `$candidate 'write-test.tmp' - Set-Content -LiteralPath `$probePath -Value 'ok' -Encoding ASCII - Remove-Item -LiteralPath `$probePath -Force -ErrorAction SilentlyContinue - `$markerDir = `$candidate - break - } - catch { - } - } - - if (-not `$markerDir) { - return - } - - `$markerFile = Join-Path `$markerDir ("logon-{0}-{1}.marker" -f `$env:USERNAME, `$SessionId) - if (Test-Path -LiteralPath `$markerFile) { - return - } - - Set-Content -LiteralPath `$markerFile -Value ((Get-Date).ToUniversalTime().ToString('o')) -Encoding UTF8 - - `$bucketId = ('{0}_{1}' -f `$bucketPrefix, `$script:Hostname) - Ensure-Bucket -BucketId `$bucketId -ClientName 'aw-session-events' -BucketType 'aw.session.event' - - `$payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - eventType = 'logon' - username = `$env:USERNAME - userId = "`$(`$env:USERDOMAIN)\`$(`$env:USERNAME)" - sessionId = `$SessionId - hostname = `$script:Hostname - source = 'launch-watchers-awatch-rus' - } - } | ConvertTo-Json -Depth 5 -Compress - - try { - Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$bucketId/heartbeat?pulsetime=1" -Json `$payload - } - catch { - Remove-Item -LiteralPath `$markerFile -Force -ErrorAction SilentlyContinue - throw - } -} - -function Start-CollectorScriptIfNeeded { - param( - [string]`$ScriptPath, - [string]`$ConfigPath, - [string]`$PowerShellExe, - [int]`$SessionId - ) - - if ([string]::IsNullOrWhiteSpace(`$ScriptPath)) { - return - } - - if (-not (Test-Path -LiteralPath `$ScriptPath)) { - return - } - - if (Test-CollectorRunning -ScriptPath `$ScriptPath -SessionId `$SessionId) { - return - } - - if ((Get-CollectorPowerShellProcessCount) -ge `$script:MaxCollectorPowerShellProcesses) { - return - } - - `$staParam = if (`$ScriptPath -like "*endpoint-signals*") { "-STA" } else { `$null } - `$argumentList = @('-NoProfile', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass') - if (`$staParam) { `$argumentList += `$staParam } - `$argumentList += @('-File', `$ScriptPath, '-ConfigPath', `$ConfigPath) - Start-Process -FilePath `$PowerShellExe -ArgumentList `$argumentList -WindowStyle Hidden -} - -`$config = Get-DeploymentConfig -Path `$ConfigPath -`$sessionId = (Get-Process -Id `$PID).SessionId -`$installRoot = [string]`$config.paths.installRoot -`$stateRoot = [string]`$config.paths.stateRoot -`$script:ApiBase = '{0}://{1}:{2}/api/0' -f [string]`$config.server.scheme, [string]`$config.server.host, [string]`$config.server.port -`$script:Hostname = `$env:COMPUTERNAME -`$script:KnownBuckets = @{} -`$collectorScript = [string]`$config.paths.collectorScript -`$endpointCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]`$config.paths.endpointCollectorScript } else { Join-Path `$stateRoot 'dlp-endpoint-signals-collector.ps1' } -`$fileCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'fileCollectorScript') { [string]`$config.paths.fileCollectorScript } else { Join-Path `$stateRoot 'file-operations-collector.ps1' } -`$sessionCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]`$config.paths.sessionCollectorScript } else { Join-Path `$stateRoot 'worktime-session-collector.ps1' } -`$afkExe = Join-Path `$installRoot 'aw-watcher-afk\aw-watcher-afk.exe' -`$windowExe = Join-Path `$installRoot 'aw-watcher-window\aw-watcher-window.exe' -`$serverArgs = @('--host', [string]`$config.server.host, '--port', [string]`$config.server.port) -`$powershellExe = Join-Path `$env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' -`$afkEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]`$config.collectors.afkEnabled } else { `$true } -`$windowEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]`$config.collectors.windowEnabled } else { `$true } -`$fileOpsEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { [bool]`$config.collectors.fileOpsEnabled } else { `$true } -`$emailEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'emailEnabled') { [bool]`$config.collectors.emailEnabled } else { `$false } -`$emailCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'emailCollectorScript') { [string]`$config.paths.emailCollectorScript } else { Join-Path `$stateRoot 'email-outbound-collector.ps1' } -`$launchLockPath = New-LaunchLock -StateRoot `$stateRoot -SessionId `$sessionId -if (-not `$launchLockPath) { - return -} - -try { - if (`$afkEnabled -and -not (Test-Path -LiteralPath `$afkExe)) { - throw "Не найден aw-watcher-afk.exe: `$afkExe" - } - - if (`$windowEnabled -and -not (Test-Path -LiteralPath `$windowExe)) { - throw "Не найден aw-watcher-window.exe: `$windowExe" - } - - if (`$afkEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-afk' -SessionId `$sessionId)) { - Start-Process -FilePath `$afkExe -ArgumentList `$serverArgs -WindowStyle Hidden - } - - if (`$windowEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-window' -SessionId `$sessionId)) { - Start-Process -FilePath `$windowExe -ArgumentList `$serverArgs -WindowStyle Hidden - } - - try { - Send-LogonMarkerIfNeeded -Config `$config -SessionId `$sessionId - } - catch { - } - Start-CollectorScriptIfNeeded -ScriptPath `$collectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - Start-CollectorScriptIfNeeded -ScriptPath `$endpointCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - if (`$fileOpsEnabled) { - Start-CollectorScriptIfNeeded -ScriptPath `$fileCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - } - Start-CollectorScriptIfNeeded -ScriptPath `$sessionCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - if (`$emailEnabled -and (Test-Path -LiteralPath `$emailCollectorScript)) { - Start-CollectorScriptIfNeeded -ScriptPath `$emailCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - } -} -finally { - if (`$launchLockPath -and (Test-Path -LiteralPath `$launchLockPath)) { - Remove-Item -LiteralPath `$launchLockPath -Force -ErrorAction SilentlyContinue - } -} -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding UTF8 -} - -function Write-ActivityWatchRecoveryScript { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $content = @" -param( - [string]`$ConfigPath = '$ConfigPath' -) - -Set-StrictMode -Version Latest -`$ErrorActionPreference = 'Continue' - -function Get-DeploymentConfig { - param([string]`$Path) - return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json -} - -function Get-RecoveryConfigPaths { - param([string]`$PrimaryConfigPath) - - `$paths = New-Object System.Collections.Generic.List[string] - if (`$PrimaryConfigPath -and (Test-Path -LiteralPath `$PrimaryConfigPath)) { - `$paths.Add((Resolve-Path -LiteralPath `$PrimaryConfigPath).Path) - } - - `$searchRoot = `$env:ProgramData - if (`$PrimaryConfigPath) { - `$stateRoot = Split-Path -Path `$PrimaryConfigPath -Parent - `$candidateRoot = Split-Path -Path `$stateRoot -Parent - if (`$candidateRoot -and (Test-Path -LiteralPath `$candidateRoot)) { - `$searchRoot = `$candidateRoot - } - } - - if (Test-Path -LiteralPath `$searchRoot) { - Get-ChildItem -LiteralPath `$searchRoot -Directory -ErrorAction SilentlyContinue | - Where-Object { `$_.Name -like 'ActivityWatch*' } | - ForEach-Object { - `$candidate = Join-Path `$_.FullName 'deployment-config.json' - if (Test-Path -LiteralPath `$candidate) { - `$paths.Add(`$candidate) - } - } - } - - return @(`$paths | Sort-Object -Unique) -} - -function Get-RecoveryTaskNames { - param([string[]]`$ConfigPaths) - - `$taskNames = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) - foreach (`$candidatePath in @(`$ConfigPaths)) { - try { - `$config = Get-DeploymentConfig -Path `$candidatePath - foreach (`$task in @(`$config.userTasks)) { - `$taskName = [string]`$task.launchTaskName - if (-not [string]::IsNullOrWhiteSpace(`$taskName)) { - [void]`$taskNames.Add(`$taskName) - } - } - } - catch { - } - } - - return @(`$taskNames) -} - -function New-RecoveryLock { - param([string]`$PrimaryConfigPath) - - `$stateRoot = if (`$PrimaryConfigPath) { Split-Path -Path `$PrimaryConfigPath -Parent } else { Join-Path `$env:ProgramData 'AWatch-rus' } - if (-not (Test-Path -LiteralPath `$stateRoot)) { - New-Item -Path `$stateRoot -ItemType Directory -Force | Out-Null - } - - `$lockPath = Join-Path `$stateRoot 'recovery-loop.lock' - if (Test-Path -LiteralPath `$lockPath) { - try { - `$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json - `$existingPid = [int]`$lockData.pid - if (`$existingPid -gt 0 -and (Get-Process -Id `$existingPid -ErrorAction SilentlyContinue)) { - return `$null - } - } - catch { - } - } - - `$payload = @{ - pid = `$PID - createdAt = (Get-Date).ToUniversalTime().ToString('o') - } | ConvertTo-Json -Compress - Set-Content -LiteralPath `$lockPath -Value `$payload -Encoding UTF8 - return `$lockPath -} - -function Start-TaskIfNotRunning { - param([string]`$TaskName) - if ([string]::IsNullOrWhiteSpace(`$TaskName)) { - return - } - - try { - `$task = Get-ScheduledTask -TaskName `$TaskName -ErrorAction SilentlyContinue - if (-not `$task) { - return - } - if ([string]`$task.State -eq 'Running') { - return - } - Start-ScheduledTask -TaskName `$TaskName -ErrorAction SilentlyContinue - } - catch { - } -} - -`$recoveryLockPath = New-RecoveryLock -PrimaryConfigPath `$ConfigPath -if (-not `$recoveryLockPath) { - return -} - -try { - while (`$true) { - `$sleepSeconds = 180 - try { - `$configPaths = Get-RecoveryConfigPaths -PrimaryConfigPath `$ConfigPath - foreach (`$taskName in Get-RecoveryTaskNames -ConfigPaths `$configPaths) { - Start-TaskIfNotRunning -TaskName `$taskName - } - - `$config = Get-DeploymentConfig -Path `$ConfigPath - if (`$config -and `$config.recovery -and `$config.recovery.intervalSeconds) { - `$sleepSeconds = [Math]::Max([int]`$config.recovery.intervalSeconds, 30) - } - } - catch { - } - - Start-Sleep -Seconds `$sleepSeconds - } -} -finally { - if (`$recoveryLockPath -and (Test-Path -LiteralPath `$recoveryLockPath)) { - Remove-Item -LiteralPath `$recoveryLockPath -Force -ErrorAction SilentlyContinue - } -} -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding UTF8 -} - -function Get-ActivityWatchHiddenLauncherPath { - param( - [Parameter(Mandatory = $true)] - [string]$ScriptPath - ) - - $directory = Split-Path -Path $ScriptPath -Parent - $baseName = [IO.Path]::GetFileNameWithoutExtension($ScriptPath) - return Join-Path $directory ("{0}-hidden.vbs" -f $baseName) -} - -function Write-ActivityWatchHiddenPowerShellWrapper { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $directory = Split-Path -Path $Path -Parent - if ($directory) { - New-ActivityWatchDirectory -Path $directory - } - - $powershellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' - $escapedPowerShellExe = $powershellExe.Replace('"', '""') - $escapedScriptPath = $ScriptPath.Replace('"', '""') - $escapedConfigPath = $ConfigPath.Replace('"', '""') - - $content = @" -Set shell = CreateObject("WScript.Shell") -shell.Run """$escapedPowerShellExe"" -NoProfile -ExecutionPolicy Bypass -File ""$escapedScriptPath"" -ConfigPath ""$escapedConfigPath""", 0, False -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding ASCII -} - -function Remove-LegacyActivityWatchEntries { - $legacyTaskNames = @( - 'ActivityWatch Watchers', - 'ActivityWatch Guard', - 'ActivityWatch Heal' - ) - - foreach ($taskName in $legacyTaskNames) { - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue - } - - $runKey = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run' - foreach ($name in 'ActivityWatchAFK', 'ActivityWatchWindow', 'ActivityWatchBrowserCollector') { - Remove-ItemProperty -Path $runKey -Name $name -ErrorAction SilentlyContinue - } -} - -function Remove-ActivityWatchScheduledTask { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName - ) - - Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue - & cmd.exe /c "schtasks /Delete /TN `"$TaskName`" /F >nul 2>&1" | Out-Null - - for ($attempt = 0; $attempt -lt 10; $attempt++) { - $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue - if (-not $task) { - return - } - - Start-Sleep -Milliseconds 300 - } -} - -function Set-ActivityWatchScheduledTaskAction { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [Parameter(Mandatory = $true)] - [string]$Execute, - [Parameter(Mandatory = $true)] - [string]$Arguments - ) - - $taskCommand = ('"{0}" {1}' -f $Execute, $Arguments) - & schtasks.exe /Change /TN $TaskName /TR $taskCommand | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "schtasks.exe /Change завершился с ошибкой для $TaskName" - } -} - -function Get-ActivityWatchScheduledTaskByCommand { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [string]$CommandMatch - ) - - $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue - if ($task) { - return $task - } - - if ([string]::IsNullOrWhiteSpace($CommandMatch)) { - return $null - } - - foreach ($candidate in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch*' })) { - foreach ($action in @($candidate.Actions)) { - if ([string]$action.Arguments -like "*$CommandMatch*") { - return $candidate - } - } - } - - return $null -} - -function Remove-StaleActivityWatchUserTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath - ) - - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath - $desiredTaskNames = @($TaskDefinitions | ForEach-Object { [string]$_.LaunchTaskName }) - - foreach ($candidate in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch*' })) { - $taskName = [string]$candidate.TaskName - if ($desiredTaskNames -contains $taskName) { - continue - } - - $usesCurrentLauncher = $false - foreach ($action in @($candidate.Actions)) { - if ([string]$action.Arguments -like "*$launcherPath*") { - $usesCurrentLauncher = $true - break - } - } - - if ($usesCurrentLauncher) { - Remove-ActivityWatchScheduledTask -TaskName $taskName - } - } -} - -function Register-ActivityWatchUserTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe' - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath - Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $LaunchScriptPath -ConfigPath $ConfigPath - Remove-StaleActivityWatchUserTasks -TaskDefinitions $TaskDefinitions -LaunchScriptPath $LaunchScriptPath - - foreach ($definition in $TaskDefinitions) { - $action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`"" - $trigger = New-ScheduledTaskTrigger -AtLogOn -User $definition.UserId - $principal = New-ScheduledTaskPrincipal -UserId $definition.UserId -LogonType Interactive -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0) - $existingTask = Get-ActivityWatchScheduledTaskByCommand -TaskName $definition.LaunchTaskName -CommandMatch $ConfigPath - - if ($existingTask) { - Set-ActivityWatchScheduledTaskAction -TaskName $existingTask.TaskName -Execute $wscriptExe -Arguments $action.Arguments - continue - } - - Remove-ActivityWatchScheduledTask -TaskName $definition.LaunchTaskName - Register-ScheduledTask -TaskName $definition.LaunchTaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null - } -} - -function Register-ActivityWatchRecoveryTask { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [Parameter(Mandatory = $true)] - [string]$RecoveryScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - Remove-ActivityWatchScheduledTask -TaskName $TaskName - - $wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe' - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $RecoveryScriptPath - Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $RecoveryScriptPath -ConfigPath $ConfigPath - $action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`"" - $trigger = New-ScheduledTaskTrigger -AtStartup - $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -Hidden -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0) - - Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null -} - -function Set-ActivityWatchAcl { - param( - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [Parameter(Mandatory = $true)] - [string]$LogsRoot - ) - - foreach ($path in $InstallRoot, $StateRoot, $LogsRoot) { - New-ActivityWatchDirectory -Path $path - } - - & icacls $InstallRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $InstallRoot" - } - - & icacls $StateRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $StateRoot" - } - - & icacls $LogsRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(M)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $LogsRoot" - } -} - -function Start-ActivityWatchTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [string]$RecoveryTaskName = 'ActivityWatch Recovery' - ) - - foreach ($definition in $TaskDefinitions) { - Start-ScheduledTask -TaskName $definition.LaunchTaskName -ErrorAction SilentlyContinue - } - - Start-ScheduledTask -TaskName $RecoveryTaskName -ErrorAction SilentlyContinue -} - -Export-ModuleMember -Function *-ActivityWatch*, Assert-Administrator, Normalize-ActivityWatchUsers, Get-ActivityWatchPackageUrl, Remove-LegacyActivityWatchEntries -; } - - Start-Sleep -Seconds `$sleepSeconds - } -} -finally { - if (`$recoveryLockPath -and (Test-Path -LiteralPath `$recoveryLockPath)) { - Remove-Item -LiteralPath `$recoveryLockPath -Force -ErrorAction SilentlyContinue - } -} -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding UTF8 -} - -function Get-ActivityWatchHiddenLauncherPath { - param( - [Parameter(Mandatory = $true)] - [string]$ScriptPath - ) - - $directory = Split-Path -Path $ScriptPath -Parent - $baseName = [IO.Path]::GetFileNameWithoutExtension($ScriptPath) - return Join-Path $directory ("{0}-hidden.vbs" -f $baseName) -} - -function Write-ActivityWatchHiddenPowerShellWrapper { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $directory = Split-Path -Path $Path -Parent - if ($directory) { - New-ActivityWatchDirectory -Path $directory - } - - $powershellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' - $escapedPowerShellExe = $powershellExe.Replace('"', '""') - $escapedScriptPath = $ScriptPath.Replace('"', '""') - $escapedConfigPath = $ConfigPath.Replace('"', '""') - - $content = @" -Set shell = CreateObject("WScript.Shell") -shell.Run """$escapedPowerShellExe"" -NoProfile -ExecutionPolicy Bypass -File ""$escapedScriptPath"" -ConfigPath ""$escapedConfigPath""", 0, False -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding ASCII -} - -function Remove-LegacyActivityWatchEntries { - $legacyTaskNames = @( - 'ActivityWatch Watchers', - 'ActivityWatch Guard', - 'ActivityWatch Heal' - ) - - foreach ($taskName in $legacyTaskNames) { - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue - } - - $runKey = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run' - foreach ($name in 'ActivityWatchAFK', 'ActivityWatchWindow', 'ActivityWatchBrowserCollector') { - Remove-ItemProperty -Path $runKey -Name $name -ErrorAction SilentlyContinue - } -} - -function Remove-ActivityWatchScheduledTask { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName - ) - - Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue - & cmd.exe /c "schtasks /Delete /TN `"$TaskName`" /F >nul 2>&1" | Out-Null - - for ($attempt = 0; $attempt -lt 10; $attempt++) { - $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue - if (-not $task) { - return - } - - Start-Sleep -Milliseconds 300 - } -} - -function Set-ActivityWatchScheduledTaskAction { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [Parameter(Mandatory = $true)] - [string]$Execute, - [Parameter(Mandatory = $true)] - [string]$Arguments - ) - - $taskCommand = ('"{0}" {1}' -f $Execute, $Arguments) - & schtasks.exe /Change /TN $TaskName /TR $taskCommand | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "schtasks.exe /Change завершился с ошибкой для $TaskName" - } -} - -function Get-ActivityWatchScheduledTaskByCommand { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [string]$CommandMatch - ) - - $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue - if ($task) { - return $task - } - - if ([string]::IsNullOrWhiteSpace($CommandMatch)) { - return $null - } - - foreach ($candidate in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch*' })) { - foreach ($action in @($candidate.Actions)) { - if ([string]$action.Arguments -like "*$CommandMatch*") { - return $candidate - } - } - } - - return $null -} - -function Remove-StaleActivityWatchUserTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath - ) - - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath - $desiredTaskNames = @($TaskDefinitions | ForEach-Object { [string]$_.LaunchTaskName }) - - foreach ($candidate in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch*' })) { - $taskName = [string]$candidate.TaskName - if ($desiredTaskNames -contains $taskName) { - continue - } - - $usesCurrentLauncher = $false - foreach ($action in @($candidate.Actions)) { - if ([string]$action.Arguments -like "*$launcherPath*") { - $usesCurrentLauncher = $true - break - } - } - - if ($usesCurrentLauncher) { - Remove-ActivityWatchScheduledTask -TaskName $taskName - } - } -} - -function Register-ActivityWatchUserTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe' - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath - Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $LaunchScriptPath -ConfigPath $ConfigPath - Remove-StaleActivityWatchUserTasks -TaskDefinitions $TaskDefinitions -LaunchScriptPath $LaunchScriptPath - - foreach ($definition in $TaskDefinitions) { - $action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`"" - $trigger = New-ScheduledTaskTrigger -AtLogOn -User $definition.UserId - $principal = New-ScheduledTaskPrincipal -UserId $definition.UserId -LogonType Interactive -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0) - $existingTask = Get-ActivityWatchScheduledTaskByCommand -TaskName $definition.LaunchTaskName -CommandMatch $ConfigPath - - if ($existingTask) { - Set-ActivityWatchScheduledTaskAction -TaskName $existingTask.TaskName -Execute $wscriptExe -Arguments $action.Arguments - continue - } - - Remove-ActivityWatchScheduledTask -TaskName $definition.LaunchTaskName - Register-ScheduledTask -TaskName $definition.LaunchTaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null - } -} - -function Register-ActivityWatchRecoveryTask { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [Parameter(Mandatory = $true)] - [string]$RecoveryScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - Remove-ActivityWatchScheduledTask -TaskName $TaskName - - $wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe' - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $RecoveryScriptPath - Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $RecoveryScriptPath -ConfigPath $ConfigPath - $action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`"" - $trigger = New-ScheduledTaskTrigger -AtStartup - $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -Hidden -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0) - - Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null -} - -function Set-ActivityWatchAcl { - param( - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [Parameter(Mandatory = $true)] - [string]$LogsRoot - ) - - foreach ($path in $InstallRoot, $StateRoot, $LogsRoot) { - New-ActivityWatchDirectory -Path $path - } - - & icacls $InstallRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $InstallRoot" - } - - & icacls $StateRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $StateRoot" - } - - & icacls $LogsRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(M)' | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "icacls завершился с ошибкой для $LogsRoot" - } -} - -function Start-ActivityWatchTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [string]$RecoveryTaskName = 'ActivityWatch Recovery' - ) - - foreach ($definition in $TaskDefinitions) { - Start-ScheduledTask -TaskName $definition.LaunchTaskName -ErrorAction SilentlyContinue - } - - Start-ScheduledTask -TaskName $RecoveryTaskName -ErrorAction SilentlyContinue -} - -Export-ModuleMember -Function *-ActivityWatch*, Assert-Administrator, Normalize-ActivityWatchUsers, Get-ActivityWatchPackageUrl, Remove-LegacyActivityWatchEntries diff --git a/windows/email-outbound-collector.ps1 b/windows/email-outbound-collector.ps1 index 078533c..7a1600f 100644 --- a/windows/email-outbound-collector.ps1 +++ b/windows/email-outbound-collector.ps1 @@ -1,56 +1,4 @@ -<# -.SYNOPSIS - DLP email outbound collector for AWatch-rus (Phase 2.5). - Monitors outgoing email via Outlook COM Sent Items polling - and/or SMTP network connection detection. - -.DESCRIPTION - Two collection modes (configurable, can run simultaneously): - - outlook : Polls Outlook Sent Items via COM for new messages. - - smtp : Monitors SMTP connections (ports 25/587/465) via - Get-NetTCPConnection for any process sending mail. - - Sends heartbeats to AW bucket `aw-email-monitor_`. - Evaluates DLP policy rules from `endpoint.email[]` section. - Supports enforcement: action="block" moves the email to Drafts - (Outlook mode) or logs with enforced=false (SMTP mode). -#> -[CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$PolicyPath, - [string]$LogPath, - [int]$PollSeconds, - [ValidateSet('outlook', 'smtp', 'both')] - [string]$Mode = 'both' -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -# --------------------------------------------------------------------------- -# Shared infrastructure (mirrors other collectors) -# --------------------------------------------------------------------------- - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - return $null -} - -function Write-CollectorLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { return } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { Write-Error <# +<# .SYNOPSIS DLP email outbound collector for AWatch-rus (Phase 2.5). Monitors outgoing email via Outlook COM Sent Items polling @@ -632,2279 +580,3 @@ while ($true) { Start-Sleep -Seconds $resolvedPollSeconds } -; } -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType - ) - if ($script:KnownBuckets.ContainsKey($BucketId)) { return } - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - $script:KnownBuckets[$BucketId] = $true -} - -function Get-StringHash { - param([AllowNull()][string]$Value) - if ($null -eq $Value) { return $null } - $bytes = [Text.Encoding]::UTF8.GetBytes($Value) - $sha = [Security.Cryptography.SHA256]::Create() - try { - ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { $sha.Dispose() } -} - -function Send-EmailHeartbeat { - param( - [string]$SignalType, - [hashtable]$Data - ) - $bucketId = 'aw-email-monitor_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-email-monitor' -BucketType 'aw.dlp.email' - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'email-outbound-collector' - } + $Data - } | ConvertTo-Json -Depth 6 -Compress - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Send-EmailIncidentHeartbeat { - param( - [string]$RuleId, - [string]$Action, - [string]$Severity, - [string]$Message, - [hashtable]$Data - ) - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = $RuleId - action = $Action - severity = $Severity - message = $Message - signalType = 'email_outbound' - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'email-outbound-collector' - } + $Data - } | ConvertTo-Json -Depth 7 -Compress - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Show-EnforcementNotification { - param( - [Parameter(Mandatory = $true)][string]$Title, - [Parameter(Mandatory = $true)][string]$Body - ) - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue - $icon = New-Object System.Windows.Forms.NotifyIcon - $icon.Icon = [System.Drawing.SystemIcons]::Warning - $icon.BalloonTipTitle = $Title - $icon.BalloonTipText = $Body - $icon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Warning - $icon.Visible = $true - $icon.ShowBalloonTip(5000) - Start-Sleep -Milliseconds 200 - $icon.Dispose() - } - catch { Write-Error <# -.SYNOPSIS - DLP email outbound collector for AWatch-rus (Phase 2.5). - Monitors outgoing email via Outlook COM Sent Items polling - and/or SMTP network connection detection. - -.DESCRIPTION - Two collection modes (configurable, can run simultaneously): - - outlook : Polls Outlook Sent Items via COM for new messages. - - smtp : Monitors SMTP connections (ports 25/587/465) via - Get-NetTCPConnection for any process sending mail. - - Sends heartbeats to AW bucket `aw-email-monitor_`. - Evaluates DLP policy rules from `endpoint.email[]` section. - Supports enforcement: action="block" moves the email to Drafts - (Outlook mode) or logs with enforced=false (SMTP mode). -#> -[CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$PolicyPath, - [string]$LogPath, - [int]$PollSeconds, - [ValidateSet('outlook', 'smtp', 'both')] - [string]$Mode = 'both' -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -# --------------------------------------------------------------------------- -# Shared infrastructure (mirrors other collectors) -# --------------------------------------------------------------------------- - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - return $null -} - -function Write-CollectorLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { return } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { } -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType - ) - if ($script:KnownBuckets.ContainsKey($BucketId)) { return } - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - $script:KnownBuckets[$BucketId] = $true -} - -function Get-StringHash { - param([AllowNull()][string]$Value) - if ($null -eq $Value) { return $null } - $bytes = [Text.Encoding]::UTF8.GetBytes($Value) - $sha = [Security.Cryptography.SHA256]::Create() - try { - ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { $sha.Dispose() } -} - -function Send-EmailHeartbeat { - param( - [string]$SignalType, - [hashtable]$Data - ) - $bucketId = 'aw-email-monitor_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-email-monitor' -BucketType 'aw.dlp.email' - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'email-outbound-collector' - } + $Data - } | ConvertTo-Json -Depth 6 -Compress - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Send-EmailIncidentHeartbeat { - param( - [string]$RuleId, - [string]$Action, - [string]$Severity, - [string]$Message, - [hashtable]$Data - ) - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = $RuleId - action = $Action - severity = $Severity - message = $Message - signalType = 'email_outbound' - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'email-outbound-collector' - } + $Data - } | ConvertTo-Json -Depth 7 -Compress - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Show-EnforcementNotification { - param( - [Parameter(Mandatory = $true)][string]$Title, - [Parameter(Mandatory = $true)][string]$Body - ) - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue - $icon = New-Object System.Windows.Forms.NotifyIcon - $icon.Icon = [System.Drawing.SystemIcons]::Warning - $icon.BalloonTipTitle = $Title - $icon.BalloonTipText = $Body - $icon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Warning - $icon.Visible = $true - $icon.ShowBalloonTip(5000) - Start-Sleep -Milliseconds 200 - $icon.Dispose() - } - catch { } -} - -# --------------------------------------------------------------------------- -# DLP Policy -# --------------------------------------------------------------------------- - -function Load-EmailPolicy { - param([string]$Path) - - $script:Policy = [ordered]@{ - defaults = [ordered]@{ - enabled = $true - cooldownSeconds = 300 - action = 'alert' - severity = 'medium' - } - endpoint = [ordered]@{ - email = @() - } - } - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-CollectorLog ("policy not found, using defaults: {0}" -f $Path) - return - } - - try { - $raw = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - if ($raw.defaults) { - if ($raw.defaults.PSObject.Properties.Name -contains 'enabled') { $script:Policy.defaults.enabled = [bool]$raw.defaults.enabled } - if ($raw.defaults.cooldownSeconds) { $script:Policy.defaults.cooldownSeconds = [int]$raw.defaults.cooldownSeconds } - if ($raw.defaults.action) { $script:Policy.defaults.action = [string]$raw.defaults.action } - if ($raw.defaults.severity) { $script:Policy.defaults.severity = [string]$raw.defaults.severity } - } - if ($raw.endpoint -and $raw.endpoint.email) { - $script:Policy.endpoint.email = @($raw.endpoint.email) - } - } - catch { - Write-CollectorLog ("policy parse failed: {0}" -f $_.Exception.Message) - } -} - -function Should-EmitByCooldown { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - $now = (Get-Date).ToUniversalTime() - if ($script:Cooldown.ContainsKey($Fingerprint)) { - $last = [datetime]$script:Cooldown[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - $script:Cooldown[$Fingerprint] = $now - return $true -} - -# --------------------------------------------------------------------------- -# Email DLP rule evaluation -# --------------------------------------------------------------------------- - -function Evaluate-EmailRules { - param( - [string]$Subject, - [string]$RecipientsJoined, - [string]$SenderAddress, - [int]$AttachmentCount, - [string]$AttachmentNames, - [int]$BodyLength, - [string]$MessageId, - $OutlookMailItem - ) - - foreach ($rule in @($script:Policy.endpoint.email)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $matched = $true - - if ($rule.subjectRegex) { - $matched = $matched -and ($Subject -match [string]$rule.subjectRegex) - } - if ($rule.recipientRegex) { - $matched = $matched -and ($RecipientsJoined -match [string]$rule.recipientRegex) - } - if ($rule.senderRegex) { - $matched = $matched -and ($SenderAddress -match [string]$rule.senderRegex) - } - if ($rule.attachmentRegex -and $AttachmentNames) { - $matched = $matched -and ($AttachmentNames -match [string]$rule.attachmentRegex) - } - if ($rule.minAttachments) { - $matched = $matched -and ($AttachmentCount -ge [int]$rule.minAttachments) - } - if ($rule.minBodyLength) { - $matched = $matched -and ($BodyLength -ge [int]$rule.minBodyLength) - } - if ($rule.externalOnly -and [bool]$rule.externalOnly) { - $internalDomain = if ($rule.internalDomain) { [string]$rule.internalDomain } else { '' } - if ($internalDomain -and $RecipientsJoined -notmatch [regex]::Escape($internalDomain)) { - # all recipients are external — continue matching - } - elseif ($internalDomain) { - $matched = $false - } - } - - if (-not $matched) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "email|$ruleId|$MessageId|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Email rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block' -and $null -ne $OutlookMailItem) { - $enforced = Invoke-EmailEnforcement -MailItem $OutlookMailItem -RuleId $ruleId - Show-EnforcementNotification -Title 'DLP: письмо перемещено в черновики' -Body $message - } - elseif ($action -eq 'block') { - Show-EnforcementNotification -Title 'DLP: обнаружена отправка письма' -Body $message - } - - Send-EmailIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -Data @{ - subject = (Get-StringHash -Value $Subject) - recipients = (Get-StringHash -Value $RecipientsJoined) - sender = $SenderAddress - attachmentCount = $AttachmentCount - attachmentNames = $AttachmentNames - bodyLength = $BodyLength - enforced = $enforced - } - Write-CollectorLog ("incident email rule={0} action={1} severity={2} enforced={3} subject_hash={4}" -f $ruleId, $action, $severity, $enforced, (Get-StringHash -Value $Subject)) - } -} - -function Invoke-EmailEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)]$MailItem, - [string]$RuleId - ) - try { - $draftsFolder = $script:OutlookNamespace.GetDefaultFolder(16) # olFolderDrafts - $MailItem.Move($draftsFolder) | Out-Null - Write-CollectorLog ("enforcement: email moved to Drafts rule={0} subject_hash={1}" -f $RuleId, (Get-StringHash -Value $MailItem.Subject)) - return $true - } - catch { - Write-CollectorLog ("enforcement: email move to Drafts failed rule={0}: {1}" -f $RuleId, $_.Exception.Message) - return $false - } -} - -# --------------------------------------------------------------------------- -# Outlook Sent Items polling -# --------------------------------------------------------------------------- - -function Initialize-OutlookCom { - try { - $script:OutlookApp = New-Object -ComObject Outlook.Application - $script:OutlookNamespace = $script:OutlookApp.GetNamespace('MAPI') - $script:SentFolder = $script:OutlookNamespace.GetDefaultFolder(5) # olFolderSentMail - Write-CollectorLog "Outlook COM initialized, Sent Items folder opened" - return $true - } - catch { - Write-CollectorLog ("Outlook COM init failed: {0}" -f $_.Exception.Message) - return $false - } -} - -function Get-OutlookSentItems { - param([datetime]$Since) - - $results = @() - try { - $items = $script:SentFolder.Items - $items.Sort('[SentOn]', $true) - - $filter = "[SentOn] >= '{0}'" -f $Since.ToString('MM/dd/yyyy HH:mm') - $restricted = $items.Restrict($filter) - - foreach ($item in $restricted) { - try { - if ($item.Class -ne 43) { continue } # olMail = 43 - - $recipients = @() - for ($i = 1; $i -le $item.Recipients.Count; $i++) { - $recip = $item.Recipients.Item($i) - $recipients += [string]$recip.Address - } - - $attachmentNames = @() - for ($i = 1; $i -le $item.Attachments.Count; $i++) { - $attachmentNames += [string]$item.Attachments.Item($i).FileName - } - - $results += [pscustomobject]@{ - EntryID = [string]$item.EntryID - Subject = [string]$item.Subject - SenderAddress = [string]$item.SenderEmailAddress - SenderName = [string]$item.SenderName - Recipients = $recipients - RecipientsJoined = ($recipients -join '; ') - AttachmentCount = [int]$item.Attachments.Count - AttachmentNames = ($attachmentNames -join '; ') - BodyLength = if ($item.Body) { $item.Body.Length } else { 0 } - SentOn = $item.SentOn - MailItem = $item - } - } - catch { } - } - } - catch { - Write-CollectorLog ("Outlook Sent Items scan failed: {0}" -f $_.Exception.Message) - } - return $results -} - -function Poll-OutlookSentItems { - $items = Get-OutlookSentItems -Since $script:OutlookLastPoll - - foreach ($item in $items) { - $entryId = $item.EntryID - if ($script:SeenEntryIds.ContainsKey($entryId)) { continue } - $script:SeenEntryIds[$entryId] = (Get-Date).ToUniversalTime() - - $subjectHash = Get-StringHash -Value $item.Subject - - Send-EmailHeartbeat -SignalType 'email_sent' -Data @{ - subject = $subjectHash - sender = [string]$item.SenderAddress - senderName = [string]$item.SenderName - recipientCount = $item.Recipients.Count - recipients = (Get-StringHash -Value $item.RecipientsJoined) - attachmentCount = [int]$item.AttachmentCount - attachmentNames = [string]$item.AttachmentNames - bodyLength = [int]$item.BodyLength - sentOn = if ($item.SentOn) { $item.SentOn.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') } else { '' } - collectionMode = 'outlook' - } - Write-CollectorLog ("email_sent outlook subject_hash={0} to={1} attachments={2}" -f $subjectHash, $item.Recipients.Count, $item.AttachmentCount) - - Evaluate-EmailRules ` - -Subject $item.Subject ` - -RecipientsJoined $item.RecipientsJoined ` - -SenderAddress $item.SenderAddress ` - -AttachmentCount $item.AttachmentCount ` - -AttachmentNames $item.AttachmentNames ` - -BodyLength $item.BodyLength ` - -MessageId $entryId ` - -OutlookMailItem $item.MailItem - } - - $script:OutlookLastPoll = (Get-Date).AddSeconds(-10) - - # Cleanup old entry IDs (keep last 24h) - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-24) - foreach ($k in @($script:SeenEntryIds.Keys)) { - if ([datetime]$script:SeenEntryIds[$k] -lt $cleanupBefore) { - $script:SeenEntryIds.Remove($k) - } - } -} - -# --------------------------------------------------------------------------- -# SMTP network connection monitoring -# --------------------------------------------------------------------------- - -function Poll-SmtpConnections { - try { - $smtpPorts = @(25, 587, 465, 2525) - $connections = Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue | - Where-Object { $smtpPorts -contains $_.RemotePort } - - foreach ($conn in @($connections)) { - $processId = [int]$conn.OwningProcess - $remoteAddr = [string]$conn.RemoteAddress - $remotePort = [int]$conn.RemotePort - $fingerprint = "{0}:{1}:{2}" -f $processId, $remoteAddr, $remotePort - if ($script:SeenSmtpConnections.ContainsKey($fingerprint)) { continue } - $script:SeenSmtpConnections[$fingerprint] = (Get-Date).ToUniversalTime() - - $processName = '' - try { - $proc = Get-Process -Id $processId -ErrorAction SilentlyContinue - $processName = [string]$proc.ProcessName - } - catch { } - - Send-EmailHeartbeat -SignalType 'smtp_connection' -Data @{ - remoteAddress = $remoteAddr - remotePort = $remotePort - processId = $processId - processName = $processName - localPort = [int]$conn.LocalPort - collectionMode = 'smtp' - } - Write-CollectorLog ("smtp_connection process={0}({1}) remote={2}:{3}" -f $processName, $processId, $remoteAddr, $remotePort) - - Evaluate-EmailRules ` - -Subject '' ` - -RecipientsJoined $remoteAddr ` - -SenderAddress $env:USERNAME ` - -AttachmentCount 0 ` - -AttachmentNames '' ` - -BodyLength 0 ` - -MessageId $fingerprint ` - -OutlookMailItem $null - } - - # Cleanup old SMTP connections (keep last 8h) - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenSmtpConnections.Keys)) { - if ([datetime]$script:SeenSmtpConnections[$k] -lt $cleanupBefore) { - $script:SeenSmtpConnections.Remove($k) - } - } - } - catch { - Write-CollectorLog ("SMTP poll error: {0}" -f $_.Exception.Message) - } -} - -# --------------------------------------------------------------------------- -# Initialization -# --------------------------------------------------------------------------- - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } -$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } -$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } -$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' } -$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 10 } -$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } -$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("email-outbound-{0}.log" -f $env:USERNAME) } -$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:Cooldown = @{} -$script:SeenEntryIds = @{} -$script:SeenSmtpConnections = @{} -$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30) -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:OutlookApp = $null -$script:OutlookNamespace = $null -$script:SentFolder = $null -$script:OutlookLastPoll = (Get-Date).AddMinutes(-5) - -Load-EmailPolicy -Path $resolvedPolicyPath -Write-CollectorLog ("email collector started mode={0} against {1}" -f $Mode, $script:ApiBase) - -$useOutlook = ($Mode -eq 'outlook' -or $Mode -eq 'both') -$useSmtp = ($Mode -eq 'smtp' -or $Mode -eq 'both') -$outlookReady = $false - -if ($useOutlook) { - $outlookReady = Initialize-OutlookCom - if (-not $outlookReady -and $Mode -eq 'outlook') { - Write-CollectorLog "Outlook COM not available, collector will retry" - } -} - -# --------------------------------------------------------------------------- -# Main loop -# --------------------------------------------------------------------------- - -while ($true) { - try { - if (-not $script:Policy.defaults.enabled) { - Start-Sleep -Seconds $resolvedPollSeconds - continue - } - - if ($useOutlook) { - if (-not $outlookReady) { - $outlookReady = Initialize-OutlookCom - } - if ($outlookReady) { - try { - Poll-OutlookSentItems - } - catch { - Write-CollectorLog ("outlook poll error: {0}" -f $_.Exception.Message) - $outlookReady = $false - $script:OutlookApp = $null - $script:OutlookNamespace = $null - $script:SentFolder = $null - } - } - } - - if ($useSmtp) { - try { - Poll-SmtpConnections - } - catch { - Write-CollectorLog ("smtp poll error: {0}" -f $_.Exception.Message) - } - } - } - catch { - Write-CollectorLog ("collector error: {0}" -f $_.Exception.Message) - } - - Start-Sleep -Seconds $resolvedPollSeconds -} -; } -} - -# --------------------------------------------------------------------------- -# DLP Policy -# --------------------------------------------------------------------------- - -function Load-EmailPolicy { - param([string]$Path) - - $script:Policy = [ordered]@{ - defaults = [ordered]@{ - enabled = $true - cooldownSeconds = 300 - action = 'alert' - severity = 'medium' - } - endpoint = [ordered]@{ - email = @() - } - } - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-CollectorLog ("policy not found, using defaults: {0}" -f $Path) - return - } - - try { - $raw = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - if ($raw.defaults) { - if ($raw.defaults.PSObject.Properties.Name -contains 'enabled') { $script:Policy.defaults.enabled = [bool]$raw.defaults.enabled } - if ($raw.defaults.cooldownSeconds) { $script:Policy.defaults.cooldownSeconds = [int]$raw.defaults.cooldownSeconds } - if ($raw.defaults.action) { $script:Policy.defaults.action = [string]$raw.defaults.action } - if ($raw.defaults.severity) { $script:Policy.defaults.severity = [string]$raw.defaults.severity } - } - if ($raw.endpoint -and $raw.endpoint.email) { - $script:Policy.endpoint.email = @($raw.endpoint.email) - } - } - catch { - Write-CollectorLog ("policy parse failed: {0}" -f $_.Exception.Message) - } -} - -function Should-EmitByCooldown { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - $now = (Get-Date).ToUniversalTime() - if ($script:Cooldown.ContainsKey($Fingerprint)) { - $last = [datetime]$script:Cooldown[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - $script:Cooldown[$Fingerprint] = $now - return $true -} - -# --------------------------------------------------------------------------- -# Email DLP rule evaluation -# --------------------------------------------------------------------------- - -function Evaluate-EmailRules { - param( - [string]$Subject, - [string]$RecipientsJoined, - [string]$SenderAddress, - [int]$AttachmentCount, - [string]$AttachmentNames, - [int]$BodyLength, - [string]$MessageId, - $OutlookMailItem - ) - - foreach ($rule in @($script:Policy.endpoint.email)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $matched = $true - - if ($rule.subjectRegex) { - $matched = $matched -and ($Subject -match [string]$rule.subjectRegex) - } - if ($rule.recipientRegex) { - $matched = $matched -and ($RecipientsJoined -match [string]$rule.recipientRegex) - } - if ($rule.senderRegex) { - $matched = $matched -and ($SenderAddress -match [string]$rule.senderRegex) - } - if ($rule.attachmentRegex -and $AttachmentNames) { - $matched = $matched -and ($AttachmentNames -match [string]$rule.attachmentRegex) - } - if ($rule.minAttachments) { - $matched = $matched -and ($AttachmentCount -ge [int]$rule.minAttachments) - } - if ($rule.minBodyLength) { - $matched = $matched -and ($BodyLength -ge [int]$rule.minBodyLength) - } - if ($rule.externalOnly -and [bool]$rule.externalOnly) { - $internalDomain = if ($rule.internalDomain) { [string]$rule.internalDomain } else { '' } - if ($internalDomain -and $RecipientsJoined -notmatch [regex]::Escape($internalDomain)) { - # all recipients are external — continue matching - } - elseif ($internalDomain) { - $matched = $false - } - } - - if (-not $matched) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "email|$ruleId|$MessageId|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Email rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block' -and $null -ne $OutlookMailItem) { - $enforced = Invoke-EmailEnforcement -MailItem $OutlookMailItem -RuleId $ruleId - Show-EnforcementNotification -Title 'DLP: письмо перемещено в черновики' -Body $message - } - elseif ($action -eq 'block') { - Show-EnforcementNotification -Title 'DLP: обнаружена отправка письма' -Body $message - } - - Send-EmailIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -Data @{ - subject = (Get-StringHash -Value $Subject) - recipients = (Get-StringHash -Value $RecipientsJoined) - sender = $SenderAddress - attachmentCount = $AttachmentCount - attachmentNames = $AttachmentNames - bodyLength = $BodyLength - enforced = $enforced - } - Write-CollectorLog ("incident email rule={0} action={1} severity={2} enforced={3} subject_hash={4}" -f $ruleId, $action, $severity, $enforced, (Get-StringHash -Value $Subject)) - } -} - -function Invoke-EmailEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)]$MailItem, - [string]$RuleId - ) - try { - $draftsFolder = $script:OutlookNamespace.GetDefaultFolder(16) # olFolderDrafts - $MailItem.Move($draftsFolder) | Out-Null - Write-CollectorLog ("enforcement: email moved to Drafts rule={0} subject_hash={1}" -f $RuleId, (Get-StringHash -Value $MailItem.Subject)) - return $true - } - catch { - Write-CollectorLog ("enforcement: email move to Drafts failed rule={0}: {1}" -f $RuleId, $_.Exception.Message) - return $false - } -} - -# --------------------------------------------------------------------------- -# Outlook Sent Items polling -# --------------------------------------------------------------------------- - -function Initialize-OutlookCom { - try { - $script:OutlookApp = New-Object -ComObject Outlook.Application - $script:OutlookNamespace = $script:OutlookApp.GetNamespace('MAPI') - $script:SentFolder = $script:OutlookNamespace.GetDefaultFolder(5) # olFolderSentMail - Write-CollectorLog "Outlook COM initialized, Sent Items folder opened" - return $true - } - catch { - Write-CollectorLog ("Outlook COM init failed: {0}" -f $_.Exception.Message) - return $false - } -} - -function Get-OutlookSentItems { - param([datetime]$Since) - - $results = @() - try { - $items = $script:SentFolder.Items - $items.Sort('[SentOn]', $true) - - $filter = "[SentOn] >= '{0}'" -f $Since.ToString('MM/dd/yyyy HH:mm') - $restricted = $items.Restrict($filter) - - foreach ($item in $restricted) { - try { - if ($item.Class -ne 43) { continue } # olMail = 43 - - $recipients = @() - for ($i = 1; $i -le $item.Recipients.Count; $i++) { - $recip = $item.Recipients.Item($i) - $recipients += [string]$recip.Address - } - - $attachmentNames = @() - for ($i = 1; $i -le $item.Attachments.Count; $i++) { - $attachmentNames += [string]$item.Attachments.Item($i).FileName - } - - $results += [pscustomobject]@{ - EntryID = [string]$item.EntryID - Subject = [string]$item.Subject - SenderAddress = [string]$item.SenderEmailAddress - SenderName = [string]$item.SenderName - Recipients = $recipients - RecipientsJoined = ($recipients -join '; ') - AttachmentCount = [int]$item.Attachments.Count - AttachmentNames = ($attachmentNames -join '; ') - BodyLength = if ($item.Body) { $item.Body.Length } else { 0 } - SentOn = $item.SentOn - MailItem = $item - } - } - catch { Write-Error <# -.SYNOPSIS - DLP email outbound collector for AWatch-rus (Phase 2.5). - Monitors outgoing email via Outlook COM Sent Items polling - and/or SMTP network connection detection. - -.DESCRIPTION - Two collection modes (configurable, can run simultaneously): - - outlook : Polls Outlook Sent Items via COM for new messages. - - smtp : Monitors SMTP connections (ports 25/587/465) via - Get-NetTCPConnection for any process sending mail. - - Sends heartbeats to AW bucket `aw-email-monitor_`. - Evaluates DLP policy rules from `endpoint.email[]` section. - Supports enforcement: action="block" moves the email to Drafts - (Outlook mode) or logs with enforced=false (SMTP mode). -#> -[CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$PolicyPath, - [string]$LogPath, - [int]$PollSeconds, - [ValidateSet('outlook', 'smtp', 'both')] - [string]$Mode = 'both' -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -# --------------------------------------------------------------------------- -# Shared infrastructure (mirrors other collectors) -# --------------------------------------------------------------------------- - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - return $null -} - -function Write-CollectorLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { return } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { } -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType - ) - if ($script:KnownBuckets.ContainsKey($BucketId)) { return } - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - $script:KnownBuckets[$BucketId] = $true -} - -function Get-StringHash { - param([AllowNull()][string]$Value) - if ($null -eq $Value) { return $null } - $bytes = [Text.Encoding]::UTF8.GetBytes($Value) - $sha = [Security.Cryptography.SHA256]::Create() - try { - ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { $sha.Dispose() } -} - -function Send-EmailHeartbeat { - param( - [string]$SignalType, - [hashtable]$Data - ) - $bucketId = 'aw-email-monitor_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-email-monitor' -BucketType 'aw.dlp.email' - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'email-outbound-collector' - } + $Data - } | ConvertTo-Json -Depth 6 -Compress - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Send-EmailIncidentHeartbeat { - param( - [string]$RuleId, - [string]$Action, - [string]$Severity, - [string]$Message, - [hashtable]$Data - ) - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = $RuleId - action = $Action - severity = $Severity - message = $Message - signalType = 'email_outbound' - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'email-outbound-collector' - } + $Data - } | ConvertTo-Json -Depth 7 -Compress - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Show-EnforcementNotification { - param( - [Parameter(Mandatory = $true)][string]$Title, - [Parameter(Mandatory = $true)][string]$Body - ) - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue - $icon = New-Object System.Windows.Forms.NotifyIcon - $icon.Icon = [System.Drawing.SystemIcons]::Warning - $icon.BalloonTipTitle = $Title - $icon.BalloonTipText = $Body - $icon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Warning - $icon.Visible = $true - $icon.ShowBalloonTip(5000) - Start-Sleep -Milliseconds 200 - $icon.Dispose() - } - catch { } -} - -# --------------------------------------------------------------------------- -# DLP Policy -# --------------------------------------------------------------------------- - -function Load-EmailPolicy { - param([string]$Path) - - $script:Policy = [ordered]@{ - defaults = [ordered]@{ - enabled = $true - cooldownSeconds = 300 - action = 'alert' - severity = 'medium' - } - endpoint = [ordered]@{ - email = @() - } - } - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-CollectorLog ("policy not found, using defaults: {0}" -f $Path) - return - } - - try { - $raw = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - if ($raw.defaults) { - if ($raw.defaults.PSObject.Properties.Name -contains 'enabled') { $script:Policy.defaults.enabled = [bool]$raw.defaults.enabled } - if ($raw.defaults.cooldownSeconds) { $script:Policy.defaults.cooldownSeconds = [int]$raw.defaults.cooldownSeconds } - if ($raw.defaults.action) { $script:Policy.defaults.action = [string]$raw.defaults.action } - if ($raw.defaults.severity) { $script:Policy.defaults.severity = [string]$raw.defaults.severity } - } - if ($raw.endpoint -and $raw.endpoint.email) { - $script:Policy.endpoint.email = @($raw.endpoint.email) - } - } - catch { - Write-CollectorLog ("policy parse failed: {0}" -f $_.Exception.Message) - } -} - -function Should-EmitByCooldown { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - $now = (Get-Date).ToUniversalTime() - if ($script:Cooldown.ContainsKey($Fingerprint)) { - $last = [datetime]$script:Cooldown[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - $script:Cooldown[$Fingerprint] = $now - return $true -} - -# --------------------------------------------------------------------------- -# Email DLP rule evaluation -# --------------------------------------------------------------------------- - -function Evaluate-EmailRules { - param( - [string]$Subject, - [string]$RecipientsJoined, - [string]$SenderAddress, - [int]$AttachmentCount, - [string]$AttachmentNames, - [int]$BodyLength, - [string]$MessageId, - $OutlookMailItem - ) - - foreach ($rule in @($script:Policy.endpoint.email)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $matched = $true - - if ($rule.subjectRegex) { - $matched = $matched -and ($Subject -match [string]$rule.subjectRegex) - } - if ($rule.recipientRegex) { - $matched = $matched -and ($RecipientsJoined -match [string]$rule.recipientRegex) - } - if ($rule.senderRegex) { - $matched = $matched -and ($SenderAddress -match [string]$rule.senderRegex) - } - if ($rule.attachmentRegex -and $AttachmentNames) { - $matched = $matched -and ($AttachmentNames -match [string]$rule.attachmentRegex) - } - if ($rule.minAttachments) { - $matched = $matched -and ($AttachmentCount -ge [int]$rule.minAttachments) - } - if ($rule.minBodyLength) { - $matched = $matched -and ($BodyLength -ge [int]$rule.minBodyLength) - } - if ($rule.externalOnly -and [bool]$rule.externalOnly) { - $internalDomain = if ($rule.internalDomain) { [string]$rule.internalDomain } else { '' } - if ($internalDomain -and $RecipientsJoined -notmatch [regex]::Escape($internalDomain)) { - # all recipients are external — continue matching - } - elseif ($internalDomain) { - $matched = $false - } - } - - if (-not $matched) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "email|$ruleId|$MessageId|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Email rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block' -and $null -ne $OutlookMailItem) { - $enforced = Invoke-EmailEnforcement -MailItem $OutlookMailItem -RuleId $ruleId - Show-EnforcementNotification -Title 'DLP: письмо перемещено в черновики' -Body $message - } - elseif ($action -eq 'block') { - Show-EnforcementNotification -Title 'DLP: обнаружена отправка письма' -Body $message - } - - Send-EmailIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -Data @{ - subject = (Get-StringHash -Value $Subject) - recipients = (Get-StringHash -Value $RecipientsJoined) - sender = $SenderAddress - attachmentCount = $AttachmentCount - attachmentNames = $AttachmentNames - bodyLength = $BodyLength - enforced = $enforced - } - Write-CollectorLog ("incident email rule={0} action={1} severity={2} enforced={3} subject_hash={4}" -f $ruleId, $action, $severity, $enforced, (Get-StringHash -Value $Subject)) - } -} - -function Invoke-EmailEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)]$MailItem, - [string]$RuleId - ) - try { - $draftsFolder = $script:OutlookNamespace.GetDefaultFolder(16) # olFolderDrafts - $MailItem.Move($draftsFolder) | Out-Null - Write-CollectorLog ("enforcement: email moved to Drafts rule={0} subject_hash={1}" -f $RuleId, (Get-StringHash -Value $MailItem.Subject)) - return $true - } - catch { - Write-CollectorLog ("enforcement: email move to Drafts failed rule={0}: {1}" -f $RuleId, $_.Exception.Message) - return $false - } -} - -# --------------------------------------------------------------------------- -# Outlook Sent Items polling -# --------------------------------------------------------------------------- - -function Initialize-OutlookCom { - try { - $script:OutlookApp = New-Object -ComObject Outlook.Application - $script:OutlookNamespace = $script:OutlookApp.GetNamespace('MAPI') - $script:SentFolder = $script:OutlookNamespace.GetDefaultFolder(5) # olFolderSentMail - Write-CollectorLog "Outlook COM initialized, Sent Items folder opened" - return $true - } - catch { - Write-CollectorLog ("Outlook COM init failed: {0}" -f $_.Exception.Message) - return $false - } -} - -function Get-OutlookSentItems { - param([datetime]$Since) - - $results = @() - try { - $items = $script:SentFolder.Items - $items.Sort('[SentOn]', $true) - - $filter = "[SentOn] >= '{0}'" -f $Since.ToString('MM/dd/yyyy HH:mm') - $restricted = $items.Restrict($filter) - - foreach ($item in $restricted) { - try { - if ($item.Class -ne 43) { continue } # olMail = 43 - - $recipients = @() - for ($i = 1; $i -le $item.Recipients.Count; $i++) { - $recip = $item.Recipients.Item($i) - $recipients += [string]$recip.Address - } - - $attachmentNames = @() - for ($i = 1; $i -le $item.Attachments.Count; $i++) { - $attachmentNames += [string]$item.Attachments.Item($i).FileName - } - - $results += [pscustomobject]@{ - EntryID = [string]$item.EntryID - Subject = [string]$item.Subject - SenderAddress = [string]$item.SenderEmailAddress - SenderName = [string]$item.SenderName - Recipients = $recipients - RecipientsJoined = ($recipients -join '; ') - AttachmentCount = [int]$item.Attachments.Count - AttachmentNames = ($attachmentNames -join '; ') - BodyLength = if ($item.Body) { $item.Body.Length } else { 0 } - SentOn = $item.SentOn - MailItem = $item - } - } - catch { } - } - } - catch { - Write-CollectorLog ("Outlook Sent Items scan failed: {0}" -f $_.Exception.Message) - } - return $results -} - -function Poll-OutlookSentItems { - $items = Get-OutlookSentItems -Since $script:OutlookLastPoll - - foreach ($item in $items) { - $entryId = $item.EntryID - if ($script:SeenEntryIds.ContainsKey($entryId)) { continue } - $script:SeenEntryIds[$entryId] = (Get-Date).ToUniversalTime() - - $subjectHash = Get-StringHash -Value $item.Subject - - Send-EmailHeartbeat -SignalType 'email_sent' -Data @{ - subject = $subjectHash - sender = [string]$item.SenderAddress - senderName = [string]$item.SenderName - recipientCount = $item.Recipients.Count - recipients = (Get-StringHash -Value $item.RecipientsJoined) - attachmentCount = [int]$item.AttachmentCount - attachmentNames = [string]$item.AttachmentNames - bodyLength = [int]$item.BodyLength - sentOn = if ($item.SentOn) { $item.SentOn.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') } else { '' } - collectionMode = 'outlook' - } - Write-CollectorLog ("email_sent outlook subject_hash={0} to={1} attachments={2}" -f $subjectHash, $item.Recipients.Count, $item.AttachmentCount) - - Evaluate-EmailRules ` - -Subject $item.Subject ` - -RecipientsJoined $item.RecipientsJoined ` - -SenderAddress $item.SenderAddress ` - -AttachmentCount $item.AttachmentCount ` - -AttachmentNames $item.AttachmentNames ` - -BodyLength $item.BodyLength ` - -MessageId $entryId ` - -OutlookMailItem $item.MailItem - } - - $script:OutlookLastPoll = (Get-Date).AddSeconds(-10) - - # Cleanup old entry IDs (keep last 24h) - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-24) - foreach ($k in @($script:SeenEntryIds.Keys)) { - if ([datetime]$script:SeenEntryIds[$k] -lt $cleanupBefore) { - $script:SeenEntryIds.Remove($k) - } - } -} - -# --------------------------------------------------------------------------- -# SMTP network connection monitoring -# --------------------------------------------------------------------------- - -function Poll-SmtpConnections { - try { - $smtpPorts = @(25, 587, 465, 2525) - $connections = Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue | - Where-Object { $smtpPorts -contains $_.RemotePort } - - foreach ($conn in @($connections)) { - $processId = [int]$conn.OwningProcess - $remoteAddr = [string]$conn.RemoteAddress - $remotePort = [int]$conn.RemotePort - $fingerprint = "{0}:{1}:{2}" -f $processId, $remoteAddr, $remotePort - if ($script:SeenSmtpConnections.ContainsKey($fingerprint)) { continue } - $script:SeenSmtpConnections[$fingerprint] = (Get-Date).ToUniversalTime() - - $processName = '' - try { - $proc = Get-Process -Id $processId -ErrorAction SilentlyContinue - $processName = [string]$proc.ProcessName - } - catch { } - - Send-EmailHeartbeat -SignalType 'smtp_connection' -Data @{ - remoteAddress = $remoteAddr - remotePort = $remotePort - processId = $processId - processName = $processName - localPort = [int]$conn.LocalPort - collectionMode = 'smtp' - } - Write-CollectorLog ("smtp_connection process={0}({1}) remote={2}:{3}" -f $processName, $processId, $remoteAddr, $remotePort) - - Evaluate-EmailRules ` - -Subject '' ` - -RecipientsJoined $remoteAddr ` - -SenderAddress $env:USERNAME ` - -AttachmentCount 0 ` - -AttachmentNames '' ` - -BodyLength 0 ` - -MessageId $fingerprint ` - -OutlookMailItem $null - } - - # Cleanup old SMTP connections (keep last 8h) - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenSmtpConnections.Keys)) { - if ([datetime]$script:SeenSmtpConnections[$k] -lt $cleanupBefore) { - $script:SeenSmtpConnections.Remove($k) - } - } - } - catch { - Write-CollectorLog ("SMTP poll error: {0}" -f $_.Exception.Message) - } -} - -# --------------------------------------------------------------------------- -# Initialization -# --------------------------------------------------------------------------- - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } -$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } -$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } -$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' } -$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 10 } -$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } -$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("email-outbound-{0}.log" -f $env:USERNAME) } -$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:Cooldown = @{} -$script:SeenEntryIds = @{} -$script:SeenSmtpConnections = @{} -$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30) -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:OutlookApp = $null -$script:OutlookNamespace = $null -$script:SentFolder = $null -$script:OutlookLastPoll = (Get-Date).AddMinutes(-5) - -Load-EmailPolicy -Path $resolvedPolicyPath -Write-CollectorLog ("email collector started mode={0} against {1}" -f $Mode, $script:ApiBase) - -$useOutlook = ($Mode -eq 'outlook' -or $Mode -eq 'both') -$useSmtp = ($Mode -eq 'smtp' -or $Mode -eq 'both') -$outlookReady = $false - -if ($useOutlook) { - $outlookReady = Initialize-OutlookCom - if (-not $outlookReady -and $Mode -eq 'outlook') { - Write-CollectorLog "Outlook COM not available, collector will retry" - } -} - -# --------------------------------------------------------------------------- -# Main loop -# --------------------------------------------------------------------------- - -while ($true) { - try { - if (-not $script:Policy.defaults.enabled) { - Start-Sleep -Seconds $resolvedPollSeconds - continue - } - - if ($useOutlook) { - if (-not $outlookReady) { - $outlookReady = Initialize-OutlookCom - } - if ($outlookReady) { - try { - Poll-OutlookSentItems - } - catch { - Write-CollectorLog ("outlook poll error: {0}" -f $_.Exception.Message) - $outlookReady = $false - $script:OutlookApp = $null - $script:OutlookNamespace = $null - $script:SentFolder = $null - } - } - } - - if ($useSmtp) { - try { - Poll-SmtpConnections - } - catch { - Write-CollectorLog ("smtp poll error: {0}" -f $_.Exception.Message) - } - } - } - catch { - Write-CollectorLog ("collector error: {0}" -f $_.Exception.Message) - } - - Start-Sleep -Seconds $resolvedPollSeconds -} -; } - } - } - catch { - Write-CollectorLog ("Outlook Sent Items scan failed: {0}" -f $_.Exception.Message) - } - return $results -} - -function Poll-OutlookSentItems { - $items = Get-OutlookSentItems -Since $script:OutlookLastPoll - - foreach ($item in $items) { - $entryId = $item.EntryID - if ($script:SeenEntryIds.ContainsKey($entryId)) { continue } - $script:SeenEntryIds[$entryId] = (Get-Date).ToUniversalTime() - - $subjectHash = Get-StringHash -Value $item.Subject - - Send-EmailHeartbeat -SignalType 'email_sent' -Data @{ - subject = $subjectHash - sender = [string]$item.SenderAddress - senderName = [string]$item.SenderName - recipientCount = $item.Recipients.Count - recipients = (Get-StringHash -Value $item.RecipientsJoined) - attachmentCount = [int]$item.AttachmentCount - attachmentNames = [string]$item.AttachmentNames - bodyLength = [int]$item.BodyLength - sentOn = if ($item.SentOn) { $item.SentOn.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') } else { '' } - collectionMode = 'outlook' - } - Write-CollectorLog ("email_sent outlook subject_hash={0} to={1} attachments={2}" -f $subjectHash, $item.Recipients.Count, $item.AttachmentCount) - - Evaluate-EmailRules ` - -Subject $item.Subject ` - -RecipientsJoined $item.RecipientsJoined ` - -SenderAddress $item.SenderAddress ` - -AttachmentCount $item.AttachmentCount ` - -AttachmentNames $item.AttachmentNames ` - -BodyLength $item.BodyLength ` - -MessageId $entryId ` - -OutlookMailItem $item.MailItem - } - - $script:OutlookLastPoll = (Get-Date).AddSeconds(-10) - - # Cleanup old entry IDs (keep last 24h) - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-24) - foreach ($k in @($script:SeenEntryIds.Keys)) { - if ([datetime]$script:SeenEntryIds[$k] -lt $cleanupBefore) { - $script:SeenEntryIds.Remove($k) - } - } -} - -# --------------------------------------------------------------------------- -# SMTP network connection monitoring -# --------------------------------------------------------------------------- - -function Poll-SmtpConnections { - try { - $smtpPorts = @(25, 587, 465, 2525) - $connections = Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue | - Where-Object { $smtpPorts -contains $_.RemotePort } - - foreach ($conn in @($connections)) { - $processId = [int]$conn.OwningProcess - $remoteAddr = [string]$conn.RemoteAddress - $remotePort = [int]$conn.RemotePort - $fingerprint = "{0}:{1}:{2}" -f $processId, $remoteAddr, $remotePort - if ($script:SeenSmtpConnections.ContainsKey($fingerprint)) { continue } - $script:SeenSmtpConnections[$fingerprint] = (Get-Date).ToUniversalTime() - - $processName = '' - try { - $proc = Get-Process -Id $processId -ErrorAction SilentlyContinue - $processName = [string]$proc.ProcessName - } - catch { Write-Error <# -.SYNOPSIS - DLP email outbound collector for AWatch-rus (Phase 2.5). - Monitors outgoing email via Outlook COM Sent Items polling - and/or SMTP network connection detection. - -.DESCRIPTION - Two collection modes (configurable, can run simultaneously): - - outlook : Polls Outlook Sent Items via COM for new messages. - - smtp : Monitors SMTP connections (ports 25/587/465) via - Get-NetTCPConnection for any process sending mail. - - Sends heartbeats to AW bucket `aw-email-monitor_`. - Evaluates DLP policy rules from `endpoint.email[]` section. - Supports enforcement: action="block" moves the email to Drafts - (Outlook mode) or logs with enforced=false (SMTP mode). -#> -[CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$PolicyPath, - [string]$LogPath, - [int]$PollSeconds, - [ValidateSet('outlook', 'smtp', 'both')] - [string]$Mode = 'both' -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -# --------------------------------------------------------------------------- -# Shared infrastructure (mirrors other collectors) -# --------------------------------------------------------------------------- - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - return $null -} - -function Write-CollectorLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { return } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { } -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType - ) - if ($script:KnownBuckets.ContainsKey($BucketId)) { return } - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - $script:KnownBuckets[$BucketId] = $true -} - -function Get-StringHash { - param([AllowNull()][string]$Value) - if ($null -eq $Value) { return $null } - $bytes = [Text.Encoding]::UTF8.GetBytes($Value) - $sha = [Security.Cryptography.SHA256]::Create() - try { - ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { $sha.Dispose() } -} - -function Send-EmailHeartbeat { - param( - [string]$SignalType, - [hashtable]$Data - ) - $bucketId = 'aw-email-monitor_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-email-monitor' -BucketType 'aw.dlp.email' - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'email-outbound-collector' - } + $Data - } | ConvertTo-Json -Depth 6 -Compress - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Send-EmailIncidentHeartbeat { - param( - [string]$RuleId, - [string]$Action, - [string]$Severity, - [string]$Message, - [hashtable]$Data - ) - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = $RuleId - action = $Action - severity = $Severity - message = $Message - signalType = 'email_outbound' - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'email-outbound-collector' - } + $Data - } | ConvertTo-Json -Depth 7 -Compress - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Show-EnforcementNotification { - param( - [Parameter(Mandatory = $true)][string]$Title, - [Parameter(Mandatory = $true)][string]$Body - ) - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue - $icon = New-Object System.Windows.Forms.NotifyIcon - $icon.Icon = [System.Drawing.SystemIcons]::Warning - $icon.BalloonTipTitle = $Title - $icon.BalloonTipText = $Body - $icon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Warning - $icon.Visible = $true - $icon.ShowBalloonTip(5000) - Start-Sleep -Milliseconds 200 - $icon.Dispose() - } - catch { } -} - -# --------------------------------------------------------------------------- -# DLP Policy -# --------------------------------------------------------------------------- - -function Load-EmailPolicy { - param([string]$Path) - - $script:Policy = [ordered]@{ - defaults = [ordered]@{ - enabled = $true - cooldownSeconds = 300 - action = 'alert' - severity = 'medium' - } - endpoint = [ordered]@{ - email = @() - } - } - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-CollectorLog ("policy not found, using defaults: {0}" -f $Path) - return - } - - try { - $raw = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - if ($raw.defaults) { - if ($raw.defaults.PSObject.Properties.Name -contains 'enabled') { $script:Policy.defaults.enabled = [bool]$raw.defaults.enabled } - if ($raw.defaults.cooldownSeconds) { $script:Policy.defaults.cooldownSeconds = [int]$raw.defaults.cooldownSeconds } - if ($raw.defaults.action) { $script:Policy.defaults.action = [string]$raw.defaults.action } - if ($raw.defaults.severity) { $script:Policy.defaults.severity = [string]$raw.defaults.severity } - } - if ($raw.endpoint -and $raw.endpoint.email) { - $script:Policy.endpoint.email = @($raw.endpoint.email) - } - } - catch { - Write-CollectorLog ("policy parse failed: {0}" -f $_.Exception.Message) - } -} - -function Should-EmitByCooldown { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - $now = (Get-Date).ToUniversalTime() - if ($script:Cooldown.ContainsKey($Fingerprint)) { - $last = [datetime]$script:Cooldown[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - $script:Cooldown[$Fingerprint] = $now - return $true -} - -# --------------------------------------------------------------------------- -# Email DLP rule evaluation -# --------------------------------------------------------------------------- - -function Evaluate-EmailRules { - param( - [string]$Subject, - [string]$RecipientsJoined, - [string]$SenderAddress, - [int]$AttachmentCount, - [string]$AttachmentNames, - [int]$BodyLength, - [string]$MessageId, - $OutlookMailItem - ) - - foreach ($rule in @($script:Policy.endpoint.email)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $matched = $true - - if ($rule.subjectRegex) { - $matched = $matched -and ($Subject -match [string]$rule.subjectRegex) - } - if ($rule.recipientRegex) { - $matched = $matched -and ($RecipientsJoined -match [string]$rule.recipientRegex) - } - if ($rule.senderRegex) { - $matched = $matched -and ($SenderAddress -match [string]$rule.senderRegex) - } - if ($rule.attachmentRegex -and $AttachmentNames) { - $matched = $matched -and ($AttachmentNames -match [string]$rule.attachmentRegex) - } - if ($rule.minAttachments) { - $matched = $matched -and ($AttachmentCount -ge [int]$rule.minAttachments) - } - if ($rule.minBodyLength) { - $matched = $matched -and ($BodyLength -ge [int]$rule.minBodyLength) - } - if ($rule.externalOnly -and [bool]$rule.externalOnly) { - $internalDomain = if ($rule.internalDomain) { [string]$rule.internalDomain } else { '' } - if ($internalDomain -and $RecipientsJoined -notmatch [regex]::Escape($internalDomain)) { - # all recipients are external — continue matching - } - elseif ($internalDomain) { - $matched = $false - } - } - - if (-not $matched) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "email|$ruleId|$MessageId|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Email rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block' -and $null -ne $OutlookMailItem) { - $enforced = Invoke-EmailEnforcement -MailItem $OutlookMailItem -RuleId $ruleId - Show-EnforcementNotification -Title 'DLP: письмо перемещено в черновики' -Body $message - } - elseif ($action -eq 'block') { - Show-EnforcementNotification -Title 'DLP: обнаружена отправка письма' -Body $message - } - - Send-EmailIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -Data @{ - subject = (Get-StringHash -Value $Subject) - recipients = (Get-StringHash -Value $RecipientsJoined) - sender = $SenderAddress - attachmentCount = $AttachmentCount - attachmentNames = $AttachmentNames - bodyLength = $BodyLength - enforced = $enforced - } - Write-CollectorLog ("incident email rule={0} action={1} severity={2} enforced={3} subject_hash={4}" -f $ruleId, $action, $severity, $enforced, (Get-StringHash -Value $Subject)) - } -} - -function Invoke-EmailEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)]$MailItem, - [string]$RuleId - ) - try { - $draftsFolder = $script:OutlookNamespace.GetDefaultFolder(16) # olFolderDrafts - $MailItem.Move($draftsFolder) | Out-Null - Write-CollectorLog ("enforcement: email moved to Drafts rule={0} subject_hash={1}" -f $RuleId, (Get-StringHash -Value $MailItem.Subject)) - return $true - } - catch { - Write-CollectorLog ("enforcement: email move to Drafts failed rule={0}: {1}" -f $RuleId, $_.Exception.Message) - return $false - } -} - -# --------------------------------------------------------------------------- -# Outlook Sent Items polling -# --------------------------------------------------------------------------- - -function Initialize-OutlookCom { - try { - $script:OutlookApp = New-Object -ComObject Outlook.Application - $script:OutlookNamespace = $script:OutlookApp.GetNamespace('MAPI') - $script:SentFolder = $script:OutlookNamespace.GetDefaultFolder(5) # olFolderSentMail - Write-CollectorLog "Outlook COM initialized, Sent Items folder opened" - return $true - } - catch { - Write-CollectorLog ("Outlook COM init failed: {0}" -f $_.Exception.Message) - return $false - } -} - -function Get-OutlookSentItems { - param([datetime]$Since) - - $results = @() - try { - $items = $script:SentFolder.Items - $items.Sort('[SentOn]', $true) - - $filter = "[SentOn] >= '{0}'" -f $Since.ToString('MM/dd/yyyy HH:mm') - $restricted = $items.Restrict($filter) - - foreach ($item in $restricted) { - try { - if ($item.Class -ne 43) { continue } # olMail = 43 - - $recipients = @() - for ($i = 1; $i -le $item.Recipients.Count; $i++) { - $recip = $item.Recipients.Item($i) - $recipients += [string]$recip.Address - } - - $attachmentNames = @() - for ($i = 1; $i -le $item.Attachments.Count; $i++) { - $attachmentNames += [string]$item.Attachments.Item($i).FileName - } - - $results += [pscustomobject]@{ - EntryID = [string]$item.EntryID - Subject = [string]$item.Subject - SenderAddress = [string]$item.SenderEmailAddress - SenderName = [string]$item.SenderName - Recipients = $recipients - RecipientsJoined = ($recipients -join '; ') - AttachmentCount = [int]$item.Attachments.Count - AttachmentNames = ($attachmentNames -join '; ') - BodyLength = if ($item.Body) { $item.Body.Length } else { 0 } - SentOn = $item.SentOn - MailItem = $item - } - } - catch { } - } - } - catch { - Write-CollectorLog ("Outlook Sent Items scan failed: {0}" -f $_.Exception.Message) - } - return $results -} - -function Poll-OutlookSentItems { - $items = Get-OutlookSentItems -Since $script:OutlookLastPoll - - foreach ($item in $items) { - $entryId = $item.EntryID - if ($script:SeenEntryIds.ContainsKey($entryId)) { continue } - $script:SeenEntryIds[$entryId] = (Get-Date).ToUniversalTime() - - $subjectHash = Get-StringHash -Value $item.Subject - - Send-EmailHeartbeat -SignalType 'email_sent' -Data @{ - subject = $subjectHash - sender = [string]$item.SenderAddress - senderName = [string]$item.SenderName - recipientCount = $item.Recipients.Count - recipients = (Get-StringHash -Value $item.RecipientsJoined) - attachmentCount = [int]$item.AttachmentCount - attachmentNames = [string]$item.AttachmentNames - bodyLength = [int]$item.BodyLength - sentOn = if ($item.SentOn) { $item.SentOn.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') } else { '' } - collectionMode = 'outlook' - } - Write-CollectorLog ("email_sent outlook subject_hash={0} to={1} attachments={2}" -f $subjectHash, $item.Recipients.Count, $item.AttachmentCount) - - Evaluate-EmailRules ` - -Subject $item.Subject ` - -RecipientsJoined $item.RecipientsJoined ` - -SenderAddress $item.SenderAddress ` - -AttachmentCount $item.AttachmentCount ` - -AttachmentNames $item.AttachmentNames ` - -BodyLength $item.BodyLength ` - -MessageId $entryId ` - -OutlookMailItem $item.MailItem - } - - $script:OutlookLastPoll = (Get-Date).AddSeconds(-10) - - # Cleanup old entry IDs (keep last 24h) - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-24) - foreach ($k in @($script:SeenEntryIds.Keys)) { - if ([datetime]$script:SeenEntryIds[$k] -lt $cleanupBefore) { - $script:SeenEntryIds.Remove($k) - } - } -} - -# --------------------------------------------------------------------------- -# SMTP network connection monitoring -# --------------------------------------------------------------------------- - -function Poll-SmtpConnections { - try { - $smtpPorts = @(25, 587, 465, 2525) - $connections = Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue | - Where-Object { $smtpPorts -contains $_.RemotePort } - - foreach ($conn in @($connections)) { - $processId = [int]$conn.OwningProcess - $remoteAddr = [string]$conn.RemoteAddress - $remotePort = [int]$conn.RemotePort - $fingerprint = "{0}:{1}:{2}" -f $processId, $remoteAddr, $remotePort - if ($script:SeenSmtpConnections.ContainsKey($fingerprint)) { continue } - $script:SeenSmtpConnections[$fingerprint] = (Get-Date).ToUniversalTime() - - $processName = '' - try { - $proc = Get-Process -Id $processId -ErrorAction SilentlyContinue - $processName = [string]$proc.ProcessName - } - catch { } - - Send-EmailHeartbeat -SignalType 'smtp_connection' -Data @{ - remoteAddress = $remoteAddr - remotePort = $remotePort - processId = $processId - processName = $processName - localPort = [int]$conn.LocalPort - collectionMode = 'smtp' - } - Write-CollectorLog ("smtp_connection process={0}({1}) remote={2}:{3}" -f $processName, $processId, $remoteAddr, $remotePort) - - Evaluate-EmailRules ` - -Subject '' ` - -RecipientsJoined $remoteAddr ` - -SenderAddress $env:USERNAME ` - -AttachmentCount 0 ` - -AttachmentNames '' ` - -BodyLength 0 ` - -MessageId $fingerprint ` - -OutlookMailItem $null - } - - # Cleanup old SMTP connections (keep last 8h) - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenSmtpConnections.Keys)) { - if ([datetime]$script:SeenSmtpConnections[$k] -lt $cleanupBefore) { - $script:SeenSmtpConnections.Remove($k) - } - } - } - catch { - Write-CollectorLog ("SMTP poll error: {0}" -f $_.Exception.Message) - } -} - -# --------------------------------------------------------------------------- -# Initialization -# --------------------------------------------------------------------------- - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } -$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } -$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } -$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' } -$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 10 } -$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } -$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("email-outbound-{0}.log" -f $env:USERNAME) } -$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:Cooldown = @{} -$script:SeenEntryIds = @{} -$script:SeenSmtpConnections = @{} -$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30) -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:OutlookApp = $null -$script:OutlookNamespace = $null -$script:SentFolder = $null -$script:OutlookLastPoll = (Get-Date).AddMinutes(-5) - -Load-EmailPolicy -Path $resolvedPolicyPath -Write-CollectorLog ("email collector started mode={0} against {1}" -f $Mode, $script:ApiBase) - -$useOutlook = ($Mode -eq 'outlook' -or $Mode -eq 'both') -$useSmtp = ($Mode -eq 'smtp' -or $Mode -eq 'both') -$outlookReady = $false - -if ($useOutlook) { - $outlookReady = Initialize-OutlookCom - if (-not $outlookReady -and $Mode -eq 'outlook') { - Write-CollectorLog "Outlook COM not available, collector will retry" - } -} - -# --------------------------------------------------------------------------- -# Main loop -# --------------------------------------------------------------------------- - -while ($true) { - try { - if (-not $script:Policy.defaults.enabled) { - Start-Sleep -Seconds $resolvedPollSeconds - continue - } - - if ($useOutlook) { - if (-not $outlookReady) { - $outlookReady = Initialize-OutlookCom - } - if ($outlookReady) { - try { - Poll-OutlookSentItems - } - catch { - Write-CollectorLog ("outlook poll error: {0}" -f $_.Exception.Message) - $outlookReady = $false - $script:OutlookApp = $null - $script:OutlookNamespace = $null - $script:SentFolder = $null - } - } - } - - if ($useSmtp) { - try { - Poll-SmtpConnections - } - catch { - Write-CollectorLog ("smtp poll error: {0}" -f $_.Exception.Message) - } - } - } - catch { - Write-CollectorLog ("collector error: {0}" -f $_.Exception.Message) - } - - Start-Sleep -Seconds $resolvedPollSeconds -} -; } - - Send-EmailHeartbeat -SignalType 'smtp_connection' -Data @{ - remoteAddress = $remoteAddr - remotePort = $remotePort - processId = $processId - processName = $processName - localPort = [int]$conn.LocalPort - collectionMode = 'smtp' - } - Write-CollectorLog ("smtp_connection process={0}({1}) remote={2}:{3}" -f $processName, $processId, $remoteAddr, $remotePort) - - Evaluate-EmailRules ` - -Subject '' ` - -RecipientsJoined $remoteAddr ` - -SenderAddress $env:USERNAME ` - -AttachmentCount 0 ` - -AttachmentNames '' ` - -BodyLength 0 ` - -MessageId $fingerprint ` - -OutlookMailItem $null - } - - # Cleanup old SMTP connections (keep last 8h) - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenSmtpConnections.Keys)) { - if ([datetime]$script:SeenSmtpConnections[$k] -lt $cleanupBefore) { - $script:SeenSmtpConnections.Remove($k) - } - } - } - catch { - Write-CollectorLog ("SMTP poll error: {0}" -f $_.Exception.Message) - } -} - -# --------------------------------------------------------------------------- -# Initialization -# --------------------------------------------------------------------------- - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } -$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } -$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } -$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' } -$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 10 } -$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } -$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("email-outbound-{0}.log" -f $env:USERNAME) } -$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:Cooldown = @{} -$script:SeenEntryIds = @{} -$script:SeenSmtpConnections = @{} -$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30) -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:OutlookApp = $null -$script:OutlookNamespace = $null -$script:SentFolder = $null -$script:OutlookLastPoll = (Get-Date).AddMinutes(-5) - -Load-EmailPolicy -Path $resolvedPolicyPath -Write-CollectorLog ("email collector started mode={0} against {1}" -f $Mode, $script:ApiBase) - -$useOutlook = ($Mode -eq 'outlook' -or $Mode -eq 'both') -$useSmtp = ($Mode -eq 'smtp' -or $Mode -eq 'both') -$outlookReady = $false - -if ($useOutlook) { - $outlookReady = Initialize-OutlookCom - if (-not $outlookReady -and $Mode -eq 'outlook') { - Write-CollectorLog "Outlook COM not available, collector will retry" - } -} - -# --------------------------------------------------------------------------- -# Main loop -# --------------------------------------------------------------------------- - -while ($true) { - try { - if (-not $script:Policy.defaults.enabled) { - Start-Sleep -Seconds $resolvedPollSeconds - continue - } - - if ($useOutlook) { - if (-not $outlookReady) { - $outlookReady = Initialize-OutlookCom - } - if ($outlookReady) { - try { - Poll-OutlookSentItems - } - catch { - Write-CollectorLog ("outlook poll error: {0}" -f $_.Exception.Message) - $outlookReady = $false - $script:OutlookApp = $null - $script:OutlookNamespace = $null - $script:SentFolder = $null - } - } - } - - if ($useSmtp) { - try { - Poll-SmtpConnections - } - catch { - Write-CollectorLog ("smtp poll error: {0}" -f $_.Exception.Message) - } - } - } - catch { - Write-CollectorLog ("collector error: {0}" -f $_.Exception.Message) - } - - Start-Sleep -Seconds $resolvedPollSeconds -} diff --git a/windows/file-operations-collector.ps1 b/windows/file-operations-collector.ps1 index a51cffd..bc63d55 100644 --- a/windows/file-operations-collector.ps1 +++ b/windows/file-operations-collector.ps1 @@ -1,46 +1,4 @@ -[CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$PolicyPath, - [string]$LogPath, - [int]$PollSeconds = 10, - [string[]]$WatchPaths = @('Desktop', 'Documents', 'Downloads') -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -# Force TLS 1.2 and load networking types -[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -Add-Type -AssemblyName System.Net.Http - -# Bucket registry -$script:KnownBuckets = @{} -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = [System.Diagnostics.Process]::GetCurrentProcess().SessionId - -# Настройка логирования -$script:LogPath = $LogPath -$script:LocalAgentLogsEnabled = [bool]$LogPath - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - return $null -} - -function Write-FileCollectorLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { return } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} [FileCollector] {1}' -f (Get-Date -Format s), $Message) - } catch { Write-Error [CmdletBinding()] +[CmdletBinding()] param( [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', [string]$ServerHost, @@ -266,1090 +224,3 @@ finally { $w.Dispose() } } -; } -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - $httpClient = $null - try { - $httpClient = New-Object System.Net.Http.HttpClient - $content = New-Object System.Net.Http.StringContent($Json, [System.Text.Encoding]::UTF8, "application/json") - $response = $httpClient.PostAsync($Uri, $content).Result - if (-not $response.IsSuccessStatusCode) { - $status = [int]$response.StatusCode - $reason = [string]$response.ReasonPhrase - $body = $response.Content.ReadAsStringAsync().Result - Write-FileCollectorLog ("POST failed: uri={0} status={1} reason={2} body={3}" -f $Uri, $status, $reason, $body) - } - } catch { - Write-FileCollectorLog "POST Error: $($_.Exception.Message)" - } finally { - if ($null -ne $httpClient) { - $httpClient.Dispose() - } - } -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - try { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null - $script:KnownBuckets[$BucketId] = $true - return - } - catch { Write-Error [CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$PolicyPath, - [string]$LogPath, - [int]$PollSeconds = 10, - [string[]]$WatchPaths = @('Desktop', 'Documents', 'Downloads') -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -# Force TLS 1.2 and load networking types -[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -Add-Type -AssemblyName System.Net.Http - -# Bucket registry -$script:KnownBuckets = @{} -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = [System.Diagnostics.Process]::GetCurrentProcess().SessionId - -# Настройка логирования -$script:LogPath = $LogPath -$script:LocalAgentLogsEnabled = [bool]$LogPath - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - return $null -} - -function Write-FileCollectorLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { return } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} [FileCollector] {1}' -f (Get-Date -Format s), $Message) - } catch {} -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - $httpClient = $null - try { - $httpClient = New-Object System.Net.Http.HttpClient - $content = New-Object System.Net.Http.StringContent($Json, [System.Text.Encoding]::UTF8, "application/json") - $response = $httpClient.PostAsync($Uri, $content).Result - if (-not $response.IsSuccessStatusCode) { - $status = [int]$response.StatusCode - $reason = [string]$response.ReasonPhrase - $body = $response.Content.ReadAsStringAsync().Result - Write-FileCollectorLog ("POST failed: uri={0} status={1} reason={2} body={3}" -f $Uri, $status, $reason, $body) - } - } catch { - Write-FileCollectorLog "POST Error: $($_.Exception.Message)" - } finally { - if ($null -ne $httpClient) { - $httpClient.Dispose() - } - } -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - try { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null - $script:KnownBuckets[$BucketId] = $true - return - } - catch { - } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - try { - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - } - catch { - try { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null - } - catch { - Write-FileCollectorLog "Bucket create/check failed for ${BucketId}: $($_.Exception.Message)" - throw - } - } - $script:KnownBuckets[$BucketId] = $true -} - -function Send-FileOperationEvent { - param( - [string]$Operation, - [string]$FilePath, - [string]$OldFilePath = $null, - [long]$Size = 0 - ) - - $bucketId = 'aw-file-operations_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-file-operations' -BucketType 'aw.file.operation' - - $data = @{ - operation = $Operation - path = $FilePath - extension = [System.IO.Path]::GetExtension($FilePath) - username = $env:USERNAME - hostname = $script:Hostname - } - if ($OldFilePath) { $data.oldPath = $OldFilePath } - if ($Size -gt 0) { $data.size = $Size } - - # Детекция архивации (упрощенная) - if ($Operation -eq 'Created' -and $data.extension -match '\.(zip|7z|rar|tar|gz)$') { - $data.archiveHint = $true - } - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = $data - } | ConvertTo-Json -Depth 5 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=15" -Json $payload -} - -$config = Get-DeploymentConfig -Path $ConfigPath -if (-not $config) { throw "Configuration file not found: $ConfigPath" } - -$scheme = if ($ServerScheme) { $ServerScheme } elseif ($config.server.scheme) { $config.server.scheme } else { 'http' } -$hostName = if ($ServerHost) { $ServerHost } elseif ($config.server.host) { $config.server.host } else { 'localhost' } -$port = if ($ServerPort) { $ServerPort } elseif ($config.server.port) { $config.server.port } else { 5600 } -$script:ApiBase = "{0}://{1}:{2}/api/0" -f $scheme, $hostName, $port - -$bucketId = 'aw-file-operations_' + $script:Hostname -Ensure-Bucket -BucketId $bucketId -ClientName 'aw-file-operations' -BucketType 'aw.file.operation' - -# Resolve paths for monitoring -$resolvedPaths = @() -foreach ($p in $WatchPaths) { - $fullPath = $p - if (-not [System.IO.Path]::IsPathRooted($p)) { - try { - if ($p -eq 'Desktop') { $fullPath = [Environment]::GetFolderPath('Desktop') } - elseif ($p -eq 'Documents') { $fullPath = [Environment]::GetFolderPath('MyDocuments') } - elseif ($p -eq 'Downloads') { $fullPath = Join-Path $env:USERPROFILE 'Downloads' } - } catch {} - } - if ($fullPath -and (Test-Path -LiteralPath $fullPath)) { - $resolvedPaths += $fullPath - } -} - -if ($resolvedPaths.Count -eq 0) { - Write-FileCollectorLog "No valid watch paths found. Exiting." - exit 0 -} - -Write-FileCollectorLog "Starting watch on paths: $($resolvedPaths -join ', ')" - -$watchers = @() -$subscriptions = @() -foreach ($path in $resolvedPaths) { - $watcher = New-Object System.IO.FileSystemWatcher - $watcher.Path = $path - $watcher.IncludeSubdirectories = $true - $watcher.EnableRaisingEvents = $true - - $onChanged = Register-ObjectEvent $watcher "Created" -Action { - $path = $Event.SourceEventArgs.FullPath - $size = 0 - try { if (Test-Path -LiteralPath $path) { $size = (Get-Item -LiteralPath $path).Length } } catch {} - Send-FileOperationEvent -Operation 'Created' -FilePath $path -Size $size - } - $onDeleted = Register-ObjectEvent $watcher "Deleted" -Action { - Send-FileOperationEvent -Operation 'Deleted' -FilePath $Event.SourceEventArgs.FullPath - } - $onRenamed = Register-ObjectEvent $watcher "Renamed" -Action { - Send-FileOperationEvent -Operation 'Renamed' -FilePath $Event.SourceEventArgs.FullPath -OldFilePath $Event.SourceEventArgs.OldFullPath - } - - $watchers += $watcher - $subscriptions += @($onChanged, $onDeleted, $onRenamed) -} - -Write-FileCollectorLog "Collector started. Waiting for events..." - -try { - while ($true) { - Start-Sleep -Seconds $PollSeconds - } -} -finally { - Write-FileCollectorLog "Stopping collector..." - foreach ($sub in @($subscriptions)) { - try { - if ($sub -and $sub.Id) { - Unregister-Event -SubscriptionId $sub.Id -ErrorAction SilentlyContinue - Remove-Job -Id $sub.Id -Force -ErrorAction SilentlyContinue - } - } catch {} - } - foreach ($w in $watchers) { - $w.EnableRaisingEvents = $false - $w.Dispose() - } -} -; } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - try { - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - } - catch { - try { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null - } - catch { - Write-FileCollectorLog "Bucket create/check failed for ${BucketId}: $($_.Exception.Message)" - throw - } - } - $script:KnownBuckets[$BucketId] = $true -} - -function Send-FileOperationEvent { - param( - [string]$Operation, - [string]$FilePath, - [string]$OldFilePath = $null, - [long]$Size = 0 - ) - - $bucketId = 'aw-file-operations_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-file-operations' -BucketType 'aw.file.operation' - - $data = @{ - operation = $Operation - path = $FilePath - extension = [System.IO.Path]::GetExtension($FilePath) - username = $env:USERNAME - hostname = $script:Hostname - } - if ($OldFilePath) { $data.oldPath = $OldFilePath } - if ($Size -gt 0) { $data.size = $Size } - - # Детекция архивации (упрощенная) - if ($Operation -eq 'Created' -and $data.extension -match '\.(zip|7z|rar|tar|gz)$') { - $data.archiveHint = $true - } - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = $data - } | ConvertTo-Json -Depth 5 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=15" -Json $payload -} - -$config = Get-DeploymentConfig -Path $ConfigPath -if (-not $config) { throw "Configuration file not found: $ConfigPath" } - -$scheme = if ($ServerScheme) { $ServerScheme } elseif ($config.server.scheme) { $config.server.scheme } else { 'http' } -$hostName = if ($ServerHost) { $ServerHost } elseif ($config.server.host) { $config.server.host } else { 'localhost' } -$port = if ($ServerPort) { $ServerPort } elseif ($config.server.port) { $config.server.port } else { 5600 } -$script:ApiBase = "{0}://{1}:{2}/api/0" -f $scheme, $hostName, $port - -$bucketId = 'aw-file-operations_' + $script:Hostname -Ensure-Bucket -BucketId $bucketId -ClientName 'aw-file-operations' -BucketType 'aw.file.operation' - -# Resolve paths for monitoring -$resolvedPaths = @() -foreach ($p in $WatchPaths) { - $fullPath = $p - if (-not [System.IO.Path]::IsPathRooted($p)) { - try { - if ($p -eq 'Desktop') { $fullPath = [Environment]::GetFolderPath('Desktop') } - elseif ($p -eq 'Documents') { $fullPath = [Environment]::GetFolderPath('MyDocuments') } - elseif ($p -eq 'Downloads') { $fullPath = Join-Path $env:USERPROFILE 'Downloads' } - } catch { Write-Error [CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$PolicyPath, - [string]$LogPath, - [int]$PollSeconds = 10, - [string[]]$WatchPaths = @('Desktop', 'Documents', 'Downloads') -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -# Force TLS 1.2 and load networking types -[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -Add-Type -AssemblyName System.Net.Http - -# Bucket registry -$script:KnownBuckets = @{} -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = [System.Diagnostics.Process]::GetCurrentProcess().SessionId - -# Настройка логирования -$script:LogPath = $LogPath -$script:LocalAgentLogsEnabled = [bool]$LogPath - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - return $null -} - -function Write-FileCollectorLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { return } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} [FileCollector] {1}' -f (Get-Date -Format s), $Message) - } catch {} -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - $httpClient = $null - try { - $httpClient = New-Object System.Net.Http.HttpClient - $content = New-Object System.Net.Http.StringContent($Json, [System.Text.Encoding]::UTF8, "application/json") - $response = $httpClient.PostAsync($Uri, $content).Result - if (-not $response.IsSuccessStatusCode) { - $status = [int]$response.StatusCode - $reason = [string]$response.ReasonPhrase - $body = $response.Content.ReadAsStringAsync().Result - Write-FileCollectorLog ("POST failed: uri={0} status={1} reason={2} body={3}" -f $Uri, $status, $reason, $body) - } - } catch { - Write-FileCollectorLog "POST Error: $($_.Exception.Message)" - } finally { - if ($null -ne $httpClient) { - $httpClient.Dispose() - } - } -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - try { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null - $script:KnownBuckets[$BucketId] = $true - return - } - catch { - } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - try { - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - } - catch { - try { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null - } - catch { - Write-FileCollectorLog "Bucket create/check failed for ${BucketId}: $($_.Exception.Message)" - throw - } - } - $script:KnownBuckets[$BucketId] = $true -} - -function Send-FileOperationEvent { - param( - [string]$Operation, - [string]$FilePath, - [string]$OldFilePath = $null, - [long]$Size = 0 - ) - - $bucketId = 'aw-file-operations_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-file-operations' -BucketType 'aw.file.operation' - - $data = @{ - operation = $Operation - path = $FilePath - extension = [System.IO.Path]::GetExtension($FilePath) - username = $env:USERNAME - hostname = $script:Hostname - } - if ($OldFilePath) { $data.oldPath = $OldFilePath } - if ($Size -gt 0) { $data.size = $Size } - - # Детекция архивации (упрощенная) - if ($Operation -eq 'Created' -and $data.extension -match '\.(zip|7z|rar|tar|gz)$') { - $data.archiveHint = $true - } - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = $data - } | ConvertTo-Json -Depth 5 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=15" -Json $payload -} - -$config = Get-DeploymentConfig -Path $ConfigPath -if (-not $config) { throw "Configuration file not found: $ConfigPath" } - -$scheme = if ($ServerScheme) { $ServerScheme } elseif ($config.server.scheme) { $config.server.scheme } else { 'http' } -$hostName = if ($ServerHost) { $ServerHost } elseif ($config.server.host) { $config.server.host } else { 'localhost' } -$port = if ($ServerPort) { $ServerPort } elseif ($config.server.port) { $config.server.port } else { 5600 } -$script:ApiBase = "{0}://{1}:{2}/api/0" -f $scheme, $hostName, $port - -$bucketId = 'aw-file-operations_' + $script:Hostname -Ensure-Bucket -BucketId $bucketId -ClientName 'aw-file-operations' -BucketType 'aw.file.operation' - -# Resolve paths for monitoring -$resolvedPaths = @() -foreach ($p in $WatchPaths) { - $fullPath = $p - if (-not [System.IO.Path]::IsPathRooted($p)) { - try { - if ($p -eq 'Desktop') { $fullPath = [Environment]::GetFolderPath('Desktop') } - elseif ($p -eq 'Documents') { $fullPath = [Environment]::GetFolderPath('MyDocuments') } - elseif ($p -eq 'Downloads') { $fullPath = Join-Path $env:USERPROFILE 'Downloads' } - } catch {} - } - if ($fullPath -and (Test-Path -LiteralPath $fullPath)) { - $resolvedPaths += $fullPath - } -} - -if ($resolvedPaths.Count -eq 0) { - Write-FileCollectorLog "No valid watch paths found. Exiting." - exit 0 -} - -Write-FileCollectorLog "Starting watch on paths: $($resolvedPaths -join ', ')" - -$watchers = @() -$subscriptions = @() -foreach ($path in $resolvedPaths) { - $watcher = New-Object System.IO.FileSystemWatcher - $watcher.Path = $path - $watcher.IncludeSubdirectories = $true - $watcher.EnableRaisingEvents = $true - - $onChanged = Register-ObjectEvent $watcher "Created" -Action { - $path = $Event.SourceEventArgs.FullPath - $size = 0 - try { if (Test-Path -LiteralPath $path) { $size = (Get-Item -LiteralPath $path).Length } } catch {} - Send-FileOperationEvent -Operation 'Created' -FilePath $path -Size $size - } - $onDeleted = Register-ObjectEvent $watcher "Deleted" -Action { - Send-FileOperationEvent -Operation 'Deleted' -FilePath $Event.SourceEventArgs.FullPath - } - $onRenamed = Register-ObjectEvent $watcher "Renamed" -Action { - Send-FileOperationEvent -Operation 'Renamed' -FilePath $Event.SourceEventArgs.FullPath -OldFilePath $Event.SourceEventArgs.OldFullPath - } - - $watchers += $watcher - $subscriptions += @($onChanged, $onDeleted, $onRenamed) -} - -Write-FileCollectorLog "Collector started. Waiting for events..." - -try { - while ($true) { - Start-Sleep -Seconds $PollSeconds - } -} -finally { - Write-FileCollectorLog "Stopping collector..." - foreach ($sub in @($subscriptions)) { - try { - if ($sub -and $sub.Id) { - Unregister-Event -SubscriptionId $sub.Id -ErrorAction SilentlyContinue - Remove-Job -Id $sub.Id -Force -ErrorAction SilentlyContinue - } - } catch {} - } - foreach ($w in $watchers) { - $w.EnableRaisingEvents = $false - $w.Dispose() - } -} -; } - } - if ($fullPath -and (Test-Path -LiteralPath $fullPath)) { - $resolvedPaths += $fullPath - } -} - -if ($resolvedPaths.Count -eq 0) { - Write-FileCollectorLog "No valid watch paths found. Exiting." - exit 0 -} - -Write-FileCollectorLog "Starting watch on paths: $($resolvedPaths -join ', ')" - -$watchers = @() -$subscriptions = @() -foreach ($path in $resolvedPaths) { - $watcher = New-Object System.IO.FileSystemWatcher - $watcher.Path = $path - $watcher.IncludeSubdirectories = $true - $watcher.EnableRaisingEvents = $true - - $onChanged = Register-ObjectEvent $watcher "Created" -Action { - $path = $Event.SourceEventArgs.FullPath - $size = 0 - try { if (Test-Path -LiteralPath $path) { $size = (Get-Item -LiteralPath $path).Length } } catch { Write-Error [CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$PolicyPath, - [string]$LogPath, - [int]$PollSeconds = 10, - [string[]]$WatchPaths = @('Desktop', 'Documents', 'Downloads') -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -# Force TLS 1.2 and load networking types -[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -Add-Type -AssemblyName System.Net.Http - -# Bucket registry -$script:KnownBuckets = @{} -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = [System.Diagnostics.Process]::GetCurrentProcess().SessionId - -# Настройка логирования -$script:LogPath = $LogPath -$script:LocalAgentLogsEnabled = [bool]$LogPath - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - return $null -} - -function Write-FileCollectorLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { return } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} [FileCollector] {1}' -f (Get-Date -Format s), $Message) - } catch {} -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - $httpClient = $null - try { - $httpClient = New-Object System.Net.Http.HttpClient - $content = New-Object System.Net.Http.StringContent($Json, [System.Text.Encoding]::UTF8, "application/json") - $response = $httpClient.PostAsync($Uri, $content).Result - if (-not $response.IsSuccessStatusCode) { - $status = [int]$response.StatusCode - $reason = [string]$response.ReasonPhrase - $body = $response.Content.ReadAsStringAsync().Result - Write-FileCollectorLog ("POST failed: uri={0} status={1} reason={2} body={3}" -f $Uri, $status, $reason, $body) - } - } catch { - Write-FileCollectorLog "POST Error: $($_.Exception.Message)" - } finally { - if ($null -ne $httpClient) { - $httpClient.Dispose() - } - } -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - try { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null - $script:KnownBuckets[$BucketId] = $true - return - } - catch { - } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - try { - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - } - catch { - try { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null - } - catch { - Write-FileCollectorLog "Bucket create/check failed for ${BucketId}: $($_.Exception.Message)" - throw - } - } - $script:KnownBuckets[$BucketId] = $true -} - -function Send-FileOperationEvent { - param( - [string]$Operation, - [string]$FilePath, - [string]$OldFilePath = $null, - [long]$Size = 0 - ) - - $bucketId = 'aw-file-operations_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-file-operations' -BucketType 'aw.file.operation' - - $data = @{ - operation = $Operation - path = $FilePath - extension = [System.IO.Path]::GetExtension($FilePath) - username = $env:USERNAME - hostname = $script:Hostname - } - if ($OldFilePath) { $data.oldPath = $OldFilePath } - if ($Size -gt 0) { $data.size = $Size } - - # Детекция архивации (упрощенная) - if ($Operation -eq 'Created' -and $data.extension -match '\.(zip|7z|rar|tar|gz)$') { - $data.archiveHint = $true - } - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = $data - } | ConvertTo-Json -Depth 5 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=15" -Json $payload -} - -$config = Get-DeploymentConfig -Path $ConfigPath -if (-not $config) { throw "Configuration file not found: $ConfigPath" } - -$scheme = if ($ServerScheme) { $ServerScheme } elseif ($config.server.scheme) { $config.server.scheme } else { 'http' } -$hostName = if ($ServerHost) { $ServerHost } elseif ($config.server.host) { $config.server.host } else { 'localhost' } -$port = if ($ServerPort) { $ServerPort } elseif ($config.server.port) { $config.server.port } else { 5600 } -$script:ApiBase = "{0}://{1}:{2}/api/0" -f $scheme, $hostName, $port - -$bucketId = 'aw-file-operations_' + $script:Hostname -Ensure-Bucket -BucketId $bucketId -ClientName 'aw-file-operations' -BucketType 'aw.file.operation' - -# Resolve paths for monitoring -$resolvedPaths = @() -foreach ($p in $WatchPaths) { - $fullPath = $p - if (-not [System.IO.Path]::IsPathRooted($p)) { - try { - if ($p -eq 'Desktop') { $fullPath = [Environment]::GetFolderPath('Desktop') } - elseif ($p -eq 'Documents') { $fullPath = [Environment]::GetFolderPath('MyDocuments') } - elseif ($p -eq 'Downloads') { $fullPath = Join-Path $env:USERPROFILE 'Downloads' } - } catch {} - } - if ($fullPath -and (Test-Path -LiteralPath $fullPath)) { - $resolvedPaths += $fullPath - } -} - -if ($resolvedPaths.Count -eq 0) { - Write-FileCollectorLog "No valid watch paths found. Exiting." - exit 0 -} - -Write-FileCollectorLog "Starting watch on paths: $($resolvedPaths -join ', ')" - -$watchers = @() -$subscriptions = @() -foreach ($path in $resolvedPaths) { - $watcher = New-Object System.IO.FileSystemWatcher - $watcher.Path = $path - $watcher.IncludeSubdirectories = $true - $watcher.EnableRaisingEvents = $true - - $onChanged = Register-ObjectEvent $watcher "Created" -Action { - $path = $Event.SourceEventArgs.FullPath - $size = 0 - try { if (Test-Path -LiteralPath $path) { $size = (Get-Item -LiteralPath $path).Length } } catch {} - Send-FileOperationEvent -Operation 'Created' -FilePath $path -Size $size - } - $onDeleted = Register-ObjectEvent $watcher "Deleted" -Action { - Send-FileOperationEvent -Operation 'Deleted' -FilePath $Event.SourceEventArgs.FullPath - } - $onRenamed = Register-ObjectEvent $watcher "Renamed" -Action { - Send-FileOperationEvent -Operation 'Renamed' -FilePath $Event.SourceEventArgs.FullPath -OldFilePath $Event.SourceEventArgs.OldFullPath - } - - $watchers += $watcher - $subscriptions += @($onChanged, $onDeleted, $onRenamed) -} - -Write-FileCollectorLog "Collector started. Waiting for events..." - -try { - while ($true) { - Start-Sleep -Seconds $PollSeconds - } -} -finally { - Write-FileCollectorLog "Stopping collector..." - foreach ($sub in @($subscriptions)) { - try { - if ($sub -and $sub.Id) { - Unregister-Event -SubscriptionId $sub.Id -ErrorAction SilentlyContinue - Remove-Job -Id $sub.Id -Force -ErrorAction SilentlyContinue - } - } catch {} - } - foreach ($w in $watchers) { - $w.EnableRaisingEvents = $false - $w.Dispose() - } -} -; } - Send-FileOperationEvent -Operation 'Created' -FilePath $path -Size $size - } - $onDeleted = Register-ObjectEvent $watcher "Deleted" -Action { - Send-FileOperationEvent -Operation 'Deleted' -FilePath $Event.SourceEventArgs.FullPath - } - $onRenamed = Register-ObjectEvent $watcher "Renamed" -Action { - Send-FileOperationEvent -Operation 'Renamed' -FilePath $Event.SourceEventArgs.FullPath -OldFilePath $Event.SourceEventArgs.OldFullPath - } - - $watchers += $watcher - $subscriptions += @($onChanged, $onDeleted, $onRenamed) -} - -Write-FileCollectorLog "Collector started. Waiting for events..." - -try { - while ($true) { - Start-Sleep -Seconds $PollSeconds - } -} -finally { - Write-FileCollectorLog "Stopping collector..." - foreach ($sub in @($subscriptions)) { - try { - if ($sub -and $sub.Id) { - Unregister-Event -SubscriptionId $sub.Id -ErrorAction SilentlyContinue - Remove-Job -Id $sub.Id -Force -ErrorAction SilentlyContinue - } - } catch { Write-Error [CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$PolicyPath, - [string]$LogPath, - [int]$PollSeconds = 10, - [string[]]$WatchPaths = @('Desktop', 'Documents', 'Downloads') -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -# Force TLS 1.2 and load networking types -[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -Add-Type -AssemblyName System.Net.Http - -# Bucket registry -$script:KnownBuckets = @{} -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = [System.Diagnostics.Process]::GetCurrentProcess().SessionId - -# Настройка логирования -$script:LogPath = $LogPath -$script:LocalAgentLogsEnabled = [bool]$LogPath - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - return $null -} - -function Write-FileCollectorLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { return } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} [FileCollector] {1}' -f (Get-Date -Format s), $Message) - } catch {} -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - $httpClient = $null - try { - $httpClient = New-Object System.Net.Http.HttpClient - $content = New-Object System.Net.Http.StringContent($Json, [System.Text.Encoding]::UTF8, "application/json") - $response = $httpClient.PostAsync($Uri, $content).Result - if (-not $response.IsSuccessStatusCode) { - $status = [int]$response.StatusCode - $reason = [string]$response.ReasonPhrase - $body = $response.Content.ReadAsStringAsync().Result - Write-FileCollectorLog ("POST failed: uri={0} status={1} reason={2} body={3}" -f $Uri, $status, $reason, $body) - } - } catch { - Write-FileCollectorLog "POST Error: $($_.Exception.Message)" - } finally { - if ($null -ne $httpClient) { - $httpClient.Dispose() - } - } -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - try { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null - $script:KnownBuckets[$BucketId] = $true - return - } - catch { - } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - try { - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - } - catch { - try { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null - } - catch { - Write-FileCollectorLog "Bucket create/check failed for ${BucketId}: $($_.Exception.Message)" - throw - } - } - $script:KnownBuckets[$BucketId] = $true -} - -function Send-FileOperationEvent { - param( - [string]$Operation, - [string]$FilePath, - [string]$OldFilePath = $null, - [long]$Size = 0 - ) - - $bucketId = 'aw-file-operations_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-file-operations' -BucketType 'aw.file.operation' - - $data = @{ - operation = $Operation - path = $FilePath - extension = [System.IO.Path]::GetExtension($FilePath) - username = $env:USERNAME - hostname = $script:Hostname - } - if ($OldFilePath) { $data.oldPath = $OldFilePath } - if ($Size -gt 0) { $data.size = $Size } - - # Детекция архивации (упрощенная) - if ($Operation -eq 'Created' -and $data.extension -match '\.(zip|7z|rar|tar|gz)$') { - $data.archiveHint = $true - } - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = $data - } | ConvertTo-Json -Depth 5 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=15" -Json $payload -} - -$config = Get-DeploymentConfig -Path $ConfigPath -if (-not $config) { throw "Configuration file not found: $ConfigPath" } - -$scheme = if ($ServerScheme) { $ServerScheme } elseif ($config.server.scheme) { $config.server.scheme } else { 'http' } -$hostName = if ($ServerHost) { $ServerHost } elseif ($config.server.host) { $config.server.host } else { 'localhost' } -$port = if ($ServerPort) { $ServerPort } elseif ($config.server.port) { $config.server.port } else { 5600 } -$script:ApiBase = "{0}://{1}:{2}/api/0" -f $scheme, $hostName, $port - -$bucketId = 'aw-file-operations_' + $script:Hostname -Ensure-Bucket -BucketId $bucketId -ClientName 'aw-file-operations' -BucketType 'aw.file.operation' - -# Resolve paths for monitoring -$resolvedPaths = @() -foreach ($p in $WatchPaths) { - $fullPath = $p - if (-not [System.IO.Path]::IsPathRooted($p)) { - try { - if ($p -eq 'Desktop') { $fullPath = [Environment]::GetFolderPath('Desktop') } - elseif ($p -eq 'Documents') { $fullPath = [Environment]::GetFolderPath('MyDocuments') } - elseif ($p -eq 'Downloads') { $fullPath = Join-Path $env:USERPROFILE 'Downloads' } - } catch {} - } - if ($fullPath -and (Test-Path -LiteralPath $fullPath)) { - $resolvedPaths += $fullPath - } -} - -if ($resolvedPaths.Count -eq 0) { - Write-FileCollectorLog "No valid watch paths found. Exiting." - exit 0 -} - -Write-FileCollectorLog "Starting watch on paths: $($resolvedPaths -join ', ')" - -$watchers = @() -$subscriptions = @() -foreach ($path in $resolvedPaths) { - $watcher = New-Object System.IO.FileSystemWatcher - $watcher.Path = $path - $watcher.IncludeSubdirectories = $true - $watcher.EnableRaisingEvents = $true - - $onChanged = Register-ObjectEvent $watcher "Created" -Action { - $path = $Event.SourceEventArgs.FullPath - $size = 0 - try { if (Test-Path -LiteralPath $path) { $size = (Get-Item -LiteralPath $path).Length } } catch {} - Send-FileOperationEvent -Operation 'Created' -FilePath $path -Size $size - } - $onDeleted = Register-ObjectEvent $watcher "Deleted" -Action { - Send-FileOperationEvent -Operation 'Deleted' -FilePath $Event.SourceEventArgs.FullPath - } - $onRenamed = Register-ObjectEvent $watcher "Renamed" -Action { - Send-FileOperationEvent -Operation 'Renamed' -FilePath $Event.SourceEventArgs.FullPath -OldFilePath $Event.SourceEventArgs.OldFullPath - } - - $watchers += $watcher - $subscriptions += @($onChanged, $onDeleted, $onRenamed) -} - -Write-FileCollectorLog "Collector started. Waiting for events..." - -try { - while ($true) { - Start-Sleep -Seconds $PollSeconds - } -} -finally { - Write-FileCollectorLog "Stopping collector..." - foreach ($sub in @($subscriptions)) { - try { - if ($sub -and $sub.Id) { - Unregister-Event -SubscriptionId $sub.Id -ErrorAction SilentlyContinue - Remove-Job -Id $sub.Id -Force -ErrorAction SilentlyContinue - } - } catch {} - } - foreach ($w in $watchers) { - $w.EnableRaisingEvents = $false - $w.Dispose() - } -} -; } - } - foreach ($w in $watchers) { - $w.EnableRaisingEvents = $false - $w.Dispose() - } -}