<# .SYNOPSIS Диагностика RDP Login Monitor после входа по RDP (или при «тишине» в Telegram/SAC). .DESCRIPTION Собирает: процесс монитора, задачи планировщика, настройки (без секретов), хвост login_monitor.log, Security 4624/4625/4778/4634, симуляцию фильтров монитора, SAC spool, сессии RDP. Отчёт: C:\ProgramData\RDP-login-monitor\Logs\diagnose_YYYYMMDD_HHmmss.txt .EXAMPLE powershell.exe -NoProfile -ExecutionPolicy Bypass -File Diagnose-RdpLoginMonitor.ps1 -MinutesBack 15 -ExpectedUser jdoe .NOTES Рекомендуется запуск от администратора. Без прав Security-журнал может быть неполным. #> [CmdletBinding()] param( [string]$InstallRoot = "$env:ProgramData\RDP-login-monitor", [int]$MinutesBack = 15, [int]$MonitorLogTailLines = 120, [string]$ExpectedUser = '', [string]$OutputPath = '' ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Continue' function Write-Section { param([string]$Title) $line = ('=' * 72) "`n$line`n $Title`n$line`n" } function Redact-SettingsText { param([string]$Text) if ([string]::IsNullOrWhiteSpace($Text)) { return '(файл пуст или не прочитан)' } $out = $Text $out = [regex]::Replace($out, '(?m)^(\s*\$(?:TelegramBotToken|TelegramChatID|SacApiKey|MailSmtpPassword|TelegramBotTokenProtectedB64|TelegramChatIDProtectedB64|MailSmtpPasswordProtectedB64)\s*=).*', '${1}***REDACTED***') return $out } function Get-EventDataMapFromEvent { param($Event) $map = @{} try { $xml = [xml]$Event.ToXml() foreach ($d in $xml.Event.EventData.Data) { $name = [string]$d.Name if ([string]::IsNullOrWhiteSpace($name)) { continue } $map[$name] = [string]$d.'#text' } } catch { } return $map } function Get-LoginFieldsFromEvent { param($Event) $map = Get-EventDataMapFromEvent -Event $Event $username = $map['TargetUserName'] if ([string]::IsNullOrWhiteSpace($username)) { $username = $map['AccountName'] } if ([string]::IsNullOrWhiteSpace($username)) { $username = $map['UserName'] } if ([string]::IsNullOrWhiteSpace($username)) { $username = '-' } $computerName = $map['WorkstationName'] if ([string]::IsNullOrWhiteSpace($computerName)) { $computerName = $map['ComputerName'] } if ([string]::IsNullOrWhiteSpace($computerName)) { $computerName = '-' } $sourceIP = $map['IpAddress'] if ([string]::IsNullOrWhiteSpace($sourceIP)) { $sourceIP = $map['SourceNetworkAddress'] } if ([string]::IsNullOrWhiteSpace($sourceIP)) { $sourceIP = '-' } $logonType = 0 if ($map.ContainsKey('LogonType') -and $map['LogonType'] -match '(\d+)') { $logonType = [int]$Matches[1] } $processName = $map['LogonProcessName'] if ([string]::IsNullOrWhiteSpace($processName)) { $processName = $map['AuthenticationPackageName'] } if ([string]::IsNullOrWhiteSpace($processName)) { $processName = '-' } [pscustomobject]@{ TimeCreated = $Event.TimeCreated EventId = [int]$Event.Id Username = $username.Trim() ComputerName = $computerName.Trim() SourceIP = $sourceIP.Trim() LogonType = $logonType ProcessName = $processName.Trim() } } function Test-IsWorkstationOs { try { $pt = (Get-CimInstance Win32_OperatingSystem -ErrorAction Stop).ProductType return ($pt -eq 1) } catch { return $false } } function Get-MonitorVerdictFor4624 { param( $Fields, [bool]$IsWorkstation ) $reasons = [System.Collections.Generic.List[string]]::new() if ($Fields.EventId -ne 4624) { return [pscustomobject]@{ Verdict = 'N/A'; Reasons = @('не 4624') } } if ($IsWorkstation) { $allowed = @(10) $modeLabel = 'workstation LT10' } else { $allowed = @(2, 3, 10) $modeLabel = 'server LT2/3/10' } if ($allowed -notcontains $Fields.LogonType) { $reasons.Add("LogonType $($Fields.LogonType) not in $modeLabel") } $u = $Fields.Username if ([string]::IsNullOrWhiteSpace($u) -or $u -eq '-') { $reasons.Add('пустой Username') } if ($u -match '(?i)(\\)?DWM-\d+') { $reasons.Add('DWM-*') } if ($u -match '(?i)(\\)?UMFD-\d+') { $reasons.Add('UMFD-*') } if ($u -like '*$') { $reasons.Add('machine account ($)') } if ($u -match '(?i)^(SYSTEM|LOCAL SERVICE|NETWORK SERVICE|ANONYMOUS LOGON)$') { $reasons.Add('служебная учётная запись') } if ($Fields.SourceIP -eq '127.0.0.1' -or $Fields.SourceIP -like 'fe80:*' -or $Fields.SourceIP -eq '::1') { $reasons.Add("локальный IP ($($Fields.SourceIP))") } if ($Fields.ComputerName -eq '-' -or $Fields.ComputerName -eq 'N/A') { $reasons.Add('WorkstationName = - (встроенный фильтр монитора)') } if ($Fields.ProcessName -like '*NtLmSsp*') { $reasons.Add('Process NtLmSsp') } if ($reasons.Count -eq 0) { return [pscustomobject]@{ Verdict = 'WOULD_NOTIFY'; Reasons = @('проходит фильтры монитора (4624)') } } return [pscustomobject]@{ Verdict = 'WOULD_SKIP'; Reasons = $reasons } } function Get-ExternalCommandOutput { param( [string]$Label, [scriptblock]$Block ) $sb = New-Object System.Text.StringBuilder [void]$sb.AppendLine("--- $Label ---") try { $out = & $Block 2>&1 if ($null -eq $out) { [void]$sb.AppendLine('(нет вывода)') } else { foreach ($line in @($out)) { [void]$sb.AppendLine([string]$line) } } } catch { [void]$sb.AppendLine("ERROR: $($_.Exception.Message)") } return $sb.ToString() } $startedAt = Get-Date $isAdmin = $false try { $id = [Security.Principal.WindowsIdentity]::GetCurrent() $p = New-Object Security.Principal.WindowsPrincipal($id) $isAdmin = $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } catch { } if (-not $OutputPath) { $logDir = Join-Path $InstallRoot 'Logs' if (-not (Test-Path -LiteralPath $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } $OutputPath = Join-Path $logDir ("diagnose_{0:yyyyMMdd_HHmmss}.txt" -f $startedAt) } $report = New-Object System.Text.StringBuilder [void]$report.AppendLine('RDP Login Monitor — диагностический отчёт') [void]$report.AppendLine("Сформирован: $($startedAt.ToString('yyyy-MM-dd HH:mm:ss'))") [void]$report.AppendLine("Компьютер: $env:COMPUTERNAME") [void]$report.AppendLine("Пользователь сеанса: $([Environment]::UserDomainName)\$([Environment]::UserName)") [void]$report.AppendLine("Elevated (Admin): $isAdmin") [void]$report.AppendLine("InstallRoot: $InstallRoot") [void]$report.AppendLine("MinutesBack: $MinutesBack") if ($ExpectedUser) { [void]$report.AppendLine("ExpectedUser: $ExpectedUser") } [void]$report.AppendLine("ReportPath: $OutputPath") [void]$report.Append((Write-Section '1. Процесс Login_Monitor.ps1')) $procs = @( Get-CimInstance Win32_Process -Filter "Name='powershell.exe' OR Name='pwsh.exe'" -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -match 'Login_Monitor\.ps1' } ) if ($procs.Count -eq 0) { [void]$report.AppendLine('НЕ НАЙДЕН процесс Login_Monitor.ps1') } else { foreach ($proc in $procs) { [void]$report.AppendLine("PID=$($proc.ProcessId) Start=$($proc.CreationDate)") [void]$report.AppendLine(" $($proc.CommandLine)") } } [void]$report.Append((Write-Section '2. Scheduled Tasks')) foreach ($tn in @('RDP-Login-Monitor', 'RDP-Login-Monitor-Watchdog')) { [void]$report.AppendLine("--- $tn ---") $q = schtasks.exe /Query /TN $tn /FO LIST /V 2>&1 if ($LASTEXITCODE -ne 0) { [void]$report.AppendLine(' (задача не найдена или ошибка запроса)') } else { foreach ($line in @($q)) { [void]$report.AppendLine(" $line") } } } [void]$report.Append((Write-Section '3. Версии / deploy markers')) foreach ($rel in @('version.txt', 'deployed_version.txt', 'deploy_last_update.txt', 'restart.request')) { $fp = Join-Path $InstallRoot $rel [void]$report.AppendLine("--- $rel ---") if (Test-Path -LiteralPath $fp) { Get-Content -LiteralPath $fp -ErrorAction SilentlyContinue | ForEach-Object { [void]$report.AppendLine(" $_") } } else { [void]$report.AppendLine(' (нет файла)') } } $lmPath = Join-Path $InstallRoot 'Login_Monitor.ps1' if (Test-Path -LiteralPath $lmPath) { $verLine = Select-String -LiteralPath $lmPath -Pattern '^\$ScriptVersion\s*=' -ErrorAction SilentlyContinue | Select-Object -First 1 if ($verLine) { [void]$report.AppendLine("Login_Monitor.ps1: $($verLine.Line.Trim())") } } [void]$report.Append((Write-Section '4. login_monitor.settings.ps1 (redacted)')) $settingsPath = Join-Path $InstallRoot 'login_monitor.settings.ps1' if (Test-Path -LiteralPath $settingsPath) { $raw = Get-Content -LiteralPath $settingsPath -Raw -ErrorAction SilentlyContinue [void]$report.AppendLine(Redact-SettingsText -Text $raw) } else { [void]$report.AppendLine('Файл settings не найден') } [void]$report.AppendLine('') [void]$report.AppendLine('--- ignore.lst ---') $ignorePath = Join-Path $InstallRoot 'ignore.lst' if (Test-Path -LiteralPath $ignorePath) { Get-Content -LiteralPath $ignorePath | ForEach-Object { [void]$report.AppendLine(" $_") } } else { [void]$report.AppendLine(' (нет файла)') } [void]$report.Append((Write-Section '5. SAC spool / heartbeat')) $spoolDir = Join-Path $InstallRoot 'sac-spool' if (Test-Path -LiteralPath $spoolDir) { $spoolFiles = @(Get-ChildItem -LiteralPath $spoolDir -Filter '*.json' -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending) [void]$report.AppendLine("sac-spool: файлов $($spoolFiles.Count)") $spoolFiles | Select-Object -First 5 | ForEach-Object { [void]$report.AppendLine(" $($_.Name) $($_.LastWriteTime)") } } else { [void]$report.AppendLine('sac-spool: каталог отсутствует') } $hbPath = Join-Path $InstallRoot 'heartbeat.txt' if (Test-Path -LiteralPath $hbPath) { [void]$report.AppendLine("heartbeat.txt: $(Get-Content -LiteralPath $hbPath -First 1)") } else { [void]$report.AppendLine('heartbeat.txt: нет') } [void]$report.Append((Write-Section "6. login_monitor.log (last $MonitorLogTailLines lines)")) $monLog = Join-Path $InstallRoot 'Logs\login_monitor.log' if (Test-Path -LiteralPath $monLog) { Get-Content -LiteralPath $monLog -Tail $MonitorLogTailLines -ErrorAction SilentlyContinue | ForEach-Object { [void]$report.AppendLine($_) } } else { [void]$report.AppendLine('login_monitor.log не найден') } [void]$report.AppendLine('') [void]$report.AppendLine('--- login_monitor.log: Notify / Skip / dedup / rdp.login (last 50 matches) ---') if (Test-Path -LiteralPath $monLog) { Select-String -LiteralPath $monLog -Pattern 'Notify:|Skip 4624|Notify dedup|SAC: accepted.*rdp\.login|type=rdp\.login' -ErrorAction SilentlyContinue | Select-Object -Last 50 | ForEach-Object { [void]$report.AppendLine($_.Line) } } [void]$report.Append((Write-Section '7. RDP / interactive sessions')) [void]$report.AppendLine((Get-ExternalCommandOutput -Label 'quser' -Block { quser.exe 2>&1 })) [void]$report.AppendLine((Get-ExternalCommandOutput -Label 'query session' -Block { query.exe session 2>&1 })) [void]$report.Append((Write-Section "8. Security log (last $MinutesBack min)")) if (-not $isAdmin) { [void]$report.AppendLine('WARN: скрипт не от администратора — чтение Security может быть неполным.') } $since = (Get-Date).AddMinutes(-1 * [math]::Abs($MinutesBack)) $RcmLogName = 'Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational' $isWs = Test-IsWorkstationOs [void]$report.AppendLine("OS ProductType workstation=$isWs (server mode LT 2/3/10, workstation LT 10)") [void]$report.AppendLine("Window StartTime >= $($since.ToString('yyyy-MM-dd HH:mm:ss'))") [void]$report.AppendLine('') foreach ($eid in @(4624, 4625, 4778, 4634)) { [void]$report.AppendLine("--- Event ID $eid ---") try { $evs = @(Get-WinEvent -FilterHashtable @{ LogName = 'Security' ID = $eid StartTime = $since } -ErrorAction Stop | Sort-Object TimeCreated) } catch { [void]$report.AppendLine(" (нет событий или ошибка: $($_.Exception.Message))") continue } if ($evs.Count -eq 0) { [void]$report.AppendLine(' (нет событий в окне)') continue } foreach ($ev in $evs) { if ($eid -eq 4624 -or $eid -eq 4625) { $f = Get-LoginFieldsFromEvent -Event $ev $verdict = if ($eid -eq 4624) { Get-MonitorVerdictFor4624 -Fields $f -IsWorkstation $isWs } else { $null } $marker = '' if ($ExpectedUser -and $f.Username -like "*$ExpectedUser*") { $marker = ' <<<< EXPECTED USER' } [void]$report.AppendLine( (" {0:yyyy-MM-dd HH:mm:ss} ID={1} User={2} LT={3} IP={4} Wks={5} Proc={6}{7}" -f $f.TimeCreated, $f.EventId, $f.Username, $f.LogonType, $f.SourceIP, $f.ComputerName, $f.ProcessName, $marker) ) if ($verdict) { [void]$report.AppendLine(" MONITOR: $($verdict.Verdict) — $($verdict.Reasons -join '; ')") } } else { [void]$report.AppendLine(" $($ev.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss')) RecordId=$($ev.RecordId)") $f = Get-LoginFieldsFromEvent -Event $ev if ($f.Username -ne '-') { [void]$report.AppendLine(" User=$($f.Username) LT=$($f.LogonType) IP=$($f.SourceIP)") } } } } [void]$report.Append((Write-Section '9. Краткий итог')) $recent4624 = @() try { $recent4624 = @(Get-WinEvent -FilterHashtable @{ LogName = 'Security' ID = 4624 StartTime = $since } -ErrorAction SilentlyContinue) } catch { } $notifyable = @() foreach ($ev in $recent4624) { $f = Get-LoginFieldsFromEvent -Event $ev $v = Get-MonitorVerdictFor4624 -Fields $f -IsWorkstation $isWs if ($v.Verdict -eq 'WOULD_NOTIFY') { $notifyable += $f } } [void]$report.AppendLine("Security 4624 в окне: $($recent4624.Count)") [void]$report.AppendLine("Из них базовые фильтры монитора пропустили бы: $($notifyable.Count)") try { $reconnectCount = @(Get-WinEvent -FilterHashtable @{ LogName = 'Security'; ID = 4778; StartTime = $since } -ErrorAction SilentlyContinue).Count [void]$report.AppendLine("Security 4778 (reconnect): $reconnectCount") } catch { [void]$report.AppendLine('Security 4778 (reconnect): n/a') } $sacLoginLines = @() if (Test-Path -LiteralPath $monLog) { $sinceLog = $since.ToString('yyyy-MM-dd HH:mm') $sacLoginLines = @(Select-String -LiteralPath $monLog -Pattern 'SAC: accepted.*type=rdp\.login\.success' -ErrorAction SilentlyContinue | Where-Object { $_.Line -ge $sinceLog } | Select-Object -ExpandProperty Line) } [void]$report.AppendLine("SAC ingest rdp.login.success в login_monitor.log (после $sinceLog): $($sacLoginLines.Count)") foreach ($ln in $sacLoginLines) { [void]$report.AppendLine(" $ln") } if ($notifyable.Count -eq 0 -and $recent4624.Count -gt 0) { [void]$report.AppendLine('') [void]$report.AppendLine('Все 4624 в окне отфильтрованы базовыми правилами — см. MONITOR: WOULD_SKIP выше.') [void]$report.AppendLine('Дополнительно: ignore.lst, IgnoreAdvapiNetworkLogonSourceIps в settings, dedup 90s.') } elseif ($recent4624.Count -eq 0) { [void]$report.AppendLine('') [void]$report.AppendLine('Нет 4624 в окне — возможен reconnect (4778) без нового 4624.') [void]$report.AppendLine('Повторите: Sign out → новый RDP → скрипт с -MinutesBack 5.') } [void]$report.AppendLine('') [void]$report.AppendLine('UseSAC=exclusive: Telegram по rdp.login.success только из SAC (не локально агентом).') [void]$report.AppendLine('На сервере RDS без аудита Security 4624 — смотрите RCM Operational 1149 (2.1.5-SAC+: исправлен silent skip при ComputerName=-).') [void]$report.AppendLine('rdp.login.success = severity info; при SAC min_severity=warning Telegram не уйдёт, но событие в UI SAC должно быть.') [void]$report.Append((Write-Section '10. RCM Operational 1149')) $recent1149 = @() try { $recent1149 = @(Get-WinEvent -FilterHashtable @{ LogName = $RcmLogName ID = 1149 StartTime = $since } -ErrorAction SilentlyContinue) } catch { } [void]$report.AppendLine("RCM 1149 в окне ($RcmLogName): $($recent1149.Count)") foreach ($ev in $recent1149 | Select-Object -First 8) { $u = '-'; $ip = '-' try { if ($ev.Properties.Count -gt 0) { $u = [string]$ev.Properties[0].Value } if ($ev.Properties.Count -gt 2) { $ip = [string]$ev.Properties[2].Value } } catch { } [void]$report.AppendLine(" $($ev.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss')) User=$u IP=$ip") } $rcmNotifyLines = @() if (Test-Path -LiteralPath $monLog) { $sinceLog = $since.ToString('yyyy-MM-dd HH:mm') $rcmNotifyLines = @(Select-String -LiteralPath $monLog -Pattern 'Notify RCM 1149|Skip 1149' -ErrorAction SilentlyContinue | Where-Object { $_.Line -ge $sinceLog } | Select-Object -ExpandProperty Line) } [void]$report.AppendLine("Строки Notify/Skip 1149 в login_monitor.log: $($rcmNotifyLines.Count)") foreach ($ln in $rcmNotifyLines | Select-Object -Last 10) { [void]$report.AppendLine(" $ln") } if ($recent1149.Count -gt 0 -and $rcmNotifyLines.Count -eq 0) { [void]$report.AppendLine('ВНИМАНИЕ: 1149 в журнале есть, в логе агента нет Notify/Skip — вероятен баг 2.1.4 (все 1149 отбрасывались) или агент не работал в момент входа.') } [void]$report.AppendLine('') [void]$report.AppendLine('Проверьте SAC: type=rdp.login.success, hostname, время входа, event_id из login_monitor.log.') $text = $report.ToString() [System.IO.File]::WriteAllText($OutputPath, $text, (New-Object System.Text.UTF8Encoding $true)) Write-Host '' Write-Host "Отчёт сохранён: $OutputPath" -ForegroundColor Green Write-Host 'Пришлите этот файл для анализа.' -ForegroundColor Cyan Write-Host '' Get-Content -LiteralPath $OutputPath -Tail 25