fix(aw): harden collectors and grafana exports
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 `
|
||||
|
||||
@@ -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 `
|
||||
|
||||
@@ -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 `
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 `
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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]@{
|
||||
|
||||
Reference in New Issue
Block a user