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:
@@ -1,5 +1,5 @@
|
||||
# Запуск: от администратора на ТОМ ЖЕ компьютере, где будет Login_Monitor.ps1.
|
||||
# Результат (Base64) вставьте в $TelegramBotTokenProtectedB64 / $TelegramChatIDProtectedB64.
|
||||
# Результат (Base64) вставьте в $TelegramBotTokenProtectedB64 / $TelegramChatIDProtectedB64 / $MailSmtpPasswordProtectedB64.
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$PlainText
|
||||
)
|
||||
|
||||
+380
-9
@@ -3,6 +3,7 @@
|
||||
Мониторинг логинов/попыток входа с уведомлениями в Telegram
|
||||
.DESCRIPTION
|
||||
Отслеживает события входа в систему (Security 4624/4625) и события RD Gateway (302/303),
|
||||
на заданном КД — блокировки учётных записей (Security 4740) с IP из логов IIS ActiveSync,
|
||||
отправляет уведомления в Telegram, делает ротацию логов, heartbeat в файл и ежедневный отчет.
|
||||
.NOTES
|
||||
Требуется: PowerShell 5.0+, запуск от администратора.
|
||||
@@ -28,6 +29,7 @@ param(
|
||||
[string]$TelegramChatID = '<TELEGRAM_CHAT_ID>',
|
||||
[string]$TelegramBotTokenProtectedB64 = "",
|
||||
[string]$TelegramChatIDProtectedB64 = "",
|
||||
[string]$MailSmtpPasswordProtectedB64 = "",
|
||||
[switch]$Watchdog,
|
||||
[switch]$InstallTasks,
|
||||
[switch]$SkipScheduledTaskMaintenance
|
||||
@@ -69,7 +71,7 @@ $script:MonitorSingletonLockStream = $null
|
||||
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
|
||||
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
|
||||
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
|
||||
$ScriptVersion = "1.4.4"
|
||||
$ScriptVersion = "1.5.2"
|
||||
|
||||
# Логи (все под InstallRoot)
|
||||
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
||||
@@ -148,6 +150,25 @@ $IgnoreAdvapiNetworkLogonSourceIps = @(
|
||||
)
|
||||
$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 ($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 {
|
||||
param([string]$Text)
|
||||
if ($null -eq $Text) { return '' }
|
||||
@@ -561,6 +646,10 @@ function Send-TelegramMessage {
|
||||
}
|
||||
|
||||
function Test-TelegramConnection {
|
||||
if (-not (Test-NotifyTelegramConfigured)) {
|
||||
Write-Log "Telegram: канал не настроен, проверка пропущена."
|
||||
return $false
|
||||
}
|
||||
Write-Log "Проверка подключения к Telegram API..."
|
||||
try {
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
|
||||
@@ -577,6 +666,105 @@ function Test-TelegramConnection {
|
||||
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 {
|
||||
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
|
||||
@@ -963,6 +1151,8 @@ function Send-Heartbeat {
|
||||
} else {
|
||||
$message += "`r`n🚫 <b>Игнорируются:</b> не задано (ignore.lst отсутствует или пуст)."
|
||||
}
|
||||
$notifyChain = Get-NotifyChainHuman
|
||||
$message += "`r`n📢 <b>Каналы уведомлений:</b> $(ConvertTo-TelegramHtml $notifyChain)"
|
||||
if (Test-RDSDeploymentPresent) {
|
||||
$message += "`r`n🔐 <b>RDS (хост сессий):</b> обнаружены компоненты RDS помимо чистого шлюза — в мониторинг входят входы по RDP/RDS на этом узле (Security 4624/4625, типы входа по настройке скрипта)."
|
||||
}
|
||||
@@ -974,8 +1164,16 @@ function Send-Heartbeat {
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
Send-TelegramMessage -Message $message | Out-Null
|
||||
Write-Log "Отправлено уведомление о запуске скрипта"
|
||||
if (Test-Lockout4740MonitoringActive) {
|
||||
$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 {
|
||||
Write-TextFileUtf8Bom -Path $HeartbeatFile -Text $timestamp
|
||||
}
|
||||
@@ -992,7 +1190,7 @@ function Send-StopNotification {
|
||||
$message += "🕐 Время остановки: $timestamp`r`n"
|
||||
$message += "📋 Причина: $hReason"
|
||||
|
||||
Send-TelegramMessage -Message $message | Out-Null
|
||||
Send-MonitorNotification -Message $message -EmailSubject "RDP Login Monitor: остановка" | Out-Null
|
||||
Write-Log "Уведомление об остановке отправлено: $Reason"
|
||||
}
|
||||
|
||||
@@ -1602,7 +1800,7 @@ function Send-DailyReport {
|
||||
} else {
|
||||
$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-Log "Ежедневный отчет отправлен"
|
||||
return $true
|
||||
@@ -1635,6 +1833,128 @@ function Check-AndSendDailyReport {
|
||||
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 {
|
||||
param(
|
||||
[int]$MonitorInterval = 5,
|
||||
@@ -1655,6 +1975,17 @@ function Start-LoginMonitor {
|
||||
Write-Log "Режим сервера: Security — LogonType 2, 3, 10"
|
||||
}
|
||||
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
|
||||
Send-Heartbeat -IsStartup
|
||||
@@ -1674,6 +2005,7 @@ function Start-LoginMonitor {
|
||||
$lastCheckTime = (Get-Date).AddSeconds(-10)
|
||||
$lastGatewayCheckTime = (Get-Date).AddSeconds(-10)
|
||||
$lastRcmCheckTime = (Get-Date).AddSeconds(-10)
|
||||
$lastLockout4740CheckTime = (Get-Date).AddSeconds(-10)
|
||||
$monitorEvents = @(4624, 4625, 4648)
|
||||
|
||||
while ($true) {
|
||||
@@ -1733,7 +2065,8 @@ function Start-LoginMonitor {
|
||||
-SecurityLogComputerName $event.MachineName
|
||||
|
||||
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)
|
||||
@@ -1759,7 +2092,8 @@ function Start-LoginMonitor {
|
||||
-ErrorCode $ei.ErrorCode `
|
||||
-TimeCreated $ei.TimeCreated
|
||||
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)
|
||||
}
|
||||
@@ -1783,12 +2117,39 @@ function Start-LoginMonitor {
|
||||
$msg = Format-Rcm1149Event -Username $rcmInfo.Username -ClientIP $rcmInfo.ClientIP `
|
||||
-TimeCreated $rcmInfo.TimeCreated -SecurityLogComputerName $event.MachineName
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if ($now -ge $nextHeartbeatTime) {
|
||||
Send-Heartbeat
|
||||
@@ -1810,7 +2171,17 @@ function Start-LoginMonitor {
|
||||
|
||||
$script:StopNotificationSent = $false
|
||||
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
|
||||
} catch {
|
||||
if ($_.Exception -is [System.Management.Automation.PipelineStoppedException]) {
|
||||
|
||||
@@ -5,7 +5,7 @@ PowerShell-набор для мониторинга входов в Windows с
|
||||
## Актуальная схема (рекомендуется)
|
||||
|
||||
- Базовый путь установки: **`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`** создаёт:
|
||||
- `RDP-Login-Monitor` (основной монитор),
|
||||
- `RDP-Login-Monitor-Watchdog` (контроль процесса каждые 5 минут).
|
||||
@@ -27,13 +27,15 @@ PowerShell-набор для мониторинга входов в Windows с
|
||||
2. Скопируйте в неё как минимум:
|
||||
- `Login_Monitor.ps1`
|
||||
- (для доменного развёртывания отдельно на шаре) `Deploy-LoginMonitor.ps1` и `version.txt`.
|
||||
3. Откройте `Login_Monitor.ps1` и задайте токен/чат:
|
||||
- `$TelegramBotToken` или `...ProtectedB64`
|
||||
- `$TelegramChatID` или `...ProtectedB64`
|
||||
3. Откройте `Login_Monitor.ps1` и задайте каналы оповещений:
|
||||
- **Telegram:** `$TelegramBotToken` / `$TelegramChatID` или `...ProtectedB64`
|
||||
- **Email (SMTP):** `$MailSmtpHost`, `$MailFrom`, `$MailTo`, при необходимости `$MailSmtpUser` / `$MailSmtpPassword` (или `$MailSmtpPasswordProtectedB64` через DPAPI)
|
||||
- **Порядок:** `$NotifyOrder` — пусто = авто (Telegram → Email, только настроенные); иначе `telegram,email` или `email` и т.п. (допускаются `tg`, `mail`)
|
||||
4. Запускайте с правами администратора (чтение `Security` журнала и регистрация задач).
|
||||
5. Логи и служебные файлы будут в:
|
||||
- `C:\ProgramData\RDP-login-monitor\Logs\`
|
||||
6. (Опционально) Подавление части алертов по списку — см. раздел **«7) ignore.lst»** ниже.
|
||||
7. (Опционально) Мониторинг блокировок AD на КД — **`$LockoutMonitorDomainController`**, **`$NetBiosDomainName`**, **`$ExchangeIisLogPath`** (UNC к логам IIS ActiveSync), **`$ExchangeServerHostForIisExclude`** (IP сервера Exchange, не считать клиентским). В оповещении: пользователь из 4740 и IP из IIS (401 + ActiveSync). На других узлах блок 4740 не активен.
|
||||
|
||||
## 2) Ручной запуск
|
||||
|
||||
@@ -67,6 +69,7 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-logi
|
||||
- Heartbeat:
|
||||
- `C:\ProgramData\RDP-login-monitor\Logs\last_heartbeat.txt` обновляется по интервалу **`$HeartbeatInterval`** (по умолчанию раз в час).
|
||||
- Ежедневный отчёт: после первого прохождения дневного слота (по умолчанию **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 не дублирует формулировку «хост сессий».
|
||||
|
||||
## 5) Автоматический перезапуск при падении
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.4.4
|
||||
1.5.2
|
||||
|
||||
Reference in New Issue
Block a user