From d88ef781845814e6c30a17470b6195b5bb20c70f Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Mon, 4 May 2026 21:47:03 +0300 Subject: [PATCH] Fix DLP collector defaults, HTTP error handling, and cleanup --- powershell-analysis.md | 93 ++++++++++++++++++++++ windows/dlp-endpoint-signals-collector.ps1 | 39 +++++++-- windows/file-operations-collector.ps1 | 22 ++++- 3 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 powershell-analysis.md diff --git a/powershell-analysis.md b/powershell-analysis.md new file mode 100644 index 0000000..d7af0ee --- /dev/null +++ b/powershell-analysis.md @@ -0,0 +1,93 @@ +# Анализ DLP-скриптов PowerShell (работоспособность) + +Дата анализа: **2026-05-04 (UTC)** + +## Проверенный scope + +- `windows/dlp-endpoint-signals-collector.ps1` +- `windows/file-operations-collector.ps1` +- `windows/dlp-policy.example.json` +- `windows/web-category-rules.example.json` + +## Ключевой итог + +DLP-скрипты в целом рабочие по архитектуре (heartbeat в ActivityWatch, policy-driven правила, cooldown, enforcement), но есть **критичный риск misconfiguration** и несколько эксплуатационных рисков. + +--- + +## Что точно хорошо + +1. В обоих коллекторах включены `Set-StrictMode -Version Latest` и `$ErrorActionPreference = 'Stop'`. +2. Есть отправка событий в отдельные bucket’ы (`aw-dlp-endpoint-signals_*`, `aw-dlp-incidents_*`, `aw-file-operations_*`). +3. В endpoint-коллекторе реализованы: + - правила по буферу обмена / USB / печати, + - suppression через cooldown (`Should-EmitByCooldown`), + - опциональный screenshot capture при инциденте. +4. В file collector есть наблюдение за `Desktop/Documents/Downloads` через `FileSystemWatcher`. + +--- + +## Найденные проблемы и риски + +### 1) Критично: дефолтный путь конфига в endpoint-скрипте не совпадает с проектом + +- `dlp-endpoint-signals-collector.ps1` использует по умолчанию: + - `C:\ProgramData\ActivityWatch\deployment-config.json` +- Остальной проект использует namespace `AWatch-rus` (`C:\ProgramData\AWatch-rus\...`). + +**Риск:** endpoint-коллектор может стартовать без нужного deployment-конфига и работать с неверными/пустыми параметрами. + +### 2) Нет строгой проверки HTTP-результата в file collector + +В `file-operations-collector.ps1` POST выполняется через `HttpClient`, но код ответа не валидируется (`IsSuccessStatusCode` не проверяется), ошибки частично только логируются. + +**Риск:** «тихая» потеря telemetry при 4xx/5xx. + +### 3) Watcher не снимает event subscriptions явно + +Есть `Register-ObjectEvent`, но в `finally` disposal только watcher-объектов; отписка событий (`Unregister-Event`) явно не делается. + +**Риск:** при рестартах/долгой работе возможно накопление подписок в сессии. + +### 4) Screenshot/GUI-зависимость для enforcement + +`Capture-IncidentScreenshot` и balloon notification завязаны на `System.Windows.Forms/System.Drawing`. + +**Риск:** в non-interactive / service context часть enforcement UX может не работать (событие уйдёт, но скриншот/уведомление может не сформироваться). + +--- + +## Рекомендации (приоритет) + +1. **P1:** выровнять дефолтный `ConfigPath` в `dlp-endpoint-signals-collector.ps1` на `C:\ProgramData\AWatch-rus\deployment-config.json`. +2. **P1:** добавить проверку `response.IsSuccessStatusCode` в `file-operations-collector.ps1` и логировать body/status при ошибках. +3. **P2:** сохранить subscription-объекты `Register-ObjectEvent` и делать `Unregister-Event` в `finally`. +4. **P2:** для enforcement/UI добавить fallback режим «headless» (только лог + heartbeat). + +--- + +## Что не удалось проверить в текущей среде + +В этом контейнере отсутствует `pwsh`, поэтому не выполнены: + +- синтаксический parse всех `*.ps1/*.psm1` через PowerShell parser; +- `Test-ModuleManifest`; +- smoke-run на Windows API (`Get-WinEvent`, `Get-Partition`, `Get-Disk`, `Set-Clipboard`, `Win32_PrintJob`). + +--- + +## Команды для целевой Windows-проверки + +```powershell +# 1) Синтаксис +Get-ChildItem .\windows -Recurse -Include *.ps1,*.psm1 | ForEach-Object { + [void][System.Management.Automation.Language.Parser]::ParseFile($_.FullName,[ref]$null,[ref]$errs) + if($errs){ "FAIL $($_.FullName)" } else { "OK $($_.FullName)" } +} + +# 2) Быстрый запуск file collector (с логом) +.\windows\file-operations-collector.ps1 -ConfigPath 'C:\ProgramData\AWatch-rus\deployment-config.json' -LogPath 'C:\ProgramData\AWatch-rus\collector-fileops.log' + +# 3) Быстрый запуск endpoint collector (с логом) +.\windows\dlp-endpoint-signals-collector.ps1 -ConfigPath 'C:\ProgramData\AWatch-rus\deployment-config.json' -PolicyPath 'C:\ProgramData\AWatch-rus\dlp-policy.json' -LogPath 'C:\ProgramData\AWatch-rus\collector-endpoint.log' +``` diff --git a/windows/dlp-endpoint-signals-collector.ps1 b/windows/dlp-endpoint-signals-collector.ps1 index 1052c02..74f67d0 100644 --- a/windows/dlp-endpoint-signals-collector.ps1 +++ b/windows/dlp-endpoint-signals-collector.ps1 @@ -1,6 +1,6 @@ [CmdletBinding()] param( - [string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json', + [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', [string]$ServerHost, [int]$ServerPort, [ValidateSet('http', 'https')] @@ -224,6 +224,10 @@ function Show-EnforcementNotification { [Parameter(Mandatory = $true)][string]$Title, [Parameter(Mandatory = $true)][string]$Body ) + if ($script:HeadlessMode) { + Write-EndpointLog ("headless mode: skip notification title={0}" -f $Title) + return $false + } try { Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue $icon = New-Object System.Windows.Forms.NotifyIcon @@ -235,9 +239,11 @@ function Show-EnforcementNotification { $icon.ShowBalloonTip(5000) Start-Sleep -Milliseconds 200 $icon.Dispose() + return $true } catch { Write-EndpointLog ("notification failed: {0}" -f $_.Exception.Message) + return $false } } @@ -416,8 +422,13 @@ function Evaluate-ClipboardRules { $enforced = $false if ($action -eq 'block') { - $enforced = Invoke-ClipboardEnforcement - Show-EnforcementNotification -Title 'DLP: буфер обмена очищен' -Body $message + if ($script:HeadlessMode) { + Write-EndpointLog ("headless fallback: clipboard rule={0} requires block, skipped interactive enforcement" -f $ruleId) + } + else { + $enforced = Invoke-ClipboardEnforcement + [void](Show-EnforcementNotification -Title 'DLP: буфер обмена очищен' -Body $message) + } } Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{ @@ -451,8 +462,13 @@ function Evaluate-UsbRules { $enforced = $false if ($action -eq 'block') { - $enforced = Invoke-UsbWriteBlockEnforcement -DriveLetter $DriveLetter - Show-EnforcementNotification -Title 'DLP: USB заблокирован для записи' -Body $message + if ($script:HeadlessMode) { + Write-EndpointLog ("headless fallback: usb rule={0} requires block, skipped interactive enforcement drive={1}" -f $ruleId, $DriveLetter) + } + else { + $enforced = Invoke-UsbWriteBlockEnforcement -DriveLetter $DriveLetter + [void](Show-EnforcementNotification -Title 'DLP: USB заблокирован для записи' -Body $message) + } } Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{ @@ -496,8 +512,13 @@ function Evaluate-PrintRules { $enforced = $false if ($action -eq 'block') { - $enforced = Invoke-PrintJobEnforcement -PrinterName $PrinterName -DocumentName $DocumentName -Owner $Owner - Show-EnforcementNotification -Title 'DLP: печать заблокирована' -Body $message + if ($script:HeadlessMode) { + Write-EndpointLog ("headless fallback: print rule={0} requires block, skipped interactive enforcement printer={1}" -f $ruleId, $PrinterName) + } + else { + $enforced = Invoke-PrintJobEnforcement -PrinterName $PrinterName -DocumentName $DocumentName -Owner $Owner + [void](Show-EnforcementNotification -Title 'DLP: печать заблокирована' -Body $message) + } } Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'print_job' -Data @{ @@ -760,9 +781,13 @@ $script:LogPath = $resolvedLogPath $script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot $script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled $script:ScreenshotTypesLoaded = $false +$script:HeadlessMode = ($env:SESSIONNAME -eq 'Service') -or (-not [Environment]::UserInteractive) Load-DlpPolicy -Path $resolvedPolicyPath Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase) +if ($script:HeadlessMode) { + Write-EndpointLog "headless mode enabled: enforcement UI is disabled, incident heartbeat and logs only" +} while ($true) { try { diff --git a/windows/file-operations-collector.ps1 b/windows/file-operations-collector.ps1 index 3fb5246..ca543a3 100644 --- a/windows/file-operations-collector.ps1 +++ b/windows/file-operations-collector.ps1 @@ -48,13 +48,23 @@ function Invoke-AwJsonPost { [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 - $httpClient.Dispose() + 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() + } } } @@ -144,6 +154,7 @@ if ($resolvedPaths.Count -eq 0) { Write-FileCollectorLog "Starting watch on paths: $($resolvedPaths -join ', ')" $watchers = @() +$subscriptions = @() foreach ($path in $resolvedPaths) { $watcher = New-Object System.IO.FileSystemWatcher $watcher.Path = $path @@ -164,6 +175,7 @@ foreach ($path in $resolvedPaths) { } $watchers += $watcher + $subscriptions += @($onChanged, $onDeleted, $onRenamed) } Write-FileCollectorLog "Collector started. Waiting for events..." @@ -175,6 +187,14 @@ try { } 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()