Fix DLP collector defaults, HTTP error handling, and cleanup
This commit is contained in:
@@ -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'
|
||||||
|
```
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json',
|
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||||
[string]$ServerHost,
|
[string]$ServerHost,
|
||||||
[int]$ServerPort,
|
[int]$ServerPort,
|
||||||
[ValidateSet('http', 'https')]
|
[ValidateSet('http', 'https')]
|
||||||
@@ -224,6 +224,10 @@ function Show-EnforcementNotification {
|
|||||||
[Parameter(Mandatory = $true)][string]$Title,
|
[Parameter(Mandatory = $true)][string]$Title,
|
||||||
[Parameter(Mandatory = $true)][string]$Body
|
[Parameter(Mandatory = $true)][string]$Body
|
||||||
)
|
)
|
||||||
|
if ($script:HeadlessMode) {
|
||||||
|
Write-EndpointLog ("headless mode: skip notification title={0}" -f $Title)
|
||||||
|
return $false
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue
|
Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue
|
||||||
$icon = New-Object System.Windows.Forms.NotifyIcon
|
$icon = New-Object System.Windows.Forms.NotifyIcon
|
||||||
@@ -235,9 +239,11 @@ function Show-EnforcementNotification {
|
|||||||
$icon.ShowBalloonTip(5000)
|
$icon.ShowBalloonTip(5000)
|
||||||
Start-Sleep -Milliseconds 200
|
Start-Sleep -Milliseconds 200
|
||||||
$icon.Dispose()
|
$icon.Dispose()
|
||||||
|
return $true
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
Write-EndpointLog ("notification failed: {0}" -f $_.Exception.Message)
|
Write-EndpointLog ("notification failed: {0}" -f $_.Exception.Message)
|
||||||
|
return $false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,8 +422,13 @@ function Evaluate-ClipboardRules {
|
|||||||
|
|
||||||
$enforced = $false
|
$enforced = $false
|
||||||
if ($action -eq 'block') {
|
if ($action -eq 'block') {
|
||||||
|
if ($script:HeadlessMode) {
|
||||||
|
Write-EndpointLog ("headless fallback: clipboard rule={0} requires block, skipped interactive enforcement" -f $ruleId)
|
||||||
|
}
|
||||||
|
else {
|
||||||
$enforced = Invoke-ClipboardEnforcement
|
$enforced = Invoke-ClipboardEnforcement
|
||||||
Show-EnforcementNotification -Title 'DLP: буфер обмена очищен' -Body $message
|
[void](Show-EnforcementNotification -Title 'DLP: буфер обмена очищен' -Body $message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{
|
Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{
|
||||||
@@ -451,8 +462,13 @@ function Evaluate-UsbRules {
|
|||||||
|
|
||||||
$enforced = $false
|
$enforced = $false
|
||||||
if ($action -eq 'block') {
|
if ($action -eq 'block') {
|
||||||
|
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
|
$enforced = Invoke-UsbWriteBlockEnforcement -DriveLetter $DriveLetter
|
||||||
Show-EnforcementNotification -Title 'DLP: USB заблокирован для записи' -Body $message
|
[void](Show-EnforcementNotification -Title 'DLP: USB заблокирован для записи' -Body $message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{
|
Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{
|
||||||
@@ -496,8 +512,13 @@ function Evaluate-PrintRules {
|
|||||||
|
|
||||||
$enforced = $false
|
$enforced = $false
|
||||||
if ($action -eq 'block') {
|
if ($action -eq 'block') {
|
||||||
|
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
|
$enforced = Invoke-PrintJobEnforcement -PrinterName $PrinterName -DocumentName $DocumentName -Owner $Owner
|
||||||
Show-EnforcementNotification -Title 'DLP: печать заблокирована' -Body $message
|
[void](Show-EnforcementNotification -Title 'DLP: печать заблокирована' -Body $message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'print_job' -Data @{
|
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:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot
|
||||||
$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled
|
$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled
|
||||||
$script:ScreenshotTypesLoaded = $false
|
$script:ScreenshotTypesLoaded = $false
|
||||||
|
$script:HeadlessMode = ($env:SESSIONNAME -eq 'Service') -or (-not [Environment]::UserInteractive)
|
||||||
|
|
||||||
Load-DlpPolicy -Path $resolvedPolicyPath
|
Load-DlpPolicy -Path $resolvedPolicyPath
|
||||||
Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase)
|
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) {
|
while ($true) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -48,13 +48,23 @@ function Invoke-AwJsonPost {
|
|||||||
[Parameter(Mandatory = $true)][string]$Uri,
|
[Parameter(Mandatory = $true)][string]$Uri,
|
||||||
[Parameter(Mandatory = $true)][string]$Json
|
[Parameter(Mandatory = $true)][string]$Json
|
||||||
)
|
)
|
||||||
|
$httpClient = $null
|
||||||
try {
|
try {
|
||||||
$httpClient = New-Object System.Net.Http.HttpClient
|
$httpClient = New-Object System.Net.Http.HttpClient
|
||||||
$content = New-Object System.Net.Http.StringContent($Json, [System.Text.Encoding]::UTF8, "application/json")
|
$content = New-Object System.Net.Http.StringContent($Json, [System.Text.Encoding]::UTF8, "application/json")
|
||||||
$response = $httpClient.PostAsync($Uri, $content).Result
|
$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 {
|
} catch {
|
||||||
Write-FileCollectorLog "POST Error: $($_.Exception.Message)"
|
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 ', ')"
|
Write-FileCollectorLog "Starting watch on paths: $($resolvedPaths -join ', ')"
|
||||||
|
|
||||||
$watchers = @()
|
$watchers = @()
|
||||||
|
$subscriptions = @()
|
||||||
foreach ($path in $resolvedPaths) {
|
foreach ($path in $resolvedPaths) {
|
||||||
$watcher = New-Object System.IO.FileSystemWatcher
|
$watcher = New-Object System.IO.FileSystemWatcher
|
||||||
$watcher.Path = $path
|
$watcher.Path = $path
|
||||||
@@ -164,6 +175,7 @@ foreach ($path in $resolvedPaths) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$watchers += $watcher
|
$watchers += $watcher
|
||||||
|
$subscriptions += @($onChanged, $onDeleted, $onRenamed)
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-FileCollectorLog "Collector started. Waiting for events..."
|
Write-FileCollectorLog "Collector started. Waiting for events..."
|
||||||
@@ -175,6 +187,14 @@ try {
|
|||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
Write-FileCollectorLog "Stopping collector..."
|
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) {
|
foreach ($w in $watchers) {
|
||||||
$w.EnableRaisingEvents = $false
|
$w.EnableRaisingEvents = $false
|
||||||
$w.Dispose()
|
$w.Dispose()
|
||||||
|
|||||||
Reference in New Issue
Block a user