fix(aw): harden collectors and grafana exports

This commit is contained in:
igor04091968
2026-05-28 10:54:46 +03:00
parent 524ab33f63
commit cc9e4a0669
29 changed files with 1181 additions and 131 deletions
+130 -23
View File
@@ -295,6 +295,11 @@ function Get-ActivityWatchBuiltInAdministratorName {
return $script:ActivityWatchBuiltInAdministratorName
}
if (-not [string]::IsNullOrWhiteSpace($env:AWATCH_RUS_BUILTIN_ADMINISTRATOR_NAME)) {
$script:ActivityWatchBuiltInAdministratorName = [string]$env:AWATCH_RUS_BUILTIN_ADMINISTRATOR_NAME
return $script:ActivityWatchBuiltInAdministratorName
}
try {
$account = Get-CimInstance Win32_UserAccount -Filter "LocalAccount=True" -ErrorAction Stop |
Where-Object { [string]$_.SID -match '-500$' } |
@@ -307,6 +312,11 @@ function Get-ActivityWatchBuiltInAdministratorName {
catch {
}
if ([string]$env:COMPUTERNAME -ieq 'SHARKON2025') {
$script:ActivityWatchBuiltInAdministratorName = 'Администратор'
return $script:ActivityWatchBuiltInAdministratorName
}
$script:ActivityWatchBuiltInAdministratorName = 'Administrator'
return $script:ActivityWatchBuiltInAdministratorName
}
@@ -389,11 +399,11 @@ function Normalize-ActivityWatchUsers {
}
}
$normalized = $collected |
$normalized = @($collected |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
ForEach-Object { Normalize-ActivityWatchUserId -UserId $_ -Domain $Domain } |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
Sort-Object -Unique
Sort-Object -Unique)
if (-not $normalized -or $normalized.Count -eq 0) {
throw 'Не удалось определить целевых пользователей. Укажите -Users или -UserListPath.'
@@ -767,6 +777,7 @@ function New-ActivityWatchDeploymentConfig {
[int]$EvtxRetentionDays = 14,
[string[]]$EvtxChannels = @(),
[bool]$LogonMarkerEnabled = $true,
[bool]$ProcessEventsEnabled = $true,
[Parameter(Mandatory = $true)]
[string]$LaunchScriptPath,
[Parameter(Mandatory = $true)]
@@ -888,8 +899,9 @@ function New-ActivityWatchDeploymentConfig {
}
}
sessionEvents = [pscustomobject]@{
logonEnabled = $LogonMarkerEnabled
bucketPrefix = 'aw-session-events'
logonEnabled = $LogonMarkerEnabled
processEventsEnabled = $ProcessEventsEnabled
bucketPrefix = 'aw-session-events'
}
recovery = [pscustomobject]@{
intervalSeconds = $RecoveryIntervalSeconds
@@ -964,7 +976,9 @@ Set-StrictMode -Version Latest
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
Add-Type -AssemblyName System.Net.Http
`$script:MaxCollectorPowerShellProcesses = 24
`$script:MaxCollectorPowerShellProcesses = 48
`$script:CollectorProcessSnapshotLoaded = `$false
`$script:CollectorProcessSnapshot = @()
function Get-DeploymentConfig {
param([string]`$Path)
@@ -980,6 +994,41 @@ function Test-ProcessInSession {
return [bool](Get-Process -Name `$Name -ErrorAction SilentlyContinue | Where-Object { `$_.SessionId -eq `$SessionId } | Select-Object -First 1)
}
function Get-CollectorProcessSnapshot {
if (`$script:CollectorProcessSnapshotLoaded) {
return @(`$script:CollectorProcessSnapshot)
}
`$script:CollectorProcessSnapshotLoaded = `$true
`$script:CollectorProcessSnapshot = @()
`$job = `$null
try {
`$job = Start-Job -ScriptBlock {
Get-CimInstance Win32_Process -Filter "Name = 'powershell.exe' OR Name = 'pwsh.exe'" -ErrorAction SilentlyContinue |
Where-Object {
`$_.CommandLine -match 'AWatch-rus' -and
`$_.CommandLine -match '\.ps1'
} |
Select-Object ProcessId, SessionId, CommandLine
}
if (Wait-Job -Job `$job -Timeout 4) {
`$script:CollectorProcessSnapshot = @(Receive-Job -Job `$job -ErrorAction SilentlyContinue)
}
}
catch {
`$script:CollectorProcessSnapshot = @()
}
finally {
if (`$job) {
Stop-Job -Job `$job -ErrorAction SilentlyContinue | Out-Null
Remove-Job -Job `$job -Force -ErrorAction SilentlyContinue | Out-Null
}
}
return @(`$script:CollectorProcessSnapshot)
}
function Test-CollectorRunning {
param(
[string]`$ScriptPath,
@@ -987,9 +1036,8 @@ function Test-CollectorRunning {
)
`$escapedCollector = [Regex]::Escape(`$ScriptPath)
`$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
`$processes = Get-CollectorProcessSnapshot |
Where-Object {
(`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and
`$_.SessionId -eq `$SessionId -and
`$_.CommandLine -match `$escapedCollector
}
@@ -998,14 +1046,7 @@ function Test-CollectorRunning {
}
function Get-CollectorPowerShellProcessCount {
`$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
Where-Object {
(`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and
`$_.CommandLine -match 'AWatch-rus' -and
`$_.CommandLine -match '\.ps1'
}
return @(`$processes).Count
return @(Get-CollectorProcessSnapshot).Count
}
function New-LaunchLock {
@@ -1908,8 +1949,15 @@ function Write-ActivityWatchHiddenPowerShellWrapper {
$escapedConfigPath = $ConfigPath.Replace('"', '""')
$content = @"
On Error Resume Next
Set shell = CreateObject("WScript.Shell")
shell.Run """$escapedPowerShellExe"" -NoProfile -ExecutionPolicy Bypass -File ""$escapedScriptPath"" -ConfigPath ""$escapedConfigPath""", 0, False
q = Chr(34)
command = q & "$escapedPowerShellExe" & q & " -NoProfile -ExecutionPolicy Bypass -File " & q & "$escapedScriptPath" & q & " -ConfigPath " & q & "$escapedConfigPath" & q
shell.Run command, 0, False
If Err.Number <> 0 Then
WScript.Quit 1
End If
WScript.Quit 0
"@
Set-Content -LiteralPath $Path -Value $content -Encoding ASCII
@@ -1927,7 +1975,7 @@ function Remove-LegacyActivityWatchEntries {
)
foreach ($taskName in $legacyTaskNames) {
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
Remove-ActivityWatchScheduledTask -TaskName $taskName
}
$legacyTaskPatterns = @(
@@ -1943,7 +1991,15 @@ function Remove-LegacyActivityWatchEntries {
'ActivityWatch File1C Upload'
)
foreach ($task in @(Get-ScheduledTask -ErrorAction SilentlyContinue)) {
$scheduledTasks = @()
try {
$scheduledTasks = @(Get-ScheduledTask -ErrorAction Stop)
}
catch {
$scheduledTasks = @()
}
foreach ($task in $scheduledTasks) {
$taskName = [string]$task.TaskName
if ([string]::IsNullOrWhiteSpace($taskName) -or $managedTaskNames -contains $taskName -or $taskName -like 'ActivityWatch Launch *') {
continue
@@ -1985,11 +2041,28 @@ function Remove-ActivityWatchScheduledTask {
[string]$TaskName
)
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
try {
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction Stop
}
catch {
}
& cmd.exe /c "schtasks /Delete /TN `"$TaskName`" /F >nul 2>&1" | Out-Null
if ($LASTEXITCODE -eq 0) {
return
}
for ($attempt = 0; $attempt -lt 10; $attempt++) {
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
$task = $null
try {
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction Stop
}
catch {
& cmd.exe /c "schtasks /Query /TN `"$TaskName`" >nul 2>&1" | Out-Null
if ($LASTEXITCODE -ne 0) {
return
}
}
if (-not $task) {
return
}
@@ -2138,11 +2211,45 @@ function Register-ActivityWatchRecoveryTask {
$launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $RecoveryScriptPath
Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $RecoveryScriptPath -ConfigPath $ConfigPath
$action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`""
$trigger = New-ScheduledTaskTrigger -AtStartup
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
$sessionRecords = @()
try {
$sessionRecords = @(Get-ActivityWatchSessionRecords)
}
catch {
$sessionRecords = @()
}
$liveSession = @(Get-ActivityWatchLiveInteractiveSessions -SessionRecords $sessionRecords) | Select-Object -First 1
$interactiveUserId = $null
if ($liveSession -and -not [string]::IsNullOrWhiteSpace([string]$liveSession.UserName)) {
$rawUser = [string]$liveSession.UserName
$interactiveUserId = if ($rawUser -match '^[^\\]+\\') { $rawUser } else { ('{0}\{1}' -f $env:COMPUTERNAME, $rawUser) }
}
if ($interactiveUserId) {
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $interactiveUserId
$principal = New-ScheduledTaskPrincipal -UserId $interactiveUserId -LogonType Interactive -RunLevel Highest
}
else {
$trigger = New-ScheduledTaskTrigger -AtStartup
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
}
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -Hidden -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0)
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null
try {
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -ErrorAction Stop | Out-Null
}
catch {
$taskCommand = ('"{0}" {1}' -f $wscriptExe, $action.Arguments)
if ($interactiveUserId) {
& schtasks.exe /Create /TN $TaskName /SC ONLOGON /RU $interactiveUserId /IT /RL HIGHEST /F /TR $taskCommand | Out-Null
}
else {
& schtasks.exe /Create /TN $TaskName /SC ONSTART /RU SYSTEM /RL HIGHEST /F /TR $taskCommand | Out-Null
}
if ($LASTEXITCODE -ne 0) {
throw
}
}
}
function Register-ActivityWatchHayabusaAutoUploadTask {
+2
View File
@@ -27,6 +27,7 @@ param(
[int]$EvtxRetentionDays = 14,
[string[]]$EvtxChannels = @(),
[bool]$LogonMarkerEnabled = $true,
[bool]$ProcessEventsEnabled = $true,
[string]$AwHostname,
[string]$CustomRulesPath,
[string]$CustomPolicyPath,
@@ -140,6 +141,7 @@ $config = New-ActivityWatchDeploymentConfig `
-EvtxRetentionDays $EvtxRetentionDays `
-EvtxChannels $EvtxChannels `
-LogonMarkerEnabled $LogonMarkerEnabled `
-ProcessEventsEnabled $ProcessEventsEnabled `
-AwHostname $AwHostname `
-PolicyMode $PolicyMode `
-PolicyEngineEnabled $PolicyEngineEnabled `
+3
View File
@@ -27,6 +27,7 @@ param(
[int]$EvtxRetentionDays = 14,
[string[]]$EvtxChannels = @(),
[bool]$LogonMarkerEnabled = $true,
[bool]$ProcessEventsEnabled = $true,
[string]$AwHostname,
[string]$CustomRulesPath,
[string]$CustomPolicyPath,
@@ -99,6 +100,7 @@ if (-not (Test-Path -LiteralPath $deployScript)) {
-EvtxRetentionDays $EvtxRetentionDays `
-EvtxChannels $EvtxChannels `
-LogonMarkerEnabled $LogonMarkerEnabled `
-ProcessEventsEnabled $ProcessEventsEnabled `
-AwHostname $AwHostname `
-CustomRulesPath $CustomRulesPath `
-CustomPolicyPath $CustomPolicyPath `
@@ -145,6 +147,7 @@ if (-not $SkipHardening) {
-EvtxRetentionDays $EvtxRetentionDays `
-EvtxChannels $EvtxChannels `
-LogonMarkerEnabled $LogonMarkerEnabled `
-ProcessEventsEnabled $ProcessEventsEnabled `
-AwHostname $AwHostname `
-CustomRulesPath $CustomRulesPath `
-CustomPolicyPath $CustomPolicyPath `
+2
View File
@@ -25,6 +25,7 @@ param(
[int]$EvtxRetentionDays = 14,
[string[]]$EvtxChannels = @(),
[bool]$LogonMarkerEnabled = $true,
[bool]$ProcessEventsEnabled = $true,
[string]$AwHostname,
[string]$CustomRulesPath,
[string]$CustomPolicyPath
@@ -102,6 +103,7 @@ $config = New-ActivityWatchDeploymentConfig `
-EvtxRetentionDays $EvtxRetentionDays `
-EvtxChannels $EvtxChannels `
-LogonMarkerEnabled $LogonMarkerEnabled `
-ProcessEventsEnabled $ProcessEventsEnabled `
-AwHostname $AwHostname `
-LaunchScriptPath $launchScriptPath `
-RecoveryScriptPath $recoveryScriptPath `
+14 -5
View File
@@ -170,10 +170,19 @@ function Get-1CFileInfobases {
}
function Get-HostSample {
$os = Get-CimInstance Win32_OperatingSystem
$cpuSample = Get-CimInstance Win32_Processor -ErrorAction SilentlyContinue |
Measure-Object -Property LoadPercentage -Average
$cpu = if ($cpuSample.Count -gt 0 -and $null -ne $cpuSample.Average) { [double]$cpuSample.Average } else { 0 }
$cpu = 0.0
$ramPct = 0.0
try {
Add-Type -AssemblyName Microsoft.VisualBasic -ErrorAction Stop
$computerInfo = New-Object Microsoft.VisualBasic.Devices.ComputerInfo
$totalMemory = [double]$computerInfo.TotalPhysicalMemory
$availableMemory = [double]$computerInfo.AvailablePhysicalMemory
if ($totalMemory -gt 0) {
$ramPct = (($totalMemory - $availableMemory) / $totalMemory) * 100
}
} catch {
Write-RunLog ("warning: host memory sample fallback reason={0}" -f $_.Exception.Message)
}
$disk = Get-PSDrive -Name E -ErrorAction SilentlyContinue
$rdp = (quser 2>$null | Select-Object -Skip 1 | Measure-Object).Count
@@ -181,7 +190,7 @@ function Get-HostSample {
ts = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
host = $env:COMPUTERNAME
cpu_pct = [math]::Round($cpu, 2)
ram_pct = [math]::Round((($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) / $os.TotalVisibleMemorySize) * 100, 2)
ram_pct = [math]::Round($ramPct, 2)
disk_free_gb = if ($disk) { [math]::Round($disk.Free / 1GB, 2) } else { 0 }
disk_latency_ms = 0
smb_errors = 0
+3
View File
@@ -24,6 +24,7 @@ param(
[int]$EvtxRetentionDays,
[string[]]$EvtxChannels,
[bool]$LogonMarkerEnabled,
[bool]$ProcessEventsEnabled,
[string]$AwHostname,
[string]$CustomRulesPath,
[string]$CustomPolicyPath,
@@ -93,6 +94,7 @@ $effectiveEvtxExportRoot = if ($PSBoundParameters.ContainsKey('EvtxExportRoot')
$effectiveEvtxRetentionDays = if ($PSBoundParameters.ContainsKey('EvtxRetentionDays')) { [int]$EvtxRetentionDays } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'forensics' -and $existingConfig.forensics.PSObject.Properties.Name -contains 'retentionDays') { [int]$existingConfig.forensics.retentionDays } else { 14 }
$effectiveEvtxChannels = if ($PSBoundParameters.ContainsKey('EvtxChannels')) { @($EvtxChannels) } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'forensics' -and $existingConfig.forensics.PSObject.Properties.Name -contains 'evtxChannels') { @($existingConfig.forensics.evtxChannels) } else { @() }
$effectiveLogonMarkerEnabled = if ($PSBoundParameters.ContainsKey('LogonMarkerEnabled')) { [bool]$LogonMarkerEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'sessionEvents' -and $existingConfig.sessionEvents.PSObject.Properties.Name -contains 'logonEnabled') { [bool]$existingConfig.sessionEvents.logonEnabled } else { $true }
$effectiveProcessEventsEnabled = if ($PSBoundParameters.ContainsKey('ProcessEventsEnabled')) { [bool]$ProcessEventsEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'sessionEvents' -and $existingConfig.sessionEvents.PSObject.Properties.Name -contains 'processEventsEnabled') { [bool]$existingConfig.sessionEvents.processEventsEnabled } else { $true }
$effectiveAwHostname = if ($PSBoundParameters.ContainsKey('AwHostname') -and -not [string]::IsNullOrWhiteSpace($AwHostname)) { [string]$AwHostname } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$existingConfig.awHostname)) { [string]$existingConfig.awHostname } else { [string]$env:COMPUTERNAME }
$effectiveVersion = if ($Version) { $Version } elseif ($existingConfig) { [string]$existingConfig.package.version } else { 'v0.13.2' }
$effectivePolicyMode = if ($PSBoundParameters.ContainsKey('PolicyMode') -and $PolicyMode) { [string]$PolicyMode } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'policyEngine' -and $existingConfig.policyEngine.PSObject.Properties.Name -contains 'mode') { [string]$existingConfig.policyEngine.mode } else { 'local' }
@@ -202,6 +204,7 @@ $config = New-ActivityWatchDeploymentConfig `
-EvtxRetentionDays $effectiveEvtxRetentionDays `
-EvtxChannels $effectiveEvtxChannels `
-LogonMarkerEnabled $effectiveLogonMarkerEnabled `
-ProcessEventsEnabled $effectiveProcessEventsEnabled `
-AwHostname $effectiveAwHostname `
-PolicyMode $effectivePolicyMode `
-PolicyEngineEnabled $effectivePolicyEngineEnabled `
+15
View File
@@ -36,6 +36,15 @@ $queueMaxDepth = 1000
$afkExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]$config.collectors.afkEnabled } else { $true }
$windowExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]$config.collectors.windowEnabled } else { $true }
$fileOpsExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { [bool]$config.collectors.fileOpsEnabled } else { $true }
$sessionEventsConfig = if ($config.PSObject.Properties.Name -contains 'sessionEvents') { $config.sessionEvents } else { $null }
$sessionLogonEnabled = if ($sessionEventsConfig -and $sessionEventsConfig.PSObject.Properties.Name -contains 'logonEnabled') { [bool]$sessionEventsConfig.logonEnabled } else { $false }
$sessionProcessEventsEnabled = if ($sessionEventsConfig -and $sessionEventsConfig.PSObject.Properties.Name -contains 'processEventsEnabled') { [bool]$sessionEventsConfig.processEventsEnabled } else { $true }
$sessionEventsBucketId = if ($sessionEventsConfig -and $sessionEventsConfig.PSObject.Properties.Name -contains 'bucketPrefix' -and -not [string]::IsNullOrWhiteSpace([string]$sessionEventsConfig.bucketPrefix)) {
('{0}_{1}' -f [string]$sessionEventsConfig.bucketPrefix, $awHostname)
}
else {
'aw-session-events_' + $awHostname
}
function Get-LoggedOnUsers {
param(
@@ -535,6 +544,12 @@ $result = [ordered]@{
jobTitlePolicyEnabled = $printJobTitlePolicyEnabled
ok = [bool]($printServiceOperationalEnabled -and $printJobTitlePolicyEnabled)
}
sessionEvents = [ordered]@{
bucketId = $sessionEventsBucketId
logonEnabled = [bool]$sessionLogonEnabled
processEventsEnabled = [bool]$sessionProcessEventsEnabled
ok = $true
}
forensics = [ordered]@{
evtxExportRoot = if ($config.PSObject.Properties.Name -contains 'forensics' -and $config.forensics.PSObject.Properties.Name -contains 'evtxExportRoot') { [string]$config.forensics.evtxExportRoot } else { $null }
retentionDays = if ($config.PSObject.Properties.Name -contains 'forensics' -and $config.forensics.PSObject.Properties.Name -contains 'retentionDays') { [int]$config.forensics.retentionDays } else { $null }
+277 -2
View File
@@ -94,11 +94,13 @@ function Ensure-Bucket {
param(
[Parameter(Mandatory = $true)][string]$ApiBase,
[Parameter(Mandatory = $true)][string]$BucketId,
[Parameter(Mandatory = $true)][string]$HostnameValue
[Parameter(Mandatory = $true)][string]$HostnameValue,
[string]$ClientName = 'aw-worktime-session-collector',
[string]$BucketType = 'aw.worktime.session'
)
try { Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" -ErrorAction Stop | Out-Null; return } catch { Write-Verbose "Bucket not found, creating: $BucketId" }
$body = @{ client='aw-worktime-session-collector'; type='aw.worktime.session'; hostname=$HostnameValue } | ConvertTo-Json -Compress
$body = @{ client=$ClientName; type=$BucketType; hostname=$HostnameValue } | ConvertTo-Json -Compress
$attempts = 0
while ($attempts -lt 3) {
$attempts++
@@ -230,16 +232,282 @@ function Get-CanonicalUserId {
return "$HostnameValue\$normalizedUser"
}
function Get-SessionEventsBucketId {
param(
[pscustomobject]$Config,
[string]$HostnameValue
)
$prefix = 'aw-session-events'
if (
$Config -and
$Config.PSObject.Properties.Name -contains 'sessionEvents' -and
$Config.sessionEvents -and
$Config.sessionEvents.PSObject.Properties.Name -contains 'bucketPrefix' -and
-not [string]::IsNullOrWhiteSpace([string]$Config.sessionEvents.bucketPrefix)
) {
$prefix = [string]$Config.sessionEvents.bucketPrefix
}
return ('{0}_{1}' -f $prefix, $HostnameValue)
}
function Test-SessionProcessEventsEnabled {
param([pscustomobject]$Config)
if (
$Config -and
$Config.PSObject.Properties.Name -contains 'sessionEvents' -and
$Config.sessionEvents -and
$Config.sessionEvents.PSObject.Properties.Name -contains 'processEventsEnabled'
) {
return [bool]$Config.sessionEvents.processEventsEnabled
}
return $true
}
function Get-ProcessStatePath {
param([pscustomobject]$Config)
$stateRoot = ''
if ($Config -and $Config.PSObject.Properties.Name -contains 'paths' -and $Config.paths) {
if ($Config.paths.PSObject.Properties.Name -contains 'stateRoot') {
$stateRoot = [string]$Config.paths.stateRoot
}
}
if ([string]::IsNullOrWhiteSpace($stateRoot)) {
$stateRoot = 'C:\ProgramData\AWatch-rus'
}
return (Join-Path $stateRoot 'session-process-state.json')
}
function Load-ProcessState {
param([string]$Path)
$map = @{}
try {
if (Test-Path -LiteralPath $Path) {
$raw = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop
if (-not [string]::IsNullOrWhiteSpace($raw)) {
$obj = $raw | ConvertFrom-Json -ErrorAction Stop
foreach ($item in @($obj.processes)) {
if (-not $item) { continue }
$key = [string]$item.key
if ([string]::IsNullOrWhiteSpace($key)) { continue }
$map[$key] = $item
}
}
}
}
catch {
Write-Verbose "Process state load error: $($_.Exception.Message)"
}
return $map
}
function Save-ProcessState {
param(
[string]$Path,
[hashtable]$Map
)
try {
$dir = Split-Path -Path $Path -Parent
if ($dir -and -not (Test-Path -LiteralPath $dir)) {
New-Item -Path $dir -ItemType Directory -Force | Out-Null
}
$items = @()
foreach ($entry in $Map.GetEnumerator()) {
$value = $entry.Value
if ($null -eq $value) { continue }
$items += [pscustomobject]@{
key = [string]$entry.Key
processId = [int]$value.processId
sessionId = [int]$value.sessionId
username = [string]$value.username
userId = [string]$value.userId
state = [string]$value.state
processName = [string]$value.processName
commandLine = [string]$value.commandLine
createdAt = [string]$value.createdAt
hostname = [string]$value.hostname
}
}
$payload = [pscustomobject]@{ processes = $items }
$payload | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $Path -Encoding UTF8
}
catch {
Write-Verbose "Process state save error: $($_.Exception.Message)"
}
}
function Test-ExcludedSessionProcess {
param(
[string]$Name,
[string]$CommandLine
)
$n = [string]$Name
if ([string]::IsNullOrWhiteSpace($n)) { return $true }
if ($n -match '^(Idle|System|Registry|svchost|services|lsass|winlogon|csrss|fontdrvhost|dwm|taskhostw|sihost|explorer)\.exe$') { return $true }
if ($n -match '^(aw-watcher-afk|aw-watcher-window|conhost)\.exe$') { return $true }
return $false
}
function Get-SessionProcessSnapshot {
param(
[pscustomobject]$Config,
[string]$HostnameValue,
[object[]]$SessionRecords
)
$bySession = @{}
foreach ($rec in @($SessionRecords)) {
if ($null -eq $rec) { continue }
$sid = [int]$rec.sessionId
$bySession[$sid] = [pscustomobject]@{
username = [string]$rec.username
userId = Get-CanonicalUserId -Config $Config -HostnameValue $HostnameValue -Username ([string]$rec.username)
state = [string]$rec.state
}
}
$snapshot = @{}
if ($bySession.Count -eq 0) {
return $snapshot
}
try {
$procs = Get-Process -ErrorAction Stop | Where-Object { $bySession.ContainsKey([int]$_.SessionId) }
}
catch {
Write-Verbose "Process snapshot error: $($_.Exception.Message)"
return $snapshot
}
foreach ($proc in @($procs)) {
try {
$sid = [int]$proc.SessionId
}
catch {
continue
}
if (-not $bySession.ContainsKey($sid)) { continue }
$name = [string]$proc.ProcessName
if ($name -and $name -notmatch '\.exe$') {
$name = "$name.exe"
}
$commandLine = ''
if (Test-ExcludedSessionProcess -Name $name -CommandLine $commandLine) { continue }
$createdAt = ''
try {
if ($proc.StartTime) {
$createdAt = $proc.StartTime.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
}
}
catch {
$createdAt = ''
}
if ([string]::IsNullOrWhiteSpace($createdAt)) {
$createdAt = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
}
$key = ('{0}|{1}|{2}' -f $sid, [int]$proc.Id, $createdAt)
$sessionMeta = $bySession[$sid]
$snapshot[$key] = [pscustomobject]@{
processId = [int]$proc.Id
sessionId = $sid
username = [string]$sessionMeta.username
userId = [string]$sessionMeta.userId
state = [string]$sessionMeta.state
processName = $name
commandLine = $commandLine
createdAt = $createdAt
hostname = $HostnameValue
}
}
return $snapshot
}
function Publish-SessionProcessEvents {
param(
[string]$ApiBase,
[string]$BucketId,
[hashtable]$Previous,
[hashtable]$Current
)
foreach ($entry in $Current.GetEnumerator()) {
if ($Previous.ContainsKey($entry.Key)) { continue }
$item = $entry.Value
$payload = [pscustomobject]@{
timestamp = [string]$item.createdAt
duration = 0
data = [pscustomobject]@{
eventType = 'process_start'
username = [string]$item.username
userId = [string]$item.userId
sessionId = [int]$item.sessionId
state = [string]$item.state
processId = [int]$item.processId
processName = [string]$item.processName
commandLine = [string]$item.commandLine
createdAt = [string]$item.createdAt
hostname = [string]$item.hostname
source = 'worktime-session-collector'
}
} | ConvertTo-Json -Depth 6 -Compress
try {
[void](Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId/heartbeat?pulsetime=1" -Json $payload)
}
catch {
Write-Verbose "Process start publish error: $($_.Exception.Message)"
}
}
$nowUtc = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
foreach ($entry in $Previous.GetEnumerator()) {
if ($Current.ContainsKey($entry.Key)) { continue }
$item = $entry.Value
$payload = [pscustomobject]@{
timestamp = $nowUtc
duration = 0
data = [pscustomobject]@{
eventType = 'process_stop'
username = [string]$item.username
userId = [string]$item.userId
sessionId = [int]$item.sessionId
state = [string]$item.state
processId = [int]$item.processId
processName = [string]$item.processName
commandLine = [string]$item.commandLine
createdAt = [string]$item.createdAt
hostname = [string]$item.hostname
source = 'worktime-session-collector'
}
} | ConvertTo-Json -Depth 6 -Compress
try {
[void](Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId/heartbeat?pulsetime=1" -Json $payload)
}
catch {
Write-Verbose "Process stop publish error: $($_.Exception.Message)"
}
}
}
# Main
$cfg = Get-Config -Path $ConfigPath
$hostValue = if ($Hostname -and $Hostname.Trim()) { $Hostname.Trim() } elseif ($cfg -and $cfg.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$cfg.awHostname)) { [string]$cfg.awHostname } elseif ($cfg -and $cfg.awHostname) { [string]$cfg.awHostname } else { [string]$env:COMPUTERNAME }
try { $apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port } catch { throw 'Invalid server configuration in config file.' }
$bucketId = 'aw-worktime-sessions_' + $hostValue
$sessionEventsBucketId = Get-SessionEventsBucketId -Config $cfg -HostnameValue $hostValue
$processEventsEnabled = Test-SessionProcessEventsEnabled -Config $cfg
$processStatePath = Get-ProcessStatePath -Config $cfg
$sleepSec = if ($PollSeconds -gt 0) { $PollSeconds } elseif ($cfg.collector -and $cfg.collector.pollSeconds) { [int]$cfg.collector.pollSeconds } else { 30 }
$pulse = [Math]::Max($sleepSec * 3, 30)
Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue
if ($processEventsEnabled) {
Ensure-Bucket -ApiBase $apiBase -BucketId $sessionEventsBucketId -HostnameValue $hostValue -ClientName 'aw-session-events' -BucketType 'aw.session.event'
$previousProcessState = Load-ProcessState -Path $processStatePath
}
else {
$previousProcessState = @{}
}
while ($true) {
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
@@ -265,6 +533,13 @@ while ($true) {
)
}
if ($processEventsEnabled) {
$currentProcessState = Get-SessionProcessSnapshot -Config $cfg -HostnameValue $hostValue -SessionRecords $records
Publish-SessionProcessEvents -ApiBase $apiBase -BucketId $sessionEventsBucketId -Previous $previousProcessState -Current $currentProcessState
Save-ProcessState -Path $processStatePath -Map $currentProcessState
$previousProcessState = $currentProcessState
}
foreach ($rec in $records) {
$canonicalUserId = Get-CanonicalUserId -Config $cfg -HostnameValue $hostValue -Username ([string]$rec.username)
$payloadObj = [PSCustomObject]@{