feat(detmir): ship ops, dlp, 1c and mcp updates

This commit is contained in:
igor04091968
2026-05-27 05:56:43 +03:00
parent 033ae810f3
commit 3042bd5042
85 changed files with 12028 additions and 460 deletions
+284 -23
View File
@@ -1,5 +1,6 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$script:ActivityWatchBuiltInAdministratorName = $null
function Assert-Administrator {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
@@ -265,6 +266,85 @@ function Get-ActivityWatchExecutableMap {
return [pscustomobject]$map
}
function Repair-ActivityWatchPotentialMojibake {
param([string]$Value)
if ([string]::IsNullOrWhiteSpace($Value)) {
return $Value
}
if ($Value -notmatch '[\u0400-\u04FF]') {
return $Value
}
try {
$bytes = [Text.Encoding]::GetEncoding(1251).GetBytes($Value)
$repaired = [Text.Encoding]::UTF8.GetString($bytes)
if (-not [string]::IsNullOrWhiteSpace($repaired) -and $repaired -match '[\u0400-\u04FF]') {
return $repaired
}
}
catch {
}
return $Value
}
function Get-ActivityWatchBuiltInAdministratorName {
if ($script:ActivityWatchBuiltInAdministratorName) {
return $script:ActivityWatchBuiltInAdministratorName
}
try {
$account = Get-CimInstance Win32_UserAccount -Filter "LocalAccount=True" -ErrorAction Stop |
Where-Object { [string]$_.SID -match '-500$' } |
Select-Object -First 1
if ($account -and -not [string]::IsNullOrWhiteSpace([string]$account.Name)) {
$script:ActivityWatchBuiltInAdministratorName = [string]$account.Name
return $script:ActivityWatchBuiltInAdministratorName
}
}
catch {
}
$script:ActivityWatchBuiltInAdministratorName = 'Administrator'
return $script:ActivityWatchBuiltInAdministratorName
}
function Normalize-ActivityWatchUserId {
param(
[string]$UserId,
[string]$Domain
)
if ([string]::IsNullOrWhiteSpace($UserId)) {
return $null
}
$normalized = Repair-ActivityWatchPotentialMojibake -Value $UserId.Trim()
$resolvedDomain = $null
$leafUser = $normalized
if ($normalized -match '^([^\\]+)\\(.+)$') {
$resolvedDomain = Repair-ActivityWatchPotentialMojibake -Value $Matches[1]
$leafUser = Repair-ActivityWatchPotentialMojibake -Value $Matches[2]
}
if ($leafUser -match '^(?i:administrator|администратор)$') {
$leafUser = Get-ActivityWatchBuiltInAdministratorName
}
if ([string]::IsNullOrWhiteSpace($resolvedDomain) -and -not [string]::IsNullOrWhiteSpace($Domain)) {
$resolvedDomain = Repair-ActivityWatchPotentialMojibake -Value $Domain.Trim()
}
if (-not [string]::IsNullOrWhiteSpace($resolvedDomain)) {
return ('{0}\{1}' -f $resolvedDomain, $leafUser)
}
return $leafUser
}
function Normalize-ActivityWatchUsers {
param(
[string[]]$Users,
@@ -311,14 +391,8 @@ function Normalize-ActivityWatchUsers {
$normalized = $collected |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
ForEach-Object {
if ($Domain -and ($_ -notmatch '[\\@]')) {
'{0}\{1}' -f $Domain, $_
}
else {
$_
}
} |
ForEach-Object { Normalize-ActivityWatchUserId -UserId $_ -Domain $Domain } |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
Sort-Object -Unique
if (-not $normalized -or $normalized.Count -eq 0) {
@@ -354,9 +428,10 @@ function New-ActivityWatchUserTaskDefinitions {
)
$result = foreach ($user in $Users) {
$token = Get-ActivityWatchTaskNameToken -UserId $user
$normalizedUser = Normalize-ActivityWatchUserId -UserId $user
$token = Get-ActivityWatchTaskNameToken -UserId $normalizedUser
[pscustomobject]@{
UserId = $user
UserId = $normalizedUser
LaunchTaskName = "ActivityWatch Launch [$token]"
}
}
@@ -482,10 +557,11 @@ function Resolve-ActivityWatchUserCandidates {
[string]$UserId
)
$normalizedUserId = Normalize-ActivityWatchUserId -UserId $UserId
$candidateIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
[void]$candidateIds.Add($UserId)
[void]$candidateIds.Add($normalizedUserId)
$leafUser = $UserId
$leafUser = $normalizedUserId
if ($leafUser -match '^[^\\]+\\(.+)$') {
$leafUser = $Matches[1]
[void]$candidateIds.Add($leafUser)
@@ -739,6 +815,9 @@ function New-ActivityWatchDeploymentConfig {
}
$effectivePolicyEngineHost = if ([string]::IsNullOrWhiteSpace($PolicyEngineHost)) { $ServerHost } else { $PolicyEngineHost }
$effectivePolicyCachePath = if ([string]::IsNullOrWhiteSpace($PolicyCachePath)) { Join-Path $StateRoot 'dlp-policy-cache.json' } else { $PolicyCachePath }
if ($File1CAutoUploadEnabled -and [string]::IsNullOrWhiteSpace($File1CTargetHost)) {
throw 'File1CTargetHost is required when File1CAutoUploadEnabled is true.'
}
return [pscustomobject]@{
version = 1
@@ -1068,12 +1147,12 @@ function Send-LogonMarkerIfNeeded {
`$stateRoot = [string]`$Config.paths.stateRoot
`$markerRoots = New-Object System.Collections.Generic.List[string]
if (-not [string]::IsNullOrWhiteSpace(`$env:LOCALAPPDATA)) {
`$markerRoots.Add((Join-Path `$env:LOCALAPPDATA 'AWatch-rus\markers'))
}
if (-not [string]::IsNullOrWhiteSpace(`$stateRoot)) {
`$markerRoots.Add((Join-Path `$stateRoot 'markers'))
}
if (-not [string]::IsNullOrWhiteSpace(`$env:LOCALAPPDATA)) {
`$markerRoots.Add((Join-Path `$env:LOCALAPPDATA 'AWatch-rus\markers'))
}
`$markerDir = `$null
foreach (`$candidate in `$markerRoots) {
@@ -1289,7 +1368,7 @@ function Get-ActivityWatchRecoveryTaskDefinitions {
$config = Read-ActivityWatchDeploymentConfig -Path $candidatePath
foreach ($task in @($config.userTasks)) {
$taskName = [string]$task.launchTaskName
$userId = [string]$task.userId
$userId = Normalize-ActivityWatchUserId -UserId ([string]$task.userId)
if (-not [string]::IsNullOrWhiteSpace($taskName) -and -not $taskMap.Contains($taskName)) {
$taskMap[$taskName] = [pscustomobject]@{
taskName = $taskName
@@ -1493,6 +1572,123 @@ function Get-ActivityWatchDisconnectedInteractiveSessions {
return @($result | Sort-Object SessionId -Unique)
}
function Get-ActivityWatchMarkerDirectories {
param([string]$StateRoot)
$roots = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
if (-not [string]::IsNullOrWhiteSpace($StateRoot)) {
[void]$roots.Add((Join-Path $StateRoot 'markers'))
}
$usersRoot = Join-Path $env:SystemDrive 'Users'
if (Test-Path -LiteralPath $usersRoot) {
foreach ($dir in @(Get-ChildItem -LiteralPath $usersRoot -Directory -ErrorAction SilentlyContinue)) {
[void]$roots.Add((Join-Path $dir.FullName 'AppData\Local\AWatch-rus\markers'))
}
}
return @($roots)
}
function Remove-ActivityWatchLogonMarkersForSession {
param(
[Parameter(Mandatory = $true)]
[string]$StateRoot,
[int]$SessionId,
[string]$UserName
)
$userCandidates = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
if (-not [string]::IsNullOrWhiteSpace($UserName)) {
[void]$userCandidates.Add($UserName)
if ($UserName -match '^[^\\]+\\(.+)$') {
[void]$userCandidates.Add($Matches[1])
}
}
foreach ($markerDir in @(Get-ActivityWatchMarkerDirectories -StateRoot $StateRoot)) {
if (-not (Test-Path -LiteralPath $markerDir)) {
continue
}
foreach ($marker in @(Get-ChildItem -LiteralPath $markerDir -Filter '*.marker' -File -ErrorAction SilentlyContinue)) {
$name = [string]$marker.BaseName
if ($name -notmatch '^logon-(.+)-(\d+)-') {
continue
}
$markerUser = [string]$Matches[1]
$markerSessionId = [int]$Matches[2]
if ($markerSessionId -ne $SessionId) {
continue
}
if ($userCandidates.Count -gt 0 -and -not $userCandidates.Contains($markerUser)) {
continue
}
Remove-Item -LiteralPath $marker.FullName -Force -ErrorAction SilentlyContinue
}
}
}
function Stop-ActivityWatchProcessesInNonLiveSessions {
param(
[Parameter(Mandatory = $true)]
[object[]]$SessionRecords,
[Parameter(Mandatory = $true)]
[pscustomobject]$Config
)
$stateRoot = if ($Config.paths.PSObject.Properties.Name -contains 'stateRoot') { [string]$Config.paths.stateRoot } else { Join-Path $env:ProgramData 'AWatch-rus' }
$sessionIds = @(
$SessionRecords |
Where-Object { -not $_.IsLive -and $_.SessionId -gt 0 } |
ForEach-Object { [int]$_.SessionId } |
Sort-Object -Unique
)
if (-not $sessionIds -or $sessionIds.Count -eq 0) {
return
}
$sessionScopedScripts = New-Object System.Collections.Generic.List[string]
foreach ($propertyName in @('collectorScript', 'endpointCollectorScript', 'fileCollectorScript', 'emailCollectorScript', 'launchScript')) {
if ($Config.paths.PSObject.Properties.Name -contains $propertyName) {
$candidatePath = [string]$Config.paths.$propertyName
if (-not [string]::IsNullOrWhiteSpace($candidatePath)) {
$sessionScopedScripts.Add($candidatePath) | Out-Null
}
}
}
foreach ($session in @($SessionRecords | Where-Object { -not $_.IsLive -and $_.SessionId -gt 0 })) {
Remove-ActivityWatchLogonMarkersForSession -StateRoot $stateRoot -SessionId ([int]$session.SessionId) -UserName ([string]$session.UserName)
}
Get-Process -Name 'aw-watcher-afk','aw-watcher-window' -ErrorAction SilentlyContinue |
Where-Object { $sessionIds -contains [int]$_.SessionId } |
ForEach-Object {
Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
}
Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
Where-Object {
($_.Name -ieq 'powershell.exe' -or $_.Name -ieq 'pwsh.exe') -and
($sessionIds -contains [int]$_.SessionId)
} |
ForEach-Object {
$commandLine = [string]$_.CommandLine
foreach ($scriptPath in $sessionScopedScripts) {
if (-not [string]::IsNullOrWhiteSpace($scriptPath) -and $commandLine -match [Regex]::Escape($scriptPath)) {
Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
break
}
}
}
}
function Promote-ActivityWatchDisconnectedSessionToConsole {
param(
[pscustomobject[]]$TaskDefinitions,
@@ -1648,6 +1844,8 @@ function Invoke-ActivityWatchRecoveryLoop {
$config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath
$taskDefs = Get-ActivityWatchRecoveryTaskDefinitions -ConfigPaths $configPaths
$sessionRecords = Get-ActivityWatchSessionRecords
Stop-ActivityWatchProcessesInNonLiveSessions -SessionRecords $sessionRecords -Config $config
$sessionRecords = Get-ActivityWatchSessionRecords
$stateRoot = [string]$config.paths.stateRoot
$sessionCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]$config.paths.sessionCollectorScript } else { Join-Path $stateRoot 'worktime-session-collector.ps1' }
Start-ActivityWatchCollectorScriptGlobalIfNeeded -ScriptPath $sessionCollectorScript -ConfigPath $ConfigPath
@@ -1721,13 +1919,60 @@ function Remove-LegacyActivityWatchEntries {
$legacyTaskNames = @(
'ActivityWatch Watchers',
'ActivityWatch Guard',
'ActivityWatch Heal'
'ActivityWatch Heal',
'AWatchRusStandaloneAgent',
'AWatch Worktime Collector',
'AW DLP Endpoint ADMIN',
'AW DLP Endpoint USER1'
)
foreach ($taskName in $legacyTaskNames) {
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
}
$legacyTaskPatterns = @(
'browser-domains-native-collector.ps1',
'file-operations-collector.ps1',
'dlp-endpoint-signals-collector.ps1',
'worktime-session-collector.ps1',
'aw-standalone-service.ps1'
)
$managedTaskNames = @(
'ActivityWatch Recovery',
'ActivityWatch Hayabusa Upload',
'ActivityWatch File1C Upload'
)
foreach ($task in @(Get-ScheduledTask -ErrorAction SilentlyContinue)) {
$taskName = [string]$task.TaskName
if ([string]::IsNullOrWhiteSpace($taskName) -or $managedTaskNames -contains $taskName -or $taskName -like 'ActivityWatch Launch *') {
continue
}
$isLegacyCollectorTask = $false
foreach ($action in @($task.Actions)) {
$execute = if ($action.PSObject.Properties.Name -contains 'Execute') { [string]$action.Execute } else { '' }
$arguments = if ($action.PSObject.Properties.Name -contains 'Arguments') { [string]$action.Arguments } else { '' }
$commandLine = ('{0} {1}' -f $execute, $arguments).Trim()
if ([string]::IsNullOrWhiteSpace($commandLine)) {
continue
}
foreach ($pattern in $legacyTaskPatterns) {
if ($commandLine -match [Regex]::Escape($pattern)) {
$isLegacyCollectorTask = $true
break
}
}
if ($isLegacyCollectorTask) {
break
}
}
if ($isLegacyCollectorTask) {
Remove-ActivityWatchScheduledTask -TaskName $taskName
}
}
$runKey = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run'
foreach ($name in 'ActivityWatchAFK', 'ActivityWatchWindow', 'ActivityWatchBrowserCollector') {
Remove-ItemProperty -Path $runKey -Name $name -ErrorAction SilentlyContinue
@@ -1778,7 +2023,8 @@ function Set-ActivityWatchScheduledTaskAction {
$taskCommand = ('"{0}" {1}' -f $Execute, $Arguments)
& schtasks.exe /Change /TN $TaskName /TR $taskCommand | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "Не удалось обновить action задачи ${TaskName}: $($_.Exception.Message)"
Write-Host "skip task action update for $TaskName because the existing principal/action cannot be updated non-interactively: $($_.Exception.Message)"
return $false
}
return $true
}
@@ -1973,13 +2219,28 @@ function Register-ActivityWatchFile1CAutoUploadTask {
$intervalHours = [Math]::Max(1, [int]$automation.intervalHours)
$powerShellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
$taskCommand = "`"$powerShellExe`" -NoProfile -ExecutionPolicy Bypass -File `"$uploadScript`" -ConfigPath `"$ConfigPath`""
if (Set-ActivityWatchScheduledTaskAction -TaskName $taskName -Execute $powerShellExe -Arguments "-NoProfile -ExecutionPolicy Bypass -File `"$uploadScript`" -ConfigPath `"$ConfigPath`"") {
return
$runnerUserId = $null
if ($automation.PSObject.Properties.Name -contains 'runAsUser' -and -not [string]::IsNullOrWhiteSpace([string]$automation.runAsUser)) {
$runnerUserId = [string]$automation.runAsUser
}
if ([string]::IsNullOrWhiteSpace($runnerUserId) -and $config.PSObject.Properties.Name -contains 'userTasks') {
$runnerUserId = @(
@($config.userTasks | ForEach-Object { [string]$_.userId }) |
Where-Object { $_ -match '(^|\\)(Администратор|Administrator)$' } |
Select-Object -First 1
) | Select-Object -First 1
}
if ([string]::IsNullOrWhiteSpace($runnerUserId) -and $config.PSObject.Properties.Name -contains 'userTasks') {
$runnerUserId = @($config.userTasks | ForEach-Object { [string]$_.userId } | Select-Object -First 1) | Select-Object -First 1
}
Remove-ActivityWatchScheduledTask -TaskName $taskName
& schtasks.exe /Create /TN $taskName /TR $taskCommand /SC HOURLY /MO $intervalHours /ST 00:00 /RU SYSTEM /RL HIGHEST /F | Out-Null
if (-not [string]::IsNullOrWhiteSpace($runnerUserId)) {
& schtasks.exe /Create /TN $taskName /TR $taskCommand /SC HOURLY /MO $intervalHours /ST 00:00 /RU $runnerUserId /RL HIGHEST /F | Out-Null
}
else {
& schtasks.exe /Create /TN $taskName /TR $taskCommand /SC HOURLY /MO $intervalHours /ST 00:00 /RU SYSTEM /RL HIGHEST /F | Out-Null
}
if ($LASTEXITCODE -ne 0) {
throw "Не удалось создать scheduled task $taskName через schtasks.exe"
}
@@ -2004,7 +2265,7 @@ function Set-ActivityWatchAcl {
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
& 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)(M)' | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "icacls завершился с ошибкой для $StateRoot"
}