feat: aggregated 4625 burst alerts (two-tier rate limit, v1.5.10)
- Tier 1: 5 failures per 60s per IP+user - Tier 2: 12 failures per 60s per IP (spraying) - Suppress per-event 4625 during burst; no auto-ban
This commit is contained in:
+309
-14
@@ -2,7 +2,8 @@
|
|||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Мониторинг логинов/попыток входа с уведомлениями в Telegram
|
Мониторинг логинов/попыток входа с уведомлениями в Telegram
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Отслеживает события входа в систему (Security 4624/4625) и события RD Gateway (302/303),
|
Отслеживает события входа в систему (Security 4624/4625), агрегированные оповещения при всплеске 4625,
|
||||||
|
события RD Gateway (302/303),
|
||||||
на заданном КД — блокировки учётных записей (Security 4740) с IP из логов IIS ActiveSync,
|
на заданном КД — блокировки учётных записей (Security 4740) с IP из логов IIS ActiveSync,
|
||||||
отправляет уведомления в Telegram, делает ротацию логов, heartbeat в файл и ежедневный отчет.
|
отправляет уведомления в Telegram, делает ротацию логов, heartbeat в файл и ежедневный отчет.
|
||||||
.NOTES
|
.NOTES
|
||||||
@@ -71,7 +72,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.5.9"
|
$ScriptVersion = "1.5.10"
|
||||||
|
|
||||||
# Логи (все под InstallRoot)
|
# Логи (все под InstallRoot)
|
||||||
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
||||||
@@ -162,6 +163,18 @@ $ExchangeIisLogTailLines = 5000
|
|||||||
# Окно поиска IP в IIS: только строки за N минут до события 4740 (локальное время сервера IIS).
|
# Окно поиска IP в IIS: только строки за N минут до события 4740 (локальное время сервера IIS).
|
||||||
$ExchangeIisLogMinutesBeforeLockout = 30
|
$ExchangeIisLogMinutesBeforeLockout = 30
|
||||||
|
|
||||||
|
# Агрегация неудачных входов Security 4625 (вариант C: два порога). Автоблокировка IP не выполняется.
|
||||||
|
$FailedLogonRateLimitEnabled = $true
|
||||||
|
$FailedLogonRateLimitSuppressIndividualWhileBurst = $true
|
||||||
|
# Уровень 1: подбор одной учётной записи с одного источника (IP + пользователь).
|
||||||
|
$FailedLogonRateLimitUserIpWindowSeconds = 60
|
||||||
|
$FailedLogonRateLimitUserIpThreshold = 5
|
||||||
|
$FailedLogonRateLimitUserIpCooldownSeconds = 300
|
||||||
|
# Уровень 2: много неудачных попыток с одного IP (password spraying / перебор логинов).
|
||||||
|
$FailedLogonRateLimitIpWindowSeconds = 60
|
||||||
|
$FailedLogonRateLimitIpThreshold = 12
|
||||||
|
$FailedLogonRateLimitIpCooldownSeconds = 300
|
||||||
|
|
||||||
# Очередь оповещений: telegram, email (или tg, mail). Пусто = авто: настроенные каналы, порядок telegram → email.
|
# Очередь оповещений: telegram, email (или tg, mail). Пусто = авто: настроенные каналы, порядок telegram → email.
|
||||||
$NotifyOrder = "tg"
|
$NotifyOrder = "tg"
|
||||||
$MailSmtpHost = ""
|
$MailSmtpHost = ""
|
||||||
@@ -1184,6 +1197,9 @@ function Send-Heartbeat {
|
|||||||
}
|
}
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
|
if ($FailedLogonRateLimitEnabled) {
|
||||||
|
$message += "`r`n🛡️ <b>Агрегация 4625:</b> уровень 1 — $FailedLogonRateLimitUserIpThreshold за $FailedLogonRateLimitUserIpWindowSeconds с (IP+пользователь); уровень 2 — $FailedLogonRateLimitIpThreshold за $FailedLogonRateLimitIpWindowSeconds с (только IP). Cooldown ${FailedLogonRateLimitUserIpCooldownSeconds}/${FailedLogonRateLimitIpCooldownSeconds} с. Автобан: нет."
|
||||||
|
}
|
||||||
if (Test-Lockout4740MonitoringActive) {
|
if (Test-Lockout4740MonitoringActive) {
|
||||||
$message += "`r`n🔒 <b>Блокировки AD:</b> на этом КД отслеживается Security <b>4740</b> (блокировка учётной записи)."
|
$message += "`r`n🔒 <b>Блокировки AD:</b> на этом КД отслеживается Security <b>4740</b> (блокировка учётной записи)."
|
||||||
if (-not [string]::IsNullOrWhiteSpace($ExchangeIisLogPath)) {
|
if (-not [string]::IsNullOrWhiteSpace($ExchangeIisLogPath)) {
|
||||||
@@ -1707,6 +1723,250 @@ function Format-LoginEvent {
|
|||||||
return $message
|
return $message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$script:FailedLogonBuckets = @{}
|
||||||
|
|
||||||
|
function Get-FailedLogonSourceKeyPart {
|
||||||
|
param(
|
||||||
|
[string]$SourceIP,
|
||||||
|
[string]$ComputerName
|
||||||
|
)
|
||||||
|
|
||||||
|
$ip = if ($null -ne $SourceIP) { $SourceIP.Trim() } else { '' }
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($ip) -and $ip -ne '-' -and $ip -ne '::1' -and $ip -ne '127.0.0.1' -and $ip -notlike 'fe80:*') {
|
||||||
|
return "ip:$ip"
|
||||||
|
}
|
||||||
|
|
||||||
|
$wks = if ($null -ne $ComputerName) { $ComputerName.Trim() } else { '' }
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($wks) -and $wks -ne '-') {
|
||||||
|
return "wks:$wks"
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-FailedLogonNormalizedUsername {
|
||||||
|
param([string]$Username)
|
||||||
|
|
||||||
|
$u = if ($null -ne $Username) { $Username.Trim() } else { '' }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($u) -or $u -eq '-') { return '(не указан)' }
|
||||||
|
return $u
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-FailedLogonSourceDisplayLabel {
|
||||||
|
param([string]$SourceKeyPart)
|
||||||
|
|
||||||
|
if ($SourceKeyPart -like 'ip:*') { return $SourceKeyPart.Substring(3) }
|
||||||
|
if ($SourceKeyPart -like 'wks:*') { return ('рабочая станция ' + $SourceKeyPart.Substring(4)) }
|
||||||
|
return 'неизвестный источник'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Update-FailedLogonRateLimitBucket {
|
||||||
|
param(
|
||||||
|
[string]$BucketKey,
|
||||||
|
[int]$WindowSeconds,
|
||||||
|
[string]$Username,
|
||||||
|
[int]$LogonType,
|
||||||
|
[datetime]$TimeCreated
|
||||||
|
)
|
||||||
|
|
||||||
|
$cutoff = (Get-Date).AddSeconds(-$WindowSeconds)
|
||||||
|
|
||||||
|
if (-not $script:FailedLogonBuckets.ContainsKey($BucketKey)) {
|
||||||
|
$script:FailedLogonBuckets[$BucketKey] = [pscustomobject]@{
|
||||||
|
Attempts = [System.Collections.ArrayList]@()
|
||||||
|
LastBurstAlertUtc = $null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$bucket = $script:FailedLogonBuckets[$BucketKey]
|
||||||
|
$null = $bucket.Attempts.Add([pscustomobject]@{
|
||||||
|
Time = $TimeCreated
|
||||||
|
Username = (Get-FailedLogonNormalizedUsername -Username $Username)
|
||||||
|
LogonType = $LogonType
|
||||||
|
})
|
||||||
|
|
||||||
|
$fresh = [System.Collections.ArrayList]@()
|
||||||
|
foreach ($a in $bucket.Attempts) {
|
||||||
|
if ($a.Time -ge $cutoff) { $null = $fresh.Add($a) }
|
||||||
|
}
|
||||||
|
$bucket.Attempts = $fresh
|
||||||
|
|
||||||
|
if ($fresh.Count -eq 0) {
|
||||||
|
$bucket.LastBurstAlertUtc = $null
|
||||||
|
}
|
||||||
|
|
||||||
|
return $bucket
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-FailedLogonBurstMessage {
|
||||||
|
param(
|
||||||
|
[ValidateSet('UserIp', 'Ip')]
|
||||||
|
[string]$TierKind,
|
||||||
|
[string]$SourceKeyPart,
|
||||||
|
[string]$FocusUsername,
|
||||||
|
[string]$SecurityLogComputerName,
|
||||||
|
[System.Collections.ArrayList]$Attempts,
|
||||||
|
[int]$Threshold,
|
||||||
|
[int]$WindowSeconds
|
||||||
|
)
|
||||||
|
|
||||||
|
$sourceLabel = Get-FailedLogonSourceDisplayLabel -SourceKeyPart $SourceKeyPart
|
||||||
|
$hLog = ConvertTo-TelegramHtml $(if ([string]::IsNullOrWhiteSpace($SecurityLogComputerName)) { $env:COMPUTERNAME } else { $SecurityLogComputerName })
|
||||||
|
$hSource = ConvertTo-TelegramHtml $sourceLabel
|
||||||
|
$count = $Attempts.Count
|
||||||
|
$times = @($Attempts | ForEach-Object { $_.Time } | Sort-Object)
|
||||||
|
$first = $times[0]
|
||||||
|
$last = $times[-1]
|
||||||
|
|
||||||
|
$byUser = @{}
|
||||||
|
foreach ($a in $Attempts) {
|
||||||
|
if (-not $byUser.ContainsKey($a.Username)) { $byUser[$a.Username] = 0 }
|
||||||
|
$byUser[$a.Username]++
|
||||||
|
}
|
||||||
|
$userLines = @($byUser.GetEnumerator() | Sort-Object -Property Value -Descending | ForEach-Object {
|
||||||
|
'{0} ({1})' -f (ConvertTo-TelegramHtml $_.Key), $_.Value
|
||||||
|
})
|
||||||
|
|
||||||
|
$ltSet = @($Attempts | ForEach-Object { $_.LogonType } | Sort-Object -Unique)
|
||||||
|
$ltNames = @($ltSet | ForEach-Object { '{0} ({1})' -f (ConvertTo-TelegramHtml (Get-LogonTypeName -LogonType $_)), $_ })
|
||||||
|
$ltText = if ($ltNames.Count -gt 0) { ($ltNames -join ', ') } else { '-' }
|
||||||
|
|
||||||
|
if ($TierKind -eq 'UserIp') {
|
||||||
|
$title = '🚨 МАССОВЫЕ НЕУДАЧНЫЕ ВХОДЫ (4625) — учётная запись'
|
||||||
|
$tierLine = 'Уровень: <b>IP + пользователь</b> (подбор одного логина)'
|
||||||
|
$hUser = ConvertTo-TelegramHtml $FocusUsername
|
||||||
|
$focusLine = "👤 Учётная запись: $hUser`r`n"
|
||||||
|
} else {
|
||||||
|
$title = '🚨 МАССОВЫЕ НЕУДАЧНЫЕ ВХОДЫ (4625) — источник IP'
|
||||||
|
$tierLine = 'Уровень: <b>только IP</b> (несколько учётных записей / spraying)'
|
||||||
|
$focusLine = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = "<b>$title</b>`r`n"
|
||||||
|
$message += "$tierLine`r`n"
|
||||||
|
$message += "🏢 Сервер (журнал Security): $hLog`r`n"
|
||||||
|
$message += "🎯 Источник: $hSource`r`n"
|
||||||
|
$message += $focusLine
|
||||||
|
$message += "📊 За последние $WindowSeconds с: <b>$count</b> попыток (порог $Threshold)`r`n"
|
||||||
|
$message += "🕐 В окне: $(ConvertTo-TelegramHtml ($first.ToString('dd.MM.yyyy HH:mm:ss'))) — $(ConvertTo-TelegramHtml ($last.ToString('dd.MM.yyyy HH:mm:ss')))`r`n"
|
||||||
|
$message += "👤 В попытках: $($userLines -join '; ')`r`n"
|
||||||
|
$message += "🔑 Типы входа: $ltText`r`n"
|
||||||
|
$message += "<i>⚠️ Возможный брутфорс. Одиночные 4625 в окне подавлены. Автоблокировка IP не выполняется.</i>`r`n"
|
||||||
|
$message += "🔢 Event ID: 4625 (агрегат)"
|
||||||
|
|
||||||
|
return $message
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-FailedLogonBurstAlertIfNeeded {
|
||||||
|
param(
|
||||||
|
[ValidateSet('UserIp', 'Ip')]
|
||||||
|
[string]$TierKind,
|
||||||
|
[string]$BucketKey,
|
||||||
|
[string]$SourceKeyPart,
|
||||||
|
[string]$FocusUsername,
|
||||||
|
[string]$SecurityLogComputerName,
|
||||||
|
[int]$WindowSeconds,
|
||||||
|
[int]$Threshold,
|
||||||
|
[int]$CooldownSeconds
|
||||||
|
)
|
||||||
|
|
||||||
|
$bucket = $script:FailedLogonBuckets[$BucketKey]
|
||||||
|
if ($null -eq $bucket) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = $bucket.Attempts.Count
|
||||||
|
if ($count -lt $Threshold) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$cooldownOk = $true
|
||||||
|
if ($null -ne $bucket.LastBurstAlertUtc) {
|
||||||
|
$elapsed = ((Get-Date).ToUniversalTime() - $bucket.LastBurstAlertUtc.ToUniversalTime()).TotalSeconds
|
||||||
|
$cooldownOk = ($elapsed -ge $CooldownSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $cooldownOk) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$bucket.LastBurstAlertUtc = (Get-Date).ToUniversalTime()
|
||||||
|
$msg = Format-FailedLogonBurstMessage -TierKind $TierKind -SourceKeyPart $SourceKeyPart `
|
||||||
|
-FocusUsername $FocusUsername -SecurityLogComputerName $SecurityLogComputerName `
|
||||||
|
-Attempts $bucket.Attempts -Threshold $Threshold -WindowSeconds $WindowSeconds
|
||||||
|
|
||||||
|
return [pscustomobject]@{
|
||||||
|
Tier = $TierKind
|
||||||
|
Message = $msg
|
||||||
|
Count = $count
|
||||||
|
BucketKey = $BucketKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-FailedLogonRateLimitDecision4625 {
|
||||||
|
param(
|
||||||
|
[string]$SourceIP,
|
||||||
|
[string]$ComputerName,
|
||||||
|
[string]$Username,
|
||||||
|
[int]$LogonType,
|
||||||
|
[datetime]$TimeCreated,
|
||||||
|
[string]$SecurityLogComputerName
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $FailedLogonRateLimitEnabled) {
|
||||||
|
return [pscustomobject]@{
|
||||||
|
SendIndividual = $true
|
||||||
|
BurstAlerts = @()
|
||||||
|
UserIpCount = 0
|
||||||
|
IpCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourcePart = Get-FailedLogonSourceKeyPart -SourceIP $SourceIP -ComputerName $ComputerName
|
||||||
|
$normUser = Get-FailedLogonNormalizedUsername -Username $Username
|
||||||
|
$userIpKey = "tier1:$sourcePart|$normUser"
|
||||||
|
$ipKey = "tier2:$sourcePart"
|
||||||
|
|
||||||
|
$userBucket = Update-FailedLogonRateLimitBucket -BucketKey $userIpKey `
|
||||||
|
-WindowSeconds $FailedLogonRateLimitUserIpWindowSeconds `
|
||||||
|
-Username $Username -LogonType $LogonType -TimeCreated $TimeCreated
|
||||||
|
|
||||||
|
$ipBucket = Update-FailedLogonRateLimitBucket -BucketKey $ipKey `
|
||||||
|
-WindowSeconds $FailedLogonRateLimitIpWindowSeconds `
|
||||||
|
-Username $Username -LogonType $LogonType -TimeCreated $TimeCreated
|
||||||
|
|
||||||
|
$burstAlerts = [System.Collections.ArrayList]@()
|
||||||
|
|
||||||
|
$burstUser = Get-FailedLogonBurstAlertIfNeeded -TierKind 'UserIp' -BucketKey $userIpKey `
|
||||||
|
-SourceKeyPart $sourcePart -FocusUsername $normUser -SecurityLogComputerName $SecurityLogComputerName `
|
||||||
|
-WindowSeconds $FailedLogonRateLimitUserIpWindowSeconds `
|
||||||
|
-Threshold $FailedLogonRateLimitUserIpThreshold `
|
||||||
|
-CooldownSeconds $FailedLogonRateLimitUserIpCooldownSeconds
|
||||||
|
if ($null -ne $burstUser) { $null = $burstAlerts.Add($burstUser) }
|
||||||
|
|
||||||
|
$burstIp = Get-FailedLogonBurstAlertIfNeeded -TierKind 'Ip' -BucketKey $ipKey `
|
||||||
|
-SourceKeyPart $sourcePart -FocusUsername $normUser -SecurityLogComputerName $SecurityLogComputerName `
|
||||||
|
-WindowSeconds $FailedLogonRateLimitIpWindowSeconds `
|
||||||
|
-Threshold $FailedLogonRateLimitIpThreshold `
|
||||||
|
-CooldownSeconds $FailedLogonRateLimitIpCooldownSeconds
|
||||||
|
if ($null -ne $burstIp) { $null = $burstAlerts.Add($burstIp) }
|
||||||
|
|
||||||
|
$inBurst = ($userBucket.Attempts.Count -ge $FailedLogonRateLimitUserIpThreshold) `
|
||||||
|
-or ($ipBucket.Attempts.Count -ge $FailedLogonRateLimitIpThreshold)
|
||||||
|
|
||||||
|
$sendIndividual = $true
|
||||||
|
if ($FailedLogonRateLimitSuppressIndividualWhileBurst -and $inBurst) {
|
||||||
|
$sendIndividual = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
return [pscustomobject]@{
|
||||||
|
SendIndividual = $sendIndividual
|
||||||
|
BurstAlerts = @($burstAlerts)
|
||||||
|
UserIpCount = $userBucket.Attempts.Count
|
||||||
|
IpCount = $ipBucket.Attempts.Count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Test-RDGatewayLog {
|
function Test-RDGatewayLog {
|
||||||
try {
|
try {
|
||||||
$logExists = Get-WinEvent -ListLog $RDGatewayLogName -ErrorAction SilentlyContinue
|
$logExists = Get-WinEvent -ListLog $RDGatewayLogName -ErrorAction SilentlyContinue
|
||||||
@@ -2108,6 +2368,9 @@ function Start-LoginMonitor {
|
|||||||
}
|
}
|
||||||
Write-Log "========================================"
|
Write-Log "========================================"
|
||||||
Write-Log "Каналы уведомлений: $(Get-NotifyChainHuman)"
|
Write-Log "Каналы уведомлений: $(Get-NotifyChainHuman)"
|
||||||
|
if ($FailedLogonRateLimitEnabled) {
|
||||||
|
Write-Log "Агрегация 4625: tier1 $FailedLogonRateLimitUserIpThreshold/$FailedLogonRateLimitUserIpWindowSeconds с (IP+user), tier2 $FailedLogonRateLimitIpThreshold/$FailedLogonRateLimitIpWindowSeconds с (IP); suppressIndividual=$FailedLogonRateLimitSuppressIndividualWhileBurst"
|
||||||
|
}
|
||||||
|
|
||||||
$lockout4740Enabled = Test-Lockout4740MonitoringActive
|
$lockout4740Enabled = Test-Lockout4740MonitoringActive
|
||||||
if ($lockout4740Enabled) {
|
if ($lockout4740Enabled) {
|
||||||
@@ -2190,19 +2453,51 @@ function Start-LoginMonitor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not $shouldIgnore) {
|
if (-not $shouldIgnore) {
|
||||||
$formattedMessage = Format-LoginEvent -EventID $event.Id `
|
if ($event.Id -eq 4625 -and $FailedLogonRateLimitEnabled) {
|
||||||
-Username $eventInfo.Username `
|
$rl = Get-FailedLogonRateLimitDecision4625 -SourceIP $eventInfo.SourceIP `
|
||||||
-ComputerName $eventInfo.ComputerName `
|
-ComputerName $eventInfo.ComputerName -Username $eventInfo.Username `
|
||||||
-SourceIP $eventInfo.SourceIP `
|
-LogonType $eventInfo.LogonType -TimeCreated $eventInfo.TimeCreated `
|
||||||
-ProcessName $eventInfo.ProcessName `
|
-SecurityLogComputerName $event.MachineName
|
||||||
-TimeCreated $eventInfo.TimeCreated `
|
|
||||||
-LogonType $eventInfo.LogonType `
|
|
||||||
-LogonTypeName $logonTypeName `
|
|
||||||
-SecurityLogComputerName $event.MachineName
|
|
||||||
|
|
||||||
Write-Log "Notify: ID=$($event.Id) User=$($eventInfo.Username) LT=$($eventInfo.LogonType) IP=$($eventInfo.SourceIP)"
|
if ($rl.SendIndividual) {
|
||||||
Send-MonitorNotification -Message $formattedMessage `
|
$formattedMessage = Format-LoginEvent -EventID $event.Id `
|
||||||
-EmailSubject "RDP Login Monitor: вход (ID $($event.Id))" | Out-Null
|
-Username $eventInfo.Username `
|
||||||
|
-ComputerName $eventInfo.ComputerName `
|
||||||
|
-SourceIP $eventInfo.SourceIP `
|
||||||
|
-ProcessName $eventInfo.ProcessName `
|
||||||
|
-TimeCreated $eventInfo.TimeCreated `
|
||||||
|
-LogonType $eventInfo.LogonType `
|
||||||
|
-LogonTypeName $logonTypeName `
|
||||||
|
-SecurityLogComputerName $event.MachineName
|
||||||
|
|
||||||
|
Write-Log "Notify: ID=4625 User=$($eventInfo.Username) LT=$($eventInfo.LogonType) IP=$($eventInfo.SourceIP) (tier1=$($rl.UserIpCount) tier2=$($rl.IpCount))"
|
||||||
|
Send-MonitorNotification -Message $formattedMessage `
|
||||||
|
-EmailSubject "RDP Login Monitor: неудачный вход (4625)" | Out-Null
|
||||||
|
} else {
|
||||||
|
Write-Log "Notify suppressed 4625: User=$($eventInfo.Username) IP=$($eventInfo.SourceIP) tier1=$($rl.UserIpCount)/$FailedLogonRateLimitUserIpThreshold tier2=$($rl.IpCount)/$FailedLogonRateLimitIpThreshold"
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($burst in $rl.BurstAlerts) {
|
||||||
|
$tierLabel = if ($burst.Tier -eq 'UserIp') { 'IP+user' } else { 'IP' }
|
||||||
|
Write-Log "Notify burst 4625 ($tierLabel): count=$($burst.Count) key=$($burst.BucketKey)"
|
||||||
|
Send-MonitorNotification -Message $burst.Message `
|
||||||
|
-EmailSubject "RDP Login Monitor: брутфорс 4625 ($tierLabel)" | Out-Null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$formattedMessage = Format-LoginEvent -EventID $event.Id `
|
||||||
|
-Username $eventInfo.Username `
|
||||||
|
-ComputerName $eventInfo.ComputerName `
|
||||||
|
-SourceIP $eventInfo.SourceIP `
|
||||||
|
-ProcessName $eventInfo.ProcessName `
|
||||||
|
-TimeCreated $eventInfo.TimeCreated `
|
||||||
|
-LogonType $eventInfo.LogonType `
|
||||||
|
-LogonTypeName $logonTypeName `
|
||||||
|
-SecurityLogComputerName $event.MachineName
|
||||||
|
|
||||||
|
Write-Log "Notify: ID=$($event.Id) User=$($eventInfo.Username) LT=$($eventInfo.LogonType) IP=$($eventInfo.SourceIP)"
|
||||||
|
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)
|
||||||
|
|||||||
@@ -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`**, на **КД, где запущен монитор** (имя совпадает с **`$LockoutMonitorDomainController`**) — **`4740`** (блокировка УЗ + IP из IIS ActiveSync), **ежедневный отчёт** (активные сессии через `quser`), **heartbeat**, **ротация логов**, уведомления в Telegram и/или Email.
|
- Основной скрипт: **`Login_Monitor.ps1`** — журнал Security **`4624`/`4625`** (логика зависит от типа ОС: рабочая станция или сервер/КД), при всплеске **`4625`** — **агрегированные оповещения** (два порога: IP+пользователь и только IP), при наличии журнала — **Remote Connection Manager `1149`** (часто актуально для РС с RDP), при роли **RD Gateway** — **`302`/`303`**, на **КД, где запущен монитор** (имя совпадает с **`$LockoutMonitorDomainController`**) — **`4740`** (блокировка УЗ + IP из IIS ActiveSync), **ежедневный отчёт** (активные сессии через `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 минут).
|
||||||
@@ -19,6 +19,7 @@ PowerShell-набор для мониторинга входов в Windows с
|
|||||||
- **Кодировка логов**: `login_monitor.log` / `watchdog.log` пишутся как **UTF-8 с BOM** (и при необходимости BOM добавляется к уже существующему файлу), чтобы в **FAR/старых просмотрщиках** не было ситуации “в консоли нормально, а в файле РЈРІРµ…” из‑за неверной авто-кодировки.
|
- **Кодировка логов**: `login_monitor.log` / `watchdog.log` пишутся как **UTF-8 с BOM** (и при необходимости BOM добавляется к уже существующему файлу), чтобы в **FAR/старых просмотрщиках** не было ситуации “в консоли нормально, а в файле РЈРІРµ…” из‑за неверной авто-кодировки.
|
||||||
- **`auditpol` на русской Windows**: настройка/проверка аудита опирается на категорию **`Вход/выход`** и подкатегории **`Вход в систему` / `Выход из системы`** (ожидается строка **`Успех и сбой`**). Это устраняет ошибки вида `0x00000057` из‑за несуществующего на RU ОС имени `Logon`.
|
- **`auditpol` на русской Windows**: настройка/проверка аудита опирается на категорию **`Вход/выход`** и подкатегории **`Вход в систему` / `Выход из системы`** (ожидается строка **`Успех и сбой`**). Это устраняет ошибки вида `0x00000057` из‑за несуществующего на RU ОС имени `Logon`.
|
||||||
- **Стабильность**: `auditpol` вызывается по полному пути `%SystemRoot%\System32\auditpol.exe` (без зависимости от PATH), stdout+stderr объединяются через `ProcessStartInfo`.
|
- **Стабильность**: `auditpol` вызывается по полному пути `%SystemRoot%\System32\auditpol.exe` (без зависимости от PATH), stdout+stderr объединяются через `ProcessStartInfo`.
|
||||||
|
- **Агрегация 4625 (брутфорс)**: при включённом `$FailedLogonRateLimitEnabled` — уровень 1: **5** неудачных попыток за **60** с с одного источника для **одной** учётной записи (IP+user); уровень 2: **12** попыток за **60** с с одного IP (несколько логинов). Пока порог не достигнут — поштучные 4625; при всплеске — сводные алерты, одиночные подавляются. Параметры в начале `Login_Monitor.ps1`. Автоблокировка IP не выполняется.
|
||||||
|
|
||||||
## 1) Подготовка
|
## 1) Подготовка
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -5,7 +5,7 @@ PowerShell toolkit for monitoring Windows logons with Telegram and/or Email (SMT
|
|||||||
## Recommended layout
|
## Recommended layout
|
||||||
|
|
||||||
- Installation root: **`C:\ProgramData\RDP-login-monitor\`**.
|
- Installation root: **`C:\ProgramData\RDP-login-monitor\`**.
|
||||||
- Main script: **`Login_Monitor.ps1`** — Security log **`4624`/`4625`** (behavior depends on OS type: workstation vs server/domain controller), optional **Remote Connection Manager `1149`** when the log is available (often useful for RDP-enabled workstations), **RD Gateway** events **`302`/`303`** when the gateway role/log is present, on the **DC where the monitor runs** (hostname matches **`$LockoutMonitorDomainController`**) — **`4740`** (account lockout + IPs from IIS ActiveSync), **daily report** (active sessions via `quser`), **heartbeat**, **log rotation**, alerts via Telegram and/or Email.
|
- Main script: **`Login_Monitor.ps1`** — Security log **`4624`/`4625`** (behavior depends on OS type: workstation vs server/domain controller), **aggregated `4625` burst alerts** (two tiers: IP+user and IP-only), optional **Remote Connection Manager `1149`** when the log is available (often useful for RDP-enabled workstations), **RD Gateway** events **`302`/`303`** when the gateway role/log is present, on the **DC where the monitor runs** (hostname matches **`$LockoutMonitorDomainController`**) — **`4740`** (account lockout + IPs from IIS ActiveSync), **daily report** (active sessions via `quser`), **heartbeat**, **log rotation**, alerts via Telegram and/or Email.
|
||||||
- Scheduled tasks: run **`Login_Monitor.ps1 -InstallTasks`** to register:
|
- Scheduled tasks: run **`Login_Monitor.ps1 -InstallTasks`** to register:
|
||||||
- `RDP-Login-Monitor` (main monitor),
|
- `RDP-Login-Monitor` (main monitor),
|
||||||
- `RDP-Login-Monitor-Watchdog` (process health check every 5 minutes).
|
- `RDP-Login-Monitor-Watchdog` (process health check every 5 minutes).
|
||||||
@@ -19,6 +19,7 @@ PowerShell toolkit for monitoring Windows logons with Telegram and/or Email (SMT
|
|||||||
- **Log encoding**: `login_monitor.log` / `watchdog.log` are written as **UTF-8 with BOM** (BOM is applied to existing files if missing) so viewers like **FAR Manager** do not mis-detect encoding.
|
- **Log encoding**: `login_monitor.log` / `watchdog.log` are written as **UTF-8 with BOM** (BOM is applied to existing files if missing) so viewers like **FAR Manager** do not mis-detect encoding.
|
||||||
- **`auditpol` on Russian Windows**: auditing checks use the **`Вход/выход`** category and **`Вход в систему` / `Выход из системы`** subcategories (expect **`Успех и сбой`**), avoiding errors such as `0x00000057` when English names like `Logon` are absent on a localized OS.
|
- **`auditpol` on Russian Windows**: auditing checks use the **`Вход/выход`** category and **`Вход в систему` / `Выход из системы`** subcategories (expect **`Успех и сбой`**), avoiding errors such as `0x00000057` when English names like `Logon` are absent on a localized OS.
|
||||||
- **Stability**: `auditpol` is invoked via full path `%SystemRoot%\System32\auditpol.exe` (no PATH dependency); stdout and stderr are merged via `ProcessStartInfo`.
|
- **Stability**: `auditpol` is invoked via full path `%SystemRoot%\System32\auditpol.exe` (no PATH dependency); stdout and stderr are merged via `ProcessStartInfo`.
|
||||||
|
- **`4625` burst alerts**: when `$FailedLogonRateLimitEnabled` is true — tier 1: **5** failures in **60** s per **IP+user**; tier 2: **12** in **60** s per **IP** (spray). Below thresholds, individual `4625` alerts are sent; during a burst, aggregated alerts replace per-event noise. No automatic IP blocking. Tune at the top of `Login_Monitor.ps1`.
|
||||||
|
|
||||||
## 1) Preparation
|
## 1) Preparation
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
1.5.9
|
1.5.10
|
||||||
|
|||||||
Reference in New Issue
Block a user