fix: restore RD Gateway 302/303 polling and SAC ingest 1.2.35-SAC

Fix poll cursor skipping events at time boundary, add 60min lookback on start, parse RU gateway messages, and map 303 disconnect to rdg.connection.disconnected.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-01 08:14:37 +10:00
parent f970e284ac
commit 543369832a
2 changed files with 103 additions and 15 deletions
+102 -14
View File
@@ -82,7 +82,7 @@ $script:MonitorStopRequested = $false
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах). # строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только # Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1). # исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
$ScriptVersion = "1.2.34-SAC" $ScriptVersion = "1.2.35-SAC"
# Логи (все под InstallRoot) # Логи (все под InstallRoot)
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log" $LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
@@ -118,6 +118,8 @@ $LastReportFile = Join-Path $script:InstallRoot "Logs\last_daily_report.txt"
$EnableRDGatewayMonitoring = $true $EnableRDGatewayMonitoring = $true
$RDGatewayLogName = "Microsoft-Windows-TerminalServices-Gateway/Operational" $RDGatewayLogName = "Microsoft-Windows-TerminalServices-Gateway/Operational"
$RDGatewayEvents = @(302, 303) $RDGatewayEvents = @(302, 303)
# При старте цикла опроса — сколько минут назад читать 302/303 (чтобы не пропустить события до запуска монитора).
$GatewayEventsLookbackMinutes = 60
# RDP Remote Connection Manager (workstations): User authentication succeeded — событие 1149 # RDP Remote Connection Manager (workstations): User authentication succeeded — событие 1149
$RcmLogName = "Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational" $RcmLogName = "Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational"
@@ -2792,6 +2794,22 @@ function Get-FailedLogonRateLimitDecision4625 {
} }
} }
function Update-MonitorPollCursor {
param(
[datetime]$CurrentCursor,
[array]$Events
)
if (-not $Events -or @($Events).Count -eq 0) {
return $CurrentCursor
}
$afterCursor = @($Events | Where-Object { $_.TimeCreated -gt $CurrentCursor })
if ($afterCursor.Count -eq 0) {
return $CurrentCursor
}
$maxTime = ($afterCursor | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum)
return $maxTime.AddMilliseconds(500)
}
function Test-RDGatewayLog { function Test-RDGatewayLog {
try { try {
$logExists = Get-WinEvent -ListLog $RDGatewayLogName -ErrorAction SilentlyContinue $logExists = Get-WinEvent -ListLog $RDGatewayLogName -ErrorAction SilentlyContinue
@@ -2805,6 +2823,26 @@ function Test-RDGatewayLog {
return $false return $false
} }
function Get-RDGatewayMessageFallback {
param([string]$Message)
if ([string]::IsNullOrWhiteSpace($Message)) { return $null }
$result = @{}
if ($Message -match '(?i)Пользователь\s+"([^"]+)"|User\s+"([^"]+)"') {
$result.Username = if ($Matches[1]) { $Matches[1] } else { $Matches[2] }
}
if ($Message -match '(?i)(?:компьютере|computer)\s+"([0-9a-fA-F:\.]+)"') {
$result.ExternalIP = $Matches[1]
}
if ($Message -match '(?i)(?:ресурсу|resource)\s*:\s*"([^"]+)"|(?:ресурсу|resource)\s+"([^"]+)"') {
$result.InternalIP = if ($Matches[1]) { $Matches[1] } else { $Matches[2] }
}
if ($Message -match '(?i)(?:протокол подключения|connection protocol)\s+"([^"]+)"') {
$result.Protocol = $Matches[1]
}
if ($result.Count -eq 0) { return $null }
return $result
}
function Test-RdpMonitorStringLooksLikeIPv4 { function Test-RdpMonitorStringLooksLikeIPv4 {
param([string]$Value) param([string]$Value)
if ([string]::IsNullOrWhiteSpace($Value)) { return $false } if ([string]::IsNullOrWhiteSpace($Value)) { return $false }
@@ -2909,6 +2947,27 @@ function Get-RDGatewayEventInfo {
$eventData.InternalIP = $eventData.Protocol.Trim() $eventData.InternalIP = $eventData.Protocol.Trim()
$eventData.Protocol = 'N/A' $eventData.Protocol = 'N/A'
} }
$needsFallback = ($eventData.Username -eq 'N/A' -or [string]::IsNullOrWhiteSpace($eventData.Username)) -or
($eventData.ExternalIP -eq 'N/A' -or [string]::IsNullOrWhiteSpace($eventData.ExternalIP)) -or
($eventData.InternalIP -eq 'N/A' -or [string]::IsNullOrWhiteSpace($eventData.InternalIP))
if ($needsFallback) {
$fb = Get-RDGatewayMessageFallback -Message ([string]$Event.Message)
if ($null -ne $fb) {
if (($eventData.Username -eq 'N/A' -or [string]::IsNullOrWhiteSpace($eventData.Username)) -and $fb.Username) {
$eventData.Username = [string]$fb.Username
}
if (($eventData.ExternalIP -eq 'N/A' -or [string]::IsNullOrWhiteSpace($eventData.ExternalIP)) -and $fb.ExternalIP) {
$eventData.ExternalIP = [string]$fb.ExternalIP
}
if (($eventData.InternalIP -eq 'N/A' -or [string]::IsNullOrWhiteSpace($eventData.InternalIP)) -and $fb.InternalIP) {
$eventData.InternalIP = [string]$fb.InternalIP
}
if (($eventData.Protocol -eq 'N/A' -or [string]::IsNullOrWhiteSpace($eventData.Protocol)) -and $fb.Protocol) {
$eventData.Protocol = [string]$fb.Protocol
}
}
}
} catch { } catch {
Write-Log "Ошибка при извлечении RD Gateway события: $($_.Exception.Message)" Write-Log "Ошибка при извлечении RD Gateway события: $($_.Exception.Message)"
} }
@@ -3266,7 +3325,15 @@ function Start-LoginMonitor {
$nextRotationCheck = Check-AndRotateLog $nextRotationCheck = Check-AndRotateLog
$nextReportCheck = Check-AndSendDailyReport $nextReportCheck = Check-AndSendDailyReport
$lastCheckTime = (Get-Date).AddSeconds(-10) $lastCheckTime = (Get-Date).AddSeconds(-10)
$lastGatewayCheckTime = (Get-Date).AddSeconds(-10) if ($rdGatewayAvailable) {
$lastGatewayCheckTime = (Get-Date).AddMinutes(-1 * [math]::Max(1, $GatewayEventsLookbackMinutes))
Write-Log "RD Gateway: опрос 302/303 включён, lookback ${GatewayEventsLookbackMinutes} мин (с $($lastGatewayCheckTime.ToString('dd.MM.yyyy HH:mm:ss')))"
} else {
$lastGatewayCheckTime = (Get-Date).AddSeconds(-10)
if ($EnableRDGatewayMonitoring) {
Write-Log "RD Gateway: журнал недоступен — опрос 302/303 отключён ($RDGatewayLogName)"
}
}
$lastRcmCheckTime = (Get-Date).AddSeconds(-10) $lastRcmCheckTime = (Get-Date).AddSeconds(-10)
$lastRcmShadowCheckTime = (Get-Date).AddSeconds(-10) $lastRcmShadowCheckTime = (Get-Date).AddSeconds(-10)
$lastWinRmCheckTime = (Get-Date).AddSeconds(-10) $lastWinRmCheckTime = (Get-Date).AddSeconds(-10)
@@ -3440,7 +3507,7 @@ function Start-LoginMonitor {
} | Out-Null } | Out-Null
} }
} }
$lastCheckTime = ($events | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1) $lastCheckTime = Update-MonitorPollCursor -CurrentCursor $lastCheckTime -Events @($events)
} }
if ($rdGatewayAvailable) { if ($rdGatewayAvailable) {
@@ -3451,10 +3518,16 @@ function Start-LoginMonitor {
} -ErrorAction SilentlyContinue } -ErrorAction SilentlyContinue
if ($gatewayEvents) { if ($gatewayEvents) {
foreach ($event in $gatewayEvents) { $gatewaySorted = @($gatewayEvents | Sort-Object TimeCreated, RecordId)
$gatewayFetched = $gatewaySorted.Count
$gatewayNotified = 0
foreach ($event in $gatewaySorted) {
if ($event.TimeCreated -le $lastGatewayCheckTime) { continue } if ($event.TimeCreated -le $lastGatewayCheckTime) { continue }
$ei = Get-RDGatewayEventInfo -Event $event $ei = Get-RDGatewayEventInfo -Event $event
if ($ei.Username -like "*$") { continue } if ($ei.Username.EndsWith('$', [StringComparison]::OrdinalIgnoreCase)) {
Write-Log "Skip RDG $($event.Id): machine account $($ei.Username)"
continue
}
$msg = Format-RDGatewayEvent -EventID $event.Id ` $msg = Format-RDGatewayEvent -EventID $event.Id `
-Username $ei.Username ` -Username $ei.Username `
-ExternalIP $ei.ExternalIP ` -ExternalIP $ei.ExternalIP `
@@ -3462,22 +3535,37 @@ function Start-LoginMonitor {
-Protocol $ei.Protocol ` -Protocol $ei.Protocol `
-ErrorCode $ei.ErrorCode ` -ErrorCode $ei.ErrorCode `
-TimeCreated $ei.TimeCreated -TimeCreated $ei.TimeCreated
Write-Log "Notify RDG: ID=$($event.Id) User=$($ei.Username)" $hasRdgError = ($ei.ErrorCode -ne '0' -and $ei.ErrorCode -ne 'N/A' -and -not [string]::IsNullOrWhiteSpace($ei.ErrorCode))
$rdgType = if ($event.Id -eq 302) { 'rdg.connection.success' } else { 'rdg.connection.failed' } if ($event.Id -eq 302) {
$rdgSev = if ($event.Id -eq 302) { 'info' } else { 'warning' } $rdgType = 'rdg.connection.success'
$rdgSev = 'info'
} elseif ($hasRdgError) {
$rdgType = 'rdg.connection.failed'
$rdgSev = 'warning'
} else {
$rdgType = 'rdg.connection.disconnected'
$rdgSev = 'info'
}
Write-Log "Notify RDG: ID=$($event.Id) User=$($ei.Username) Target=$($ei.InternalIP) Type=$rdgType"
Send-MonitorNotification -Message $msg ` Send-MonitorNotification -Message $msg `
-EmailSubject "RDP Login Monitor: RD Gateway ($($event.Id))" ` -EmailSubject "RDP Login Monitor: RD Gateway ($($event.Id))" `
-SacEventType $rdgType -SacSeverity $rdgSev ` -SacEventType $rdgType -SacSeverity $rdgSev `
-SacTitle "RD Gateway event $($event.Id)" ` -SacTitle "RD Gateway event $($event.Id)" `
-SacSummary "RDG $($event.Id) $($ei.Username)" ` -SacSummary "RDG $($event.Id) $($ei.Username) -> $($ei.InternalIP)" `
-SacDetails @{ -SacDetails @{
user = $ei.Username user = $ei.Username
external_ip = $ei.ExternalIP external_ip = $ei.ExternalIP
internal_ip = $ei.InternalIP internal_ip = $ei.InternalIP
protocol = $ei.Protocol
gateway_error_code = $ei.ErrorCode
event_id_windows = [int]$event.Id event_id_windows = [int]$event.Id
} | Out-Null } | Out-Null
$gatewayNotified++
}
$lastGatewayCheckTime = Update-MonitorPollCursor -CurrentCursor $lastGatewayCheckTime -Events $gatewaySorted
if ($gatewayFetched -gt 0) {
Write-Log "RD Gateway poll: fetched=$gatewayFetched notified=$gatewayNotified cursor=$($lastGatewayCheckTime.ToString('dd.MM.yyyy HH:mm:ss'))"
} }
$lastGatewayCheckTime = ($gatewayEvents | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1)
} }
} }
@@ -3510,7 +3598,7 @@ function Start-LoginMonitor {
event_id_windows = 1149 event_id_windows = 1149
} | Out-Null } | Out-Null
} }
$lastRcmCheckTime = ($rcmEvents | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1) $lastRcmCheckTime = Update-MonitorPollCursor -CurrentCursor $lastRcmCheckTime -Events @($rcmEvents)
} }
} }
@@ -3554,7 +3642,7 @@ function Start-LoginMonitor {
session_id = $sh.SessionId session_id = $sh.SessionId
} | Out-Null } | Out-Null
} }
$lastRcmShadowCheckTime = ($shadowEvents | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1) $lastRcmShadowCheckTime = Update-MonitorPollCursor -CurrentCursor $lastRcmShadowCheckTime -Events @($shadowEvents)
} }
} }
@@ -3604,7 +3692,7 @@ function Start-LoginMonitor {
transport = 'winrm' transport = 'winrm'
} | Out-Null } | Out-Null
} }
$lastWinRmCheckTime = ($winRmEvents | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1) $lastWinRmCheckTime = Update-MonitorPollCursor -CurrentCursor $lastWinRmCheckTime -Events @($winRmEvents)
} }
} }
@@ -3645,7 +3733,7 @@ function Start-LoginMonitor {
iis_client_ips = @($iisIps) iis_client_ips = @($iisIps)
} | Out-Null } | Out-Null
} }
$lastLockout4740CheckTime = ($lockoutEvents | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1) $lastLockout4740CheckTime = Update-MonitorPollCursor -CurrentCursor $lastLockout4740CheckTime -Events @($lockoutEvents)
} }
} }
+1 -1
View File
@@ -1 +1 @@
1.2.34-SAC 1.2.35-SAC