feat: AD lockout 4740 with IIS IPs and SMTP notifications (v1.5.2)

Monitor Security 4740 on a configured domain controller and enrich alerts
with ActiveSync client IPs from IIS logs. Add unified Telegram and email
delivery with NotifyOrder. Update README and DPAPI helper for mail password.
This commit is contained in:
2026-05-19 19:47:23 +10:00
parent 938df9bfe0
commit b7786e8690
4 changed files with 389 additions and 15 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# Запуск: от администратора на ТОМ ЖЕ компьютере, где будет Login_Monitor.ps1. # Запуск: от администратора на ТОМ ЖЕ компьютере, где будет Login_Monitor.ps1.
# Результат (Base64) вставьте в $TelegramBotTokenProtectedB64 / $TelegramChatIDProtectedB64. # Результат (Base64) вставьте в $TelegramBotTokenProtectedB64 / $TelegramChatIDProtectedB64 / $MailSmtpPasswordProtectedB64.
param( param(
[Parameter(Mandatory = $true)][string]$PlainText [Parameter(Mandatory = $true)][string]$PlainText
) )
+380 -9
View File
@@ -3,6 +3,7 @@
Мониторинг логинов/попыток входа с уведомлениями в Telegram Мониторинг логинов/попыток входа с уведомлениями в Telegram
.DESCRIPTION .DESCRIPTION
Отслеживает события входа в систему (Security 4624/4625) и события RD Gateway (302/303), Отслеживает события входа в систему (Security 4624/4625) и события RD Gateway (302/303),
на заданном КД — блокировки учётных записей (Security 4740) с IP из логов IIS ActiveSync,
отправляет уведомления в Telegram, делает ротацию логов, heartbeat в файл и ежедневный отчет. отправляет уведомления в Telegram, делает ротацию логов, heartbeat в файл и ежедневный отчет.
.NOTES .NOTES
Требуется: PowerShell 5.0+, запуск от администратора. Требуется: PowerShell 5.0+, запуск от администратора.
@@ -28,6 +29,7 @@ param(
[string]$TelegramChatID = '<TELEGRAM_CHAT_ID>', [string]$TelegramChatID = '<TELEGRAM_CHAT_ID>',
[string]$TelegramBotTokenProtectedB64 = "", [string]$TelegramBotTokenProtectedB64 = "",
[string]$TelegramChatIDProtectedB64 = "", [string]$TelegramChatIDProtectedB64 = "",
[string]$MailSmtpPasswordProtectedB64 = "",
[switch]$Watchdog, [switch]$Watchdog,
[switch]$InstallTasks, [switch]$InstallTasks,
[switch]$SkipScheduledTaskMaintenance [switch]$SkipScheduledTaskMaintenance
@@ -69,7 +71,7 @@ $script:MonitorSingletonLockStream = $null
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах). # строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только # Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1). # исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
$ScriptVersion = "1.4.4" $ScriptVersion = "1.5.2"
# Логи (все под InstallRoot) # Логи (все под InstallRoot)
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log" $LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
@@ -148,6 +150,25 @@ $IgnoreAdvapiNetworkLogonSourceIps = @(
) )
$IgnoreAdvapiNetworkLogonProcessContains = "Advapi" $IgnoreAdvapiNetworkLogonProcessContains = "Advapi"
# Блокировка учётной записи AD (Security 4740) + IP клиента из логов IIS ActiveSync.
# Мониторинг включается только если скрипт запущен на узле, имя которого совпадает с $LockoutMonitorDomainController.
$LockoutMonitorDomainController = ""
$NetBiosDomainName = ""
$ExchangeIisLogPath = ""
$ExchangeServerHostForIisExclude = ""
$ExchangeIisLogTailLines = 5000
# Очередь оповещений: telegram, email (или tg, mail). Пусто = авто: настроенные каналы, порядок telegram → email.
$NotifyOrder = ""
$MailSmtpHost = ""
$MailSmtpPort = 587
$MailSmtpUser = ""
$MailSmtpPassword = ""
$MailFrom = ""
$MailTo = ""
$MailSmtpStartTls = $true
$MailSmtpSsl = $false
# ============================================ # ============================================
# ИНИЦИАЛИЗАЦИЯ # ИНИЦИАЛИЗАЦИЯ
# ============================================ # ============================================
@@ -529,6 +550,70 @@ if (-not [string]::IsNullOrWhiteSpace($TelegramChatIDProtectedB64)) {
if ($TelegramBotToken -eq '<TELEGRAM_BOT_TOKEN>') { $TelegramBotToken = "" } if ($TelegramBotToken -eq '<TELEGRAM_BOT_TOKEN>') { $TelegramBotToken = "" }
if ($TelegramChatID -eq '<TELEGRAM_CHAT_ID>') { $TelegramChatID = "" } if ($TelegramChatID -eq '<TELEGRAM_CHAT_ID>') { $TelegramChatID = "" }
if (-not [string]::IsNullOrWhiteSpace($MailSmtpPasswordProtectedB64)) {
try {
$MailSmtpPassword = Unprotect-RdpMonitorDpapiB64 -Base64 $MailSmtpPasswordProtectedB64
} catch {
Write-Host "Ошибка расшифровки MailSmtpPassword (DPAPI): $($_.Exception.Message)"
exit 1
}
}
function Test-NotifyTelegramConfigured {
return (-not [string]::IsNullOrWhiteSpace($TelegramBotToken)) -and
(-not [string]::IsNullOrWhiteSpace($TelegramChatID))
}
function Test-NotifyEmailConfigured {
return (-not [string]::IsNullOrWhiteSpace($MailSmtpHost)) -and
(-not [string]::IsNullOrWhiteSpace($MailFrom)) -and
(-not [string]::IsNullOrWhiteSpace($MailTo))
}
function Get-NotifyOrderChannels {
$configured = [System.Collections.Generic.List[string]]::new()
if (Test-NotifyTelegramConfigured) { $configured.Add('telegram') | Out-Null }
if (Test-NotifyEmailConfigured) { $configured.Add('email') | Out-Null }
if ([string]::IsNullOrWhiteSpace($NotifyOrder)) {
return @($configured)
}
$requested = [System.Collections.Generic.List[string]]::new()
foreach ($part in ($NotifyOrder -split '[,\s;]+')) {
$p = $part.Trim().ToLowerInvariant()
if ([string]::IsNullOrWhiteSpace($p)) { continue }
$channel = switch -Regex ($p) {
'^(tg|telegram)$' { 'telegram' }
'^(mail|email|e-mail)$' { 'email' }
default {
Write-Log "NotifyOrder: неизвестный канал '$part' (ожидается telegram/tg или email/mail)"
$null
}
}
if ($null -eq $channel) { continue }
if ($configured.Contains($channel) -and -not $requested.Contains($channel)) {
$requested.Add($channel) | Out-Null
}
}
return @($requested)
}
function Get-NotifyChainHuman {
$channels = @(Get-NotifyOrderChannels)
if ($channels.Count -eq 0) {
return 'нет (ни Telegram, ни SMTP не настроены)'
}
$labels = foreach ($ch in $channels) {
switch ($ch) {
'telegram' { 'Telegram' }
'email' { 'Email (SMTP)' }
default { $ch }
}
}
return ($labels -join ' → ')
}
function ConvertTo-TelegramHtml { function ConvertTo-TelegramHtml {
param([string]$Text) param([string]$Text)
if ($null -eq $Text) { return '' } if ($null -eq $Text) { return '' }
@@ -561,6 +646,10 @@ function Send-TelegramMessage {
} }
function Test-TelegramConnection { function Test-TelegramConnection {
if (-not (Test-NotifyTelegramConfigured)) {
Write-Log "Telegram: канал не настроен, проверка пропущена."
return $false
}
Write-Log "Проверка подключения к Telegram API..." Write-Log "Проверка подключения к Telegram API..."
try { try {
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
@@ -577,6 +666,105 @@ function Test-TelegramConnection {
return $false return $false
} }
function ConvertTo-EmailHtmlBody {
param([string]$TelegramHtmlMessage)
$inner = [string]$TelegramHtmlMessage
if ([string]::IsNullOrEmpty($inner)) { $inner = '' }
$inner = $inner -replace "`r`n", "<br>`r`n"
return @"
<html>
<body style="font-family:Segoe UI,Arial,sans-serif;font-size:14px;line-height:1.4;">
$inner
</body>
</html>
"@
}
function Send-EmailNotification {
param(
[string]$Message,
[string]$Subject = "RDP Login Monitor"
)
if (-not (Test-NotifyEmailConfigured)) {
Write-Log "Email: SMTP не настроен (нужны MailSmtpHost, MailFrom, MailTo)"
return $false
}
try {
$toList = @($MailTo -split '[,;]' | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
if ($toList.Count -eq 0) {
Write-Log "Email: MailTo пуст или некорректен"
return $false
}
$mailParams = @{
To = $toList
From = $MailFrom.Trim()
Subject = $Subject
Body = (ConvertTo-EmailHtmlBody -TelegramHtmlMessage $Message)
BodyAsHtml = $true
SmtpServer = $MailSmtpHost.Trim()
Port = [int]$MailSmtpPort
Encoding = [System.Text.Encoding]::UTF8
ErrorAction = 'Stop'
}
if ($MailSmtpSsl -or $MailSmtpStartTls) {
$mailParams['UseSsl'] = $true
}
if (-not [string]::IsNullOrWhiteSpace($MailSmtpUser)) {
$securePass = if ([string]::IsNullOrWhiteSpace($MailSmtpPassword)) {
New-Object System.Security.SecureString
} else {
ConvertTo-SecureString $MailSmtpPassword -AsPlainText -Force
}
$mailParams['Credential'] = New-Object System.Management.Automation.PSCredential($MailSmtpUser.Trim(), $securePass)
}
Send-MailMessage @mailParams
return $true
} catch {
Write-Log "Ошибка отправки Email: $($_.Exception.Message)"
return $false
}
}
function Test-MailSmtpConnection {
if (-not (Test-NotifyEmailConfigured)) {
Write-Log "Email: канал не настроен, проверка пропущена."
return $false
}
Write-Log "SMTP: $($MailSmtpHost):$MailSmtpPort (STARTTLS=$MailSmtpStartTls, SSL=$MailSmtpSsl), From=$MailFrom, To=$MailTo"
if (-not [string]::IsNullOrWhiteSpace($MailSmtpUser) -and [string]::IsNullOrWhiteSpace($MailSmtpPassword)) {
Write-Log "SMTP: задан MailSmtpUser, но пароль пуст (возможна ошибка при отправке)."
}
return $true
}
function Send-MonitorNotification {
param(
[string]$Message,
[string]$EmailSubject = "RDP Login Monitor"
)
$channels = @(Get-NotifyOrderChannels)
if ($channels.Count -eq 0) {
Write-Log "Оповещение не отправлено: нет настроенных каналов (Telegram и/или SMTP)"
return $false
}
$anyOk = $false
foreach ($ch in $channels) {
$ok = switch ($ch) {
'telegram' { Send-TelegramMessage -Message $Message }
'email' { Send-EmailNotification -Message $Message -Subject $EmailSubject }
default { $false }
}
if ($ok) { $anyOk = $true }
}
return $anyOk
}
function Test-Administrator { function Test-Administrator {
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent() $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser) $principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
@@ -963,6 +1151,8 @@ function Send-Heartbeat {
} else { } else {
$message += "`r`n🚫 <b>Игнорируются:</b> не задано (ignore.lst отсутствует или пуст)." $message += "`r`n🚫 <b>Игнорируются:</b> не задано (ignore.lst отсутствует или пуст)."
} }
$notifyChain = Get-NotifyChainHuman
$message += "`r`n📢 <b>Каналы уведомлений:</b> $(ConvertTo-TelegramHtml $notifyChain)"
if (Test-RDSDeploymentPresent) { if (Test-RDSDeploymentPresent) {
$message += "`r`n🔐 <b>RDS (хост сессий):</b> обнаружены компоненты RDS помимо чистого шлюза — в мониторинг входят входы по RDP/RDS на этом узле (Security 4624/4625, типы входа по настройке скрипта)." $message += "`r`n🔐 <b>RDS (хост сессий):</b> обнаружены компоненты RDS помимо чистого шлюза — в мониторинг входят входы по RDP/RDS на этом узле (Security 4624/4625, типы входа по настройке скрипта)."
} }
@@ -974,8 +1164,16 @@ function Send-Heartbeat {
} }
} catch { } } catch { }
} }
Send-TelegramMessage -Message $message | Out-Null if (Test-Lockout4740MonitoringActive) {
Write-Log "Отправлено уведомление о запуске скрипта" $message += "`r`n🔒 <b>Блокировки AD:</b> на этом КД отслеживается Security <b>4740</b> (блокировка учётной записи)."
if (-not [string]::IsNullOrWhiteSpace($ExchangeIisLogPath)) {
$message += " При событии — IP из логов IIS ActiveSync (<code>ExchangeIisLogPath</code>)."
} else {
$message += " IP из IIS не заданы (<code>ExchangeIisLogPath</code> пуст)."
}
}
Send-MonitorNotification -Message $message -EmailSubject "RDP Login Monitor: запуск" | Out-Null
Write-Log "Отправлено уведомление о запуске скрипта (каналы: $notifyChain)"
} else { } else {
Write-TextFileUtf8Bom -Path $HeartbeatFile -Text $timestamp Write-TextFileUtf8Bom -Path $HeartbeatFile -Text $timestamp
} }
@@ -992,7 +1190,7 @@ function Send-StopNotification {
$message += "🕐 Время остановки: $timestamp`r`n" $message += "🕐 Время остановки: $timestamp`r`n"
$message += "📋 Причина: $hReason" $message += "📋 Причина: $hReason"
Send-TelegramMessage -Message $message | Out-Null Send-MonitorNotification -Message $message -EmailSubject "RDP Login Monitor: остановка" | Out-Null
Write-Log "Уведомление об остановке отправлено: $Reason" Write-Log "Уведомление об остановке отправлено: $Reason"
} }
@@ -1602,7 +1800,7 @@ function Send-DailyReport {
} else { } else {
$message += "`r`n<i>Список пользователей недоступен (quser пуст или недостаточно прав).</i>" $message += "`r`n<i>Список пользователей недоступен (quser пуст или недостаточно прав).</i>"
} }
Send-TelegramMessage -Message $message | Out-Null Send-MonitorNotification -Message $message -EmailSubject "RDP Login Monitor: ежедневный отчёт" | Out-Null
Write-TextFileUtf8Bom -Path $LastReportFile -Text ((Get-Date).ToString("yyyy-MM-dd HH:mm:ss")) Write-TextFileUtf8Bom -Path $LastReportFile -Text ((Get-Date).ToString("yyyy-MM-dd HH:mm:ss"))
Write-Log "Ежедневный отчет отправлен" Write-Log "Ежедневный отчет отправлен"
return $true return $true
@@ -1635,6 +1833,128 @@ function Check-AndSendDailyReport {
return (Get-NextLocalSlotBoundary -Hour $DailyReportHour -Minute $DailyReportMinute) return (Get-NextLocalSlotBoundary -Hour $DailyReportHour -Minute $DailyReportMinute)
} }
function Test-Lockout4740MonitoringActive {
if ([string]::IsNullOrWhiteSpace($LockoutMonitorDomainController)) { return $false }
$configured = ($LockoutMonitorDomainController -split '\.')[0].Trim()
$local = ($env:COMPUTERNAME -split '\.')[0].Trim()
return ($configured -ieq $local)
}
function Get-Lockout4740EventInfo {
param($Event)
$info = [pscustomobject]@{
TimeCreated = $Event.TimeCreated
Username = ""
Domain = ""
CallerComputer = ""
}
try {
$map = Get-EventDataMap -Event $Event
$info.Username = Get-FirstNonEmptyMapValue -DataMap $map -Keys @(
'TargetUserName', 'SamAccountName', 'AccountName'
)
$info.Domain = Get-FirstNonEmptyMapValue -DataMap $map -Keys @(
'TargetDomainName', 'TargetAccountDomain', 'AccountDomain'
)
$info.CallerComputer = Get-FirstNonEmptyMapValue -DataMap $map -Keys @(
'CallerComputerName', 'WorkstationName', 'Workstation'
)
} catch {
Write-Log "Ошибка разбора XML 4740: $($_.Exception.Message)"
}
if ([string]::IsNullOrWhiteSpace($info.Username) -and $Event.Properties.Count -ge 1) {
$info.Username = [string]$Event.Properties[0].Value
}
if ([string]::IsNullOrWhiteSpace($info.Domain) -and $Event.Properties.Count -ge 2) {
$info.Domain = [string]$Event.Properties[1].Value
}
if ([string]::IsNullOrWhiteSpace($info.CallerComputer) -and $Event.Properties.Count -ge 4) {
$info.CallerComputer = [string]$Event.Properties[3].Value
}
return $info
}
function Get-ExchangeActiveSyncIpsFromIisLog {
param(
[Parameter(Mandatory = $true)][string]$SamAccountName,
[string]$DomainNetBios = ""
)
if ([string]::IsNullOrWhiteSpace($ExchangeIisLogPath)) { return @() }
$logDir = $ExchangeIisLogPath.TrimEnd('\')
$logFile = Join-Path $logDir ("u_ex" + (Get-Date).ToUniversalTime().ToString("yyMMdd") + ".log")
if (-not (Test-Path -LiteralPath $logFile)) {
Write-Log "IIS: файл лога не найден: $logFile"
return @()
}
$domainPart = if ([string]::IsNullOrWhiteSpace($DomainNetBios)) { $NetBiosDomainName } else { $DomainNetBios }
$userPattern1 = if ([string]::IsNullOrWhiteSpace($domainPart)) {
$SamAccountName
} else {
"User=$domainPart%5C" + $SamAccountName
}
$userPattern2 = if ([string]::IsNullOrWhiteSpace($domainPart)) {
$SamAccountName
} else {
"$domainPart\" + $SamAccountName
}
$excludeHosts = @('127.0.0.1', '::1')
if (-not [string]::IsNullOrWhiteSpace($ExchangeServerHostForIisExclude)) {
$excludeHosts += $ExchangeServerHostForIisExclude.Trim()
}
$detected = [System.Collections.Generic.List[string]]::new()
try {
$lines = Get-Content -LiteralPath $logFile -Tail $ExchangeIisLogTailLines -ErrorAction Stop
foreach ($line in $lines) {
if ($line -notlike '*401 *' -or $line -notlike '*ActiveSync*') { continue }
if ($line -notlike "*$userPattern1*" -and $line -notlike "*$userPattern2*") { continue }
if ($line -notmatch '(?:\d{1,3}\.){3}\d{1,3}') { continue }
$ip = $Matches[0]
if ($excludeHosts -contains $ip) { continue }
if (-not $detected.Contains($ip)) { $detected.Add($ip) | Out-Null }
}
} catch {
Write-Log "IIS: ошибка чтения $logFile : $($_.Exception.Message)"
}
return @($detected)
}
function Format-Lockout4740TelegramMessage {
param(
[string]$Username,
[string]$Domain,
[datetime]$TimeCreated,
[string[]]$IisClientIps = @()
)
$domainLabel = if ([string]::IsNullOrWhiteSpace($Domain)) { $NetBiosDomainName } else { $Domain }
$accountDisplay = if ([string]::IsNullOrWhiteSpace($domainLabel)) {
$Username
} else {
"$domainLabel\$Username"
}
$hUser = ConvertTo-TelegramHtml $accountDisplay
$hTime = ConvertTo-TelegramHtml ($TimeCreated.ToString('dd.MM.yyyy HH:mm:ss'))
$message = "<b>🔒 Блокировка учётной записи AD (4740)</b>`r`n"
$message += "👤 Пользователь: $hUser`r`n"
$message += "🕐 Время: $hTime`r`n"
if ($IisClientIps.Count -gt 0) {
$message += "`r`n<b>🌐 IP (попытки ActiveSync, 401):</b>`r`n"
foreach ($ip in $IisClientIps) {
$netType = if ($ip -like '192.168.*' -or $ip -like '10.*' -or $ip -like '172.1[6-9].*' -or $ip -like '172.2[0-9].*' -or $ip -like '172.3[0-1].*') {
'внутренний'
} else {
'внешний'
}
$message += ('• {0} ({1})' -f (ConvertTo-TelegramHtml $ip), $netType) + "`r`n"
}
} elseif (-not [string]::IsNullOrWhiteSpace($ExchangeIisLogPath)) {
$message += "`r`n<i>IP в логах IIS ActiveSync для этого пользователя не найдены.</i>`r`n"
}
return $message
}
function Start-LoginMonitor { function Start-LoginMonitor {
param( param(
[int]$MonitorInterval = 5, [int]$MonitorInterval = 5,
@@ -1655,6 +1975,17 @@ function Start-LoginMonitor {
Write-Log "Режим сервера: Security — LogonType 2, 3, 10" Write-Log "Режим сервера: Security — LogonType 2, 3, 10"
} }
Write-Log "========================================" Write-Log "========================================"
Write-Log "Каналы уведомлений: $(Get-NotifyChainHuman)"
$lockout4740Enabled = Test-Lockout4740MonitoringActive
if ($lockout4740Enabled) {
Write-Log "Мониторинг блокировок AD (4740) включён на этом КД ($LockoutMonitorDomainController)."
if (-not [string]::IsNullOrWhiteSpace($ExchangeIisLogPath)) {
Write-Log "Обогащение: IIS ActiveSync — $ExchangeIisLogPath"
}
} elseif (-not [string]::IsNullOrWhiteSpace($LockoutMonitorDomainController)) {
Write-Log "Мониторинг 4740 задан для КД '$LockoutMonitorDomainController', но этот узел — $env:COMPUTERNAME (блокировки не отслеживаются)."
}
Cleanup-OldLogs Cleanup-OldLogs
Send-Heartbeat -IsStartup Send-Heartbeat -IsStartup
@@ -1674,6 +2005,7 @@ function Start-LoginMonitor {
$lastCheckTime = (Get-Date).AddSeconds(-10) $lastCheckTime = (Get-Date).AddSeconds(-10)
$lastGatewayCheckTime = (Get-Date).AddSeconds(-10) $lastGatewayCheckTime = (Get-Date).AddSeconds(-10)
$lastRcmCheckTime = (Get-Date).AddSeconds(-10) $lastRcmCheckTime = (Get-Date).AddSeconds(-10)
$lastLockout4740CheckTime = (Get-Date).AddSeconds(-10)
$monitorEvents = @(4624, 4625, 4648) $monitorEvents = @(4624, 4625, 4648)
while ($true) { while ($true) {
@@ -1733,7 +2065,8 @@ function Start-LoginMonitor {
-SecurityLogComputerName $event.MachineName -SecurityLogComputerName $event.MachineName
Write-Log "Notify: ID=$($event.Id) User=$($eventInfo.Username) LT=$($eventInfo.LogonType) IP=$($eventInfo.SourceIP)" Write-Log "Notify: ID=$($event.Id) User=$($eventInfo.Username) LT=$($eventInfo.LogonType) IP=$($eventInfo.SourceIP)"
Send-TelegramMessage -Message $formattedMessage | Out-Null Send-MonitorNotification -Message $formattedMessage `
-EmailSubject "RDP Login Monitor: вход (ID $($event.Id))" | Out-Null
} }
} }
$lastCheckTime = ($events | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1) $lastCheckTime = ($events | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1)
@@ -1759,7 +2092,8 @@ function Start-LoginMonitor {
-ErrorCode $ei.ErrorCode ` -ErrorCode $ei.ErrorCode `
-TimeCreated $ei.TimeCreated -TimeCreated $ei.TimeCreated
Write-Log "Notify RDG: ID=$($event.Id) User=$($ei.Username)" Write-Log "Notify RDG: ID=$($event.Id) User=$($ei.Username)"
Send-TelegramMessage -Message $msg | Out-Null Send-MonitorNotification -Message $msg `
-EmailSubject "RDP Login Monitor: RD Gateway ($($event.Id))" | Out-Null
} }
$lastGatewayCheckTime = ($gatewayEvents | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1) $lastGatewayCheckTime = ($gatewayEvents | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1)
} }
@@ -1783,12 +2117,39 @@ function Start-LoginMonitor {
$msg = Format-Rcm1149Event -Username $rcmInfo.Username -ClientIP $rcmInfo.ClientIP ` $msg = Format-Rcm1149Event -Username $rcmInfo.Username -ClientIP $rcmInfo.ClientIP `
-TimeCreated $rcmInfo.TimeCreated -SecurityLogComputerName $event.MachineName -TimeCreated $rcmInfo.TimeCreated -SecurityLogComputerName $event.MachineName
Write-Log "Notify RCM 1149: User=$($rcmInfo.Username) IP=$($rcmInfo.ClientIP)" Write-Log "Notify RCM 1149: User=$($rcmInfo.Username) IP=$($rcmInfo.ClientIP)"
Send-TelegramMessage -Message $msg | Out-Null Send-MonitorNotification -Message $msg `
-EmailSubject "RDP Login Monitor: RDP 1149" | Out-Null
} }
$lastRcmCheckTime = ($rcmEvents | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1) $lastRcmCheckTime = ($rcmEvents | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1)
} }
} }
if ($lockout4740Enabled) {
$lockoutEvents = Get-WinEvent -FilterHashtable @{
LogName = 'Security'
ID = 4740
StartTime = $lastLockout4740CheckTime
} -ErrorAction SilentlyContinue
if ($lockoutEvents) {
foreach ($event in $lockoutEvents) {
if ($event.TimeCreated -le $lastLockout4740CheckTime) { continue }
$lo = Get-Lockout4740EventInfo -Event $event
if ([string]::IsNullOrWhiteSpace($lo.Username)) { continue }
$domainForIis = if ([string]::IsNullOrWhiteSpace($lo.Domain)) { $NetBiosDomainName } else { $lo.Domain }
$iisIps = @(Get-ExchangeActiveSyncIpsFromIisLog -SamAccountName $lo.Username -DomainNetBios $domainForIis)
$msg = Format-Lockout4740TelegramMessage -Username $lo.Username -Domain $lo.Domain `
-TimeCreated $lo.TimeCreated -IisClientIps $iisIps
Write-Log "Notify 4740: User=$($lo.Username) IIS_IPs=$($iisIps -join ', ')"
Send-MonitorNotification -Message $msg `
-EmailSubject "RDP Login Monitor: блокировка УЗ $($lo.Username)" | Out-Null
}
$lastLockout4740CheckTime = ($lockoutEvents | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1)
}
}
$now = Get-Date $now = Get-Date
if ($now -ge $nextHeartbeatTime) { if ($now -ge $nextHeartbeatTime) {
Send-Heartbeat Send-Heartbeat
@@ -1810,7 +2171,17 @@ function Start-LoginMonitor {
$script:StopNotificationSent = $false $script:StopNotificationSent = $false
try { try {
Test-TelegramConnection | Out-Null $notifyChannels = @(Get-NotifyOrderChannels)
if ($notifyChannels.Count -eq 0) {
Write-Log "ВНИМАНИЕ: не настроен ни один канал оповещений (Telegram и/или SMTP в конфигурации скрипта)."
} else {
foreach ($notifyCh in $notifyChannels) {
switch ($notifyCh) {
'telegram' { Test-TelegramConnection | Out-Null }
'email' { Test-MailSmtpConnection | Out-Null }
}
}
}
Start-LoginMonitor -MonitorInterval 5 -MonitorInteractiveOnly Start-LoginMonitor -MonitorInterval 5 -MonitorInteractiveOnly
} catch { } catch {
if ($_.Exception -is [System.Management.Automation.PipelineStoppedException]) { if ($_.Exception -is [System.Management.Automation.PipelineStoppedException]) {
+7 -4
View File
@@ -5,7 +5,7 @@ PowerShell-набор для мониторинга входов в Windows с
## Актуальная схема (рекомендуется) ## Актуальная схема (рекомендуется)
- Базовый путь установки: **`C:\ProgramData\RDP-login-monitor\`**. - Базовый путь установки: **`C:\ProgramData\RDP-login-monitor\`**.
- Основной скрипт: **`Login_Monitor.ps1`** — журнал Security **`4624`/`4625`** (логика зависит от типа ОС: рабочая станция или сервер/КД), при наличии журнала — **Remote Connection Manager `1149`** (часто актуально для РС с RDP), при роли **RD Gateway****`302`/`303`**, **ежедневный отчёт** в Telegram (активные сессии через `quser`), **heartbeat**, **ротация логов**, уведомления в Telegram. - Основной скрипт: **`Login_Monitor.ps1`** — журнал Security **`4624`/`4625`** (логика зависит от типа ОС: рабочая станция или сервер/КД), при наличии журнала — **Remote Connection Manager `1149`** (часто актуально для РС с RDP), при роли **RD Gateway****`302`/`303`**, на указанном **КД****`4740`** (блокировка УЗ + IP из IIS ActiveSync), **ежедневный отчёт** в Telegram (активные сессии через `quser`), **heartbeat**, **ротация логов**, уведомления в Telegram и/или Email.
- Установка задач: запуск **`Login_Monitor.ps1 -InstallTasks`** создаёт: - Установка задач: запуск **`Login_Monitor.ps1 -InstallTasks`** создаёт:
- `RDP-Login-Monitor` (основной монитор), - `RDP-Login-Monitor` (основной монитор),
- `RDP-Login-Monitor-Watchdog` (контроль процесса каждые 5 минут). - `RDP-Login-Monitor-Watchdog` (контроль процесса каждые 5 минут).
@@ -27,13 +27,15 @@ PowerShell-набор для мониторинга входов в Windows с
2. Скопируйте в неё как минимум: 2. Скопируйте в неё как минимум:
- `Login_Monitor.ps1` - `Login_Monitor.ps1`
- (для доменного развёртывания отдельно на шаре) `Deploy-LoginMonitor.ps1` и `version.txt`. - (для доменного развёртывания отдельно на шаре) `Deploy-LoginMonitor.ps1` и `version.txt`.
3. Откройте `Login_Monitor.ps1` и задайте токен/чат: 3. Откройте `Login_Monitor.ps1` и задайте каналы оповещений:
- `$TelegramBotToken` или `...ProtectedB64` - **Telegram:** `$TelegramBotToken` / `$TelegramChatID` или `...ProtectedB64`
- `$TelegramChatID` или `...ProtectedB64` - **Email (SMTP):** `$MailSmtpHost`, `$MailFrom`, `$MailTo`, при необходимости `$MailSmtpUser` / `$MailSmtpPassword` (или `$MailSmtpPasswordProtectedB64` через DPAPI)
- **Порядок:** `$NotifyOrder` — пусто = авто (Telegram → Email, только настроенные); иначе `telegram,email` или `email` и т.п. (допускаются `tg`, `mail`)
4. Запускайте с правами администратора (чтение `Security` журнала и регистрация задач). 4. Запускайте с правами администратора (чтение `Security` журнала и регистрация задач).
5. Логи и служебные файлы будут в: 5. Логи и служебные файлы будут в:
- `C:\ProgramData\RDP-login-monitor\Logs\` - `C:\ProgramData\RDP-login-monitor\Logs\`
6. (Опционально) Подавление части алертов по списку — см. раздел **«7) ignore.lst»** ниже. 6. (Опционально) Подавление части алертов по списку — см. раздел **«7) ignore.lst»** ниже.
7. (Опционально) Мониторинг блокировок AD на КД — **`$LockoutMonitorDomainController`**, **`$NetBiosDomainName`**, **`$ExchangeIisLogPath`** (UNC к логам IIS ActiveSync), **`$ExchangeServerHostForIisExclude`** (IP сервера Exchange, не считать клиентским). В оповещении: пользователь из 4740 и IP из IIS (401 + ActiveSync). На других узлах блок 4740 не активен.
## 2) Ручной запуск ## 2) Ручной запуск
@@ -67,6 +69,7 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-logi
- Heartbeat: - Heartbeat:
- `C:\ProgramData\RDP-login-monitor\Logs\last_heartbeat.txt` обновляется по интервалу **`$HeartbeatInterval`** (по умолчанию раз в час). - `C:\ProgramData\RDP-login-monitor\Logs\last_heartbeat.txt` обновляется по интервалу **`$HeartbeatInterval`** (по умолчанию раз в час).
- Ежедневный отчёт: после первого прохождения дневного слота (по умолчанию **09:00**, задаётся **`$DailyReportHour`** / **`$DailyReportMinute`** в `Login_Monitor.ps1`) в Telegram уходит сводка по **`quser`**; метка последнего отчёта — `Logs\last_daily_report.txt`. - Ежедневный отчёт: после первого прохождения дневного слота (по умолчанию **09:00**, задаётся **`$DailyReportHour`** / **`$DailyReportMinute`** в `Login_Monitor.ps1`) в Telegram уходит сводка по **`quser`**; метка последнего отчёта — `Logs\last_daily_report.txt`.
- При старте в Telegram/Email: строка **«Каналы уведомлений»** (фактический порядок доставки), плюс режим RDS/4740 по конфигурации.
- Telegram при старте: при установленном **RD Session Host** (или аналогичных компонентах RDS, не только шлюз) — строка про входы по RDP/RDS на этом сервере; при доступном журнале **RD Gateway** — отдельная строка про подключения к **внутренним целевым ПК** через шлюз (302/303). Узел только с ролью RD Gateway не дублирует формулировку «хост сессий». - Telegram при старте: при установленном **RD Session Host** (или аналогичных компонентах RDS, не только шлюз) — строка про входы по RDP/RDS на этом сервере; при доступном журнале **RD Gateway** — отдельная строка про подключения к **внутренним целевым ПК** через шлюз (302/303). Узел только с ролью RD Gateway не дублирует формулировку «хост сессий».
## 5) Автоматический перезапуск при падении ## 5) Автоматический перезапуск при падении
+1 -1
View File
@@ -1 +1 @@
1.4.4 1.5.2