diff --git a/aw-server/aw-worktime-ui-bridge.py b/aw-server/aw-worktime-ui-bridge.py index d1a4cc3..4741d70 100644 --- a/aw-server/aw-worktime-ui-bridge.py +++ b/aw-server/aw-worktime-ui-bridge.py @@ -3,7 +3,7 @@ import json import os import urllib.error import urllib.request -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone AW_URL = os.environ.get("AW_SERVER_URL", "http://127.0.0.1:5600") @@ -70,25 +70,48 @@ def to_iso_utc(ts): return ts.replace("+00:00", "Z") +def parse_iso_utc(ts: str): + if ts.endswith("Z"): + ts = ts[:-1] + "+00:00" + return datetime.fromisoformat(ts).astimezone(timezone.utc) + + def build_window_title(users, active_count): if not users: return "RDP idle" return f"RDP active ({active_count}): " + ", ".join(users) +def _is_session_active(row_data): + if isinstance(row_data.get("active"), bool): + return row_data.get("active") + state = str(row_data.get("state", "")).strip().lower() + return state in {"active", "активно"} + + def transform(events): out_afk = [] out_win = [] last_ts = None + grouped = {} for e in events: ts = e.get("timestamp") if not ts: continue - duration = float(e.get("duration", 0.0)) - data = e.get("data") or {} - active_users = data.get("activeUsers") or [] - active_count = int(data.get("activeCount", len(active_users))) + grouped.setdefault(ts, []).append(e) + + for ts in sorted(grouped.keys()): + rows = grouped[ts] + duration = max(float(r.get("duration", 0.0)) for r in rows) + active_users = [] + for r in rows: + data = r.get("data") or {} + user = str(data.get("username", "")).strip() + if user and _is_session_active(data): + active_users.append(user) + active_users = sorted(set(active_users)) + active_count = len(active_users) is_active = active_count > 0 afk_data = {"status": "not-afk" if is_active else "afk", "source": "aw-worktime-ui-bridge"} @@ -112,18 +135,29 @@ def main(): ensure_bucket(AFK_BUCKET, "afkstatus", "aw-worktime-ui-bridge") ensure_bucket(WINDOW_BUCKET, "currentwindow", "aw-worktime-ui-bridge") - query = { - "query": [ - "events = query_bucket(find_bucket($bid));", - "RETURN = sort_by_timestamp(events);", - ], - "timeperiods": [[last_ts, to_iso_utc(datetime.now(timezone.utc).isoformat())]], - } - rows = _req("POST", f"/api/0/query/?bid={SESSIONS_BUCKET}", query) or [] - if not rows or not rows[0]: + now_utc = datetime.now(timezone.utc) + recent = _req("GET", f"/api/0/buckets/{SESSIONS_BUCKET}/events?limit=5000") or [] + if not recent: + return + + try: + last_dt = parse_iso_utc(last_ts) + except Exception: + last_dt = datetime(1970, 1, 1, tzinfo=timezone.utc) + + events = [] + for e in recent: + ts = e.get("timestamp") + if not ts: + continue + try: + if parse_iso_utc(ts) > last_dt: + events.append(e) + except Exception: + continue + if not events: return - events = rows[0] afk_events, win_events, new_last_ts = transform(events) if not afk_events or not win_events or not new_last_ts: return 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 4ac7fa3..b286e48 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 @@ -1,4 +1,4 @@ -[CmdletBinding()] +[CmdletBinding()] param( [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', [string]$ServerHost, @@ -516,35 +516,6 @@ function Test-LooksLikeMojibakeQuestionMarks { return $Value -match '\?{2,}' } -function Test-IsGenericDocumentName { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return $true } - $generic = @( - '^\s*Печать документа\s*$', - '^\s*Print Document\s*$', - '^\s*Document\s*$', - '^\s*Документ\s*$', - '^\s*Remote Downlevel Document\s*$', - '^\s*Local Downlevel Document\s*$', - '^\s*Untitled\s*$', - '^\s*Без имени\s*$', - '^\s*Без названия\s*$' - ) - foreach ($pattern in $generic) { - if ($Value -match $pattern) { return $true } - } - return $false -} - -function Test-NeedsBetterDocumentName { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return $true } - if (Test-LooksLikeMojibakeQuestionMarks -Value $Value) { return $true } - if (Test-IsGenericDocumentName -Value $Value) { return $true } - if ($Value -match '^[0-9]+$') { return $true } - return $false -} - function Normalize-OwnerForMatch { param([AllowNull()][string]$Value) if ([string]::IsNullOrWhiteSpace($Value)) { return '' } @@ -631,7 +602,7 @@ function Get-PrintServiceDocumentFallback { ) $preferred = [string]$EventSummary.DocumentName - if (-not (Test-NeedsBetterDocumentName -Value $preferred)) { + if (-not (Test-LooksLikeMojibakeQuestionMarks -Value $preferred) -and $preferred -notmatch '^[0-9]+$') { return $preferred } @@ -644,13 +615,17 @@ function Get-PrintServiceDocumentFallback { if ($candidate -eq $preferred) { continue } if ($Owner -and $candidate -like "*$Owner*") { continue } if ($PrinterName -and $candidate -like "*$PrinterName*") { continue } - if (Test-NeedsBetterDocumentName -Value $candidate) { continue } + if (Test-LooksLikeMojibakeQuestionMarks -Value $candidate) { continue } if ($candidate -match '[\\/:]' -and $candidate -match '\.[A-Za-z0-9]{1,8}$') { $pathCandidates.Add($candidate) continue } + if ($candidate -match '^[0-9]+$') { + continue + } + $textCandidates.Add($candidate) } @@ -780,6 +755,8 @@ $script:SeenPrintJob = @{} $script:SeenPrintEvent = @{} $script:LastClipboardHash = $null $script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30) +$script:SelfTestIntervalSeconds = [Math]::Max($resolvedPollSeconds * 10, 60) +$script:LastSelfTestAt = [datetime]::MinValue $script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled $script:LogPath = $resolvedLogPath $script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot @@ -791,6 +768,15 @@ Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase) while ($true) { try { + $nowUtc = (Get-Date).ToUniversalTime() + if (($nowUtc - $script:LastSelfTestAt).TotalSeconds -ge $script:SelfTestIntervalSeconds) { + Send-EndpointSignalHeartbeat -SignalType 'self_test' -Data @{ + collector = 'dlp-endpoint-signals' + policyEnabled = [bool]$script:Policy.defaults.enabled + } + $script:LastSelfTestAt = $nowUtc + } + if (-not $script:Policy.defaults.enabled) { Start-Sleep -Seconds $resolvedPollSeconds continue @@ -848,12 +834,12 @@ while ($true) { if ($script:SeenPrintJob.ContainsKey($jobId)) { continue } $script:SeenPrintJob[$jobId] = (Get-Date).ToUniversalTime() - $printerName = Normalize-PrinterForMatch -Value ([string]$job.Name) + $printerName = [string]$job.Name $documentName = [string]$job.Document $owner = [string]$job.Owner $documentNameOriginal = $documentName - if (Test-NeedsBetterDocumentName -Value $documentName) { + if (Test-LooksLikeMojibakeQuestionMarks -Value $documentName) { $eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -Owner $owner -PrinterName $printerName if ($eventDocumentName) { $documentName = $eventDocumentName diff --git a/windows/dlp-endpoint-signals-collector.ps1 b/windows/dlp-endpoint-signals-collector.ps1 index 7141937..0b2f810 100644 --- a/windows/dlp-endpoint-signals-collector.ps1 +++ b/windows/dlp-endpoint-signals-collector.ps1 @@ -1,4 +1,4 @@ -[CmdletBinding()] +[CmdletBinding()] param( [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', [string]$ServerHost, @@ -54,32 +54,13 @@ function Ensure-Bucket { return } - try { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null - $script:KnownBuckets[$BucketId] = $true - return - } - catch { - } - $body = @{ client = $ClientName type = $BucketType hostname = $script:Hostname } | ConvertTo-Json -Compress - try { - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - } - catch { - try { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null - } - catch { - Write-EndpointLog "Bucket create/check failed for ${BucketId}: $($_.Exception.Message)" - throw - } - } + Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body $script:KnownBuckets[$BucketId] = $true } @@ -243,10 +224,6 @@ function Show-EnforcementNotification { [Parameter(Mandatory = $true)][string]$Title, [Parameter(Mandatory = $true)][string]$Body ) - if ($script:HeadlessMode) { - Write-EndpointLog ("headless mode: skip notification title={0}" -f $Title) - return $false - } try { Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue $icon = New-Object System.Windows.Forms.NotifyIcon @@ -258,11 +235,9 @@ function Show-EnforcementNotification { $icon.ShowBalloonTip(5000) Start-Sleep -Milliseconds 200 $icon.Dispose() - return $true } catch { Write-EndpointLog ("notification failed: {0}" -f $_.Exception.Message) - return $false } } @@ -449,9 +424,6 @@ function Evaluate-ClipboardRules { [string]$ClipboardText, [string]$ClipboardHash ) - if ([string]::IsNullOrEmpty($ClipboardText) -or [string]::IsNullOrEmpty($ClipboardHash)) { - return - } foreach ($rule in @($script:Policy.endpoint.clipboard)) { if (-not $rule) { continue } @@ -482,13 +454,8 @@ function Evaluate-ClipboardRules { $enforced = $false if ($action -eq 'block') { - if ($script:HeadlessMode) { - Write-EndpointLog ("headless fallback: clipboard rule={0} requires block, skipped interactive enforcement" -f $ruleId) - } - else { - $enforced = Invoke-ClipboardEnforcement - [void](Show-EnforcementNotification -Title 'DLP: буфер обмена очищен' -Body $message) - } + $enforced = Invoke-ClipboardEnforcement + Show-EnforcementNotification -Title 'DLP: буфер обмена очищен' -Body $message } Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{ @@ -522,13 +489,8 @@ function Evaluate-UsbRules { $enforced = $false if ($action -eq 'block') { - if ($script:HeadlessMode) { - Write-EndpointLog ("headless fallback: usb rule={0} requires block, skipped interactive enforcement drive={1}" -f $ruleId, $DriveLetter) - } - else { - $enforced = Invoke-UsbWriteBlockEnforcement -DriveLetter $DriveLetter - [void](Show-EnforcementNotification -Title 'DLP: USB заблокирован для записи' -Body $message) - } + $enforced = Invoke-UsbWriteBlockEnforcement -DriveLetter $DriveLetter + Show-EnforcementNotification -Title 'DLP: USB заблокирован для записи' -Body $message } Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{ @@ -572,13 +534,8 @@ function Evaluate-PrintRules { $enforced = $false if ($action -eq 'block') { - if ($script:HeadlessMode) { - Write-EndpointLog ("headless fallback: print rule={0} requires block, skipped interactive enforcement printer={1}" -f $ruleId, $PrinterName) - } - else { - $enforced = Invoke-PrintJobEnforcement -PrinterName $PrinterName -DocumentName $DocumentName -Owner $Owner - [void](Show-EnforcementNotification -Title 'DLP: печать заблокирована' -Body $message) - } + $enforced = Invoke-PrintJobEnforcement -PrinterName $PrinterName -DocumentName $DocumentName -Owner $Owner + Show-EnforcementNotification -Title 'DLP: печать заблокирована' -Body $message } Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'print_job' -Data @{ @@ -597,35 +554,6 @@ function Test-LooksLikeMojibakeQuestionMarks { return $Value -match '\?{2,}' } -function Test-IsGenericDocumentName { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return $true } - $generic = @( - '^\s*Печать документа\s*$', - '^\s*Print Document\s*$', - '^\s*Document\s*$', - '^\s*Документ\s*$', - '^\s*Remote Downlevel Document\s*$', - '^\s*Local Downlevel Document\s*$', - '^\s*Untitled\s*$', - '^\s*Без имени\s*$', - '^\s*Без названия\s*$' - ) - foreach ($pattern in $generic) { - if ($Value -match $pattern) { return $true } - } - return $false -} - -function Test-NeedsBetterDocumentName { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return $true } - if (Test-LooksLikeMojibakeQuestionMarks -Value $Value) { return $true } - if (Test-IsGenericDocumentName -Value $Value) { return $true } - if ($Value -match '^[0-9]+$') { return $true } - return $false -} - function Normalize-OwnerForMatch { param([AllowNull()][string]$Value) if ([string]::IsNullOrWhiteSpace($Value)) { return '' } @@ -712,7 +640,7 @@ function Get-PrintServiceDocumentFallback { ) $preferred = [string]$EventSummary.DocumentName - if (-not (Test-NeedsBetterDocumentName -Value $preferred)) { + if (-not (Test-LooksLikeMojibakeQuestionMarks -Value $preferred) -and $preferred -notmatch '^[0-9]+$') { return $preferred } @@ -725,13 +653,17 @@ function Get-PrintServiceDocumentFallback { if ($candidate -eq $preferred) { continue } if ($Owner -and $candidate -like "*$Owner*") { continue } if ($PrinterName -and $candidate -like "*$PrinterName*") { continue } - if (Test-NeedsBetterDocumentName -Value $candidate) { continue } + if (Test-LooksLikeMojibakeQuestionMarks -Value $candidate) { continue } if ($candidate -match '[\\/:]' -and $candidate -match '\.[A-Za-z0-9]{1,8}$') { $pathCandidates.Add($candidate) continue } + if ($candidate -match '^[0-9]+$') { + continue + } + $textCandidates.Add($candidate) } @@ -830,7 +762,6 @@ function Get-BetterDocumentNameFromPrintServiceEvents { } } catch { - Write-EndpointLog ("printservice fallback failed: {0}" -f $_.Exception.Message) } return $null @@ -862,21 +793,28 @@ $script:SeenPrintJob = @{} $script:SeenPrintEvent = @{} $script:LastClipboardHash = $null $script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30) +$script:SelfTestIntervalSeconds = [Math]::Max($resolvedPollSeconds * 10, 60) +$script:LastSelfTestAt = [datetime]::MinValue $script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled $script:LogPath = $resolvedLogPath $script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot $script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled $script:ScreenshotTypesLoaded = $false -$script:HeadlessMode = ($env:SESSIONNAME -eq 'Service') -or (-not [Environment]::UserInteractive) Load-DlpPolicy -Path $resolvedPolicyPath Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase) -if ($script:HeadlessMode) { - Write-EndpointLog "headless mode enabled: enforcement UI is disabled, incident heartbeat and logs only" -} while ($true) { try { + $nowUtc = (Get-Date).ToUniversalTime() + if (($nowUtc - $script:LastSelfTestAt).TotalSeconds -ge $script:SelfTestIntervalSeconds) { + Send-EndpointSignalHeartbeat -SignalType 'self_test' -Data @{ + collector = 'dlp-endpoint-signals' + policyEnabled = [bool]$script:Policy.defaults.enabled + } + $script:LastSelfTestAt = $nowUtc + } + if (-not $script:Policy.defaults.enabled) { Start-Sleep -Seconds $resolvedPollSeconds continue @@ -897,7 +835,6 @@ while ($true) { } } catch { - Write-EndpointLog ("clipboard poll failed: {0}" -f $_.Exception.Message) } try { @@ -925,7 +862,6 @@ while ($true) { } } catch { - Write-EndpointLog ("usb poll failed: {0}" -f $_.Exception.Message) } try { @@ -936,33 +872,23 @@ while ($true) { if ($script:SeenPrintJob.ContainsKey($jobId)) { continue } $script:SeenPrintJob[$jobId] = (Get-Date).ToUniversalTime() - $printerName = Normalize-PrinterForMatch -Value ([string]$job.Name) + $printerName = [string]$job.Name $documentName = [string]$job.Document $owner = [string]$job.Owner $documentNameOriginal = $documentName - if (Test-NeedsBetterDocumentName -Value $documentName) { + if (Test-LooksLikeMojibakeQuestionMarks -Value $documentName) { $eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -Owner $owner -PrinterName $printerName if ($eventDocumentName) { $documentName = $eventDocumentName } } - $printDocumentNorm = if ($documentName) { [string]$documentName } else { '' } - $printSignalKey = ('{0}|{1}|{2}|{3}' -f - (Normalize-PrinterForMatch -Value $printerName), - (Normalize-OwnerForMatch -Value $owner), - $printDocumentNorm.ToLowerInvariant(), - 'print_job') - if (-not (Should-EmitByCooldown -Fingerprint $printSignalKey -CooldownSeconds 90)) { - continue - } Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ printerName = $printerName documentName = $documentName documentNameOriginal = $documentNameOriginal owner = $owner - eventSource = 'win32_printjob' } Evaluate-PrintRules -PrinterName $printerName -DocumentName $documentName -Owner $owner } @@ -976,7 +902,6 @@ while ($true) { } } catch { - Write-EndpointLog ("printjob poll failed: {0}" -f $_.Exception.Message) } try { @@ -1004,17 +929,6 @@ while ($true) { continue } - $effectiveDocument = if ($resolvedDocument) { [string]$resolvedDocument } else { [string]$documentName } - $printSignalKey = ('{0}|{1}|{2}|{3}' -f - (Normalize-PrinterForMatch -Value $printerName), - (Normalize-OwnerForMatch -Value $owner), - $effectiveDocument.ToLowerInvariant(), - 'print_job') - if (-not (Should-EmitByCooldown -Fingerprint $printSignalKey -CooldownSeconds 90)) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'skip' -MatchReason 'dedupe-recent-printjob' -ResolvedDocument $resolvedDocument - continue - } - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ printerName = $printerName documentName = if ($resolvedDocument) { $resolvedDocument } else { $documentName } @@ -1035,7 +949,6 @@ while ($true) { } } catch { - Write-EndpointLog ("printservice poll failed: {0}" -f $_.Exception.Message) } } catch {