fix: RCM 1149 silent drop on RDS servers (2.1.5-SAC)

Should-IgnoreEvent rejected every 1149 because ComputerName is always '-'; add persistent poll cursor, skip logging, and Diagnose on NETLOGON publish list.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
PTah
2026-06-24 14:29:02 +10:00
parent 55d58b25a3
commit ccd50a085d
6 changed files with 115 additions and 13 deletions
+36 -2
View File
@@ -300,6 +300,7 @@ if (-not $isAdmin) {
} }
$since = (Get-Date).AddMinutes(-1 * [math]::Abs($MinutesBack)) $since = (Get-Date).AddMinutes(-1 * [math]::Abs($MinutesBack))
$RcmLogName = 'Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational'
$isWs = Test-IsWorkstationOs $isWs = Test-IsWorkstationOs
[void]$report.AppendLine("OS ProductType workstation=$isWs (server mode LT 2/3/10, workstation LT 10)") [void]$report.AppendLine("OS ProductType workstation=$isWs (server mode LT 2/3/10, workstation LT 10)")
[void]$report.AppendLine("Window StartTime >= $($since.ToString('yyyy-MM-dd HH:mm:ss'))") [void]$report.AppendLine("Window StartTime >= $($since.ToString('yyyy-MM-dd HH:mm:ss'))")
@@ -399,9 +400,42 @@ if ($notifyable.Count -eq 0 -and $recent4624.Count -gt 0) {
[void]$report.AppendLine('') [void]$report.AppendLine('')
[void]$report.AppendLine('UseSAC=exclusive: Telegram по rdp.login.success только из SAC (не локально агентом).') [void]$report.AppendLine('UseSAC=exclusive: Telegram по rdp.login.success только из SAC (не локально агентом).')
[void]$report.AppendLine('На сервере RDS без аудита Security 4624 — смотрите RCM Operational 1149 (с 2.1.4-SAC мониторится на сервере).') [void]$report.AppendLine('На сервере RDS без аудита Security 4624 — смотрите RCM Operational 1149 (2.1.5-SAC+: исправлен silent skip при ComputerName=-).')
[void]$report.AppendLine('rdp.login.success = severity info; при SAC min_severity=warning Telegram не уйдёт, но событие в UI SAC должно быть.') [void]$report.AppendLine('rdp.login.success = severity info; при SAC min_severity=warning Telegram не уйдёт, но событие в UI SAC должно быть.')
[void]$report.AppendLine('Проверьте SAC: type=rdp.login.success, hostname ITIS198, время входа, event_id из login_monitor.log.')
[void]$report.Append((Write-Section '10. RCM Operational 1149'))
$recent1149 = @()
try {
$recent1149 = @(Get-WinEvent -FilterHashtable @{
LogName = $RcmLogName
ID = 1149
StartTime = $since
} -ErrorAction SilentlyContinue)
} catch { }
[void]$report.AppendLine("RCM 1149 в окне ($RcmLogName): $($recent1149.Count)")
foreach ($ev in $recent1149 | Select-Object -First 8) {
$u = '-'; $ip = '-'
try {
if ($ev.Properties.Count -gt 0) { $u = [string]$ev.Properties[0].Value }
if ($ev.Properties.Count -gt 2) { $ip = [string]$ev.Properties[2].Value }
} catch { }
[void]$report.AppendLine(" $($ev.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss')) User=$u IP=$ip")
}
$rcmNotifyLines = @()
if (Test-Path -LiteralPath $monLog) {
$sinceLog = $since.ToString('yyyy-MM-dd HH:mm')
$rcmNotifyLines = @(Select-String -LiteralPath $monLog -Pattern 'Notify RCM 1149|Skip 1149' -ErrorAction SilentlyContinue |
Where-Object { $_.Line -ge $sinceLog } |
Select-Object -ExpandProperty Line)
}
[void]$report.AppendLine("Строки Notify/Skip 1149 в login_monitor.log: $($rcmNotifyLines.Count)")
foreach ($ln in $rcmNotifyLines | Select-Object -Last 10) { [void]$report.AppendLine(" $ln") }
if ($recent1149.Count -gt 0 -and $rcmNotifyLines.Count -eq 0) {
[void]$report.AppendLine('ВНИМАНИЕ: 1149 в журнале есть, в логе агента нет Notify/Skip — вероятен баг 2.1.4 (все 1149 отбрасывались) или агент не работал в момент входа.')
}
[void]$report.AppendLine('')
[void]$report.AppendLine('Проверьте SAC: type=rdp.login.success, hostname, время входа, event_id из login_monitor.log.')
$text = $report.ToString() $text = $report.ToString()
[System.IO.File]::WriteAllText($OutputPath, $text, (New-Object System.Text.UTF8Encoding $true)) [System.IO.File]::WriteAllText($OutputPath, $text, (New-Object System.Text.UTF8Encoding $true))
+75 -8
View File
@@ -90,7 +90,7 @@ $script:SkipLogDetailLimit = 15
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах). # строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только # Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1). # исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
$ScriptVersion = "2.1.4-SAC" $ScriptVersion = "2.1.5-SAC"
# Логи (все под InstallRoot) # Логи (все под InstallRoot)
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log" $LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
@@ -115,6 +115,7 @@ $LoginSuccessNotifyDedupSeconds = 90
$HeartbeatFile = Join-Path $script:InstallRoot "Logs\last_heartbeat.txt" $HeartbeatFile = Join-Path $script:InstallRoot "Logs\last_heartbeat.txt"
$GatewayPollCursorFile = Join-Path $script:InstallRoot "Logs\last_rdgateway_poll.txt" $GatewayPollCursorFile = Join-Path $script:InstallRoot "Logs\last_rdgateway_poll.txt"
$SecurityPollCursorFile = Join-Path $script:InstallRoot "Logs\last_security_poll.txt" $SecurityPollCursorFile = Join-Path $script:InstallRoot "Logs\last_security_poll.txt"
$Rcm1149PollCursorFile = Join-Path $script:InstallRoot "Logs\last_rcm1149_poll.txt"
$DeployUpdateMarkerFile = Join-Path $script:InstallRoot "deploy_last_update.txt" $DeployUpdateMarkerFile = Join-Path $script:InstallRoot "deploy_last_update.txt"
# Построчные правила подавления уведомлений Security 4624/4625 (см. ignore.lst.example в репозитории). # Построчные правила подавления уведомлений Security 4624/4625 (см. ignore.lst.example в репозитории).
$script:IgnoreListPath = Join-Path $script:InstallRoot "ignore.lst" $script:IgnoreListPath = Join-Path $script:InstallRoot "ignore.lst"
@@ -138,6 +139,8 @@ $RDGatewayEvents = @(302, 303)
$GatewayEventsLookbackMinutes = 60 $GatewayEventsLookbackMinutes = 60
# Security 4624/4625/4648: персистентный cursor + replay не старше N мин (медленный boot / поздний старт агента). # Security 4624/4625/4648: персистентный cursor + replay не старше N мин (медленный boot / поздний старт агента).
$SecurityEventsLookbackMinutes = 60 $SecurityEventsLookbackMinutes = 60
# RCM 1149: персистентный cursor + replay (как Security).
$Rcm1149EventsLookbackMinutes = 60
# RDP Remote Connection Manager: User authentication succeeded — событие 1149 (workstation + RDS server) # RDP Remote Connection Manager: User authentication succeeded — событие 1149 (workstation + RDS server)
$EnableRcm1149Monitoring = 1 $EnableRcm1149Monitoring = 1
@@ -3228,8 +3231,10 @@ function Should-IgnoreEvent {
if ($Username -like $p) { return $true } if ($Username -like $p) { return $true }
} }
if ($ComputerName -eq "Authz" -or $ComputerName -like "Authz*") { return $true } if ($EventID -ne 1149) {
if ($ComputerName -eq "NtLmSsp" -or $ComputerName -like "NtLmSsp*" -or $ComputerName -like "*NtLmSsp*") { return $true } if ($ComputerName -eq "Authz" -or $ComputerName -like "Authz*") { return $true }
if ($ComputerName -eq "NtLmSsp" -or $ComputerName -like "NtLmSsp*" -or $ComputerName -like "*NtLmSsp*") { return $true }
}
foreach ($lp in $ExcludedLogonProcesses) { foreach ($lp in $ExcludedLogonProcesses) {
if ($ProcessName -like "*$lp*") { return $true } if ($ProcessName -like "*$lp*") { return $true }
@@ -3239,13 +3244,14 @@ function Should-IgnoreEvent {
} }
if ($SourceIP -eq "127.0.0.1" -or $SourceIP -like "fe80:*") { return $true } if ($SourceIP -eq "127.0.0.1" -or $SourceIP -like "fe80:*") { return $true }
if ($ComputerName -eq "-" -or $ComputerName -eq "N/A") { return $true } # RCM 1149 не содержит WorkstationName; caller передаёт ComputerName='-'.
if ($EventID -ne 1149 -and ($ComputerName -eq "-" -or $ComputerName -eq "N/A")) { return $true }
foreach ($pattern in $ExcludedComputerPatterns) { foreach ($pattern in $ExcludedComputerPatterns) {
if ($ComputerName -like $pattern) { return $true } if ($ComputerName -like $pattern) { return $true }
} }
if ($EventID -in 4624, 4625) { if ($EventID -in 4624, 4625, 1149) {
if (Test-RdpMonitorIgnoreListMatch -EventId ([string]$EventID) -Username $Username ` if (Test-RdpMonitorIgnoreListMatch -EventId ([string]$EventID) -Username $Username `
-ComputerName $ComputerName -SourceIP $SourceIP) { -ComputerName $ComputerName -SourceIP $SourceIP) {
return $true return $true
@@ -3757,6 +3763,51 @@ function Set-RdpSecurityPollCursor {
} }
} }
function Get-RdpRcm1149PollCursor {
$now = Get-Date
$fresh = $now.AddSeconds(-10)
$maxAgeMin = [math]::Max(1, [int]$Rcm1149EventsLookbackMinutes)
$lookbackFloor = $now.AddMinutes(-1 * $maxAgeMin)
if (-not (Test-Path -LiteralPath $Rcm1149PollCursorFile)) {
Write-Log "RCM 1149: cursor отсутствует — replay с $($lookbackFloor.ToString('dd.MM.yyyy HH:mm:ss')) (lookback ${maxAgeMin} мин)."
return $lookbackFloor
}
try {
$raw = (Get-Content -LiteralPath $Rcm1149PollCursorFile -TotalCount 1 -ErrorAction Stop).Trim()
if ([string]::IsNullOrWhiteSpace($raw)) {
Write-Log "RCM 1149: cursor пуст — replay с $($lookbackFloor.ToString('dd.MM.yyyy HH:mm:ss')) (lookback ${maxAgeMin} мин)."
return $lookbackFloor
}
$parsed = [datetime]::Parse($raw, [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::RoundtripKind)
if ($parsed.Kind -eq [DateTimeKind]::Utc) {
$parsed = $parsed.ToLocalTime()
}
if ($parsed -lt $lookbackFloor) {
Write-Log "RCM 1149: cursor старше $maxAgeMin мин — replay с $($lookbackFloor.ToString('dd.MM.yyyy HH:mm:ss'))."
return $lookbackFloor
}
Write-Log "RCM 1149: cursor=$($parsed.ToString('dd.MM.yyyy HH:mm:ss')) (lookback cap ${maxAgeMin} мин)."
return $parsed
} catch {
Write-Log "RCM 1149: не удалось прочитать cursor ($Rcm1149PollCursorFile) — replay с $($lookbackFloor.ToString('dd.MM.yyyy HH:mm:ss'))."
return $lookbackFloor
}
}
function Set-RdpRcm1149PollCursor {
param([Parameter(Mandatory = $true)][datetime]$Cursor)
try {
$dir = Split-Path -Parent $Rcm1149PollCursorFile
if (-not (Test-Path -LiteralPath $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
}
Write-TextFileUtf8Bom -Path $Rcm1149PollCursorFile -Text ($Cursor.ToString('o'))
} catch {
Write-Log "WARN: RCM 1149 cursor save failed: $($_.Exception.Message)"
}
}
function Test-RdgGatewayBenignErrorCode { function Test-RdgGatewayBenignErrorCode {
param([string]$ErrorCode) param([string]$ErrorCode)
if ([string]::IsNullOrWhiteSpace($ErrorCode)) { return $true } if ([string]::IsNullOrWhiteSpace($ErrorCode)) { return $true }
@@ -4426,7 +4477,7 @@ function Start-LoginMonitor {
Write-Log "RD Gateway: журнал недоступен — опрос 302/303 отключён ($RDGatewayLogName)" Write-Log "RD Gateway: журнал недоступен — опрос 302/303 отключён ($RDGatewayLogName)"
} }
} }
$lastRcmCheckTime = (Get-Date).AddSeconds(-10) $lastRcmCheckTime = Get-RdpRcm1149PollCursor
$lastRcmShadowCheckTime = (Get-Date).AddSeconds(-10) $lastRcmShadowCheckTime = (Get-Date).AddSeconds(-10)
$lastWinRmCheckTime = (Get-Date).AddSeconds(-10) $lastWinRmCheckTime = (Get-Date).AddSeconds(-10)
$lastAdminShare5140CheckTime = (Get-Date).AddSeconds(-10) $lastAdminShare5140CheckTime = (Get-Date).AddSeconds(-10)
@@ -4697,16 +4748,27 @@ function Start-LoginMonitor {
} -ErrorAction SilentlyContinue } -ErrorAction SilentlyContinue
if ($rcmEvents) { if ($rcmEvents) {
$rcmNotified = 0
$rcmSkipped = 0
foreach ($event in $rcmEvents) { foreach ($event in $rcmEvents) {
if ($event.TimeCreated -le $lastRcmCheckTime) { continue } if ($event.TimeCreated -le $lastRcmCheckTime) { continue }
$rcmInfo = Get-Rcm1149EventInfo -Event $event $rcmInfo = Get-Rcm1149EventInfo -Event $event
if ($rcmInfo.Username -like "*$") { continue } if ($rcmInfo.Username -like "*$") {
$rcmSkipped++
Write-Log "Skip 1149: User=$($rcmInfo.Username) IP=$($rcmInfo.ClientIP) — machine account"
continue
}
if (Should-IgnoreEvent -Username $rcmInfo.Username -ProcessName "-" ` if (Should-IgnoreEvent -Username $rcmInfo.Username -ProcessName "-" `
-ComputerName "-" -EventID 1149 -LogonType 10 -SourceIP $rcmInfo.ClientIP) { continue } -ComputerName "-" -EventID 1149 -LogonType 10 -SourceIP $rcmInfo.ClientIP) {
$rcmSkipped++
Write-Log "Skip 1149: User=$($rcmInfo.Username) IP=$($rcmInfo.ClientIP) — built-in exclusion or ignore.lst"
continue
}
$dedupKey1149 = Get-RdpLoginSuccessNotifyDedupKey -SecurityLogComputerName $event.MachineName ` $dedupKey1149 = Get-RdpLoginSuccessNotifyDedupKey -SecurityLogComputerName $event.MachineName `
-Username $rcmInfo.Username -SourceIP $rcmInfo.ClientIP -LogonType 10 -Username $rcmInfo.Username -SourceIP $rcmInfo.ClientIP -LogonType 10
if (-not (Test-RdpLoginSuccessNotifyDedupAllow -DedupKey $dedupKey1149)) { if (-not (Test-RdpLoginSuccessNotifyDedupAllow -DedupKey $dedupKey1149)) {
$rcmSkipped++
Write-Log "Notify dedup 1149: User=$($rcmInfo.Username) IP=$($rcmInfo.ClientIP) (window ${LoginSuccessNotifyDedupSeconds}s)" Write-Log "Notify dedup 1149: User=$($rcmInfo.Username) IP=$($rcmInfo.ClientIP) (window ${LoginSuccessNotifyDedupSeconds}s)"
continue continue
} }
@@ -4725,8 +4787,13 @@ function Start-LoginMonitor {
ip_address = $rcmInfo.ClientIP ip_address = $rcmInfo.ClientIP
event_id_windows = 1149 event_id_windows = 1149
} | Out-Null } | Out-Null
$rcmNotified++
} }
$lastRcmCheckTime = Update-MonitorPollCursor -CurrentCursor $lastRcmCheckTime -Events @($rcmEvents) $lastRcmCheckTime = Update-MonitorPollCursor -CurrentCursor $lastRcmCheckTime -Events @($rcmEvents)
Set-RdpRcm1149PollCursor -Cursor $lastRcmCheckTime
if ($rcmNotified -gt 0 -or $rcmSkipped -gt 0) {
Write-Log "RCM 1149 poll: fetched=$(@($rcmEvents).Count) notified=$rcmNotified skipped=$rcmSkipped cursor=$($lastRcmCheckTime.ToString('dd.MM.yyyy HH:mm:ss'))"
}
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
# RDP Login Monitor # RDP Login Monitor
**Версия:** `2.1.4-SAC` (`$ScriptVersion` + `version.txt`) **Версия:** `2.1.5-SAC` (`$ScriptVersion` + `version.txt`)
PowerShell-мониторинг Windows: RDP/RDS, RD Gateway, WinRM, admin share, блокировки УЗ, heartbeat, отчёты. PowerShell-мониторинг Windows: RDP/RDS, RD Gateway, WinRM, admin share, блокировки УЗ, heartbeat, отчёты.
+1 -1
View File
@@ -1,6 +1,6 @@
# RDP Login Monitor # RDP Login Monitor
**Version:** `2.1.4-SAC` (`$ScriptVersion` + `version.txt`) **Version:** `2.1.5-SAC` (`$ScriptVersion` + `version.txt`)
PowerShell toolkit for monitoring Windows logons with Telegram and/or Email (SMTP) notifications. PowerShell toolkit for monitoring Windows logons with Telegram and/or Email (SMTP) notifications.
+1
View File
@@ -34,6 +34,7 @@ $DistFiles = @(
'Install-DomainMonitors.ps1', 'Install-DomainMonitors.ps1',
'Deploy-DomainMonitors.ps1', 'Deploy-DomainMonitors.ps1',
'exchange_monitor.settings.example.ps1', 'exchange_monitor.settings.example.ps1',
'Diagnose-RdpLoginMonitor.ps1',
'login_monitor.settings.example.ps1' 'login_monitor.settings.example.ps1'
) )
+1 -1
View File
@@ -1 +1 @@
2.1.4-SAC 2.1.5-SAC