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.
This commit is contained in:
PTah
2026-07-13 17:10:24 +10:00
parent ac2384a479
commit 23fa83ddd6
4 changed files with 107 additions and 14 deletions
+101 -9
View File
@@ -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 = "<b>"
if ($EventID -eq 4624) { $message += "✅ УСПЕШНЫЙ ВХОД" }
elseif ($EventID -eq 4625) { $message += "❌ НЕУДАЧНАЯ ПОПЫТКА" }
elseif ($EventID -eq 4634) { $message += "🚪 ВЫХОД ИЗ СЕССИИ" }
elseif ($EventID -eq 4647) { $message += "🚪 ВЫХОД ПОЛЬЗОВАТЕЛЯ" }
else { $message += "⚠️ СОБЫТИЕ" }
$message += "</b>`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,9 +4623,16 @@ function Start-LoginMonitor {
$modeLabel = 'server LT2/3/10'
}
if ($interactiveTypes -notcontains $eventInfo.LogonType) {
$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
$ignoreReason = "EventID $($event.Id) not monitored"
@@ -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 `
+2 -1
View File
@@ -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 "\\<DC>\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 |
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -1 +1 @@
2.1.10-SAC
2.1.11-SAC