From da99d1ac984b01f2420034f5fb396bdf24b8266c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 2 May 2026 08:30:37 +0000 Subject: [PATCH] =?UTF-8?q?=D0=A0=D1=83=D1=81=D0=B8=D1=84=D0=B8=D1=86?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20DLP=20PowerShell?= =?UTF-8?q?=20=D0=B8=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B8=D0=BC=D1=8F=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=B0=20=D0=BF=D0=B5=D1=87=D0=B0=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../windows/ActivityWatch.Windows.Common.psm1 | 32 ++-- .../browser-domains-native-collector.ps1 | 20 +-- .../windows/deploy-domain-users.ps1 | 11 +- .../windows/deploy-ensemble.ps1 | 10 +- .../windows/deploy-single-user.ps1 | 15 +- .../dlp-endpoint-signals-collector.ps1 | 149 ++++++++++++++---- .../windows/hardening-recovery.ps1 | 11 +- .../windows/validate-deployment.ps1 | 16 +- .../windows/worktime-session-collector.ps1 | 145 +++++++++++++++++ windows/ActivityWatch.Windows.Common.psm1 | 22 +-- windows/browser-domains-native-collector.ps1 | 20 +-- windows/deploy-domain-users.ps1 | 8 +- windows/deploy-ensemble.ps1 | 10 +- windows/deploy-single-user.ps1 | 12 +- windows/dlp-endpoint-signals-collector.ps1 | 149 ++++++++++++++---- windows/hardening-recovery.ps1 | 11 +- windows/validate-deployment.ps1 | 2 +- windows/worktime-session-collector.ps1 | 2 +- 18 files changed, 505 insertions(+), 140 deletions(-) create mode 100644 install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1 diff --git a/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1 b/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1 index 4bca474..89affa3 100755 --- a/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1 +++ b/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1 @@ -5,7 +5,7 @@ function Assert-Administrator { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]::new($identity) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - throw 'Run this script from an elevated PowerShell session.' + throw 'Запустите этот скрипт из PowerShell с правами администратора.' } } @@ -64,7 +64,7 @@ function Get-ActivityWatchPackageRoot { Select-Object -First 1 if (-not $afkBinary) { - throw "Cannot find aw-watcher-afk.exe under $ExpandedRoot." + throw "Не удалось найти aw-watcher-afk.exe в $ExpandedRoot." } return (Split-Path -Path (Split-Path -Path $afkBinary.FullName -Parent) -Parent) @@ -130,7 +130,7 @@ function Get-ActivityWatchExecutableMap { foreach ($entry in $map.GetEnumerator()) { if (-not (Test-Path -LiteralPath $entry.Value)) { - throw "Missing required ActivityWatch binary: $($entry.Value)" + throw "Не найден обязательный исполняемый файл ActivityWatch: $($entry.Value)" } } @@ -194,7 +194,7 @@ function Normalize-ActivityWatchUsers { Sort-Object -Unique if (-not $normalized -or $normalized.Count -eq 0) { - throw 'No target users resolved. Provide -Users or -UserListPath.' + throw 'Не удалось определить целевых пользователей. Укажите -Users или -UserListPath.' } return @($normalized) @@ -243,6 +243,8 @@ function Copy-ActivityWatchCollectorAssets { [Parameter(Mandatory = $true)] [string]$EndpointCollectorScriptSource, [Parameter(Mandatory = $true)] + [string]$SessionCollectorScriptSource, + [Parameter(Mandatory = $true)] [string]$ExampleRulesSource, [Parameter(Mandatory = $true)] [string]$ExamplePolicySource, @@ -256,6 +258,7 @@ function Copy-ActivityWatchCollectorAssets { $collectorTarget = Join-Path $StateRoot 'browser-domains-native-collector.ps1' $endpointCollectorTarget = Join-Path $StateRoot 'dlp-endpoint-signals-collector.ps1' + $sessionCollectorTarget = Join-Path $StateRoot 'worktime-session-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' @@ -263,6 +266,7 @@ function Copy-ActivityWatchCollectorAssets { Copy-Item -LiteralPath $CollectorScriptSource -Destination $collectorTarget -Force Copy-Item -LiteralPath $EndpointCollectorScriptSource -Destination $endpointCollectorTarget -Force + Copy-Item -LiteralPath $SessionCollectorScriptSource -Destination $sessionCollectorTarget -Force Copy-Item -LiteralPath $ExampleRulesSource -Destination $exampleRulesTarget -Force Copy-Item -LiteralPath $ExamplePolicySource -Destination $examplePolicyTarget -Force @@ -282,6 +286,7 @@ function Copy-ActivityWatchCollectorAssets { return [pscustomobject]@{ CollectorScript = $collectorTarget EndpointCollectorScript = $endpointCollectorTarget + SessionCollectorScript = $sessionCollectorTarget ExampleRules = $exampleRulesTarget ActiveRules = $rulesTarget ExamplePolicy = $examplePolicyTarget @@ -308,6 +313,8 @@ function New-ActivityWatchDeploymentConfig { [Parameter(Mandatory = $true)] [string]$EndpointCollectorScript, [Parameter(Mandatory = $true)] + [string]$SessionCollectorScript, + [Parameter(Mandatory = $true)] [string]$RulesPath, [Parameter(Mandatory = $true)] [string]$PolicyPath, @@ -349,6 +356,7 @@ function New-ActivityWatchDeploymentConfig { logsRoot = $LogsRoot collectorScript = $CollectorScript endpointCollectorScript = $EndpointCollectorScript + sessionCollectorScript = $SessionCollectorScript rulesPath = $RulesPath policyPath = $PolicyPath launchScript = $LaunchScriptPath @@ -413,7 +421,7 @@ function Read-ActivityWatchDeploymentConfig { ) if (-not (Test-Path -LiteralPath $Path)) { - throw "Deployment config not found: $Path" + throw "Конфигурация развёртывания не найдена: $Path" } return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json @@ -631,6 +639,7 @@ function Start-CollectorScriptIfNeeded { `$script:KnownBuckets = @{} `$collectorScript = [string]`$config.paths.collectorScript `$endpointCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]`$config.paths.endpointCollectorScript } else { '' } +`$sessionCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]`$config.paths.sessionCollectorScript } else { '' } `$afkExe = Join-Path `$installRoot 'aw-watcher-afk\aw-watcher-afk.exe' `$windowExe = Join-Path `$installRoot 'aw-watcher-window\aw-watcher-window.exe' `$serverArgs = @('--host', [string]`$config.server.host, '--port', [string]`$config.server.port) @@ -639,11 +648,11 @@ function Start-CollectorScriptIfNeeded { `$windowEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]`$config.collectors.windowEnabled } else { `$true } if (`$afkEnabled -and -not (Test-Path -LiteralPath `$afkExe)) { - throw "Missing aw-watcher-afk.exe: `$afkExe" + throw "Не найден aw-watcher-afk.exe: `$afkExe" } if (`$windowEnabled -and -not (Test-Path -LiteralPath `$windowExe)) { - throw "Missing aw-watcher-window.exe: `$windowExe" + throw "Не найден aw-watcher-window.exe: `$windowExe" } if (`$afkEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-afk' -SessionId `$sessionId)) { @@ -661,6 +670,7 @@ catch { } Start-CollectorScriptIfNeeded -ScriptPath `$collectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId Start-CollectorScriptIfNeeded -ScriptPath `$endpointCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId +Start-CollectorScriptIfNeeded -ScriptPath `$sessionCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId "@ Set-Content -LiteralPath $Path -Value $content -Encoding UTF8 @@ -850,7 +860,7 @@ function Set-ActivityWatchScheduledTaskAction { $taskCommand = ('"{0}" {1}' -f $Execute, $Arguments) & schtasks.exe /Change /TN $TaskName /TR $taskCommand | Out-Null if ($LASTEXITCODE -ne 0) { - throw "schtasks.exe /Change failed for $TaskName" + throw "schtasks.exe /Change завершился с ошибкой для $TaskName" } } @@ -951,17 +961,17 @@ function Set-ActivityWatchAcl { & icacls $InstallRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null if ($LASTEXITCODE -ne 0) { - throw "icacls failed for $InstallRoot" + throw "icacls завершился с ошибкой для $InstallRoot" } & icacls $StateRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null if ($LASTEXITCODE -ne 0) { - throw "icacls failed for $StateRoot" + throw "icacls завершился с ошибкой для $StateRoot" } & icacls $LogsRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(M)' | Out-Null if ($LASTEXITCODE -ne 0) { - throw "icacls failed for $LogsRoot" + throw "icacls завершился с ошибкой для $LogsRoot" } } diff --git a/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1 b/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1 index 220961e..df24a7f 100755 --- a/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1 +++ b/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1 @@ -49,7 +49,7 @@ function Get-DeploymentConfig { } $deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } +$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'Укажите ServerHost или подготовьте deployment-config.json.' } $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' } @@ -263,11 +263,11 @@ function Load-CustomCategoryRules { if ($rules.Count -gt 0) { $script:CategoryRules = @($rules) + @($script:CategoryRules) - Write-CollectorLog ("custom rules loaded: {0}" -f $rules.Count) + Write-CollectorLog ("пользовательские правила загружены: {0}" -f $rules.Count) } } catch { - Write-CollectorLog ("custom rules load failed: {0}" -f $_.Exception.Message) + Write-CollectorLog ("не удалось загрузить пользовательские правила: {0}" -f $_.Exception.Message) } } @@ -338,7 +338,7 @@ 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) + Write-CollectorLog ("DLP-политика не найдена, DLP отключен: {0}" -f $Path) return } @@ -372,7 +372,7 @@ function Load-DlpPolicy { 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)" } + message = if ($rule.message) { [string]$rule.message } else { "Сработало DLP-правило: $($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 { @() } @@ -388,10 +388,10 @@ function Load-DlpPolicy { } $script:DlpRules = @($loaded) - Write-CollectorLog ("dlp policy loaded: enabled={0}, rules={1}" -f $script:DlpDefaults.enabled, $script:DlpRules.Count) + Write-CollectorLog ("DLP-политика загружена: включена={0}, правил={1}" -f $script:DlpDefaults.enabled, $script:DlpRules.Count) } catch { - Write-CollectorLog ("dlp policy parse failed: {0}" -f $_.Exception.Message) + Write-CollectorLog ("не удалось разобрать DLP-политику: {0}" -f $_.Exception.Message) } } @@ -625,7 +625,7 @@ function Capture-IncidentScreenshot { } } catch { - Write-CollectorLog ("screenshot capture failed: {0}" -f $_.Exception.Message) + Write-CollectorLog ("не удалось сделать снимок инцидента: {0}" -f $_.Exception.Message) return @{} } } @@ -775,7 +775,7 @@ function Send-CategoryHeartbeat { Load-CustomCategoryRules -Path $resolvedRulesPath Load-DlpPolicy -Path $resolvedPolicyPath -Write-CollectorLog ("collector started against {0}" -f $script:ApiBase) +Write-CollectorLog ("коллектор запущен для {0}" -f $script:ApiBase) while ($true) { try { @@ -815,7 +815,7 @@ while ($true) { } } catch { - Write-CollectorLog ("collector error: {0}" -f $_.Exception.Message) + Write-CollectorLog ("ошибка коллектора: {0}" -f $_.Exception.Message) } Start-Sleep -Seconds $resolvedPollSeconds diff --git a/install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1 b/install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1 index 936ce3c..676fdb9 100755 --- a/install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1 +++ b/install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1 @@ -44,6 +44,7 @@ $launchScriptPath = Join-Path $StateRoot 'launch-watchers.ps1' $recoveryScriptPath = Join-Path $StateRoot 'recovery-loop.ps1' $collectorSource = Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1' $endpointCollectorSource = Join-Path $PSScriptRoot 'dlp-endpoint-signals-collector.ps1' +$sessionCollectorSource = Join-Path $PSScriptRoot 'worktime-session-collector.ps1' $exampleRulesSource = Join-Path $PSScriptRoot 'web-category-rules.example.json' $examplePolicySource = Join-Path $PSScriptRoot 'dlp-policy.example.json' @@ -57,6 +58,7 @@ Get-ActivityWatchExecutableMap -InstallRoot $InstallRoot | Out-Null $assetResult = Copy-ActivityWatchCollectorAssets ` -CollectorScriptSource $collectorSource ` -EndpointCollectorScriptSource $endpointCollectorSource ` + -SessionCollectorScriptSource $sessionCollectorSource ` -ExampleRulesSource $exampleRulesSource ` -ExamplePolicySource $examplePolicySource ` -StateRoot $StateRoot ` @@ -76,6 +78,7 @@ $config = New-ActivityWatchDeploymentConfig ` -LogsRoot $logsRoot ` -CollectorScript $assetResult.CollectorScript ` -EndpointCollectorScript $assetResult.EndpointCollectorScript ` + -SessionCollectorScript $assetResult.SessionCollectorScript ` -RulesPath $assetResult.ActiveRules ` -PolicyPath $assetResult.ActivePolicy ` -PollSeconds $PollSeconds ` @@ -100,8 +103,8 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName -Write-Host 'ActivityWatch deployed for users:' +Write-Host 'ActivityWatch развёрнут для пользователей:' $targetUsers | ForEach-Object { Write-Host " - $_" } -Write-Host "Server: ${ServerScheme}://$ServerHost`:$ServerPort" -Write-Host "State root: $StateRoot" -Write-Host "Policy file: $($assetResult.ActivePolicy)" +Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort" +Write-Host "Каталог данных: $StateRoot" +Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)" diff --git a/install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1 b/install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1 index 129d226..58d514b 100644 --- a/install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1 +++ b/install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1 @@ -46,7 +46,7 @@ $hardeningScript = Join-Path $PSScriptRoot 'hardening-recovery.ps1' $validationScript = Join-Path $PSScriptRoot 'validate-deployment.ps1' if (-not (Test-Path -LiteralPath $deployScript)) { - throw "Missing script: $deployScript" + throw "Не найден скрипт: $deployScript" } & $deployScript ` @@ -118,7 +118,7 @@ $report = [ordered]@{ if ($ValidateAfterDeploy) { if (-not (Test-Path -LiteralPath $validationScript)) { - throw "Missing script: $validationScript" + throw "Не найден скрипт: $validationScript" } $validation = & $validationScript -ConfigPath (Join-Path $StateRoot 'deployment-config.json') @@ -132,6 +132,6 @@ if ($reportDirectory) { $report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $effectiveReportPath -Encoding UTF8 -Write-Host 'ActivityWatch ensemble deploy completed.' -Write-Host "Users: $($resolvedUsers -join ', ')" -Write-Host "Report: $effectiveReportPath" +Write-Host 'Комплексное развёртывание ActivityWatch завершено.' +Write-Host "Пользователи: $($resolvedUsers -join ', ')" +Write-Host "Отчёт: $effectiveReportPath" diff --git a/install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1 b/install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1 index d3eafef..0efd871 100755 --- a/install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1 +++ b/install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1 @@ -42,6 +42,7 @@ $launchScriptPath = Join-Path $StateRoot 'launch-watchers.ps1' $recoveryScriptPath = Join-Path $StateRoot 'recovery-loop.ps1' $collectorSource = Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1' $endpointCollectorSource = Join-Path $PSScriptRoot 'dlp-endpoint-signals-collector.ps1' +$sessionCollectorSource = Join-Path $PSScriptRoot 'worktime-session-collector.ps1' $exampleRulesSource = Join-Path $PSScriptRoot 'web-category-rules.example.json' $examplePolicySource = Join-Path $PSScriptRoot 'dlp-policy.example.json' @@ -55,6 +56,7 @@ Get-ActivityWatchExecutableMap -InstallRoot $InstallRoot | Out-Null $assetResult = Copy-ActivityWatchCollectorAssets ` -CollectorScriptSource $collectorSource ` -EndpointCollectorScriptSource $endpointCollectorSource ` + -SessionCollectorScriptSource $sessionCollectorSource ` -ExampleRulesSource $exampleRulesSource ` -ExamplePolicySource $examplePolicySource ` -StateRoot $StateRoot ` @@ -74,6 +76,7 @@ $config = New-ActivityWatchDeploymentConfig ` -LogsRoot $logsRoot ` -CollectorScript $assetResult.CollectorScript ` -EndpointCollectorScript $assetResult.EndpointCollectorScript ` + -SessionCollectorScript $assetResult.SessionCollectorScript ` -RulesPath $assetResult.ActiveRules ` -PolicyPath $assetResult.ActivePolicy ` -PollSeconds $PollSeconds ` @@ -98,9 +101,9 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName -Write-Host "ActivityWatch deployed for $TargetUser" -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)" +Write-Host "ActivityWatch развёрнут для пользователя: $TargetUser" +Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort" +Write-Host "Каталог установки: $InstallRoot" +Write-Host "Каталог данных: $StateRoot" +Write-Host "Файл правил: $($assetResult.ActiveRules)" +Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)" diff --git a/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1 b/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1 index 8ee44f5..06d7f21 100644 --- a/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1 +++ b/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1 @@ -210,7 +210,7 @@ function Capture-IncidentScreenshot { } } catch { - Write-EndpointLog ("screenshot capture failed: {0}" -f $_.Exception.Message) + Write-EndpointLog ("не удалось сделать снимок инцидента: {0}" -f $_.Exception.Message) return @{} } } @@ -246,7 +246,7 @@ function Load-DlpPolicy { } if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-EndpointLog ("policy not found, using defaults: {0}" -f $Path) + Write-EndpointLog ("DLP-политика не найдена, используются значения по умолчанию: {0}" -f $Path) return } @@ -266,7 +266,7 @@ function Load-DlpPolicy { } } catch { - Write-EndpointLog ("policy parse failed: {0}" -f $_.Exception.Message) + Write-EndpointLog ("не удалось разобрать DLP-политику: {0}" -f $_.Exception.Message) } } @@ -319,13 +319,13 @@ function Evaluate-ClipboardRules { $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Clipboard rule matched: $ruleId" } + $message = if ($rule.message) { [string]$rule.message } else { "Сработало правило буфера обмена: $ruleId" } Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{ clipboardHash = $ClipboardHash clipboardLength = $ClipboardText.Length } - Write-EndpointLog ("incident clipboard rule={0} action={1} severity={2}" -f $ruleId, $action, $severity) + Write-EndpointLog ("инцидент буфера обмена правило={0} действие={1} важность={2}" -f $ruleId, $action, $severity) } } @@ -347,13 +347,13 @@ function Evaluate-UsbRules { $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "USB rule matched: $ruleId" } + $message = if ($rule.message) { [string]$rule.message } else { "Сработало правило USB-носителя: $ruleId" } Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{ driveLetter = $DriveLetter volumeName = $VolumeName } - Write-EndpointLog ("incident usb rule={0} action={1} severity={2} drive={3}" -f $ruleId, $action, $severity, $DriveLetter) + Write-EndpointLog ("инцидент USB правило={0} действие={1} важность={2} диск={3}" -f $ruleId, $action, $severity, $DriveLetter) } } @@ -385,14 +385,14 @@ function Evaluate-PrintRules { $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Print rule matched: $ruleId" } + $message = if ($rule.message) { [string]$rule.message } else { "Сработало правило печати: $ruleId" } Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'print_job' -Data @{ printerName = $PrinterName documentName = $DocumentName owner = $Owner } - Write-EndpointLog ("incident print rule={0} action={1} severity={2} printer={3}" -f $ruleId, $action, $severity, $PrinterName) + Write-EndpointLog ("инцидент печати правило={0} действие={1} важность={2} принтер={3}" -f $ruleId, $action, $severity, $PrinterName) } } @@ -402,6 +402,52 @@ function Test-LooksLikeMojibakeQuestionMarks { return $Value -match '\?{2,}' } +function Test-DocumentNameNeedsFallback { + param([AllowNull()][string]$Value) + + if ([string]::IsNullOrWhiteSpace($Value)) { return $true } + $trimmed = $Value.Trim() + if (Test-LooksLikeMojibakeQuestionMarks -Value $trimmed) { return $true } + if ($trimmed -match '^[0-9]+$') { return $true } + if ($trimmed -match '^(?i)(print document|document|local downlevel document)$') { return $true } + return $false +} + +function Get-EventXmlValue { + param( + [Parameter(Mandatory = $true)][xml]$EventXml, + [Parameter(Mandatory = $true)][string]$Name + ) + + $node = $EventXml.Event.UserData.DocumentPrinted.$Name + if ($null -ne $node) { + return [string]$node + } + + return '' +} + +function Get-PrintJobPrinterName { + param( + [AllowNull()][string]$JobName, + [AllowNull()][string]$FallbackPrinterName + ) + + if ([string]::IsNullOrWhiteSpace($JobName)) { + if (-not [string]::IsNullOrWhiteSpace($FallbackPrinterName)) { + return $FallbackPrinterName.Trim() + } + return '' + } + + $parts = $JobName -split ',', 2 + if ($parts.Count -gt 0 -and -not [string]::IsNullOrWhiteSpace($parts[0])) { + return $parts[0].Trim() + } + + return $JobName.Trim() +} + function Normalize-OwnerForMatch { param([AllowNull()][string]$Value) if ([string]::IsNullOrWhiteSpace($Value)) { return '' } @@ -469,13 +515,50 @@ function Get-PrintServiceEventSummary { $propertyValues += [string]$prop.Value } + $xml = $null + try { + $xml = [xml]$Event.ToXml() + } + catch { + } + + $jobId = '' + $documentName = '' + $owner = '' + $portName = '' + $printerName = '' + $sizeBytes = '' + $pageCount = '' + + if ($xml) { + $jobId = Get-EventXmlValue -EventXml $xml -Name 'Param1' + $documentName = Get-EventXmlValue -EventXml $xml -Name 'Param2' + $owner = Get-EventXmlValue -EventXml $xml -Name 'Param3' + $portName = Get-EventXmlValue -EventXml $xml -Name 'Param4' + $printerName = Get-EventXmlValue -EventXml $xml -Name 'Param5' + $sizeBytes = Get-EventXmlValue -EventXml $xml -Name 'Param7' + $pageCount = Get-EventXmlValue -EventXml $xml -Name 'Param8' + } + + if ([string]::IsNullOrWhiteSpace($jobId) -and $props.Count -ge 1) { $jobId = [string]$props[0].Value } + if ([string]::IsNullOrWhiteSpace($documentName) -and $props.Count -ge 2) { $documentName = [string]$props[1].Value } + if ([string]::IsNullOrWhiteSpace($owner) -and $props.Count -ge 3) { $owner = [string]$props[2].Value } + if ([string]::IsNullOrWhiteSpace($portName) -and $props.Count -ge 4) { $portName = [string]$props[3].Value } + if ([string]::IsNullOrWhiteSpace($printerName) -and $props.Count -ge 5) { $printerName = [string]$props[4].Value } + if ([string]::IsNullOrWhiteSpace($sizeBytes) -and $props.Count -ge 7) { $sizeBytes = [string]$props[6].Value } + if ([string]::IsNullOrWhiteSpace($pageCount) -and $props.Count -ge 8) { $pageCount = [string]$props[7].Value } + [pscustomobject]@{ RecordId = [string]$Event.RecordId TimeCreated = if ($Event.TimeCreated) { $Event.TimeCreated.ToString('o') } else { '' } PropertyCount = $props.Count - DocumentName = if ($props.Count -ge 1) { [string]$props[0].Value } else { '' } - Owner = if ($props.Count -ge 2) { [string]$props[1].Value } else { '' } - PrinterName = if ($props.Count -ge 4) { [string]$props[3].Value } else { '' } + JobId = $jobId + DocumentName = $documentName + Owner = $owner + PortName = $portName + PrinterName = $printerName + SizeBytes = $sizeBytes + PageCount = $pageCount PropertyValues = $propertyValues } } @@ -488,7 +571,7 @@ function Get-PrintServiceDocumentFallback { ) $preferred = [string]$EventSummary.DocumentName - if (-not (Test-LooksLikeMojibakeQuestionMarks -Value $preferred) -and $preferred -notmatch '^[0-9]+$') { + if (-not (Test-DocumentNameNeedsFallback -Value $preferred)) { return $preferred } @@ -499,9 +582,10 @@ function Get-PrintServiceDocumentFallback { $candidate = [string]$value if ([string]::IsNullOrWhiteSpace($candidate)) { continue } if ($candidate -eq $preferred) { continue } + if ($EventSummary.JobId -and $candidate -eq [string]$EventSummary.JobId) { continue } if ($Owner -and $candidate -like "*$Owner*") { continue } if ($PrinterName -and $candidate -like "*$PrinterName*") { continue } - if (Test-LooksLikeMojibakeQuestionMarks -Value $candidate) { continue } + if (Test-DocumentNameNeedsFallback -Value $candidate) { continue } if ($candidate -match '[\\/:]' -and $candidate -match '\.[A-Za-z0-9]{1,8}$') { $pathCandidates.Add($candidate) @@ -546,7 +630,7 @@ function Write-PrintServiceEventTrace { } Write-EndpointLog ( - 'printservice-307 phase={0} recordId={1} time={2} owner={3} printer={4} document={5} resolved={6} properties=[{7}] reason={8}' -f + 'printservice-307 этап={0} recordId={1} время={2} владелец={3} принтер={4} документ={5} итоговыйДокумент={6} свойства=[{7}] причина={8}' -f $Phase, $EventSummary.RecordId, $EventSummary.TimeCreated, @@ -561,6 +645,7 @@ function Write-PrintServiceEventTrace { function Get-BetterDocumentNameFromPrintServiceEvents { param( + [string]$JobId, [string]$Owner, [string]$PrinterName ) @@ -578,32 +663,41 @@ function Get-BetterDocumentNameFromPrintServiceEvents { $summary = Get-PrintServiceEventSummary -Event $event $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $Owner -PrinterName $PrinterName + $jobMatches = if ($JobId) { [string]$summary.JobId -eq [string]$JobId } else { $true } $ownerMatches = if ($Owner) { Test-OwnerLooseMatch -Expected $Owner -Actual $summary.Owner } else { $true } $printerMatches = if ($PrinterName) { Test-PrinterLooseMatch -Expected $PrinterName -Actual $summary.PrinterName } else { $true } if ($pass -eq 'strict') { + if ($JobId -and -not $jobMatches) { + Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'несовпадение-jobid-strict' -ResolvedDocument $resolvedDocument + continue + } if ($Owner -and -not $ownerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-mismatch-strict' -ResolvedDocument $resolvedDocument + Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'несовпадение-владельца-strict' -ResolvedDocument $resolvedDocument continue } if ($PrinterName -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'printer-mismatch-strict' -ResolvedDocument $resolvedDocument + Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'несовпадение-принтера-strict' -ResolvedDocument $resolvedDocument continue } } else { - if ($Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-and-printer-mismatch-relaxed' -ResolvedDocument $resolvedDocument + if ($JobId -and (-not $jobMatches) -and $Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) { + Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'несовпадение-владельца-и-принтера-relaxed' -ResolvedDocument $resolvedDocument + continue + } + if ((-not $JobId) -and $Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) { + Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'несовпадение-владельца-и-принтера-relaxed' -ResolvedDocument $resolvedDocument continue } } if ([string]::IsNullOrWhiteSpace($resolvedDocument)) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('no-document-candidate-' + $pass) -ResolvedDocument '' + Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('нет-кандидата-документа-' + $pass) -ResolvedDocument '' continue } - $matchReasonBase = if (Test-LooksLikeMojibakeQuestionMarks -Value $summary.DocumentName) { 'fallback-used' } else { 'direct' } + $matchReasonBase = if (Test-DocumentNameNeedsFallback -Value $summary.DocumentName) { 'использован-резервный-вариант' } else { 'напрямую' } Write-PrintServiceEventTrace -EventSummary $summary -Phase 'selected' -MatchReason ($matchReasonBase + '-' + $pass) -ResolvedDocument $resolvedDocument return $resolvedDocument } @@ -616,7 +710,7 @@ function Get-BetterDocumentNameFromPrintServiceEvents { } $deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } +$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'Укажите ServerHost или подготовьте deployment-config.json.' } $resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } $resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } $resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\ActivityWatch\dlp-policy.json' } @@ -648,7 +742,7 @@ $script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled $script:ScreenshotTypesLoaded = $false Load-DlpPolicy -Path $resolvedPolicyPath -Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase) +Write-EndpointLog ("endpoint-коллектор запущен для {0}" -f $script:ApiBase) while ($true) { try { @@ -709,13 +803,13 @@ while ($true) { if ($script:SeenPrintJob.ContainsKey($jobId)) { continue } $script:SeenPrintJob[$jobId] = (Get-Date).ToUniversalTime() - $printerName = [string]$job.Name + $printerName = Get-PrintJobPrinterName -JobName ([string]$job.Name) -FallbackPrinterName ([string]$job.DriverName) $documentName = [string]$job.Document $owner = [string]$job.Owner $documentNameOriginal = $documentName - if (Test-LooksLikeMojibakeQuestionMarks -Value $documentName) { - $eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -Owner $owner -PrinterName $printerName + if (Test-DocumentNameNeedsFallback -Value $documentName) { + $eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -JobId $jobId -Owner $owner -PrinterName $printerName if ($eventDocumentName) { $documentName = $eventDocumentName } @@ -726,6 +820,7 @@ while ($true) { documentName = $documentName documentNameOriginal = $documentNameOriginal owner = $owner + printJobId = $jobId } Evaluate-PrintRules -PrinterName $printerName -DocumentName $documentName -Owner $owner } @@ -789,7 +884,7 @@ while ($true) { } } catch { - Write-EndpointLog ("collector error: {0}" -f $_.Exception.Message) + Write-EndpointLog ("ошибка коллектора: {0}" -f $_.Exception.Message) } Start-Sleep -Seconds $resolvedPollSeconds diff --git a/install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1 b/install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1 index c908b2f..05632e6 100755 --- a/install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1 +++ b/install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1 @@ -42,7 +42,7 @@ if (Test-Path -LiteralPath $ConfigPath) { } if (-not $existingConfig -and (-not $ServerHost)) { - throw 'deployment-config.json is missing. Provide -ServerHost and user parameters, or run a deploy script first.' + throw 'deployment-config.json отсутствует. Укажите -ServerHost и параметры пользователей либо сначала выполните скрипт развёртывания.' } $effectiveStateRoot = if ($StateRoot) { $StateRoot } elseif ($existingConfig) { [string]$existingConfig.paths.stateRoot } else { 'C:\ProgramData\ActivityWatch' } @@ -78,7 +78,7 @@ elseif ($existingConfig) { @($existingConfig.userTasks | ForEach-Object { [string]$_.userId }) } else { - throw 'Target users are missing.' + throw 'Не указаны целевые пользователи.' } New-ActivityWatchDirectory -Path $effectiveStateRoot @@ -96,6 +96,7 @@ Get-ActivityWatchExecutableMap -InstallRoot $effectiveInstallRoot | Out-Null $assetResult = Copy-ActivityWatchCollectorAssets ` -CollectorScriptSource (Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1') ` -EndpointCollectorScriptSource (Join-Path $PSScriptRoot 'dlp-endpoint-signals-collector.ps1') ` + -SessionCollectorScriptSource (Join-Path $PSScriptRoot 'worktime-session-collector.ps1') ` -ExampleRulesSource (Join-Path $PSScriptRoot 'web-category-rules.example.json') ` -ExamplePolicySource (Join-Path $PSScriptRoot 'dlp-policy.example.json') ` -StateRoot $effectiveStateRoot ` @@ -139,6 +140,6 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $effectiveRecoveryScript -ConfigPath $effectiveConfigPath Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName -Write-Host 'ActivityWatch hardening/recovery completed.' -Write-Host "Config: $effectiveConfigPath" -Write-Host "Users repaired: $($effectiveUsers -join ', ')" +Write-Host 'Укрепление и восстановление ActivityWatch завершены.' +Write-Host "Конфигурация: $effectiveConfigPath" +Write-Host "Пользователи восстановлены: $($effectiveUsers -join ', ')" diff --git a/install-kit-awindows-20260427-211240/windows/validate-deployment.ps1 b/install-kit-awindows-20260427-211240/windows/validate-deployment.ps1 index 71e93a0..85a0344 100644 --- a/install-kit-awindows-20260427-211240/windows/validate-deployment.ps1 +++ b/install-kit-awindows-20260427-211240/windows/validate-deployment.ps1 @@ -14,6 +14,7 @@ $installRoot = [string]$config.paths.installRoot $stateRoot = [string]$config.paths.stateRoot $collectorScript = [string]$config.paths.collectorScript $endpointCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]$config.paths.endpointCollectorScript } else { Join-Path $stateRoot 'dlp-endpoint-signals-collector.ps1' } +$sessionCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]$config.paths.sessionCollectorScript } else { Join-Path $stateRoot 'worktime-session-collector.ps1' } $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 @@ -24,6 +25,7 @@ $requiredFiles = @( (Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe'), $collectorScript, $endpointCollectorScript, + $sessionCollectorScript, $rulesPath, $policyPath, $launchScript, @@ -37,6 +39,12 @@ $missingFiles = @( $processNames = @('aw-watcher-afk', 'aw-watcher-window') $runningProcesses = Get-Process -Name $processNames -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId +$sessionCollectorProcesses = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | + Where-Object { + ($_.Name -ieq 'powershell.exe' -or $_.Name -ieq 'pwsh.exe') -and + $_.CommandLine -match [Regex]::Escape($sessionCollectorScript) + } | + Select-Object Name, ProcessId, SessionId, CommandLine $taskNames = @() if ($config.userTasks) { @@ -57,7 +65,7 @@ $tasks = foreach ($taskName in $taskNames) { else { [pscustomobject]@{ taskName = $taskName - state = 'Missing' + state = 'Отсутствует' present = $false } } @@ -81,7 +89,11 @@ $result = [ordered]@{ } processes = [ordered]@{ list = @($runningProcesses) - ok = [bool](($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge 2) + sessionCollectors = @($sessionCollectorProcesses) + ok = [bool]( + (($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge 2) -and + ($sessionCollectorProcesses.Count -ge 1) + ) } } diff --git a/install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1 b/install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1 new file mode 100644 index 0000000..214be90 --- /dev/null +++ b/install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1 @@ -0,0 +1,145 @@ +param( + [string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json', + [string]$Hostname, + [int]$PollSeconds = 30 +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Get-Config { + param([string]$Path) + + if (-not (Test-Path -LiteralPath $Path)) { + throw "Конфигурация не найдена: $Path" + } + + Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json +} + +function Invoke-AwJsonPost { + param( + [Parameter(Mandatory = $true)][string]$Uri, + [Parameter(Mandatory = $true)][string]$Json + ) + + $bytes = [Text.Encoding]::UTF8.GetBytes($Json) + Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null +} + +function Ensure-Bucket { + param( + [Parameter(Mandatory = $true)][string]$ApiBase, + [Parameter(Mandatory = $true)][string]$BucketId, + [Parameter(Mandatory = $true)][string]$HostnameValue + ) + + try { + Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null + return + } + catch { + } + + $body = @{ + client = 'aw-worktime-session-collector' + type = 'aw.worktime.session' + hostname = $HostnameValue + } | ConvertTo-Json -Compress + + Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body +} + +function Get-SessionRecords { + $records = @() + + try { + $lines = quser 2>$null + if (-not $lines) { + return @() + } + + foreach ($line in ($lines | Select-Object -Skip 1)) { + $clean = ($line -replace '^\s*>?', '').Trim() + if (-not $clean) { + continue + } + + $parts = $clean -split '\s+' + if ($parts.Count -lt 4) { + continue + } + + $sessionId = 0 + if ($parts[2] -match '^\d+$') { + $sessionId = [int]$parts[2] + } + + $records += [pscustomobject]@{ + username = $parts[0] + sessionName = $parts[1] + sessionId = $sessionId + state = $parts[3] + } + } + } + catch { + } + + return $records +} + +$cfg = Get-Config -Path $ConfigPath +$hostValue = if ($Hostname) { $Hostname } else { [string]$env:COMPUTERNAME } +$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port +$bucketId = 'aw-worktime-sessions_' + $hostValue +$pulse = 120 +$sleepSec = if ($PollSeconds -gt 0) { + $PollSeconds +} +elseif ($cfg.collector -and $cfg.collector.pollSeconds) { + [int]$cfg.collector.pollSeconds +} +else { + 30 +} + +Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue + +while ($true) { + $now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') + $records = Get-SessionRecords + if (-not $records -or $records.Count -eq 0) { + $records = @([pscustomobject]@{ + username = $env:USERNAME + sessionName = '' + sessionId = (Get-Process -Id $PID).SessionId + state = 'Unknown' + }) + } + + foreach ($rec in $records) { + $payload = @{ + timestamp = $now + duration = 0 + data = @{ + username = [string]$rec.username + userId = "$($env:USERDOMAIN)\$($rec.username)" + sessionId = [int]$rec.sessionId + sessionName = [string]$rec.sessionName + state = [string]$rec.state + active = ($rec.state -match 'Active') + hostname = $hostValue + source = 'worktime-session-collector' + } + } | ConvertTo-Json -Depth 6 -Compress + + try { + Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload + } + catch { + } + } + + Start-Sleep -Seconds $sleepSec +} diff --git a/windows/ActivityWatch.Windows.Common.psm1 b/windows/ActivityWatch.Windows.Common.psm1 index fb36dca..89affa3 100755 --- a/windows/ActivityWatch.Windows.Common.psm1 +++ b/windows/ActivityWatch.Windows.Common.psm1 @@ -5,7 +5,7 @@ function Assert-Administrator { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]::new($identity) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - throw 'Run this script from an elevated PowerShell session.' + throw 'Запустите этот скрипт из PowerShell с правами администратора.' } } @@ -64,7 +64,7 @@ function Get-ActivityWatchPackageRoot { Select-Object -First 1 if (-not $afkBinary) { - throw "Cannot find aw-watcher-afk.exe under $ExpandedRoot." + throw "Не удалось найти aw-watcher-afk.exe в $ExpandedRoot." } return (Split-Path -Path (Split-Path -Path $afkBinary.FullName -Parent) -Parent) @@ -130,7 +130,7 @@ function Get-ActivityWatchExecutableMap { foreach ($entry in $map.GetEnumerator()) { if (-not (Test-Path -LiteralPath $entry.Value)) { - throw "Missing required ActivityWatch binary: $($entry.Value)" + throw "Не найден обязательный исполняемый файл ActivityWatch: $($entry.Value)" } } @@ -194,7 +194,7 @@ function Normalize-ActivityWatchUsers { Sort-Object -Unique if (-not $normalized -or $normalized.Count -eq 0) { - throw 'No target users resolved. Provide -Users or -UserListPath.' + throw 'Не удалось определить целевых пользователей. Укажите -Users или -UserListPath.' } return @($normalized) @@ -421,7 +421,7 @@ function Read-ActivityWatchDeploymentConfig { ) if (-not (Test-Path -LiteralPath $Path)) { - throw "Deployment config not found: $Path" + throw "Конфигурация развёртывания не найдена: $Path" } return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json @@ -648,11 +648,11 @@ function Start-CollectorScriptIfNeeded { `$windowEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]`$config.collectors.windowEnabled } else { `$true } if (`$afkEnabled -and -not (Test-Path -LiteralPath `$afkExe)) { - throw "Missing aw-watcher-afk.exe: `$afkExe" + throw "Не найден aw-watcher-afk.exe: `$afkExe" } if (`$windowEnabled -and -not (Test-Path -LiteralPath `$windowExe)) { - throw "Missing aw-watcher-window.exe: `$windowExe" + throw "Не найден aw-watcher-window.exe: `$windowExe" } if (`$afkEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-afk' -SessionId `$sessionId)) { @@ -860,7 +860,7 @@ function Set-ActivityWatchScheduledTaskAction { $taskCommand = ('"{0}" {1}' -f $Execute, $Arguments) & schtasks.exe /Change /TN $TaskName /TR $taskCommand | Out-Null if ($LASTEXITCODE -ne 0) { - throw "schtasks.exe /Change failed for $TaskName" + throw "schtasks.exe /Change завершился с ошибкой для $TaskName" } } @@ -961,17 +961,17 @@ function Set-ActivityWatchAcl { & icacls $InstallRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null if ($LASTEXITCODE -ne 0) { - throw "icacls failed for $InstallRoot" + throw "icacls завершился с ошибкой для $InstallRoot" } & icacls $StateRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null if ($LASTEXITCODE -ne 0) { - throw "icacls failed for $StateRoot" + throw "icacls завершился с ошибкой для $StateRoot" } & icacls $LogsRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(M)' | Out-Null if ($LASTEXITCODE -ne 0) { - throw "icacls failed for $LogsRoot" + throw "icacls завершился с ошибкой для $LogsRoot" } } diff --git a/windows/browser-domains-native-collector.ps1 b/windows/browser-domains-native-collector.ps1 index 220961e..df24a7f 100755 --- a/windows/browser-domains-native-collector.ps1 +++ b/windows/browser-domains-native-collector.ps1 @@ -49,7 +49,7 @@ function Get-DeploymentConfig { } $deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } +$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'Укажите ServerHost или подготовьте deployment-config.json.' } $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' } @@ -263,11 +263,11 @@ function Load-CustomCategoryRules { if ($rules.Count -gt 0) { $script:CategoryRules = @($rules) + @($script:CategoryRules) - Write-CollectorLog ("custom rules loaded: {0}" -f $rules.Count) + Write-CollectorLog ("пользовательские правила загружены: {0}" -f $rules.Count) } } catch { - Write-CollectorLog ("custom rules load failed: {0}" -f $_.Exception.Message) + Write-CollectorLog ("не удалось загрузить пользовательские правила: {0}" -f $_.Exception.Message) } } @@ -338,7 +338,7 @@ 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) + Write-CollectorLog ("DLP-политика не найдена, DLP отключен: {0}" -f $Path) return } @@ -372,7 +372,7 @@ function Load-DlpPolicy { 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)" } + message = if ($rule.message) { [string]$rule.message } else { "Сработало DLP-правило: $($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 { @() } @@ -388,10 +388,10 @@ function Load-DlpPolicy { } $script:DlpRules = @($loaded) - Write-CollectorLog ("dlp policy loaded: enabled={0}, rules={1}" -f $script:DlpDefaults.enabled, $script:DlpRules.Count) + Write-CollectorLog ("DLP-политика загружена: включена={0}, правил={1}" -f $script:DlpDefaults.enabled, $script:DlpRules.Count) } catch { - Write-CollectorLog ("dlp policy parse failed: {0}" -f $_.Exception.Message) + Write-CollectorLog ("не удалось разобрать DLP-политику: {0}" -f $_.Exception.Message) } } @@ -625,7 +625,7 @@ function Capture-IncidentScreenshot { } } catch { - Write-CollectorLog ("screenshot capture failed: {0}" -f $_.Exception.Message) + Write-CollectorLog ("не удалось сделать снимок инцидента: {0}" -f $_.Exception.Message) return @{} } } @@ -775,7 +775,7 @@ function Send-CategoryHeartbeat { Load-CustomCategoryRules -Path $resolvedRulesPath Load-DlpPolicy -Path $resolvedPolicyPath -Write-CollectorLog ("collector started against {0}" -f $script:ApiBase) +Write-CollectorLog ("коллектор запущен для {0}" -f $script:ApiBase) while ($true) { try { @@ -815,7 +815,7 @@ while ($true) { } } catch { - Write-CollectorLog ("collector error: {0}" -f $_.Exception.Message) + Write-CollectorLog ("ошибка коллектора: {0}" -f $_.Exception.Message) } Start-Sleep -Seconds $resolvedPollSeconds diff --git a/windows/deploy-domain-users.ps1 b/windows/deploy-domain-users.ps1 index c6ca412..676fdb9 100755 --- a/windows/deploy-domain-users.ps1 +++ b/windows/deploy-domain-users.ps1 @@ -103,8 +103,8 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName -Write-Host 'ActivityWatch deployed for users:' +Write-Host 'ActivityWatch развёрнут для пользователей:' $targetUsers | ForEach-Object { Write-Host " - $_" } -Write-Host "Server: ${ServerScheme}://$ServerHost`:$ServerPort" -Write-Host "State root: $StateRoot" -Write-Host "Policy file: $($assetResult.ActivePolicy)" +Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort" +Write-Host "Каталог данных: $StateRoot" +Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)" diff --git a/windows/deploy-ensemble.ps1 b/windows/deploy-ensemble.ps1 index 129d226..58d514b 100644 --- a/windows/deploy-ensemble.ps1 +++ b/windows/deploy-ensemble.ps1 @@ -46,7 +46,7 @@ $hardeningScript = Join-Path $PSScriptRoot 'hardening-recovery.ps1' $validationScript = Join-Path $PSScriptRoot 'validate-deployment.ps1' if (-not (Test-Path -LiteralPath $deployScript)) { - throw "Missing script: $deployScript" + throw "Не найден скрипт: $deployScript" } & $deployScript ` @@ -118,7 +118,7 @@ $report = [ordered]@{ if ($ValidateAfterDeploy) { if (-not (Test-Path -LiteralPath $validationScript)) { - throw "Missing script: $validationScript" + throw "Не найден скрипт: $validationScript" } $validation = & $validationScript -ConfigPath (Join-Path $StateRoot 'deployment-config.json') @@ -132,6 +132,6 @@ if ($reportDirectory) { $report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $effectiveReportPath -Encoding UTF8 -Write-Host 'ActivityWatch ensemble deploy completed.' -Write-Host "Users: $($resolvedUsers -join ', ')" -Write-Host "Report: $effectiveReportPath" +Write-Host 'Комплексное развёртывание ActivityWatch завершено.' +Write-Host "Пользователи: $($resolvedUsers -join ', ')" +Write-Host "Отчёт: $effectiveReportPath" diff --git a/windows/deploy-single-user.ps1 b/windows/deploy-single-user.ps1 index dfd24b4..0efd871 100755 --- a/windows/deploy-single-user.ps1 +++ b/windows/deploy-single-user.ps1 @@ -101,9 +101,9 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName -Write-Host "ActivityWatch deployed for $TargetUser" -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)" +Write-Host "ActivityWatch развёрнут для пользователя: $TargetUser" +Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort" +Write-Host "Каталог установки: $InstallRoot" +Write-Host "Каталог данных: $StateRoot" +Write-Host "Файл правил: $($assetResult.ActiveRules)" +Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)" diff --git a/windows/dlp-endpoint-signals-collector.ps1 b/windows/dlp-endpoint-signals-collector.ps1 index 8ee44f5..06d7f21 100644 --- a/windows/dlp-endpoint-signals-collector.ps1 +++ b/windows/dlp-endpoint-signals-collector.ps1 @@ -210,7 +210,7 @@ function Capture-IncidentScreenshot { } } catch { - Write-EndpointLog ("screenshot capture failed: {0}" -f $_.Exception.Message) + Write-EndpointLog ("не удалось сделать снимок инцидента: {0}" -f $_.Exception.Message) return @{} } } @@ -246,7 +246,7 @@ function Load-DlpPolicy { } if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-EndpointLog ("policy not found, using defaults: {0}" -f $Path) + Write-EndpointLog ("DLP-политика не найдена, используются значения по умолчанию: {0}" -f $Path) return } @@ -266,7 +266,7 @@ function Load-DlpPolicy { } } catch { - Write-EndpointLog ("policy parse failed: {0}" -f $_.Exception.Message) + Write-EndpointLog ("не удалось разобрать DLP-политику: {0}" -f $_.Exception.Message) } } @@ -319,13 +319,13 @@ function Evaluate-ClipboardRules { $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Clipboard rule matched: $ruleId" } + $message = if ($rule.message) { [string]$rule.message } else { "Сработало правило буфера обмена: $ruleId" } Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{ clipboardHash = $ClipboardHash clipboardLength = $ClipboardText.Length } - Write-EndpointLog ("incident clipboard rule={0} action={1} severity={2}" -f $ruleId, $action, $severity) + Write-EndpointLog ("инцидент буфера обмена правило={0} действие={1} важность={2}" -f $ruleId, $action, $severity) } } @@ -347,13 +347,13 @@ function Evaluate-UsbRules { $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "USB rule matched: $ruleId" } + $message = if ($rule.message) { [string]$rule.message } else { "Сработало правило USB-носителя: $ruleId" } Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{ driveLetter = $DriveLetter volumeName = $VolumeName } - Write-EndpointLog ("incident usb rule={0} action={1} severity={2} drive={3}" -f $ruleId, $action, $severity, $DriveLetter) + Write-EndpointLog ("инцидент USB правило={0} действие={1} важность={2} диск={3}" -f $ruleId, $action, $severity, $DriveLetter) } } @@ -385,14 +385,14 @@ function Evaluate-PrintRules { $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Print rule matched: $ruleId" } + $message = if ($rule.message) { [string]$rule.message } else { "Сработало правило печати: $ruleId" } Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'print_job' -Data @{ printerName = $PrinterName documentName = $DocumentName owner = $Owner } - Write-EndpointLog ("incident print rule={0} action={1} severity={2} printer={3}" -f $ruleId, $action, $severity, $PrinterName) + Write-EndpointLog ("инцидент печати правило={0} действие={1} важность={2} принтер={3}" -f $ruleId, $action, $severity, $PrinterName) } } @@ -402,6 +402,52 @@ function Test-LooksLikeMojibakeQuestionMarks { return $Value -match '\?{2,}' } +function Test-DocumentNameNeedsFallback { + param([AllowNull()][string]$Value) + + if ([string]::IsNullOrWhiteSpace($Value)) { return $true } + $trimmed = $Value.Trim() + if (Test-LooksLikeMojibakeQuestionMarks -Value $trimmed) { return $true } + if ($trimmed -match '^[0-9]+$') { return $true } + if ($trimmed -match '^(?i)(print document|document|local downlevel document)$') { return $true } + return $false +} + +function Get-EventXmlValue { + param( + [Parameter(Mandatory = $true)][xml]$EventXml, + [Parameter(Mandatory = $true)][string]$Name + ) + + $node = $EventXml.Event.UserData.DocumentPrinted.$Name + if ($null -ne $node) { + return [string]$node + } + + return '' +} + +function Get-PrintJobPrinterName { + param( + [AllowNull()][string]$JobName, + [AllowNull()][string]$FallbackPrinterName + ) + + if ([string]::IsNullOrWhiteSpace($JobName)) { + if (-not [string]::IsNullOrWhiteSpace($FallbackPrinterName)) { + return $FallbackPrinterName.Trim() + } + return '' + } + + $parts = $JobName -split ',', 2 + if ($parts.Count -gt 0 -and -not [string]::IsNullOrWhiteSpace($parts[0])) { + return $parts[0].Trim() + } + + return $JobName.Trim() +} + function Normalize-OwnerForMatch { param([AllowNull()][string]$Value) if ([string]::IsNullOrWhiteSpace($Value)) { return '' } @@ -469,13 +515,50 @@ function Get-PrintServiceEventSummary { $propertyValues += [string]$prop.Value } + $xml = $null + try { + $xml = [xml]$Event.ToXml() + } + catch { + } + + $jobId = '' + $documentName = '' + $owner = '' + $portName = '' + $printerName = '' + $sizeBytes = '' + $pageCount = '' + + if ($xml) { + $jobId = Get-EventXmlValue -EventXml $xml -Name 'Param1' + $documentName = Get-EventXmlValue -EventXml $xml -Name 'Param2' + $owner = Get-EventXmlValue -EventXml $xml -Name 'Param3' + $portName = Get-EventXmlValue -EventXml $xml -Name 'Param4' + $printerName = Get-EventXmlValue -EventXml $xml -Name 'Param5' + $sizeBytes = Get-EventXmlValue -EventXml $xml -Name 'Param7' + $pageCount = Get-EventXmlValue -EventXml $xml -Name 'Param8' + } + + if ([string]::IsNullOrWhiteSpace($jobId) -and $props.Count -ge 1) { $jobId = [string]$props[0].Value } + if ([string]::IsNullOrWhiteSpace($documentName) -and $props.Count -ge 2) { $documentName = [string]$props[1].Value } + if ([string]::IsNullOrWhiteSpace($owner) -and $props.Count -ge 3) { $owner = [string]$props[2].Value } + if ([string]::IsNullOrWhiteSpace($portName) -and $props.Count -ge 4) { $portName = [string]$props[3].Value } + if ([string]::IsNullOrWhiteSpace($printerName) -and $props.Count -ge 5) { $printerName = [string]$props[4].Value } + if ([string]::IsNullOrWhiteSpace($sizeBytes) -and $props.Count -ge 7) { $sizeBytes = [string]$props[6].Value } + if ([string]::IsNullOrWhiteSpace($pageCount) -and $props.Count -ge 8) { $pageCount = [string]$props[7].Value } + [pscustomobject]@{ RecordId = [string]$Event.RecordId TimeCreated = if ($Event.TimeCreated) { $Event.TimeCreated.ToString('o') } else { '' } PropertyCount = $props.Count - DocumentName = if ($props.Count -ge 1) { [string]$props[0].Value } else { '' } - Owner = if ($props.Count -ge 2) { [string]$props[1].Value } else { '' } - PrinterName = if ($props.Count -ge 4) { [string]$props[3].Value } else { '' } + JobId = $jobId + DocumentName = $documentName + Owner = $owner + PortName = $portName + PrinterName = $printerName + SizeBytes = $sizeBytes + PageCount = $pageCount PropertyValues = $propertyValues } } @@ -488,7 +571,7 @@ function Get-PrintServiceDocumentFallback { ) $preferred = [string]$EventSummary.DocumentName - if (-not (Test-LooksLikeMojibakeQuestionMarks -Value $preferred) -and $preferred -notmatch '^[0-9]+$') { + if (-not (Test-DocumentNameNeedsFallback -Value $preferred)) { return $preferred } @@ -499,9 +582,10 @@ function Get-PrintServiceDocumentFallback { $candidate = [string]$value if ([string]::IsNullOrWhiteSpace($candidate)) { continue } if ($candidate -eq $preferred) { continue } + if ($EventSummary.JobId -and $candidate -eq [string]$EventSummary.JobId) { continue } if ($Owner -and $candidate -like "*$Owner*") { continue } if ($PrinterName -and $candidate -like "*$PrinterName*") { continue } - if (Test-LooksLikeMojibakeQuestionMarks -Value $candidate) { continue } + if (Test-DocumentNameNeedsFallback -Value $candidate) { continue } if ($candidate -match '[\\/:]' -and $candidate -match '\.[A-Za-z0-9]{1,8}$') { $pathCandidates.Add($candidate) @@ -546,7 +630,7 @@ function Write-PrintServiceEventTrace { } Write-EndpointLog ( - 'printservice-307 phase={0} recordId={1} time={2} owner={3} printer={4} document={5} resolved={6} properties=[{7}] reason={8}' -f + 'printservice-307 этап={0} recordId={1} время={2} владелец={3} принтер={4} документ={5} итоговыйДокумент={6} свойства=[{7}] причина={8}' -f $Phase, $EventSummary.RecordId, $EventSummary.TimeCreated, @@ -561,6 +645,7 @@ function Write-PrintServiceEventTrace { function Get-BetterDocumentNameFromPrintServiceEvents { param( + [string]$JobId, [string]$Owner, [string]$PrinterName ) @@ -578,32 +663,41 @@ function Get-BetterDocumentNameFromPrintServiceEvents { $summary = Get-PrintServiceEventSummary -Event $event $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $Owner -PrinterName $PrinterName + $jobMatches = if ($JobId) { [string]$summary.JobId -eq [string]$JobId } else { $true } $ownerMatches = if ($Owner) { Test-OwnerLooseMatch -Expected $Owner -Actual $summary.Owner } else { $true } $printerMatches = if ($PrinterName) { Test-PrinterLooseMatch -Expected $PrinterName -Actual $summary.PrinterName } else { $true } if ($pass -eq 'strict') { + if ($JobId -and -not $jobMatches) { + Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'несовпадение-jobid-strict' -ResolvedDocument $resolvedDocument + continue + } if ($Owner -and -not $ownerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-mismatch-strict' -ResolvedDocument $resolvedDocument + Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'несовпадение-владельца-strict' -ResolvedDocument $resolvedDocument continue } if ($PrinterName -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'printer-mismatch-strict' -ResolvedDocument $resolvedDocument + Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'несовпадение-принтера-strict' -ResolvedDocument $resolvedDocument continue } } else { - if ($Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-and-printer-mismatch-relaxed' -ResolvedDocument $resolvedDocument + if ($JobId -and (-not $jobMatches) -and $Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) { + Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'несовпадение-владельца-и-принтера-relaxed' -ResolvedDocument $resolvedDocument + continue + } + if ((-not $JobId) -and $Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) { + Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'несовпадение-владельца-и-принтера-relaxed' -ResolvedDocument $resolvedDocument continue } } if ([string]::IsNullOrWhiteSpace($resolvedDocument)) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('no-document-candidate-' + $pass) -ResolvedDocument '' + Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('нет-кандидата-документа-' + $pass) -ResolvedDocument '' continue } - $matchReasonBase = if (Test-LooksLikeMojibakeQuestionMarks -Value $summary.DocumentName) { 'fallback-used' } else { 'direct' } + $matchReasonBase = if (Test-DocumentNameNeedsFallback -Value $summary.DocumentName) { 'использован-резервный-вариант' } else { 'напрямую' } Write-PrintServiceEventTrace -EventSummary $summary -Phase 'selected' -MatchReason ($matchReasonBase + '-' + $pass) -ResolvedDocument $resolvedDocument return $resolvedDocument } @@ -616,7 +710,7 @@ function Get-BetterDocumentNameFromPrintServiceEvents { } $deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } +$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'Укажите ServerHost или подготовьте deployment-config.json.' } $resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } $resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } $resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\ActivityWatch\dlp-policy.json' } @@ -648,7 +742,7 @@ $script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled $script:ScreenshotTypesLoaded = $false Load-DlpPolicy -Path $resolvedPolicyPath -Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase) +Write-EndpointLog ("endpoint-коллектор запущен для {0}" -f $script:ApiBase) while ($true) { try { @@ -709,13 +803,13 @@ while ($true) { if ($script:SeenPrintJob.ContainsKey($jobId)) { continue } $script:SeenPrintJob[$jobId] = (Get-Date).ToUniversalTime() - $printerName = [string]$job.Name + $printerName = Get-PrintJobPrinterName -JobName ([string]$job.Name) -FallbackPrinterName ([string]$job.DriverName) $documentName = [string]$job.Document $owner = [string]$job.Owner $documentNameOriginal = $documentName - if (Test-LooksLikeMojibakeQuestionMarks -Value $documentName) { - $eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -Owner $owner -PrinterName $printerName + if (Test-DocumentNameNeedsFallback -Value $documentName) { + $eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -JobId $jobId -Owner $owner -PrinterName $printerName if ($eventDocumentName) { $documentName = $eventDocumentName } @@ -726,6 +820,7 @@ while ($true) { documentName = $documentName documentNameOriginal = $documentNameOriginal owner = $owner + printJobId = $jobId } Evaluate-PrintRules -PrinterName $printerName -DocumentName $documentName -Owner $owner } @@ -789,7 +884,7 @@ while ($true) { } } catch { - Write-EndpointLog ("collector error: {0}" -f $_.Exception.Message) + Write-EndpointLog ("ошибка коллектора: {0}" -f $_.Exception.Message) } Start-Sleep -Seconds $resolvedPollSeconds diff --git a/windows/hardening-recovery.ps1 b/windows/hardening-recovery.ps1 index c908b2f..05632e6 100755 --- a/windows/hardening-recovery.ps1 +++ b/windows/hardening-recovery.ps1 @@ -42,7 +42,7 @@ if (Test-Path -LiteralPath $ConfigPath) { } if (-not $existingConfig -and (-not $ServerHost)) { - throw 'deployment-config.json is missing. Provide -ServerHost and user parameters, or run a deploy script first.' + throw 'deployment-config.json отсутствует. Укажите -ServerHost и параметры пользователей либо сначала выполните скрипт развёртывания.' } $effectiveStateRoot = if ($StateRoot) { $StateRoot } elseif ($existingConfig) { [string]$existingConfig.paths.stateRoot } else { 'C:\ProgramData\ActivityWatch' } @@ -78,7 +78,7 @@ elseif ($existingConfig) { @($existingConfig.userTasks | ForEach-Object { [string]$_.userId }) } else { - throw 'Target users are missing.' + throw 'Не указаны целевые пользователи.' } New-ActivityWatchDirectory -Path $effectiveStateRoot @@ -96,6 +96,7 @@ Get-ActivityWatchExecutableMap -InstallRoot $effectiveInstallRoot | Out-Null $assetResult = Copy-ActivityWatchCollectorAssets ` -CollectorScriptSource (Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1') ` -EndpointCollectorScriptSource (Join-Path $PSScriptRoot 'dlp-endpoint-signals-collector.ps1') ` + -SessionCollectorScriptSource (Join-Path $PSScriptRoot 'worktime-session-collector.ps1') ` -ExampleRulesSource (Join-Path $PSScriptRoot 'web-category-rules.example.json') ` -ExamplePolicySource (Join-Path $PSScriptRoot 'dlp-policy.example.json') ` -StateRoot $effectiveStateRoot ` @@ -139,6 +140,6 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $effectiveRecoveryScript -ConfigPath $effectiveConfigPath Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName -Write-Host 'ActivityWatch hardening/recovery completed.' -Write-Host "Config: $effectiveConfigPath" -Write-Host "Users repaired: $($effectiveUsers -join ', ')" +Write-Host 'Укрепление и восстановление ActivityWatch завершены.' +Write-Host "Конфигурация: $effectiveConfigPath" +Write-Host "Пользователи восстановлены: $($effectiveUsers -join ', ')" diff --git a/windows/validate-deployment.ps1 b/windows/validate-deployment.ps1 index 9f133a4..85a0344 100644 --- a/windows/validate-deployment.ps1 +++ b/windows/validate-deployment.ps1 @@ -65,7 +65,7 @@ $tasks = foreach ($taskName in $taskNames) { else { [pscustomobject]@{ taskName = $taskName - state = 'Missing' + state = 'Отсутствует' present = $false } } diff --git a/windows/worktime-session-collector.ps1 b/windows/worktime-session-collector.ps1 index b030cc9..214be90 100644 --- a/windows/worktime-session-collector.ps1 +++ b/windows/worktime-session-collector.ps1 @@ -11,7 +11,7 @@ function Get-Config { param([string]$Path) if (-not (Test-Path -LiteralPath $Path)) { - throw "Config not found: $Path" + throw "Конфигурация не найдена: $Path" } Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json