From 8a937b8f1712f0491a2eed60ad5bf1eecec4f1e0 Mon Sep 17 00:00:00 2001 From: PTah Date: Thu, 25 Jun 2026 09:09:54 +1000 Subject: [PATCH] fix: quarantine corrupt SAC spool files, flush newest first (2.1.6-SAC) Reject null-padded spool payloads to rejected/, read UTF-16 spool, process recent events first, and raise flush batch to 20 so daily reports are not blocked by legacy junk. Co-authored-by: Cursor --- Login_Monitor.ps1 | 4 +- Sac-Client.ps1 | 111 ++++++++++++++++++++++++++++++++++++++++++---- version.txt | 2 +- 3 files changed, 106 insertions(+), 11 deletions(-) diff --git a/Login_Monitor.ps1 b/Login_Monitor.ps1 index 0ba863e..bf39517 100644 --- a/Login_Monitor.ps1 +++ b/Login_Monitor.ps1 @@ -90,7 +90,7 @@ $script:SkipLogDetailLimit = 15 # строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах). # Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только # исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1). -$ScriptVersion = "2.1.5-SAC" +$ScriptVersion = "2.1.6-SAC" # Логи (все под InstallRoot) $LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log" @@ -5016,7 +5016,7 @@ function Start-LoginMonitor { $nextReportCheck = Check-AndSendDailyReport } if ($script:SacClientLoaded) { - Invoke-SacFlushSpool -MaxFiles 5 | Out-Null + Invoke-SacFlushSpool -MaxFiles 20 | Out-Null if ((Get-SacNormalizedMode) -ne 'off' -and $now -ge $script:NextSacCommandPollTime) { Invoke-SacProcessPendingCommands | Out-Null $script:NextSacCommandPollTime = $now.AddSeconds($SacCommandPollIntervalSec) diff --git a/Sac-Client.ps1 b/Sac-Client.ps1 index a6cfa36..ff6ddd3 100644 --- a/Sac-Client.ps1 +++ b/Sac-Client.ps1 @@ -474,6 +474,84 @@ function Move-SacSpoolToRejected { Move-Item -LiteralPath $src -Destination $dst -Force -ErrorAction SilentlyContinue } +function Test-SacGuidString { + param([string]$Value) + if ([string]::IsNullOrWhiteSpace($Value)) { return $false } + return ($Value.Trim() -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') +} + +function Get-SacEventIdFromSpoolFileName { + param([string]$Path) + if ([string]::IsNullOrWhiteSpace($Path)) { return $null } + $base = [System.IO.Path]::GetFileNameWithoutExtension($Path) + if (Test-SacGuidString -Value $base) { + return $base.ToLowerInvariant() + } + return $null +} + +function Get-SacEventIdFromJsonText { + param([string]$JsonText) + if ([string]::IsNullOrWhiteSpace($JsonText)) { return $null } + if ($JsonText -match '"event_id"\s*:\s*"([0-9a-fA-F-]{36})"') { + return $Matches[1].ToLowerInvariant() + } + return $null +} + +function Test-SacSpoolBytesCorrupt { + param([byte[]]$Bytes) + if ($null -eq $Bytes -or $Bytes.Length -lt 2) { return $true } + # UTF-16/UTF-32 с нулевым стартом или «пустой» spool — не JSON (см. hex 00 00 00 00). + if ($Bytes[0] -eq 0 -and $Bytes[1] -eq 0) { return $true } + return $false +} + +function Read-SacSpoolFileBytes { + param([Parameter(Mandatory = $true)][string]$Path) + return [System.IO.File]::ReadAllBytes($Path) +} + +function Read-SacSpoolFileText { + param([Parameter(Mandatory = $true)][string]$Path) + $bytes = Read-SacSpoolFileBytes -Path $Path + if (Test-SacSpoolBytesCorrupt -Bytes $bytes) { return $null } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { + return [System.Text.Encoding]::Unicode.GetString($bytes) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0x7B -and $Bytes[1] -eq 0) { + return [System.Text.Encoding]::Unicode.GetString($bytes) + } + $utf8 = New-Object System.Text.UTF8Encoding $false + return $utf8.GetString($bytes) +} + +function Move-SacSpoolFileToRejected { + param( + [Parameter(Mandatory = $true)][string]$SpoolFilePath, + [string]$Reason = '' + ) + $eventId = Get-SacEventIdFromSpoolFileName -Path $SpoolFilePath + if ($Reason) { + $label = if ($eventId) { "$eventId.json" } else { [System.IO.Path]::GetFileName($SpoolFilePath) } + Write-SacLog "WARN: SAC spool → rejected ($Reason): $label" + } + if ($eventId) { + Move-SacSpoolToRejected -EventId $eventId + return + } + $dir = Get-SacSpoolDirResolved + $name = [System.IO.Path]::GetFileName($SpoolFilePath) + $src = Join-Path $dir $name + if (-not (Test-Path -LiteralPath $src)) { return } + $rejDir = Join-Path $dir 'rejected' + if (-not (Test-Path -LiteralPath $rejDir)) { + New-Item -ItemType Directory -Path $rejDir -Force | Out-Null + } + $dst = Join-Path $rejDir $name + Move-Item -LiteralPath $src -Destination $dst -Force -ErrorAction SilentlyContinue +} + function Get-SacPostBodyBytes { param([string]$JsonText) $bytes = Get-SacUtf8Bytes -Text $JsonText @@ -588,7 +666,10 @@ function Write-SacPostBodyDiagnostic { } function Invoke-SacPostPayload { - param([string]$JsonBody) + param( + [string]$JsonBody, + [string]$SpoolFilePath = '' + ) if (-not (Test-SacConfigured)) { return $false } if (-not (Test-SacShouldAttemptSend)) { return $false } @@ -597,11 +678,15 @@ function Invoke-SacPostPayload { if ([string]::IsNullOrWhiteSpace($ingest)) { return $false } $jsonText = Repair-SacJsonText -Text (Get-SacSingleString -Value $JsonBody -Label 'spool payload') - if ($jsonText -notmatch '"event_id"\s*:\s*"([0-9a-fA-F-]{36})"') { - Write-SacLog 'WARN: SAC JSON has no event_id (uuid); skip POST' + $eventId = Get-SacEventIdFromJsonText -JsonText $jsonText + if (-not $eventId) { + if (-not [string]::IsNullOrWhiteSpace($SpoolFilePath)) { + Move-SacSpoolFileToRejected -SpoolFilePath $SpoolFilePath -Reason 'no event_id in JSON body' + } else { + Write-SacLog 'WARN: SAC JSON has no event_id (uuid); skip POST' + } return $false } - $eventId = $Matches[1] $eventType = 'unknown' if ($jsonText -match '"type"\s*:\s*"([^"]+)"') { $eventType = $Matches[1] @@ -838,15 +923,25 @@ function Invoke-SacFlushSpool { $dir = Get-SacSpoolDirResolved if (-not (Test-Path -LiteralPath $dir)) { return } - $files = @(Get-ChildItem -LiteralPath $dir -Filter '*.json' -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime) + # Сначала свежие события (daily report); битые записи уходят в rejected и не блокируют очередь. + $files = @(Get-ChildItem -LiteralPath $dir -Filter '*.json' -File -ErrorAction SilentlyContinue | + Sort-Object LastWriteTime -Descending) $count = 0 foreach ($f in $files) { $count++ if ($count -gt $MaxFiles) { break } try { - $utf8 = New-Object System.Text.UTF8Encoding $false - $json = [System.IO.File]::ReadAllText($f.FullName, $utf8) - Invoke-SacPostPayload -JsonBody $json | Out-Null + $bytes = Read-SacSpoolFileBytes -Path $f.FullName + if (Test-SacSpoolBytesCorrupt -Bytes $bytes) { + Move-SacSpoolFileToRejected -SpoolFilePath $f.FullName -Reason 'null or empty payload' + continue + } + $json = Read-SacSpoolFileText -Path $f.FullName + if ([string]::IsNullOrWhiteSpace($json)) { + Move-SacSpoolFileToRejected -SpoolFilePath $f.FullName -Reason 'unreadable payload' + continue + } + Invoke-SacPostPayload -JsonBody $json -SpoolFilePath $f.FullName | Out-Null } catch { Write-SacLog "WARN: SAC spool flush failed for $($f.Name): $($_.Exception.Message)" } diff --git a/version.txt b/version.txt index f14000d..6cf3433 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.1.5-SAC +2.1.6-SAC