diff --git a/windows/aw-standalone-service.ps1 b/windows/aw-standalone-service.ps1 index a2cc659..58392f3 100644 --- a/windows/aw-standalone-service.ps1 +++ b/windows/aw-standalone-service.ps1 @@ -69,15 +69,40 @@ while ($true) { try { $cfg = Get-Config -Path $ConfigPath $paths = $cfg.paths + $collectors = $cfg.collectors + $isSession0 = ([System.Diagnostics.Process]::GetCurrentProcess().SessionId -eq 0) - Start-CollectorIfNeeded -ScriptPath ([string]$paths.collectorScript) -ConfigPath $ConfigPath + # In Session 0 (SYSTEM) many collectors that rely on interactive user context (browsers, Outlook) + # will crash/exit immediately. Default to starting only collectors that can run headless. + $startBrowser = $true + $startFileOps = $true + $startEmail = $true + $startWorktime = $true + if ($collectors) { + if ($collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { $startFileOps = [bool]$collectors.fileOpsEnabled } + if ($collectors.PSObject.Properties.Name -contains 'emailEnabled') { $startEmail = [bool]$collectors.emailEnabled } + } + if ($isSession0) { + $startBrowser = $false + $startEmail = $false + } + + if ($startBrowser) { + Start-CollectorIfNeeded -ScriptPath ([string]$paths.collectorScript) -ConfigPath $ConfigPath + } Start-CollectorIfNeeded -ScriptPath ([string]$paths.endpointCollectorScript) -ConfigPath $ConfigPath - Start-CollectorIfNeeded -ScriptPath ([string]$paths.fileCollectorScript) -ConfigPath $ConfigPath + if ($startFileOps) { + Start-CollectorIfNeeded -ScriptPath ([string]$paths.fileCollectorScript) -ConfigPath $ConfigPath + } if ($paths.PSObject.Properties.Name -contains 'emailCollectorScript') { - Start-CollectorIfNeeded -ScriptPath ([string]$paths.emailCollectorScript) -ConfigPath $ConfigPath + if ($startEmail) { + Start-CollectorIfNeeded -ScriptPath ([string]$paths.emailCollectorScript) -ConfigPath $ConfigPath + } } if ($paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { - Start-CollectorIfNeeded -ScriptPath ([string]$paths.sessionCollectorScript) -ConfigPath $ConfigPath + if ($startWorktime) { + Start-CollectorIfNeeded -ScriptPath ([string]$paths.sessionCollectorScript) -ConfigPath $ConfigPath + } } } catch { @@ -85,4 +110,3 @@ while ($true) { } Start-Sleep -Seconds ([Math]::Max($LoopSeconds, 5)) } - diff --git a/windows/dlp-endpoint-signals-collector.ps1 b/windows/dlp-endpoint-signals-collector.ps1 index ad4f077..2743fc1 100644 --- a/windows/dlp-endpoint-signals-collector.ps1 +++ b/windows/dlp-endpoint-signals-collector.ps1 @@ -13,6 +13,13 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +# Ensure HttpClient is available (Windows PowerShell 5 may not auto-load it) +try { + Add-Type -AssemblyName System.Net.Http +} +catch { +} + function Get-DeploymentConfig { param([string]$Path) if ($Path -and (Test-Path -LiteralPath $Path)) { @@ -39,8 +46,54 @@ function Invoke-AwJsonPost { [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 -TimeoutSec 15 -DisableKeepAlive | Out-Null + try { + $bytes = [System.Text.Encoding]::UTF8.GetBytes($Json) + $req = [System.Net.HttpWebRequest]::Create($Uri) + $req.Method = 'POST' + $req.ContentType = 'application/json' + $req.Accept = 'application/json' + $req.KeepAlive = $false + $req.Timeout = 15000 + $req.ReadWriteTimeout = 15000 + $req.ContentLength = $bytes.Length + + $stream = $req.GetRequestStream() + try { $stream.Write($bytes, 0, $bytes.Length) } finally { $stream.Close() } + + $resp = $req.GetResponse() + try { + # read body for debugging, but discard on success + $rs = $resp.GetResponseStream() + if ($rs) { $sr = New-Object System.IO.StreamReader($rs); $null = $sr.ReadToEnd(); $sr.Close() } + } finally { + $resp.Close() + } + return + } + catch [System.Net.WebException] { + $status = $null + $body = '' + try { + if ($_.Exception.Response) { + try { $status = [int]$_.Exception.Response.StatusCode } catch {} + $rs = $_.Exception.Response.GetResponseStream() + if ($rs) { $sr = New-Object System.IO.StreamReader($rs); $body = $sr.ReadToEnd(); $sr.Close() } + } + } catch {} + + # aw-server-rust may return 304 for idempotent bucket create. Treat it as OK. + if ($status -eq 304) { + Write-EndpointLog ("POST bucket exists (304): uri={0}" -f $Uri) + return + } + + Write-EndpointLog ("POST failed: uri={0} status={1} err={2} body={3}" -f $Uri, $status, $_.Exception.Message, $body) + throw + } + catch { + Write-EndpointLog ("POST error: uri={0} err={1}" -f $Uri, $_.Exception.Message) + throw + } } function Ensure-Bucket { @@ -54,13 +107,39 @@ function Ensure-Bucket { return } + if ($script:KnownBuckets.ContainsKey($BucketId)) { + return + } + + # Fast-path: if bucket already exists, don't POST. + try { + Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" -TimeoutSec 10 -DisableKeepAlive -ErrorAction Stop | Out-Null + Write-EndpointLog ("bucket ok (GET): {0}" -f $BucketId) + $script:KnownBuckets[$BucketId] = $true + return + } + catch { + Write-EndpointLog ("bucket GET failed: {0} err={1}" -f $BucketId, $_.Exception.Message) + } + $body = @{ client = $ClientName type = $BucketType hostname = $script:Hostname } | ConvertTo-Json -Compress - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body + try { + Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body + } + catch { + # If create failed (race), verify it exists now. + try { + Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" -TimeoutSec 10 -DisableKeepAlive | Out-Null + } + catch { + throw + } + } $script:KnownBuckets[$BucketId] = $true } @@ -333,11 +412,17 @@ function Get-ClipboardTextSafe { Write-EndpointLog ("clipboard direct read failed: {0}" -f $_.Exception.Message) } + # Clipboard is not reliably accessible from Session 0 (SYSTEM). Avoid noisy thread hacks there. + if ($script:SessionId -eq 0) { + return $null + } + # Fallback: read clipboard in a dedicated STA thread for RDP/user-session edge cases. try { Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null $result = [string]::Empty - $thread = [System.Threading.Thread]{ + $script:__aw_clip = $null + $threadStart = [System.Threading.ThreadStart]{ try { $script:__aw_clip = [System.Windows.Forms.Clipboard]::GetText() } @@ -345,10 +430,13 @@ function Get-ClipboardTextSafe { $script:__aw_clip = $null } } + $thread = New-Object System.Threading.Thread($threadStart) $thread.SetApartmentState([System.Threading.ApartmentState]::STA) $thread.Start() $thread.Join(3000) | Out-Null - if ($thread.IsAlive) { $thread.Abort() } + if ($thread.IsAlive) { + try { $thread.Abort() } catch {} + } $result = [string]$script:__aw_clip Remove-Variable -Name __aw_clip -Scope Script -ErrorAction SilentlyContinue return $result @@ -391,9 +479,11 @@ function Load-DlpPolicy { } if ($raw.endpoint) { - if ($raw.endpoint.clipboard) { $script:Policy.endpoint.clipboard = @($raw.endpoint.clipboard) } - if ($raw.endpoint.usb) { $script:Policy.endpoint.usb = @($raw.endpoint.usb) } - if ($raw.endpoint.print) { $script:Policy.endpoint.print = @($raw.endpoint.print) } + $props = @() + try { $props = @($raw.endpoint.PSObject.Properties.Name) } catch { $props = @() } + if ($props -contains 'clipboard' -and $raw.endpoint.clipboard) { $script:Policy.endpoint.clipboard = @($raw.endpoint.clipboard) } + if ($props -contains 'usb' -and $raw.endpoint.usb) { $script:Policy.endpoint.usb = @($raw.endpoint.usb) } + if ($props -contains 'print' -and $raw.endpoint.print) { $script:Policy.endpoint.print = @($raw.endpoint.print) } } } catch { diff --git a/windows/worktime-session-collector.ps1 b/windows/worktime-session-collector.ps1 index ba72ca7..2b3eb38 100644 --- a/windows/worktime-session-collector.ps1 +++ b/windows/worktime-session-collector.ps1 @@ -58,7 +58,15 @@ function Get-Config { } try { $bytes = [System.IO.File]::ReadAllBytes($Path) - $text = Decode-Bytes-Auto -Bytes $bytes + # Config is JSON. Prefer deterministic BOM-based decoding over heuristics. + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { + $text = [System.Text.Encoding]::UTF8.GetString($bytes) + } elseif ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { + $text = [System.Text.Encoding]::Unicode.GetString($bytes) + } else { + $text = [System.Text.Encoding]::UTF8.GetString($bytes) + } + $text = $text -replace '^\uFEFF', '' return $text | ConvertFrom-Json -ErrorAction Stop } catch { @@ -137,7 +145,9 @@ function Parse-SessionLines { if (-not $Lines) { return $records } $startIndex = 0 - if ($Lines.Count -gt 0 -and $Lines[0] -match '\b(USERNAME|Имя|Имя пользователя|Имя_пользователя)\b') { $startIndex = 1 } + # NOTE: Keep this script ASCII-only to stay compatible with Windows PowerShell 5 + # when the file is UTF-8 without BOM. Avoid Cyrillic literals in regex patterns. + if ($Lines.Count -gt 0 -and $Lines[0] -match '\b(USERNAME|UserName|USER)\b') { $startIndex = 1 } for ($i = $startIndex; $i -lt $Lines.Count; $i++) { $line = $Lines[$i].Trim() @@ -163,7 +173,9 @@ function Test-SessionIsActive { param([string]$State) if (-not $State) { return $false } $s = $State.Trim().ToLowerInvariant() - return ($s -match 'active') -or ($s -match 'актив') + # Match English "active" and Russian "актив*" without embedding Cyrillic. + # "актив" = \u0430\u043A\u0442\u0438\u0432 + return ($s -match 'active') -or ($s -match '\u0430\u043a\u0442\u0438\u0432') } # Main