fix: dedup 4624 login alerts and log Skip/dedup reasons (1.2.18-SAC)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-28 15:17:01 +10:00
parent 49359c07a6
commit 1f27272883
5 changed files with 126 additions and 11 deletions
+59 -5
View File
@@ -80,7 +80,7 @@ $script:MonitorLoopInitialized = $false
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
$ScriptVersion = "1.2.17-SAC"
$ScriptVersion = "1.2.18-SAC"
# Логи (все под InstallRoot)
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
@@ -94,6 +94,8 @@ $LogRotationMinute = 0
# Heartbeat (файл; при отсутствии обновления > HeartbeatStaleAlertMultiplier × интервал — оповещение)
$HeartbeatInterval = 3600
$HeartbeatStaleAlertMultiplier = 2
# Один RDP-вход часто даёт 2+ события 4624 с одним временем — не слать дубли в Telegram/SAC.
$LoginSuccessNotifyDedupSeconds = 90
$HeartbeatFile = Join-Path $script:InstallRoot "Logs\last_heartbeat.txt"
$DeployUpdateMarkerFile = Join-Path $script:InstallRoot "deploy_last_update.txt"
# Построчные правила подавления уведомлений Security 4624/4625 (см. ignore.lst.example в репозитории).
@@ -1900,6 +1902,37 @@ function Format-LoginEvent {
}
$script:FailedLogonBuckets = @{}
$script:LoginSuccessNotifyDedup = @{}
function Get-RdpLoginSuccessNotifyDedupKey {
param(
[string]$SecurityLogComputerName,
[string]$Username,
[string]$SourceIP,
[int]$LogonType
)
$hostPart = if ([string]::IsNullOrWhiteSpace($SecurityLogComputerName)) { $env:COMPUTERNAME } else { $SecurityLogComputerName.Trim() }
$userPart = if ($null -ne $Username) { $Username.Trim() } else { '-' }
$ipPart = if ($null -ne $SourceIP) { $SourceIP.Trim() } else { '-' }
return "$hostPart|4624|$userPart|$ipPart|$LogonType"
}
function Test-RdpLoginSuccessNotifyDedupAllow {
param(
[string]$DedupKey,
[datetime]$TimeCreated
)
if ($LoginSuccessNotifyDedupSeconds -le 0) { return $true }
if ($script:LoginSuccessNotifyDedup.ContainsKey($DedupKey)) {
$lastUtc = $script:LoginSuccessNotifyDedup[$DedupKey]
$delta = ($TimeCreated.ToUniversalTime() - $lastUtc).TotalSeconds
if ($delta -ge 0 -and $delta -lt $LoginSuccessNotifyDedupSeconds) {
return $false
}
}
$script:LoginSuccessNotifyDedup[$DedupKey] = $TimeCreated.ToUniversalTime()
return $true
}
function Get-FailedLogonSourceKeyPart {
param(
@@ -2634,34 +2667,56 @@ function Start-LoginMonitor {
$eventInfo = Get-LoginEventInfo -Event $event
$logonTypeName = Get-LogonTypeName -LogonType $eventInfo.LogonType
$shouldIgnore = $false
$ignoreReason = ''
if ($MonitorInteractiveOnly -and -not $MonitorAllEvents) {
if ($event.Id -eq 4648) {
$shouldIgnore = $true
$ignoreReason = 'EventID 4648 excluded (MonitorInteractiveOnly)'
} elseif ($event.Id -in 4624, 4625) {
if ($osKind.IsWorkstation) {
$interactiveTypes = @(10)
$modeLabel = 'workstation LT10'
} else {
$interactiveTypes = @(2, 3, 10)
$modeLabel = 'server LT2/3/10'
}
if ($interactiveTypes -notcontains $eventInfo.LogonType) {
$shouldIgnore = $true
$ignoreReason = "LogonType $($eventInfo.LogonType) not in $modeLabel"
}
} else {
$shouldIgnore = $true
$ignoreReason = "EventID $($event.Id) not monitored"
}
}
if (-not $shouldIgnore -and -not $MonitorAllEvents) {
$shouldIgnore = Should-IgnoreEvent -Username $eventInfo.Username `
if (Should-IgnoreEvent -Username $eventInfo.Username `
-ProcessName $eventInfo.ProcessName `
-ComputerName $eventInfo.ComputerName `
-EventID $event.Id `
-LogonType $eventInfo.LogonType `
-SourceIP $eventInfo.SourceIP
-SourceIP $eventInfo.SourceIP) {
$shouldIgnore = $true
$ignoreReason = 'ignore.lst or built-in exclusion (user/process/IP/workstation)'
}
}
if ($shouldIgnore) {
Write-Log "Skip $($event.Id): User=$($eventInfo.Username) LT=$($eventInfo.LogonType) IP=$($eventInfo.SourceIP) Wks=$($eventInfo.ComputerName)$ignoreReason"
continue
}
if ($event.Id -eq 4624) {
$dedupKey = Get-RdpLoginSuccessNotifyDedupKey -SecurityLogComputerName $event.MachineName `
-Username $eventInfo.Username -SourceIP $eventInfo.SourceIP -LogonType $eventInfo.LogonType
if (-not (Test-RdpLoginSuccessNotifyDedupAllow -DedupKey $dedupKey -TimeCreated $eventInfo.TimeCreated)) {
Write-Log "Notify dedup 4624: User=$($eventInfo.Username) LT=$($eventInfo.LogonType) IP=$($eventInfo.SourceIP) (window ${LoginSuccessNotifyDedupSeconds}s)"
continue
}
}
if (-not $shouldIgnore) {
if ($event.Id -eq 4625 -and $FailedLogonRateLimitEnabled) {
$rl = Get-FailedLogonRateLimitDecision4625 -SourceIP $eventInfo.SourceIP `
-ComputerName $eventInfo.ComputerName -Username $eventInfo.Username `
@@ -2733,7 +2788,6 @@ function Start-LoginMonitor {
} | Out-Null
}
}
}
$lastCheckTime = ($events | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1)
}
+2
View File
@@ -84,6 +84,8 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-logi
- Stale heartbeat: если **`last_heartbeat.txt`** не обновлялся дольше **`$HeartbeatStaleAlertMultiplier` × `$HeartbeatInterval`** — оповещение в Telegram/Email (см. п. 8 подготовки).
- При старте в Telegram/Email: строка **«Каналы уведомлений»** (фактический порядок доставки), плюс режим RDS/4740 по конфигурации.
- Telegram при старте: при установленном **RD Session Host** (или аналогичных компонентах RDS, не только шлюз) — строка про входы по RDP/RDS на этом сервере; при доступном журнале **RD Gateway** — отдельная строка про подключения к **внутренним целевым ПК** через шлюз (302/303). Узел только с ролью RD Gateway не дублирует формулировку «хост сессий».
- **Дубли Telegram на один RDP-вход:** Windows часто пишет **несколько 4624** с одним временем; с версии **1.2.18-SAC** второе уведомление за **`$LoginSuccessNotifyDedupSeconds`** (90 с) подавляется (`Notify dedup 4624` в логе).
- **В логе нет `Notify`, но 4624 в Security есть:** монитор обрабатывает только события **после** своего `StartTime` (окно опроса ~10 с при старте). Ищите строки **`Skip 4624:`** (фильтр LogonType / ignore.lst). Диагностика: **`tools\Show-Rdp4624Recent.ps1`**.
## 5) Автоматический перезапуск при падении
+51
View File
@@ -0,0 +1,51 @@
<#
.SYNOPSIS
Просмотр недавних 4624 с полями для диагностики RDP-login-monitor.
.EXAMPLE
.\Show-Rdp4624Recent.ps1
.\Show-Rdp4624Recent.ps1 -Minutes 30 -User papatramp
#>
[CmdletBinding()]
param(
[int]$Minutes = 15,
[string]$User = '',
[int]$MaxEvents = 50
)
$start = (Get-Date).AddMinutes(-$Minutes)
Write-Host "Security 4624 since $($start.ToString('yyyy-MM-dd HH:mm:ss')) (local time)" -ForegroundColor Cyan
$events = @(Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4624
StartTime = $start
} -MaxEvents $MaxEvents -ErrorAction SilentlyContinue)
if ($events.Count -eq 0) {
Write-Host 'No 4624 events in window.'
exit 0
}
function Get-EvProp($Event, [string]$Name) {
$xml = [xml]$Event.ToXml()
$n = $xml.Event.EventData.Data | Where-Object { $_.Name -eq $Name } | Select-Object -First 1
if ($null -eq $n) { return '-' }
return [string]$n.'#text'
}
$rows = foreach ($ev in $events) {
$u = Get-EvProp $ev 'TargetUserName'
if ($User -and $u -notlike "*$User*") { continue }
[pscustomobject]@{
TimeCreated = $ev.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss')
RecordId = $ev.RecordId
User = $u
LogonType = Get-EvProp $ev 'LogonType'
IpAddress = Get-EvProp $ev 'IpAddress'
Workstation = Get-EvProp $ev 'WorkstationName'
Process = Get-EvProp $ev 'LogonProcessName'
}
}
$rows | Format-Table -AutoSize
Write-Host "`nTip: monitor log — Select-String -Path 'C:\ProgramData\RDP-login-monitor\Logs\*.log' -Pattern 'Notify:|Skip 4624|Notify dedup'"
+8
View File
@@ -0,0 +1,8 @@
param([string]$Path = (Join-Path $PSScriptRoot '..\Login_Monitor.ps1'))
$errs = $null
[void][System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path $Path), [ref]$null, [ref]$errs)
if ($errs) {
$errs | ForEach-Object { $_.ToString() }
exit 1
}
Write-Output 'OK'
+1 -1
View File
@@ -1 +1 @@
1.2.17-SAC
1.2.18-SAC