From c3a49e658c75702df10c84da3c9b9fce36c4f5b0 Mon Sep 17 00:00:00 2001 From: igor04091968 Date: Sat, 25 Apr 2026 16:11:05 +0300 Subject: [PATCH] feat(dlp-phase1): add policy-based incident pipeline and capability gap analysis --- README.md | 2 + docs/dlp-gap-analysis.md | 50 ++++ docs/windows/deployment.md | 10 +- docs/windows/ensemble.md | 1 + docs/windows/validation.md | 13 + windows/ActivityWatch.Windows.Common.psm1 | 25 +- windows/browser-domains-native-collector.ps1 | 278 +++++++++++++++++++ windows/deploy-domain-users.ps1 | 14 +- windows/deploy-ensemble.ps1 | 7 +- windows/deploy-single-user.ps1 | 14 +- windows/dlp-policy.example.json | 58 ++++ windows/hardening-recovery.ps1 | 9 +- windows/validate-deployment.ps1 | 2 + 13 files changed, 473 insertions(+), 10 deletions(-) create mode 100644 docs/dlp-gap-analysis.md create mode 100644 windows/dlp-policy.example.json diff --git a/README.md b/README.md index 02b6248..ae57358 100755 --- a/README.md +++ b/README.md @@ -9,10 +9,12 @@ - `docs/runbook.md` — быстрый runbook для оператора. - `docs/operations.md` — регламент сопровождения, бэкапов, обновлений и rollback. - `docs/windows/ensemble.md` — orchestration-пакет для Windows-деплоя и проверки. +- `docs/dlp-gap-analysis.md` — разрыв до enterprise DLP и roadmap. - `proxmox/` — шаблонные скрипты подготовки и наполнения CT на стороне Proxmox. - `aw-server/` — установочные скрипты, env-шаблон, systemd unit и RU patch для Web UI. - `ansible/` — Ansible-ensemble для автоматизированного сервера (Debian/CT). - `windows/` — PowerShell toolkit: single-user, domain-users, ensemble orchestration, hardening/recovery, validation. +- `windows/` — PowerShell toolkit + phase-1 DLP policy/incident pipeline (`aw-dlp-incidents_*`). - `scripts/quality-gate.sh` — локальный preflight-пайплайн проверок. ## Базовый сценарий diff --git a/docs/dlp-gap-analysis.md b/docs/dlp-gap-analysis.md new file mode 100644 index 0000000..baa47d0 --- /dev/null +++ b/docs/dlp-gap-analysis.md @@ -0,0 +1,50 @@ +# DLP gap analysis: AWatch-rus vs enterprise DLP class + +## Текущий контур AWatch-rus + +- Endpoint activity tracking (`aw-watcher-afk`, `aw-watcher-window`). +- Browser URL/domain collection (native UIAutomation collector). +- Rule-based категоризация web-активности. +- Phase-1 DLP policy: rule match + incident bucket `aw-dlp-incidents_` + локальный incident log. +- Автоматизированный deployment (PowerShell, Ansible, Proxmox). + +## Разрыв до enterprise DLP уровня + +1. **Каналы перехвата**: почта, USB/MTP, печать, clipboard, мессенджеры, облака, file transfer. +2. **Контент-анализ**: PII/dictionaries/EDM/IDM, advanced OCR, document fingerprinting. +3. **Реагирование**: block/quarantine/workflow approvals, исключения, эскалации. +4. **Расследования**: case-management, evidence chain, immutable audit. +5. **Управление**: RBAC/SoD, policy lifecycle, multi-tenant admin model. +6. **Интеграции**: SIEM/SOAR/ITSM, AD/IdP, ticketing. + +## Реалистичный roadmap + +### Phase 1 (сделано) + +- DLP policy JSON + rules. +- Incident generation в отдельный AW bucket. +- Incident cooldown/dedup. + +### Phase 2 (следующий шаг) + +- USB/print/clipboard collectors. +- File-operation telemetry (create/copy/archive/upload hints). +- Central incident aggregation/export. + +### Phase 3 + +- Policy engine service (server-side), versioned policies, approval workflow. +- Correlation engine (user + channel + object + time). +- SIEM connector (CEF/JSON over syslog/HTTP). + +### Phase 4 + +- Advanced detectors (dictionary packs, regex packs, OCR pipeline). +- Risk scoring / UEBA. +- Compliance reports (152-ФЗ / PCI DSS / ISO 27001-aligned evidence views). + +## Reference links (product capability benchmark) + +- https://www.infowatch.ru/products/dlp-sistema-traffic-monitor/vozmozhnosti-dlp-sistemy +- https://www.infowatch.ru/products/dlp-sistema-traffic-monitor/sistemnye-trebovaniya-dlp +- https://www.infowatch.ru/company/presscenter/news/zapatentovana-tekhnologiya-dlya-raspoznavaniya-teksta-na-izobrazheniyakh diff --git a/docs/windows/deployment.md b/docs/windows/deployment.md index 863e543..4239a87 100755 --- a/docs/windows/deployment.md +++ b/docs/windows/deployment.md @@ -9,11 +9,13 @@ - `windows/validate-deployment.ps1` — машинная проверка состояния и JSON-отчёт. - `windows/browser-domains-native-collector.ps1` — native collector доменов браузера с категоризацией. - `windows/web-category-rules.example.json` — пример кастомных правил категоризации. +- `windows/dlp-policy.example.json` — пример DLP-политики (phase-1: alerting incidents). ## Что делает пакет - Ставит `aw-watcher-afk` и `aw-watcher-window` из официального Windows ZIP ActivityWatch. - Копирует browser-domain collector в `C:\ProgramData\ActivityWatch`. +- Копирует DLP policy в `C:\ProgramData\ActivityWatch\dlp-policy.json`. - Создаёт per-user задачи `ActivityWatch Launch [...]` с запуском при логоне. - Создаёт системную задачу `ActivityWatch Recovery`, которая циклически перезапускает per-user launch tasks. - Применяет ACL к `C:\Program Files\ActivityWatch`, `C:\ProgramData\ActivityWatch` и каталогу логов. @@ -36,7 +38,8 @@ Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -ServerHost aw.example.local ` -ServerPort 5600 ` -TargetUser 'CONTOSO\svc.activity.user01' ` - -CustomRulesPath .\windows\web-category-rules.example.json + -CustomRulesPath .\windows\web-category-rules.example.json ` + -CustomPolicyPath .\windows\dlp-policy.example.json ``` Локальный пользователь: @@ -85,7 +88,8 @@ CSV-формат: колонка `User`, `Username`, `SamAccountName` или `Lo -ServerPort 5600 ` -Domain CONTOSO ` -UserListPath C:\Temp\aw-users.txt ` - -CustomRulesPath C:\Temp\web-category-rules.json + -CustomRulesPath C:\Temp\web-category-rules.json ` + -CustomPolicyPath C:\Temp\dlp-policy.json ``` Если список уже содержит `DOMAIN\user`, параметр `-Domain` не нужен. @@ -98,6 +102,7 @@ CSV-формат: колонка `User`, `Username`, `SamAccountName` или `Lo -ServerPort 5600 ` -Domain CONTOSO ` -Users user1,user2,user3,user4,user5 ` + -CustomPolicyPath C:\Temp\dlp-policy.json ` -ValidateAfterDeploy ``` @@ -119,6 +124,7 @@ CSV-формат: колонка `User`, `Username`, `SamAccountName` или `Lo - `C:\ProgramData\ActivityWatch\launch-watchers.ps1` — per-user launcher. - `C:\ProgramData\ActivityWatch\recovery-loop.ps1` — system recovery loop. - `C:\ProgramData\ActivityWatch\browser-domains-native-collector.ps1` — runtime collector. +- `C:\ProgramData\ActivityWatch\dlp-policy.json` — активная DLP-политика. - `C:\ProgramData\ActivityWatch\logs\` — логи collector'а. ## Повторный прогон diff --git a/docs/windows/ensemble.md b/docs/windows/ensemble.md index 582ff27..abb1a6c 100644 --- a/docs/windows/ensemble.md +++ b/docs/windows/ensemble.md @@ -22,6 +22,7 @@ C:\Deploy\AWatch-rus\windows\deploy-ensemble.ps1 ` -ServerPort 5600 ` -Domain AD ` -Users user1,user2,user3,user4,user5 ` + -CustomPolicyPath C:\Deploy\AWatch-rus\windows\dlp-policy.example.json ` -ValidateAfterDeploy ``` diff --git a/docs/windows/validation.md b/docs/windows/validation.md index 8838a6c..f524b9f 100755 --- a/docs/windows/validation.md +++ b/docs/windows/validation.md @@ -20,6 +20,7 @@ $report | ConvertTo-Json -Depth 12 Test-Path 'C:\Program Files\ActivityWatch\aw-watcher-afk\aw-watcher-afk.exe' Test-Path 'C:\Program Files\ActivityWatch\aw-watcher-window\aw-watcher-window.exe' Test-Path 'C:\ProgramData\ActivityWatch\browser-domains-native-collector.ps1' +Test-Path 'C:\ProgramData\ActivityWatch\dlp-policy.json' Test-Path 'C:\ProgramData\ActivityWatch\deployment-config.json' ``` @@ -72,6 +73,7 @@ Invoke-WebRequest https://aw.example.local/api/0/info - `aw-watcher-window_` - `aw-watcher-web-edge_` или другой browser bucket - `aw-detmir-web-category_` +- `aw-dlp-incidents_` (при срабатывании policy rule с `action=alert|block|quarantine`) Проверка через API: @@ -86,6 +88,17 @@ Invoke-WebRequest http://aw.example.local:5600/api/0/buckets | Select-Object -Ex 3. Проверьте category bucket на сервере. 4. Убедитесь, что поля `domain`, `rootDomain`, `category`, `categoryGroup`, `categoryRule` заполнены. +## Проверка DLP phase-1 + +1. В `dlp-policy.json` задайте правило на тестовый домен. +2. Откройте этот домен в браузере. +3. Проверьте `aw-dlp-incidents_` через API. +4. Проверьте локальный лог: + +```powershell +Get-Content "C:\ProgramData\ActivityWatch\logs\dlp-incidents-$env:USERNAME.log" -Tail 50 +``` + ## Проверка восстановления 1. Завершите `aw-watcher-afk.exe` и `aw-watcher-window.exe` у тестового пользователя. diff --git a/windows/ActivityWatch.Windows.Common.psm1 b/windows/ActivityWatch.Windows.Common.psm1 index d0c4cf6..ea481bb 100755 --- a/windows/ActivityWatch.Windows.Common.psm1 +++ b/windows/ActivityWatch.Windows.Common.psm1 @@ -243,8 +243,11 @@ function Copy-ActivityWatchCollectorAssets { [Parameter(Mandatory = $true)] [string]$ExampleRulesSource, [Parameter(Mandatory = $true)] + [string]$ExamplePolicySource, + [Parameter(Mandatory = $true)] [string]$StateRoot, - [string]$CustomRulesSource + [string]$CustomRulesSource, + [string]$CustomPolicySource ) New-ActivityWatchDirectory -Path $StateRoot @@ -252,19 +255,32 @@ function Copy-ActivityWatchCollectorAssets { $collectorTarget = Join-Path $StateRoot 'browser-domains-native-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 $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 } + 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 ExampleRules = $exampleRulesTarget ActiveRules = $rulesTarget + ExamplePolicy = $examplePolicyTarget + ActivePolicy = $policyTarget } } @@ -287,6 +303,8 @@ function New-ActivityWatchDeploymentConfig { [Parameter(Mandatory = $true)] [string]$RulesPath, [Parameter(Mandatory = $true)] + [string]$PolicyPath, + [Parameter(Mandatory = $true)] [int]$PollSeconds, [Parameter(Mandatory = $true)] [int]$PulseSeconds, @@ -315,6 +333,7 @@ function New-ActivityWatchDeploymentConfig { logsRoot = $LogsRoot collectorScript = $CollectorScript rulesPath = $RulesPath + policyPath = $PolicyPath launchScript = $LaunchScriptPath recoveryScript = $RecoveryScriptPath } @@ -326,6 +345,10 @@ function New-ActivityWatchDeploymentConfig { intervalSeconds = $RecoveryIntervalSeconds taskName = 'ActivityWatch Recovery' } + dlp = [pscustomobject]@{ + incidentBucketPrefix = 'aw-dlp-incidents' + enabled = $true + } package = [pscustomobject]@{ version = $PackageVersion } diff --git a/windows/browser-domains-native-collector.ps1 b/windows/browser-domains-native-collector.ps1 index e231f05..61bd96c 100755 --- a/windows/browser-domains-native-collector.ps1 +++ b/windows/browser-domains-native-collector.ps1 @@ -6,7 +6,9 @@ param( [ValidateSet('http', 'https')] [string]$ServerScheme, [string]$RulesPath, + [string]$PolicyPath, [string]$LogPath, + [string]$IncidentLogPath, [int]$PollSeconds, [int]$PulseSeconds ) @@ -51,10 +53,12 @@ $resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig $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' } $resolvedRulesPath = if ($RulesPath) { $RulesPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.rulesPath } else { 'C:\ProgramData\ActivityWatch\web-category-rules.json' } +$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\ActivityWatch\dlp-policy.json' } $resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 } $resolvedPulseSeconds = if ($PSBoundParameters.ContainsKey('PulseSeconds')) { $PulseSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pulseSeconds } else { 30 } $resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\ActivityWatch\logs' } $resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("browser-domains-{0}.log" -f $env:USERNAME) } +$resolvedIncidentLogPath = if ($IncidentLogPath) { $IncidentLogPath } else { Join-Path $resolvedLogsRoot ("dlp-incidents-{0}.log" -f $env:USERNAME) } if (-not (Test-Path -LiteralPath $resolvedLogsRoot)) { New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null @@ -65,6 +69,15 @@ $script:Hostname = $env:COMPUTERNAME $script:SessionId = (Get-Process -Id $PID).SessionId $script:KnownBuckets = @{} $script:LogPath = $resolvedLogPath +$script:IncidentLogPath = $resolvedIncidentLogPath +$script:IncidentState = @{} +$script:DlpRules = @() +$script:DlpDefaults = [ordered]@{ + enabled = $false + cooldownSeconds = 300 + action = 'log' + severity = 'low' +} $script:BrowserMap = @{ msedge = 'edge' chrome = 'chrome' @@ -96,6 +109,16 @@ function Write-CollectorLog { } } +function Write-DlpIncidentLog { + param([string]$Message) + + try { + Add-Content -LiteralPath $script:IncidentLogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) + } + catch { + } +} + function Test-DomainMatch { param( [string]$Host, @@ -255,6 +278,248 @@ function Get-WebCategory { } } +function Test-DomainListMatch { + param( + [string]$Host, + [string[]]$Domains + ) + + if (-not $Domains -or $Domains.Count -eq 0) { + return $false + } + + foreach ($domain in $Domains) { + if (Test-DomainMatch -Host $Host -RuleDomain $domain) { + return $true + } + } + + return $false +} + +function Test-DlpRuleTimeWindow { + param( + [int]$CurrentHour, + [AllowNull()][int]$HourFrom, + [AllowNull()][int]$HourTo + ) + + if ($null -eq $HourFrom -or $null -eq $HourTo) { + return $true + } + + if ($HourFrom -eq $HourTo) { + return $true + } + + if ($HourFrom -lt $HourTo) { + return ($CurrentHour -ge $HourFrom -and $CurrentHour -lt $HourTo) + } + + return ($CurrentHour -ge $HourFrom -or $CurrentHour -lt $HourTo) +} + +function Load-DlpPolicy { + param([string]$Path) + + if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { + Write-CollectorLog ("dlp policy not found, disabled: {0}" -f $Path) + return + } + + try { + $parsed = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json + $defaults = $parsed.defaults + if ($defaults) { + if ($defaults.PSObject.Properties.Name -contains 'enabled') { + $script:DlpDefaults.enabled = [bool]$defaults.enabled + } + if ($defaults.cooldownSeconds) { + $script:DlpDefaults.cooldownSeconds = [int]$defaults.cooldownSeconds + } + if ($defaults.action) { + $script:DlpDefaults.action = [string]$defaults.action + } + if ($defaults.severity) { + $script:DlpDefaults.severity = [string]$defaults.severity + } + } + + $loaded = @() + foreach ($rule in @($parsed.rules)) { + if (-not $rule) { continue } + $when = $rule.when + if (-not $when) { + $when = [pscustomobject]@{} + } + $loaded += [pscustomobject]@{ + id = [string]$rule.id + enabled = if ($rule.PSObject.Properties.Name -contains 'enabled') { [bool]$rule.enabled } else { $true } + action = if ($rule.action) { [string]$rule.action } else { [string]$script:DlpDefaults.action } + severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:DlpDefaults.severity } + message = if ($rule.message) { [string]$rule.message } else { "DLP rule matched: $($rule.id)" } + cooldownSeconds = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:DlpDefaults.cooldownSeconds } + when = [pscustomobject]@{ + domains = if ($when.PSObject.Properties.Name -contains 'domains') { @($when.domains | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } + categoryGroups = if ($when.PSObject.Properties.Name -contains 'categoryGroups') { @($when.categoryGroups | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } + categories = if ($when.PSObject.Properties.Name -contains 'categories') { @($when.categories | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } + browsers = if ($when.PSObject.Properties.Name -contains 'browsers') { @($when.browsers | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } + urlRegex = if ($when.PSObject.Properties.Name -contains 'urlRegex' -and $when.urlRegex) { [string]$when.urlRegex } else { $null } + titleRegex = if ($when.PSObject.Properties.Name -contains 'titleRegex' -and $when.titleRegex) { [string]$when.titleRegex } else { $null } + hourFrom = if ($when.PSObject.Properties.Name -contains 'hourFrom') { [int]$when.hourFrom } else { $null } + hourTo = if ($when.PSObject.Properties.Name -contains 'hourTo') { [int]$when.hourTo } else { $null } + } + } + } + + $script:DlpRules = @($loaded) + Write-CollectorLog ("dlp policy loaded: enabled={0}, rules={1}" -f $script:DlpDefaults.enabled, $script:DlpRules.Count) + } + catch { + Write-CollectorLog ("dlp policy parse failed: {0}" -f $_.Exception.Message) + } +} + +function Test-DlpRuleMatch { + param( + [pscustomobject]$Rule, + [string]$Domain, + [string]$RootDomain, + [string]$Url, + [string]$Title, + [string]$BrowserKey, + [string]$Category, + [string]$CategoryGroup + ) + + if (-not $Rule.enabled) { + return $false + } + + $when = $Rule.when + $currentHour = (Get-Date).Hour + if (-not (Test-DlpRuleTimeWindow -CurrentHour $currentHour -HourFrom $when.hourFrom -HourTo $when.hourTo)) { + return $false + } + + if ($when.domains.Count -gt 0) { + $domainMatched = (Test-DomainListMatch -Host $Domain -Domains $when.domains) -or (Test-DomainListMatch -Host $RootDomain -Domains $when.domains) + if (-not $domainMatched) { + return $false + } + } + + if ($when.categoryGroups.Count -gt 0 -and ($when.categoryGroups -notcontains $CategoryGroup.ToLowerInvariant())) { + return $false + } + + if ($when.categories.Count -gt 0 -and ($when.categories -notcontains $Category.ToLowerInvariant())) { + return $false + } + + if ($when.browsers.Count -gt 0 -and ($when.browsers -notcontains $BrowserKey.ToLowerInvariant())) { + return $false + } + + if ($when.urlRegex) { + if (-not ($Url -match $when.urlRegex)) { + return $false + } + } + + if ($when.titleRegex) { + if (-not ($Title -match $when.titleRegex)) { + return $false + } + } + + return $true +} + +function Get-DlpDecision { + param( + [string]$Domain, + [string]$RootDomain, + [string]$Url, + [string]$Title, + [string]$BrowserKey, + [string]$Category, + [string]$CategoryGroup + ) + + if (-not $script:DlpDefaults.enabled) { + return $null + } + + foreach ($rule in $script:DlpRules) { + if (Test-DlpRuleMatch -Rule $rule -Domain $Domain -RootDomain $RootDomain -Url $Url -Title $Title -BrowserKey $BrowserKey -Category $Category -CategoryGroup $CategoryGroup) { + return $rule + } + } + + return $null +} + +function Should-EmitIncident { + param( + [string]$Fingerprint, + [int]$CooldownSeconds + ) + + $now = (Get-Date).ToUniversalTime() + if ($script:IncidentState.ContainsKey($Fingerprint)) { + $last = [datetime]$script:IncidentState[$Fingerprint] + if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { + return $false + } + } + + $script:IncidentState[$Fingerprint] = $now + return $true +} + +function Send-DlpIncidentHeartbeat { + param( + [pscustomobject]$Decision, + [string]$Url, + [string]$Title, + [string]$BrowserKey, + [string]$ProcessName, + [string]$Domain, + [string]$RootDomain, + [string]$Category, + [string]$CategoryGroup + ) + + $bucketId = 'aw-dlp-incidents_' + $script:Hostname + Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' + + $event = @{ + timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') + duration = 0 + data = @{ + ruleId = [string]$Decision.id + action = [string]$Decision.action + severity = [string]$Decision.severity + message = [string]$Decision.message + url = $Url + title = $Title + browser = $BrowserKey + app = "$ProcessName.exe" + domain = $Domain + rootDomain = $RootDomain + category = $Category + categoryGroup = $CategoryGroup + username = $env:USERNAME + hostname = $script:Hostname + sessionId = $script:SessionId + source = 'uia-native-dlp' + } + } | ConvertTo-Json -Depth 5 -Compress + + Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event | Out-Null +} + function Get-ForegroundWindowContext { $handle = [NativeAwMethods]::GetForegroundWindow() if ($handle -eq [IntPtr]::Zero) { @@ -399,6 +664,7 @@ function Send-CategoryHeartbeat { } Load-CustomCategoryRules -Path $resolvedRulesPath +Load-DlpPolicy -Path $resolvedPolicyPath Write-CollectorLog ("collector started against {0}" -f $script:ApiBase) while ($true) { @@ -423,6 +689,18 @@ while ($true) { Ensure-Bucket -BucketId $bucketId -ClientName ('aw-watcher-web-' + $browserKey) Send-Heartbeat -BucketId $bucketId -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName Send-CategoryHeartbeat -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group -CategoryRule $category.Rule + + $decision = Get-DlpDecision -Domain $domain -RootDomain $rootDomain -Url $url -Title $context.Title -BrowserKey $browserKey -Category $category.Name -CategoryGroup $category.Group + if ($decision) { + $fingerprint = '{0}|{1}|{2}|{3}' -f $decision.id, $browserKey, $rootDomain, $env:USERNAME + $cooldown = [Math]::Max([int]$decision.cooldownSeconds, 30) + if (Should-EmitIncident -Fingerprint $fingerprint -CooldownSeconds $cooldown) { + Write-DlpIncidentLog ("{0} {1} {2} {3}" -f $decision.severity, $decision.action, $decision.id, $url) + if (@('alert', 'block', 'quarantine') -contains ([string]$decision.action).ToLowerInvariant()) { + Send-DlpIncidentHeartbeat -Decision $decision -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group + } + } + } } } } diff --git a/windows/deploy-domain-users.ps1 b/windows/deploy-domain-users.ps1 index 3580ca2..9dc4875 100755 --- a/windows/deploy-domain-users.ps1 +++ b/windows/deploy-domain-users.ps1 @@ -16,7 +16,8 @@ param( [int]$PollSeconds = 5, [int]$PulseSeconds = 30, [int]$RecoveryIntervalSeconds = 180, - [string]$CustomRulesPath + [string]$CustomRulesPath, + [string]$CustomPolicyPath ) Set-StrictMode -Version Latest @@ -36,6 +37,7 @@ $launchScriptPath = Join-Path $StateRoot 'launch-watchers.ps1' $recoveryScriptPath = Join-Path $StateRoot 'recovery-loop.ps1' $collectorSource = Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1' $exampleRulesSource = Join-Path $PSScriptRoot 'web-category-rules.example.json' +$examplePolicySource = Join-Path $PSScriptRoot 'dlp-policy.example.json' New-ActivityWatchDirectory -Path $StateRoot New-ActivityWatchDirectory -Path $logsRoot @@ -44,7 +46,13 @@ $archivePath = Get-ActivityWatchArchive -PackageZipPath $PackageZipPath -Package Install-ActivityWatchPackage -ArchivePath $archivePath -InstallRoot $InstallRoot -WorkingRoot $workingRoot -BackupRoot $backupRoot | Out-Null Get-ActivityWatchExecutableMap -InstallRoot $InstallRoot | Out-Null -$assetResult = Copy-ActivityWatchCollectorAssets -CollectorScriptSource $collectorSource -ExampleRulesSource $exampleRulesSource -StateRoot $StateRoot -CustomRulesSource $CustomRulesPath +$assetResult = Copy-ActivityWatchCollectorAssets ` + -CollectorScriptSource $collectorSource ` + -ExampleRulesSource $exampleRulesSource ` + -ExamplePolicySource $examplePolicySource ` + -StateRoot $StateRoot ` + -CustomRulesSource $CustomRulesPath ` + -CustomPolicySource $CustomPolicyPath $taskDefinitions = New-ActivityWatchUserTaskDefinitions -Users $targetUsers Write-ActivityWatchLaunchScript -Path $launchScriptPath -ConfigPath $configPath @@ -59,6 +67,7 @@ $config = New-ActivityWatchDeploymentConfig ` -LogsRoot $logsRoot ` -CollectorScript $assetResult.CollectorScript ` -RulesPath $assetResult.ActiveRules ` + -PolicyPath $assetResult.ActivePolicy ` -PollSeconds $PollSeconds ` -PulseSeconds $PulseSeconds ` -RecoveryIntervalSeconds $RecoveryIntervalSeconds ` @@ -78,3 +87,4 @@ Write-Host 'ActivityWatch deployed for users:' $targetUsers | ForEach-Object { Write-Host " - $_" } Write-Host "Server: $ServerScheme://$ServerHost`:$ServerPort" Write-Host "State root: $StateRoot" +Write-Host "Policy file: $($assetResult.ActivePolicy)" diff --git a/windows/deploy-ensemble.ps1 b/windows/deploy-ensemble.ps1 index 5675d08..faf048c 100644 --- a/windows/deploy-ensemble.ps1 +++ b/windows/deploy-ensemble.ps1 @@ -17,6 +17,7 @@ param( [int]$PulseSeconds = 30, [int]$RecoveryIntervalSeconds = 180, [string]$CustomRulesPath, + [string]$CustomPolicyPath, [string]$ReportPath, [switch]$SkipHardening, [switch]$ValidateAfterDeploy @@ -54,7 +55,8 @@ if (-not (Test-Path -LiteralPath $deployScript)) { -PollSeconds $PollSeconds ` -PulseSeconds $PulseSeconds ` -RecoveryIntervalSeconds $RecoveryIntervalSeconds ` - -CustomRulesPath $CustomRulesPath + -CustomRulesPath $CustomRulesPath ` + -CustomPolicyPath $CustomPolicyPath if (-not $SkipHardening) { & $hardeningScript ` @@ -68,7 +70,8 @@ if (-not $SkipHardening) { -PollSeconds $PollSeconds ` -PulseSeconds $PulseSeconds ` -RecoveryIntervalSeconds $RecoveryIntervalSeconds ` - -CustomRulesPath $CustomRulesPath + -CustomRulesPath $CustomRulesPath ` + -CustomPolicyPath $CustomPolicyPath } $report = [ordered]@{ diff --git a/windows/deploy-single-user.ps1 b/windows/deploy-single-user.ps1 index df4301e..150e44e 100755 --- a/windows/deploy-single-user.ps1 +++ b/windows/deploy-single-user.ps1 @@ -15,7 +15,8 @@ param( [int]$PollSeconds = 5, [int]$PulseSeconds = 30, [int]$RecoveryIntervalSeconds = 180, - [string]$CustomRulesPath + [string]$CustomRulesPath, + [string]$CustomPolicyPath ) Set-StrictMode -Version Latest @@ -34,6 +35,7 @@ $launchScriptPath = Join-Path $StateRoot 'launch-watchers.ps1' $recoveryScriptPath = Join-Path $StateRoot 'recovery-loop.ps1' $collectorSource = Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1' $exampleRulesSource = Join-Path $PSScriptRoot 'web-category-rules.example.json' +$examplePolicySource = Join-Path $PSScriptRoot 'dlp-policy.example.json' New-ActivityWatchDirectory -Path $StateRoot New-ActivityWatchDirectory -Path $logsRoot @@ -42,7 +44,13 @@ $archivePath = Get-ActivityWatchArchive -PackageZipPath $PackageZipPath -Package Install-ActivityWatchPackage -ArchivePath $archivePath -InstallRoot $InstallRoot -WorkingRoot $workingRoot -BackupRoot $backupRoot | Out-Null Get-ActivityWatchExecutableMap -InstallRoot $InstallRoot | Out-Null -$assetResult = Copy-ActivityWatchCollectorAssets -CollectorScriptSource $collectorSource -ExampleRulesSource $exampleRulesSource -StateRoot $StateRoot -CustomRulesSource $CustomRulesPath +$assetResult = Copy-ActivityWatchCollectorAssets ` + -CollectorScriptSource $collectorSource ` + -ExampleRulesSource $exampleRulesSource ` + -ExamplePolicySource $examplePolicySource ` + -StateRoot $StateRoot ` + -CustomRulesSource $CustomRulesPath ` + -CustomPolicySource $CustomPolicyPath $taskDefinitions = New-ActivityWatchUserTaskDefinitions -Users @($TargetUser) Write-ActivityWatchLaunchScript -Path $launchScriptPath -ConfigPath $configPath @@ -57,6 +65,7 @@ $config = New-ActivityWatchDeploymentConfig ` -LogsRoot $logsRoot ` -CollectorScript $assetResult.CollectorScript ` -RulesPath $assetResult.ActiveRules ` + -PolicyPath $assetResult.ActivePolicy ` -PollSeconds $PollSeconds ` -PulseSeconds $PulseSeconds ` -RecoveryIntervalSeconds $RecoveryIntervalSeconds ` @@ -77,3 +86,4 @@ Write-Host "Server: $ServerScheme://$ServerHost`:$ServerPort" Write-Host "Install root: $InstallRoot" Write-Host "State root: $StateRoot" Write-Host "Rules file: $($assetResult.ActiveRules)" +Write-Host "Policy file: $($assetResult.ActivePolicy)" diff --git a/windows/dlp-policy.example.json b/windows/dlp-policy.example.json new file mode 100644 index 0000000..b7d4cfd --- /dev/null +++ b/windows/dlp-policy.example.json @@ -0,0 +1,58 @@ +{ + "version": 1, + "defaults": { + "enabled": true, + "cooldownSeconds": 300, + "action": "log", + "severity": "low" + }, + "rules": [ + { + "id": "personal-web-during-workhours", + "enabled": true, + "cooldownSeconds": 600, + "action": "alert", + "severity": "medium", + "message": "Личные ресурсы в рабочее время", + "when": { + "categoryGroups": ["personal"], + "hourFrom": 9, + "hourTo": 19 + } + }, + { + "id": "high-risk-cloud-storage", + "enabled": true, + "cooldownSeconds": 900, + "action": "alert", + "severity": "high", + "message": "Подозрительный доступ к облачному хранилищу", + "when": { + "domains": [ + "dropbox.com", + "drive.google.com", + "mega.nz", + "onedrive.live.com", + "disk.yandex.ru" + ] + } + }, + { + "id": "anonymizer-and-vpn-web", + "enabled": true, + "cooldownSeconds": 900, + "action": "alert", + "severity": "high", + "message": "Использование веб-анонимайзеров / VPN-сервисов", + "when": { + "domains": [ + "hidemy.name", + "2ip.ru", + "whoer.net", + "protonvpn.com", + "nordvpn.com" + ] + } + } + ] +} diff --git a/windows/hardening-recovery.ps1 b/windows/hardening-recovery.ps1 index c44d475..e7887e8 100755 --- a/windows/hardening-recovery.ps1 +++ b/windows/hardening-recovery.ps1 @@ -14,6 +14,7 @@ param( [int]$PulseSeconds, [int]$RecoveryIntervalSeconds, [string]$CustomRulesPath, + [string]$CustomPolicyPath, [switch]$RepairPackage, [string]$Version, [string]$PackageUrl, @@ -45,6 +46,7 @@ $effectiveLaunchScript = Join-Path $effectiveStateRoot 'launch-watchers.ps1' $effectiveRecoveryScript = Join-Path $effectiveStateRoot 'recovery-loop.ps1' $effectiveCollector = Join-Path $effectiveStateRoot 'browser-domains-native-collector.ps1' $effectiveRules = Join-Path $effectiveStateRoot 'web-category-rules.json' +$effectivePolicy = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$existingConfig.paths.policyPath } else { Join-Path $effectiveStateRoot 'dlp-policy.json' } $effectiveServerHost = if ($ServerHost) { $ServerHost } elseif ($existingConfig) { [string]$existingConfig.server.host } else { $null } $effectiveServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($existingConfig) { [int]$existingConfig.server.port } else { 5600 } @@ -79,8 +81,12 @@ Get-ActivityWatchExecutableMap -InstallRoot $effectiveInstallRoot | Out-Null $assetResult = Copy-ActivityWatchCollectorAssets ` -CollectorScriptSource (Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1') ` -ExampleRulesSource (Join-Path $PSScriptRoot 'web-category-rules.example.json') ` + -ExamplePolicySource (Join-Path $PSScriptRoot 'dlp-policy.example.json') ` -StateRoot $effectiveStateRoot ` - -CustomRulesSource $CustomRulesPath + -CustomRulesSource $CustomRulesPath ` + -CustomPolicySource $CustomPolicyPath + +$effectivePolicy = [string]$assetResult.ActivePolicy $taskDefinitions = New-ActivityWatchUserTaskDefinitions -Users $effectiveUsers Write-ActivityWatchLaunchScript -Path $effectiveLaunchScript -ConfigPath $effectiveConfigPath @@ -95,6 +101,7 @@ $config = New-ActivityWatchDeploymentConfig ` -LogsRoot $effectiveLogsRoot ` -CollectorScript $effectiveCollector ` -RulesPath $effectiveRules ` + -PolicyPath $effectivePolicy ` -PollSeconds $effectivePollSeconds ` -PulseSeconds $effectivePulseSeconds ` -RecoveryIntervalSeconds $effectiveRecoveryInterval ` diff --git a/windows/validate-deployment.ps1 b/windows/validate-deployment.ps1 index 15a4822..f36cb55 100644 --- a/windows/validate-deployment.ps1 +++ b/windows/validate-deployment.ps1 @@ -14,6 +14,7 @@ $installRoot = [string]$config.paths.installRoot $stateRoot = [string]$config.paths.stateRoot $collectorScript = [string]$config.paths.collectorScript $rulesPath = [string]$config.paths.rulesPath +$policyPath = if ($config.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$config.paths.policyPath } else { Join-Path $stateRoot 'dlp-policy.json' } $launchScript = [string]$config.paths.launchScript $recoveryScript = [string]$config.paths.recoveryScript @@ -22,6 +23,7 @@ $requiredFiles = @( (Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe'), $collectorScript, $rulesPath, + $policyPath, $launchScript, $recoveryScript, $ConfigPath