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