diff --git a/Login_Monitor.ps1 b/Login_Monitor.ps1 index 672325e..bcd9688 100644 --- a/Login_Monitor.ps1 +++ b/Login_Monitor.ps1 @@ -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,35 +2667,57 @@ 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 ` - -ProcessName $eventInfo.ProcessName ` - -ComputerName $eventInfo.ComputerName ` - -EventID $event.Id ` - -LogonType $eventInfo.LogonType ` - -SourceIP $eventInfo.SourceIP + if (Should-IgnoreEvent -Username $eventInfo.Username ` + -ProcessName $eventInfo.ProcessName ` + -ComputerName $eventInfo.ComputerName ` + -EventID $event.Id ` + -LogonType $eventInfo.LogonType ` + -SourceIP $eventInfo.SourceIP) { + $shouldIgnore = $true + $ignoreReason = 'ignore.lst or built-in exclusion (user/process/IP/workstation)' + } } - if (-not $shouldIgnore) { - if ($event.Id -eq 4625 -and $FailedLogonRateLimitEnabled) { + 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 ($event.Id -eq 4625 -and $FailedLogonRateLimitEnabled) { $rl = Get-FailedLogonRateLimitDecision4625 -SourceIP $eventInfo.SourceIP ` -ComputerName $eventInfo.ComputerName -Username $eventInfo.Username ` -LogonType $eventInfo.LogonType -TimeCreated $eventInfo.TimeCreated ` @@ -2731,7 +2786,6 @@ function Start-LoginMonitor { event_id_windows = [int]$event.Id workstation_name = $eventInfo.ComputerName } | Out-Null - } } } $lastCheckTime = ($events | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1) diff --git a/README.md b/README.md index 112e1d0..ae2e5da 100644 --- a/README.md +++ b/README.md @@ -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) Автоматический перезапуск при падении diff --git a/tools/Show-Rdp4624Recent.ps1 b/tools/Show-Rdp4624Recent.ps1 new file mode 100644 index 0000000..07f1c18 --- /dev/null +++ b/tools/Show-Rdp4624Recent.ps1 @@ -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'" diff --git a/tools/Test-ScriptSyntax.ps1 b/tools/Test-ScriptSyntax.ps1 new file mode 100644 index 0000000..be21acd --- /dev/null +++ b/tools/Test-ScriptSyntax.ps1 @@ -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' diff --git a/version.txt b/version.txt index c311512..47ec3d6 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.2.17-SAC +1.2.18-SAC