From ccd50a085d6a3c44393e1aa2dbbd7f278340c605 Mon Sep 17 00:00:00 2001 From: PTah Date: Wed, 24 Jun 2026 14:29:02 +1000 Subject: [PATCH] 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 --- Diagnose-RdpLoginMonitor.ps1 | 38 ++++++++++++++++- Login_Monitor.ps1 | 83 ++++++++++++++++++++++++++++++++---- README.md | 2 +- README_eng.md | 2 +- update-rdp-monitor.ps1 | 1 + version.txt | 2 +- 6 files changed, 115 insertions(+), 13 deletions(-) diff --git a/Diagnose-RdpLoginMonitor.ps1 b/Diagnose-RdpLoginMonitor.ps1 index 03136f3..4b38d6b 100644 --- a/Diagnose-RdpLoginMonitor.ps1 +++ b/Diagnose-RdpLoginMonitor.ps1 @@ -300,6 +300,7 @@ if (-not $isAdmin) { } $since = (Get-Date).AddMinutes(-1 * [math]::Abs($MinutesBack)) +$RcmLogName = 'Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational' $isWs = Test-IsWorkstationOs [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'))") @@ -399,9 +400,42 @@ if ($notifyable.Count -eq 0 -and $recent4624.Count -gt 0) { [void]$report.AppendLine('') [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('Проверьте 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() [System.IO.File]::WriteAllText($OutputPath, $text, (New-Object System.Text.UTF8Encoding $true)) diff --git a/Login_Monitor.ps1 b/Login_Monitor.ps1 index 6624255..0ba863e 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.4-SAC" +$ScriptVersion = "2.1.5-SAC" # Логи (все под InstallRoot) $LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log" @@ -115,6 +115,7 @@ $LoginSuccessNotifyDedupSeconds = 90 $HeartbeatFile = Join-Path $script:InstallRoot "Logs\last_heartbeat.txt" $GatewayPollCursorFile = Join-Path $script:InstallRoot "Logs\last_rdgateway_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" # Построчные правила подавления уведомлений Security 4624/4625 (см. ignore.lst.example в репозитории). $script:IgnoreListPath = Join-Path $script:InstallRoot "ignore.lst" @@ -138,6 +139,8 @@ $RDGatewayEvents = @(302, 303) $GatewayEventsLookbackMinutes = 60 # Security 4624/4625/4648: персистентный cursor + replay не старше N мин (медленный boot / поздний старт агента). $SecurityEventsLookbackMinutes = 60 +# RCM 1149: персистентный cursor + replay (как Security). +$Rcm1149EventsLookbackMinutes = 60 # RDP Remote Connection Manager: User authentication succeeded — событие 1149 (workstation + RDS server) $EnableRcm1149Monitoring = 1 @@ -3228,8 +3231,10 @@ function Should-IgnoreEvent { if ($Username -like $p) { 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 } + if ($EventID -ne 1149) { + 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) { 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 ($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) { 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 ` -ComputerName $ComputerName -SourceIP $SourceIP) { 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 { param([string]$ErrorCode) if ([string]::IsNullOrWhiteSpace($ErrorCode)) { return $true } @@ -4426,7 +4477,7 @@ function Start-LoginMonitor { Write-Log "RD Gateway: журнал недоступен — опрос 302/303 отключён ($RDGatewayLogName)" } } - $lastRcmCheckTime = (Get-Date).AddSeconds(-10) + $lastRcmCheckTime = Get-RdpRcm1149PollCursor $lastRcmShadowCheckTime = (Get-Date).AddSeconds(-10) $lastWinRmCheckTime = (Get-Date).AddSeconds(-10) $lastAdminShare5140CheckTime = (Get-Date).AddSeconds(-10) @@ -4697,16 +4748,27 @@ function Start-LoginMonitor { } -ErrorAction SilentlyContinue if ($rcmEvents) { + $rcmNotified = 0 + $rcmSkipped = 0 foreach ($event in $rcmEvents) { if ($event.TimeCreated -le $lastRcmCheckTime) { continue } $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 "-" ` - -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 ` -Username $rcmInfo.Username -SourceIP $rcmInfo.ClientIP -LogonType 10 if (-not (Test-RdpLoginSuccessNotifyDedupAllow -DedupKey $dedupKey1149)) { + $rcmSkipped++ Write-Log "Notify dedup 1149: User=$($rcmInfo.Username) IP=$($rcmInfo.ClientIP) (window ${LoginSuccessNotifyDedupSeconds}s)" continue } @@ -4725,8 +4787,13 @@ function Start-LoginMonitor { ip_address = $rcmInfo.ClientIP event_id_windows = 1149 } | Out-Null + $rcmNotified++ } $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'))" + } } } diff --git a/README.md b/README.md index 4b04b3d..a56881d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 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, отчёты. diff --git a/README_eng.md b/README_eng.md index ead4708..93f2465 100644 --- a/README_eng.md +++ b/README_eng.md @@ -1,6 +1,6 @@ # 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. diff --git a/update-rdp-monitor.ps1 b/update-rdp-monitor.ps1 index 2e3af55..6511ce2 100644 --- a/update-rdp-monitor.ps1 +++ b/update-rdp-monitor.ps1 @@ -34,6 +34,7 @@ $DistFiles = @( 'Install-DomainMonitors.ps1', 'Deploy-DomainMonitors.ps1', 'exchange_monitor.settings.example.ps1', + 'Diagnose-RdpLoginMonitor.ps1', 'login_monitor.settings.example.ps1' ) diff --git a/version.txt b/version.txt index bd1caf3..f14000d 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.1.4-SAC +2.1.5-SAC