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:
+101
-9
@@ -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 `
|
||||
|
||||
@@ -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
@@ -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
@@ -1 +1 @@
|
||||
2.1.10-SAC
|
||||
2.1.11-SAC
|
||||
|
||||
Reference in New Issue
Block a user