From 23fa83ddd6e7e4f941d7d1652bf1c3d4ac96844b Mon Sep 17 00:00:00 2001 From: PTah Date: Mon, 13 Jul 2026 17:10:24 +1000 Subject: [PATCH] feat: send rdp.session.logoff on Security 4634/4647 (2.1.11-SAC) Direct LAN RDP logout is reported to SAC with the same workstation filters as login (LogonType 10), dedup against paired 4647/4634, and ignore.lst logoff scope. --- Login_Monitor.ps1 | 114 +++++++++++++++++++++++++++++++++++++++++----- README.md | 3 +- README_eng.md | 2 +- version.txt | 2 +- 4 files changed, 107 insertions(+), 14 deletions(-) diff --git a/Login_Monitor.ps1 b/Login_Monitor.ps1 index 5dab13b..28e5965 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.10-SAC" +$ScriptVersion = "2.1.11-SAC" # Логи (все под InstallRoot) $LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log" @@ -3039,10 +3039,13 @@ function Parse-RdpMonitorIgnoreListLine { if ($line[0] -eq '#' -or $line[0] -eq ';') { return $null } if ($line.StartsWith([char]0xFEFF)) { $line = $line.TrimStart([char]0xFEFF) } - $scopes = @('4624', '4625') + $scopes = @('4624', '4625', '4634', '4647') if ($line -match '^(?i)(4740|lockout|блокир)\s*:\s*(.+)$') { $scopes = @('4740') $line = $Matches[2].Trim() + } elseif ($line -match '^(?i)(logoff|logout|4634|4647)\s*:\s*(.+)$') { + $scopes = @('4634', '4647') + $line = $Matches[2].Trim() } elseif ($line -match '^(?i)(shadow|20506)\s*:\s*(.+)$') { $scopes = @('20506', '20507', '20510') $line = $Matches[2].Trim() @@ -3053,7 +3056,7 @@ function Parse-RdpMonitorIgnoreListLine { $scopes = @('5140') $line = $Matches[2].Trim() } elseif ($line -match '^(?i)(all|\*)\s*:\s*(.+)$') { - $scopes = @('4624', '4625', '4740', '20506', '20507', '20510', 'winrm', '5140') + $scopes = @('4624', '4625', '4634', '4647', '4740', '20506', '20507', '20510', 'winrm', '5140') $line = $Matches[2].Trim() } if ([string]::IsNullOrWhiteSpace($line)) { return $null } @@ -3238,7 +3241,7 @@ function Should-IgnoreEvent { if ($Username -like $p) { return $true } } - if ($EventID -ne 1149) { + if ($EventID -ne 1149 -and $EventID -notin 4634, 4647) { if ($ComputerName -eq "Authz" -or $ComputerName -like "Authz*") { return $true } if ($ComputerName -eq "NtLmSsp" -or $ComputerName -like "NtLmSsp*" -or $ComputerName -like "*NtLmSsp*") { return $true } } @@ -3251,14 +3254,14 @@ function Should-IgnoreEvent { } if ($SourceIP -eq "127.0.0.1" -or $SourceIP -like "fe80:*") { return $true } - # RCM 1149 не содержит WorkstationName; caller передаёт ComputerName='-'. - if ($EventID -ne 1149 -and ($ComputerName -eq "-" -or $ComputerName -eq "N/A")) { return $true } + # RCM 1149 и logoff 4634/4647 часто без WorkstationName в EventData. + if ($EventID -notin 1149, 4634, 4647 -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, 1149) { + if ($EventID -in 4624, 4625, 4634, 4647, 1149) { if (Test-RdpMonitorIgnoreListMatch -EventId ([string]$EventID) -Username $Username ` -ComputerName $ComputerName -SourceIP $SourceIP) { return $true @@ -3278,6 +3281,7 @@ function Get-LoginEventInfo { SourceIP = "-" ProcessName = "-" LogonType = 0 + SessionId = $null } try { @@ -3295,6 +3299,17 @@ function Get-LoginEventInfo { $eventData.ProcessName = Get-FirstNonEmptyMapValue -DataMap $map -Keys @( "SubStatus","Status","FailureReason","FailureReasonCode" ) + } elseif ($Event.Id -in 4634, 4647) { + if ($Event.Id -eq 4647) { + $subjectUser = Get-FirstNonEmptyMapValue -DataMap $map -Keys @("SubjectUserName","AccountName") + if (-not [string]::IsNullOrWhiteSpace($subjectUser)) { + $eventData.Username = $subjectUser + } + } + $sidRaw = Get-FirstNonEmptyMapValue -DataMap $map -Keys @("TargetLogonId","SubjectLogonId","LogonId") + if (-not [string]::IsNullOrWhiteSpace($sidRaw)) { + $eventData.SessionId = [string]$sidRaw.Trim() + } } } catch { Write-Log "Ошибка при извлечении данных события: $($_.Exception.Message)" @@ -3367,6 +3382,8 @@ function Format-LoginEvent { $message = "" if ($EventID -eq 4624) { $message += "✅ УСПЕШНЫЙ ВХОД" } elseif ($EventID -eq 4625) { $message += "❌ НЕУДАЧНАЯ ПОПЫТКА" } + elseif ($EventID -eq 4634) { $message += "🚪 ВЫХОД ИЗ СЕССИИ" } + elseif ($EventID -eq 4647) { $message += "🚪 ВЫХОД ПОЛЬЗОВАТЕЛЯ" } else { $message += "⚠️ СОБЫТИЕ" } $message += "`r`n" @@ -3387,6 +3404,7 @@ function Format-LoginEvent { $script:FailedLogonBuckets = @{} $script:LoginSuccessNotifyDedup = @{} +$script:LogoffNotifyDedup = @{} function Get-RdpLoginNotifyDedupHostPart { param([string]$SecurityLogComputerName) @@ -3445,6 +3463,33 @@ function Test-RdpLoginSuccessNotifyDedupAllow { return $true } +function Get-RdpLogoffNotifyDedupKey { + param( + [string]$SecurityLogComputerName, + [string]$Username, + [int]$LogonType + ) + $hostPart = Get-RdpLoginNotifyDedupHostPart -SecurityLogComputerName $SecurityLogComputerName + $userPart = Get-RdpLoginNotifyDedupUsernamePart -Username $Username + return "$hostPart|logoff|$userPart|$LogonType" +} + +function Test-RdpLogoffNotifyDedupAllow { + param([string]$DedupKey) + + if ($LoginSuccessNotifyDedupSeconds -le 0) { return $true } + $nowUtc = (Get-Date).ToUniversalTime() + if ($script:LogoffNotifyDedup.ContainsKey($DedupKey)) { + $lastUtc = $script:LogoffNotifyDedup[$DedupKey] + $delta = ($nowUtc - $lastUtc).TotalSeconds + if ($delta -ge 0 -and $delta -lt $LoginSuccessNotifyDedupSeconds) { + return $false + } + } + $script:LogoffNotifyDedup[$DedupKey] = $nowUtc + return $true +} + function Get-FailedLogonSourceKeyPart { param( [string]$SourceIP, @@ -4503,7 +4548,7 @@ function Start-LoginMonitor { $lastWinRmCheckTime = (Get-Date).AddSeconds(-10) $lastAdminShare5140CheckTime = (Get-Date).AddSeconds(-10) $lastLockout4740CheckTime = (Get-Date).AddSeconds(-10) - $monitorEvents = @(4624, 4625, 4648) + $monitorEvents = @(4624, 4625, 4634, 4647, 4648) $script:MonitorInMainLoop = $true while ($true) { @@ -4569,7 +4614,7 @@ function Start-LoginMonitor { if ($event.Id -eq 4648) { $shouldIgnore = $true $ignoreReason = 'EventID 4648 excluded (MonitorInteractiveOnly)' - } elseif ($event.Id -in 4624, 4625) { + } elseif ($event.Id -in 4624, 4625, 4634, 4647) { if ($osKind.IsWorkstation) { $interactiveTypes = @(10) $modeLabel = 'workstation LT10' @@ -4578,8 +4623,15 @@ function Start-LoginMonitor { $modeLabel = 'server LT2/3/10' } if ($interactiveTypes -notcontains $eventInfo.LogonType) { - $shouldIgnore = $true - $ignoreReason = "LogonType $($eventInfo.LogonType) not in $modeLabel" + $allow4647Workstation = ( + $osKind.IsWorkstation -and + $event.Id -eq 4647 -and + $eventInfo.LogonType -eq 0 + ) + if (-not $allow4647Workstation) { + $shouldIgnore = $true + $ignoreReason = "LogonType $($eventInfo.LogonType) not in $modeLabel" + } } } else { $shouldIgnore = $true @@ -4620,6 +4672,46 @@ function Start-LoginMonitor { } } + if ($event.Id -in 4634, 4647) { + $dedupKeyLogoff = Get-RdpLogoffNotifyDedupKey -SecurityLogComputerName $event.MachineName ` + -Username $eventInfo.Username -LogonType $eventInfo.LogonType + if (-not (Test-RdpLogoffNotifyDedupAllow -DedupKey $dedupKeyLogoff)) { + Write-Log "Notify dedup logoff $($event.Id): User=$($eventInfo.Username) LT=$($eventInfo.LogonType) (window ${LoginSuccessNotifyDedupSeconds}s)" + continue + } + + $formattedMessage = Format-LoginEvent -EventID $event.Id ` + -Username $eventInfo.Username ` + -ComputerName $eventInfo.ComputerName ` + -SourceIP $eventInfo.SourceIP ` + -ProcessName $eventInfo.ProcessName ` + -TimeCreated $eventInfo.TimeCreated ` + -LogonType $eventInfo.LogonType ` + -LogonTypeName $logonTypeName ` + -SecurityLogComputerName $event.MachineName + + $sacDetails = @{ + user = $eventInfo.Username + ip_address = $eventInfo.SourceIP + logon_type = $eventInfo.LogonType + event_id_windows = [int]$event.Id + workstation_name = $eventInfo.ComputerName + } + if ($null -ne $eventInfo.SessionId -and -not [string]::IsNullOrWhiteSpace([string]$eventInfo.SessionId)) { + $sacDetails['session_id'] = [string]$eventInfo.SessionId + } + + Write-Log "Notify logoff: ID=$($event.Id) User=$($eventInfo.Username) LT=$($eventInfo.LogonType) IP=$($eventInfo.SourceIP)" + Send-MonitorNotification -Message $formattedMessage ` + -EmailSubject "RDP Login Monitor: выход (ID $($event.Id))" ` + -SacEventType 'rdp.session.logoff' -SacSeverity 'info' ` + -SacTitle "RDP session logoff $($event.Id)" ` + -SacSummary "Logoff $($event.Id) $($eventInfo.Username)" ` + -SacOccurredAt $eventInfo.TimeCreated ` + -SacDetails $sacDetails | Out-Null + continue + } + if ($event.Id -eq 4625 -and $FailedLogonRateLimitEnabled) { $rl = Get-FailedLogonRateLimitDecision4625 -SourceIP $eventInfo.SourceIP ` -ComputerName $eventInfo.ComputerName -Username $eventInfo.Username ` diff --git a/README.md b/README.md index 0d8cdc3..ca630b9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # RDP Login Monitor -**Версия:** `2.1.10-SAC` (`$ScriptVersion` + `version.txt`) +**Версия:** `2.1.11-SAC` (`$ScriptVersion` + `version.txt`) PowerShell-мониторинг Windows: RDP/RDS, RD Gateway, WinRM, admin share, блокировки УЗ, heartbeat, отчёты. @@ -27,6 +27,7 @@ powershell -NoProfile -ExecutionPolicy Bypass -File "\\\NETLOGON\RDP-login-m | Источник | Тип SAC | |----------|---------| | Security 4624/4625 | `rdp.login.*` | +| Security 4634/4647 (прямой RDP) | `rdp.session.logoff` → закрытие сессии в SAC | | Security 5140 | `smb.admin_share.access` | | WinRM Operational 91 | `winrm.session.started` | | RD Gateway 302/303 | `rdg.connection.*` → flap 302→303 в SAC | diff --git a/README_eng.md b/README_eng.md index 9626956..6ccdd90 100644 --- a/README_eng.md +++ b/README_eng.md @@ -1,6 +1,6 @@ # RDP Login Monitor -**Version:** `2.1.5-SAC` (`$ScriptVersion` + `version.txt`) +**Version:** `2.1.11-SAC` (`$ScriptVersion` + `version.txt`) PowerShell toolkit for monitoring Windows logons with Telegram and/or Email (SMTP) notifications. diff --git a/version.txt b/version.txt index 45a6028..ed356fa 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.1.10-SAC +2.1.11-SAC