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. Co-authored-by: Cursor <cursoragent@cursor.com>
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