fix: parse RD Gateway EventInfo XML and skip ephemeral 303 alerts

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-03 10:07:51 +10:00
parent 94d12e0b5f
commit 33367b05a8
3 changed files with 163 additions and 61 deletions
+113 -60
View File
@@ -82,7 +82,7 @@ $script:MonitorStopRequested = $false
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
$ScriptVersion = "2.0.7-SAC"
$ScriptVersion = "2.0.8-SAC"
# Логи (все под InstallRoot)
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
@@ -2884,17 +2884,111 @@ function Test-RdpMonitorStringLooksLikeIPv4 {
return [bool]($t -match '^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$')
}
function Get-RdpMonitorNormalizedClientIp {
param([string]$Value)
if ([string]::IsNullOrWhiteSpace($Value)) { return $Value }
$v = $Value.Trim()
if ($v -match '^(?<ip>(?:\d{1,3}\.){3}\d{1,3}):\d+$') {
return $Matches['ip']
}
return $v
}
function Get-RDGatewayUserDataEventInfoMap {
param($Event)
$map = @{}
try {
$xml = [xml]$Event.ToXml()
$userData = $xml.Event.UserData
if ($null -eq $userData) { return $map }
$eventInfo = $userData.EventInfo
if ($null -eq $eventInfo) {
foreach ($child in @($userData.ChildNodes)) {
if ($null -ne $child -and $child.LocalName -eq 'EventInfo') {
$eventInfo = $child
break
}
}
}
if ($null -eq $eventInfo) { return $map }
foreach ($node in @($eventInfo.ChildNodes)) {
if ($null -eq $node -or $node.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
$map[$node.LocalName] = [string]$node.InnerText
}
} catch { }
return $map
}
function Get-RDGatewayEventInfoMapValue {
param(
[hashtable]$Map,
[string[]]$Keys
)
foreach ($key in $Keys) {
foreach ($mapKey in $Map.Keys) {
if ($mapKey -ieq $key) {
$v = [string]$Map[$mapKey]
if (-not [string]::IsNullOrWhiteSpace($v)) { return $v.Trim() }
}
}
}
return $null
}
function Test-Rdg303ShouldSkipNotify {
param([hashtable]$EventInfo)
if ($EventInfo.EventId -ne 303) { return $false }
$dur = [int]$EventInfo.SessionDurationSec
if ($dur -le 0) {
return $true
}
return $false
}
function Get-RDGatewayEventInfo {
param($Event)
$eventData = @{
EventId = [int]$Event.Id
TimeCreated = $Event.TimeCreated
Username = "N/A"
ExternalIP = "N/A"
InternalIP = "N/A"
Protocol = "N/A"
ErrorCode = "N/A"
SessionDurationSec = 0
BytesReceived = 0
BytesTransferred = 0
}
try {
$infoMap = Get-RDGatewayUserDataEventInfoMap -Event $Event
if ($infoMap.Count -gt 0) {
$u = Get-RDGatewayEventInfoMapValue -Map $infoMap -Keys @('Username', 'User')
$ext = Get-RDGatewayEventInfoMapValue -Map $infoMap -Keys @('IpAddress', 'ClientAddress', 'ClientIP')
$int = Get-RDGatewayEventInfoMapValue -Map $infoMap -Keys @('Resource', 'TargetServer', 'TargetName')
$proto = Get-RDGatewayEventInfoMapValue -Map $infoMap -Keys @('ConnectionProtocol', 'Protocol', 'Transport')
$err = Get-RDGatewayEventInfoMapValue -Map $infoMap -Keys @('ErrorCode', 'StatusCode', 'Error')
if ($u) { $eventData.Username = $u }
if ($ext) { $eventData.ExternalIP = (Get-RdpMonitorNormalizedClientIp -Value $ext) }
if ($int) { $eventData.InternalIP = $int }
if ($proto) { $eventData.Protocol = $proto }
if ($err) { $eventData.ErrorCode = $err }
elseif ($Event.Id -eq 302) { $eventData.ErrorCode = '0' }
$bytesRx = Get-RDGatewayEventInfoMapValue -Map $infoMap -Keys @('BytesReceived')
$bytesTx = Get-RDGatewayEventInfoMapValue -Map $infoMap -Keys @('BytesTransfered', 'BytesTransferred')
$dur = Get-RDGatewayEventInfoMapValue -Map $infoMap -Keys @('SessionDuration', 'SessionDurationSec')
if ($bytesRx) { $eventData.BytesReceived = (Convert-ToIntSafe -Value $bytesRx) }
if ($bytesTx) { $eventData.BytesTransferred = (Convert-ToIntSafe -Value $bytesTx) }
if ($dur) { $eventData.SessionDurationSec = (Convert-ToIntSafe -Value $dur) }
return $eventData
}
$map = Get-EventDataMap -Event $Event
$lc = @{}
foreach ($k in $map.Keys) {
@@ -2917,64 +3011,11 @@ function Get-RDGatewayEventInfo {
$err = Get-RdpGwMapVal @('errorcode','statuscode','error','resultcode','status')
if ($u) { $eventData.Username = $u }
if ($ext) { $eventData.ExternalIP = $ext }
if ($ext) { $eventData.ExternalIP = (Get-RdpMonitorNormalizedClientIp -Value $ext) }
if ($int) { $eventData.InternalIP = $int }
if ($proto) { $eventData.Protocol = $proto }
if ($err) { $eventData.ErrorCode = $err }
$fillFromProps = ($lc.Count -eq 0) -or (
(($eventData.InternalIP -eq 'N/A' -or [string]::IsNullOrWhiteSpace($eventData.InternalIP)) -and (
($eventData.Protocol -eq 'N/A' -or [string]::IsNullOrWhiteSpace($eventData.Protocol)) -or
(Test-RdpMonitorStringLooksLikeIPv4 $eventData.Protocol)
))
)
if ($fillFromProps) {
switch ($Event.Id) {
302 {
if ($Event.Properties.Count -gt 0) { $eventData.Username = [string]$Event.Properties[0].Value }
if ($Event.Properties.Count -gt 1) { $eventData.ExternalIP = [string]$Event.Properties[1].Value }
if ($Event.Properties.Count -gt 2) {
$p2 = [string]$Event.Properties[2].Value
$p3 = if ($Event.Properties.Count -gt 3) { [string]$Event.Properties[3].Value } else { '' }
$p4 = if ($Event.Properties.Count -gt 4) { [string]$Event.Properties[4].Value } else { '' }
if ([string]::IsNullOrWhiteSpace($p2) -and (Test-RdpMonitorStringLooksLikeIPv4 $p3)) {
$eventData.InternalIP = $p3.Trim()
if (-not [string]::IsNullOrWhiteSpace($p4)) {
$eventData.Protocol = $p4.Trim()
}
} else {
$eventData.InternalIP = $p2.Trim()
if (-not [string]::IsNullOrWhiteSpace($p3)) { $eventData.Protocol = $p3.Trim() }
}
}
$eventData.ErrorCode = "0"
}
303 {
if ($Event.Properties.Count -gt 0) { $eventData.Username = [string]$Event.Properties[0].Value }
if ($Event.Properties.Count -gt 1) { $eventData.ExternalIP = [string]$Event.Properties[1].Value }
if ($Event.Properties.Count -gt 2) {
$p2 = [string]$Event.Properties[2].Value
$p3 = if ($Event.Properties.Count -gt 3) { [string]$Event.Properties[3].Value } else { '' }
if ([string]::IsNullOrWhiteSpace($p2) -and (Test-RdpMonitorStringLooksLikeIPv4 $p3)) {
$eventData.InternalIP = $p3.Trim()
if ($Event.Properties.Count -gt 4) { $eventData.ErrorCode = [string]$Event.Properties[4].Value }
if ($Event.Properties.Count -gt 5) { $eventData.Protocol = [string]$Event.Properties[5].Value }
} else {
$eventData.InternalIP = $p2.Trim()
if (-not [string]::IsNullOrWhiteSpace($p3)) { $eventData.Protocol = $p3.Trim() }
if ($Event.Properties.Count -gt 4) { $eventData.ErrorCode = [string]$Event.Properties[4].Value }
}
}
}
}
} elseif ($Event.Id -eq 302 -and $eventData.ErrorCode -eq 'N/A') {
$eventData.ErrorCode = "0"
}
if ($Event.Id -eq 303 -and ($eventData.ErrorCode -eq 'N/A' -or [string]::IsNullOrWhiteSpace($eventData.ErrorCode)) -and $Event.Properties.Count -gt 4) {
$eventData.ErrorCode = [string]$Event.Properties[4].Value
}
elseif ($Event.Id -eq 302) { $eventData.ErrorCode = '0' }
if (($eventData.InternalIP -eq 'N/A' -or [string]::IsNullOrWhiteSpace($eventData.InternalIP)) -and
(Test-RdpMonitorStringLooksLikeIPv4 $eventData.Protocol)) {
@@ -2992,7 +3033,7 @@ function Get-RDGatewayEventInfo {
$eventData.Username = [string]$fb.Username
}
if (($eventData.ExternalIP -eq 'N/A' -or [string]::IsNullOrWhiteSpace($eventData.ExternalIP)) -and $fb.ExternalIP) {
$eventData.ExternalIP = [string]$fb.ExternalIP
$eventData.ExternalIP = (Get-RdpMonitorNormalizedClientIp -Value ([string]$fb.ExternalIP))
}
if (($eventData.InternalIP -eq 'N/A' -or [string]::IsNullOrWhiteSpace($eventData.InternalIP)) -and $fb.InternalIP) {
$eventData.InternalIP = [string]$fb.InternalIP
@@ -3016,7 +3057,8 @@ function Format-RDGatewayEvent {
[string]$InternalIP,
[string]$Protocol,
[string]$ErrorCode,
[datetime]$TimeCreated
[datetime]$TimeCreated,
[int]$SessionDurationSec = 0
)
$hUser = (ConvertTo-TelegramHtml $Username)
@@ -3041,7 +3083,10 @@ function Format-RDGatewayEvent {
$message += "🌐 IP пользователя (внешний): $hExt`r`n"
$message += "🖥️ IP внутренний: $hInt`r`n"
$message += "🔌 Протокол: $hProto`r`n"
if ($EventID -eq 303 -and $ErrorCode -ne "0" -and $ErrorCode -ne "N/A") {
if ($EventID -eq 303 -and $SessionDurationSec -gt 0) {
$message += "⏱️ Длительность сессии: $(ConvertTo-TelegramHtml ([string]$SessionDurationSec)) с`r`n"
}
if ($EventID -eq 303 -and $ErrorCode -ne "0" -and $ErrorCode -ne "N/A" -and -not [string]::IsNullOrWhiteSpace($ErrorCode)) {
$message += "⚠️ Код ошибки: $(ConvertTo-TelegramHtml $ErrorCode)`r`n"
}
$message += "🕐 Время: $hTime`r`n"
@@ -3604,13 +3649,18 @@ function Start-LoginMonitor {
Write-Log "Skip RDG $($event.Id): machine account $($ei.Username)"
continue
}
if (Test-Rdg303ShouldSkipNotify -EventInfo $ei) {
Write-Log "Skip RDG 303: ephemeral channel (SessionDuration=0, User=$($ei.Username), Target=$($ei.InternalIP), ErrorCode=$($ei.ErrorCode))"
continue
}
$msg = Format-RDGatewayEvent -EventID $event.Id `
-Username $ei.Username `
-ExternalIP $ei.ExternalIP `
-InternalIP $ei.InternalIP `
-Protocol $ei.Protocol `
-ErrorCode $ei.ErrorCode `
-TimeCreated $ei.TimeCreated
-TimeCreated $ei.TimeCreated `
-SessionDurationSec $ei.SessionDurationSec
$hasRdgError = ($ei.ErrorCode -ne '0' -and $ei.ErrorCode -ne 'N/A' -and -not [string]::IsNullOrWhiteSpace($ei.ErrorCode))
if ($event.Id -eq 302) {
$rdgType = 'rdg.connection.success'
@@ -3634,6 +3684,9 @@ function Start-LoginMonitor {
internal_ip = $ei.InternalIP
protocol = $ei.Protocol
gateway_error_code = $ei.ErrorCode
session_duration_sec = [int]$ei.SessionDurationSec
bytes_received = [int]$ei.BytesReceived
bytes_transferred = [int]$ei.BytesTransferred
event_id_windows = [int]$event.Id
} | Out-Null
$gatewayNotified++
+49
View File
@@ -0,0 +1,49 @@
# Проверка парсинга UserData/EventInfo для RD Gateway 303 (BytesReceived != ErrorCode).
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$sample303 = @'
<Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event">
<System>
<Provider Name="Microsoft-Windows-TerminalServices-Gateway" />
<EventID>303</EventID>
<TimeCreated SystemTime="2026-06-02T23:51:21.033855700Z" />
</System>
<UserData>
<EventInfo xmlns="aag">
<Username>B26\TSA</Username>
<IpAddress>95.154.72.73</IpAddress>
<Resource>192.168.164.43</Resource>
<BytesReceived>1991</BytesReceived>
<BytesTransfered>2116</BytesTransfered>
<SessionDuration>0</SessionDuration>
<ConnectionProtocol>HTTP</ConnectionProtocol>
<ErrorCode>1226</ErrorCode>
</EventInfo>
</UserData>
</Event>
'@
function Get-RDGatewayUserDataEventInfoMapFromXmlText {
param([string]$XmlText)
$map = @{}
$xml = [xml]$XmlText
$eventInfo = $xml.Event.UserData.EventInfo
foreach ($node in @($eventInfo.ChildNodes)) {
if ($null -eq $node -or $node.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
$map[$node.LocalName] = [string]$node.InnerText
}
return $map
}
$map = Get-RDGatewayUserDataEventInfoMapFromXmlText -XmlText $sample303
if ($map['ErrorCode'] -ne '1226') {
throw "Expected ErrorCode=1226, got $($map['ErrorCode'])"
}
if ($map['BytesReceived'] -ne '1991') {
throw "Expected BytesReceived=1991, got $($map['BytesReceived'])"
}
if ($map['SessionDuration'] -ne '0') {
throw "Expected SessionDuration=0, got $($map['SessionDuration'])"
}
Write-Host 'OK: RD Gateway EventInfo XML fields parsed correctly (ErrorCode != BytesReceived).'
+1 -1
View File
@@ -1 +1 @@
2.0.7-SAC
2.0.8-SAC