feat(dlp-phase1): add policy-based incident pipeline and capability gap analysis
This commit is contained in:
@@ -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-пайплайн проверок.
|
||||
|
||||
## Базовый сценарий
|
||||
|
||||
@@ -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_<host>` + локальный 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
|
||||
@@ -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'а.
|
||||
|
||||
## Повторный прогон
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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_<hostname>`
|
||||
- `aw-watcher-web-edge_<hostname>` или другой browser bucket
|
||||
- `aw-detmir-web-category_<hostname>`
|
||||
- `aw-dlp-incidents_<hostname>` (при срабатывании 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_<hostname>` через 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` у тестового пользователя.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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]@{
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 `
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user