feat: ignore.lst for 4740, IIS time window, stale heartbeat alert (v1.5.3)
- ignore.lst scopes: 4740:/all: prefixes; IP rules match IIS client IPs - IIS ActiveSync IPs only within minutes before lockout event - Alert when last_heartbeat.txt is older than 2x HeartbeatInterval
This commit is contained in:
+147
-25
@@ -71,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.5.2"
|
$ScriptVersion = "1.5.3"
|
||||||
|
|
||||||
# Логи (все под InstallRoot)
|
# Логи (все под InstallRoot)
|
||||||
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
||||||
@@ -82,8 +82,9 @@ $MaxBackupDays = 31
|
|||||||
$LogRotationHour = 0
|
$LogRotationHour = 0
|
||||||
$LogRotationMinute = 0
|
$LogRotationMinute = 0
|
||||||
|
|
||||||
# Heartbeat (только файл)
|
# Heartbeat (файл; при отсутствии обновления > HeartbeatStaleAlertMultiplier × интервал — оповещение)
|
||||||
$HeartbeatInterval = 3600
|
$HeartbeatInterval = 3600
|
||||||
|
$HeartbeatStaleAlertMultiplier = 2
|
||||||
$HeartbeatFile = Join-Path $script:InstallRoot "Logs\last_heartbeat.txt"
|
$HeartbeatFile = Join-Path $script:InstallRoot "Logs\last_heartbeat.txt"
|
||||||
$DeployUpdateMarkerFile = Join-Path $script:InstallRoot "deploy_last_update.txt"
|
$DeployUpdateMarkerFile = Join-Path $script:InstallRoot "deploy_last_update.txt"
|
||||||
# Построчные правила подавления уведомлений Security 4624/4625 (см. ignore.lst.example в репозитории).
|
# Построчные правила подавления уведомлений Security 4624/4625 (см. ignore.lst.example в репозитории).
|
||||||
@@ -157,6 +158,8 @@ $NetBiosDomainName = ""
|
|||||||
$ExchangeIisLogPath = ""
|
$ExchangeIisLogPath = ""
|
||||||
$ExchangeServerHostForIisExclude = ""
|
$ExchangeServerHostForIisExclude = ""
|
||||||
$ExchangeIisLogTailLines = 5000
|
$ExchangeIisLogTailLines = 5000
|
||||||
|
# Окно поиска IP в IIS: только строки за N минут до события 4740 (локальное время сервера IIS).
|
||||||
|
$ExchangeIisLogMinutesBeforeLockout = 30
|
||||||
|
|
||||||
# Очередь оповещений: telegram, email (или tg, mail). Пусто = авто: настроенные каналы, порядок telegram → email.
|
# Очередь оповещений: telegram, email (или tg, mail). Пусто = авто: настроенные каналы, порядок telegram → email.
|
||||||
$NotifyOrder = ""
|
$NotifyOrder = ""
|
||||||
@@ -796,6 +799,8 @@ try {
|
|||||||
|
|
||||||
$script:IsWorkstation = $false
|
$script:IsWorkstation = $false
|
||||||
$script:OsInstallKindLabel = ""
|
$script:OsInstallKindLabel = ""
|
||||||
|
$script:MonitorStartedAt = $null
|
||||||
|
$script:HeartbeatStaleAlertActive = $false
|
||||||
|
|
||||||
function Enable-SecurityAudit {
|
function Enable-SecurityAudit {
|
||||||
Write-Log "Checking security audit (auditpol) settings..."
|
Write-Log "Checking security audit (auditpol) settings..."
|
||||||
@@ -1136,7 +1141,7 @@ function Send-Heartbeat {
|
|||||||
}
|
}
|
||||||
$ignoreEntries = @(Get-RdpMonitorIgnoreListEntries)
|
$ignoreEntries = @(Get-RdpMonitorIgnoreListEntries)
|
||||||
if ($ignoreEntries.Count -gt 0) {
|
if ($ignoreEntries.Count -gt 0) {
|
||||||
$message += "`r`n🚫 <b>Игнорируются:</b> Security 4624/4625 по правилам ignore.lst`r`n"
|
$message += "`r`n🚫 <b>Игнорируются:</b> по правилам ignore.lst (4624/4625 и/или 4740)`r`n"
|
||||||
foreach ($e in $ignoreEntries) {
|
foreach ($e in $ignoreEntries) {
|
||||||
$v = ConvertTo-TelegramHtml ([string]$e.Value)
|
$v = ConvertTo-TelegramHtml ([string]$e.Value)
|
||||||
$kindLabel = switch ($e.Kind) {
|
$kindLabel = switch ($e.Kind) {
|
||||||
@@ -1153,6 +1158,7 @@ function Send-Heartbeat {
|
|||||||
}
|
}
|
||||||
$notifyChain = Get-NotifyChainHuman
|
$notifyChain = Get-NotifyChainHuman
|
||||||
$message += "`r`n📢 <b>Каналы уведомлений:</b> $(ConvertTo-TelegramHtml $notifyChain)"
|
$message += "`r`n📢 <b>Каналы уведомлений:</b> $(ConvertTo-TelegramHtml $notifyChain)"
|
||||||
|
$message += "`r`n💓 <b>Heartbeat:</b> файл каждые $HeartbeatInterval с; оповещение, если нет обновления > $($HeartbeatStaleAlertMultiplier)× интервал."
|
||||||
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, типы входа по настройке скрипта)."
|
||||||
}
|
}
|
||||||
@@ -1176,6 +1182,55 @@ function Send-Heartbeat {
|
|||||||
Write-Log "Отправлено уведомление о запуске скрипта (каналы: $notifyChain)"
|
Write-Log "Отправлено уведомление о запуске скрипта (каналы: $notifyChain)"
|
||||||
} else {
|
} else {
|
||||||
Write-TextFileUtf8Bom -Path $HeartbeatFile -Text $timestamp
|
Write-TextFileUtf8Bom -Path $HeartbeatFile -Text $timestamp
|
||||||
|
$script:HeartbeatStaleAlertActive = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-LastHeartbeatTimestamp {
|
||||||
|
if (-not (Test-Path -LiteralPath $HeartbeatFile)) { return $null }
|
||||||
|
try {
|
||||||
|
$txt = (Get-Content -LiteralPath $HeartbeatFile -ErrorAction Stop | Select-Object -First 1)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($txt)) { return $null }
|
||||||
|
return [datetime]::ParseExact($txt.Trim(), 'dd.MM.yyyy HH:mm:ss', $null)
|
||||||
|
} catch {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-AndSendHeartbeatStaleAlert {
|
||||||
|
if ($null -eq $script:MonitorStartedAt) { return }
|
||||||
|
$thresholdSec = [double]($HeartbeatInterval * $HeartbeatStaleAlertMultiplier)
|
||||||
|
if (((Get-Date) - $script:MonitorStartedAt).TotalSeconds -lt $thresholdSec) { return }
|
||||||
|
|
||||||
|
$lastHb = Get-LastHeartbeatTimestamp
|
||||||
|
$isStale = $false
|
||||||
|
if ($null -eq $lastHb) {
|
||||||
|
$isStale = $true
|
||||||
|
} elseif (((Get-Date) - $lastHb).TotalSeconds -gt $thresholdSec) {
|
||||||
|
$isStale = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $isStale) {
|
||||||
|
if ($script:HeartbeatStaleAlertActive) {
|
||||||
|
$script:HeartbeatStaleAlertActive = $false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ($script:HeartbeatStaleAlertActive) { return }
|
||||||
|
|
||||||
|
$hHost = ConvertTo-TelegramHtml $env:COMPUTERNAME
|
||||||
|
$hThreshold = ConvertTo-TelegramHtml ([int]$thresholdSec)
|
||||||
|
$lastTxt = if ($null -eq $lastHb) { 'нет данных' } else { $lastHb.ToString('dd.MM.yyyy HH:mm:ss') }
|
||||||
|
$hLast = ConvertTo-TelegramHtml $lastTxt
|
||||||
|
$msg = "<b>⚠️ Heartbeat монитора не обновлялся</b>`r`n"
|
||||||
|
$msg += "🖥️ Сервер: $hHost`r`n"
|
||||||
|
$msg += "⏱️ Порог: $hThreshold с ($HeartbeatStaleAlertMultiplier × интервал $HeartbeatInterval с)`r`n"
|
||||||
|
$msg += "📄 Последний heartbeat: $hLast`r`n"
|
||||||
|
$msg += "<i>Проверьте процесс Login_Monitor.ps1 и задачи планировщика RDP-Login-Monitor / Watchdog.</i>"
|
||||||
|
|
||||||
|
if (Send-MonitorNotification -Message $msg -EmailSubject 'RDP Login Monitor: нет heartbeat') {
|
||||||
|
$script:HeartbeatStaleAlertActive = $true
|
||||||
|
Write-Log "Отправлено оповещение: heartbeat не обновлялся дольше $thresholdSec с"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1347,8 +1402,18 @@ function Parse-RdpMonitorIgnoreListLine {
|
|||||||
if ($line[0] -eq '#' -or $line[0] -eq ';') { return $null }
|
if ($line[0] -eq '#' -or $line[0] -eq ';') { return $null }
|
||||||
if ($line.StartsWith([char]0xFEFF)) { $line = $line.TrimStart([char]0xFEFF) }
|
if ($line.StartsWith([char]0xFEFF)) { $line = $line.TrimStart([char]0xFEFF) }
|
||||||
|
|
||||||
|
$scopes = @('4624', '4625')
|
||||||
|
if ($line -match '^(?i)(4740|lockout|блокир)\s*:\s*(.+)$') {
|
||||||
|
$scopes = @('4740')
|
||||||
|
$line = $Matches[2].Trim()
|
||||||
|
} elseif ($line -match '^(?i)(all|\*)\s*:\s*(.+)$') {
|
||||||
|
$scopes = @('4624', '4625', '4740')
|
||||||
|
$line = $Matches[2].Trim()
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($line)) { return $null }
|
||||||
|
|
||||||
if ($line -notmatch ':') {
|
if ($line -notmatch ':') {
|
||||||
return [pscustomobject]@{ Kind = 'Any'; Value = $line }
|
return [pscustomobject]@{ Kind = 'Any'; Value = $line; Scopes = $scopes }
|
||||||
}
|
}
|
||||||
|
|
||||||
$idx = $line.IndexOf(':')
|
$idx = $line.IndexOf(':')
|
||||||
@@ -1357,16 +1422,16 @@ function Parse-RdpMonitorIgnoreListLine {
|
|||||||
if ([string]::IsNullOrWhiteSpace($right)) { return $null }
|
if ([string]::IsNullOrWhiteSpace($right)) { return $null }
|
||||||
|
|
||||||
if ($left -match '(?i)(рабоч|workstation|wks)') {
|
if ($left -match '(?i)(рабоч|workstation|wks)') {
|
||||||
return [pscustomobject]@{ Kind = 'Workstation'; Value = $right }
|
return [pscustomobject]@{ Kind = 'Workstation'; Value = $right; Scopes = $scopes }
|
||||||
}
|
}
|
||||||
if ($left -match '(?i)(польз|username|subject|account|target\s*user|\buser\b)') {
|
if ($left -match '(?i)(польз|username|subject|account|target\s*user|\buser\b)') {
|
||||||
return [pscustomobject]@{ Kind = 'User'; Value = $right }
|
return [pscustomobject]@{ Kind = 'User'; Value = $right; Scopes = $scopes }
|
||||||
}
|
}
|
||||||
if ($left -match '(?i)(\bip\b|ip\s*адрес|ipaddress|адрес\s*ip)') {
|
if ($left -match '(?i)(\bip\b|ip\s*адрес|ipaddress|адрес\s*ip)') {
|
||||||
return [pscustomobject]@{ Kind = 'Ip'; Value = $right }
|
return [pscustomobject]@{ Kind = 'Ip'; Value = $right; Scopes = $scopes }
|
||||||
}
|
}
|
||||||
|
|
||||||
return [pscustomobject]@{ Kind = 'Any'; Value = $right }
|
return [pscustomobject]@{ Kind = 'Any'; Value = $right; Scopes = $scopes }
|
||||||
}
|
}
|
||||||
|
|
||||||
function Get-RdpMonitorIgnoreListEntries {
|
function Get-RdpMonitorIgnoreListEntries {
|
||||||
@@ -1393,11 +1458,12 @@ function Get-RdpMonitorIgnoreListEntries {
|
|||||||
$nUser = @($arr | Where-Object { $_.Kind -eq 'User' }).Count
|
$nUser = @($arr | Where-Object { $_.Kind -eq 'User' }).Count
|
||||||
$nWks = @($arr | Where-Object { $_.Kind -eq 'Workstation' }).Count
|
$nWks = @($arr | Where-Object { $_.Kind -eq 'Workstation' }).Count
|
||||||
$nAny = @($arr | Where-Object { $_.Kind -eq 'Any' }).Count
|
$nAny = @($arr | Where-Object { $_.Kind -eq 'Any' }).Count
|
||||||
|
$n4740 = @($arr | Where-Object { $_.Scopes -contains '4740' }).Count
|
||||||
$nTotal = $arr.Count
|
$nTotal = $arr.Count
|
||||||
if ($nTotal -eq 0) {
|
if ($nTotal -eq 0) {
|
||||||
Write-Log "ignore.lst обновлён: список правил пуст, игнорирование по файлу для Security 4624/4625 не задаётся."
|
Write-Log "ignore.lst обновлён: список правил пуст."
|
||||||
} else {
|
} else {
|
||||||
Write-Log ("ignore.lst обновлён: добавлено игнорирование событий 4624/4625 по IP ({0}), пользователю ({1}), рабочей станции ({2}); универсальных правил ({3}). Всего записей: {4}." -f $nIp, $nUser, $nWks, $nAny, $nTotal)
|
Write-Log ("ignore.lst обновлён: записей {0} (IP {1}, user {2}, wks {3}, any {4}; затрагивают 4740: {5})." -f $nTotal, $nIp, $nUser, $nWks, $nAny, $n4740)
|
||||||
}
|
}
|
||||||
return $script:IgnoreListCache
|
return $script:IgnoreListCache
|
||||||
} catch {
|
} catch {
|
||||||
@@ -1410,13 +1476,25 @@ function Get-RdpMonitorIgnoreListEntries {
|
|||||||
|
|
||||||
function Test-RdpMonitorIgnoreListMatch {
|
function Test-RdpMonitorIgnoreListMatch {
|
||||||
param(
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$EventId,
|
||||||
[string]$Username,
|
[string]$Username,
|
||||||
[string]$ComputerName,
|
[string]$ComputerName,
|
||||||
[string]$SourceIP
|
[string]$SourceIP,
|
||||||
|
[string[]]$AdditionalIps = @()
|
||||||
)
|
)
|
||||||
$entries = @(Get-RdpMonitorIgnoreListEntries)
|
$entries = @(Get-RdpMonitorIgnoreListEntries | Where-Object { $_.Scopes -contains $EventId })
|
||||||
if ($entries.Count -eq 0) { return $false }
|
if ($entries.Count -eq 0) { return $false }
|
||||||
|
|
||||||
|
$ipsToCheck = [System.Collections.Generic.List[string]]::new()
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($SourceIP) -and $SourceIP -ne '-') {
|
||||||
|
$ipsToCheck.Add($SourceIP.Trim()) | Out-Null
|
||||||
|
}
|
||||||
|
foreach ($ip in $AdditionalIps) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ip)) { continue }
|
||||||
|
$t = $ip.Trim()
|
||||||
|
if (-not $ipsToCheck.Contains($t)) { $ipsToCheck.Add($t) | Out-Null }
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($e in $entries) {
|
foreach ($e in $entries) {
|
||||||
$v = [string]$e.Value
|
$v = [string]$e.Value
|
||||||
if ([string]::IsNullOrWhiteSpace($v)) { continue }
|
if ([string]::IsNullOrWhiteSpace($v)) { continue }
|
||||||
@@ -1426,19 +1504,20 @@ function Test-RdpMonitorIgnoreListMatch {
|
|||||||
if (Test-RdpMonitorUsernameMatchesToken -Username $Username -Token $v) { return $true }
|
if (Test-RdpMonitorUsernameMatchesToken -Username $Username -Token $v) { return $true }
|
||||||
}
|
}
|
||||||
'Workstation' {
|
'Workstation' {
|
||||||
|
if ($EventId -eq '4740') { continue }
|
||||||
if (-not [string]::IsNullOrWhiteSpace($ComputerName) -and $ComputerName -ne '-' -and ($ComputerName -ieq $v)) {
|
if (-not [string]::IsNullOrWhiteSpace($ComputerName) -and $ComputerName -ne '-' -and ($ComputerName -ieq $v)) {
|
||||||
return $true
|
return $true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'Ip' {
|
'Ip' {
|
||||||
if (-not [string]::IsNullOrWhiteSpace($SourceIP) -and $SourceIP -ne '-' -and ($SourceIP -ieq $v)) {
|
foreach ($checkIp in $ipsToCheck) {
|
||||||
return $true
|
if ($checkIp -ieq $v) { return $true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'Any' {
|
'Any' {
|
||||||
if (Test-RdpMonitorStringLooksLikeIPv4 $v) {
|
if (Test-RdpMonitorStringLooksLikeIPv4 $v) {
|
||||||
if (-not [string]::IsNullOrWhiteSpace($SourceIP) -and $SourceIP -ne '-' -and ($SourceIP -ieq $v)) {
|
foreach ($checkIp in $ipsToCheck) {
|
||||||
return $true
|
if ($checkIp -ieq $v) { return $true }
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1446,8 +1525,10 @@ function Test-RdpMonitorIgnoreListMatch {
|
|||||||
if (Test-RdpMonitorUsernameMatchesToken -Username $Username -Token $v) { return $true }
|
if (Test-RdpMonitorUsernameMatchesToken -Username $Username -Token $v) { return $true }
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (-not [string]::IsNullOrWhiteSpace($ComputerName) -and $ComputerName -ne '-' -and ($ComputerName -ieq $v)) {
|
if ($EventId -ne '4740') {
|
||||||
return $true
|
if (-not [string]::IsNullOrWhiteSpace($ComputerName) -and $ComputerName -ne '-' -and ($ComputerName -ieq $v)) {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (Test-RdpMonitorUsernameMatchesToken -Username $Username -Token $v) { return $true }
|
if (Test-RdpMonitorUsernameMatchesToken -Username $Username -Token $v) { return $true }
|
||||||
}
|
}
|
||||||
@@ -1456,6 +1537,14 @@ function Test-RdpMonitorIgnoreListMatch {
|
|||||||
return $false
|
return $false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Should-IgnoreLockout4740Event {
|
||||||
|
param(
|
||||||
|
[string]$Username,
|
||||||
|
[string[]]$IisClientIps = @()
|
||||||
|
)
|
||||||
|
return Test-RdpMonitorIgnoreListMatch -EventId '4740' -Username $Username -AdditionalIps $IisClientIps
|
||||||
|
}
|
||||||
|
|
||||||
function Should-IgnoreEvent {
|
function Should-IgnoreEvent {
|
||||||
param(
|
param(
|
||||||
[string]$Username,
|
[string]$Username,
|
||||||
@@ -1514,7 +1603,8 @@ function Should-IgnoreEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($EventID -in 4624, 4625) {
|
if ($EventID -in 4624, 4625) {
|
||||||
if (Test-RdpMonitorIgnoreListMatch -Username $Username -ComputerName $ComputerName -SourceIP $SourceIP) {
|
if (Test-RdpMonitorIgnoreListMatch -EventId ([string]$EventID) -Username $Username `
|
||||||
|
-ComputerName $ComputerName -SourceIP $SourceIP) {
|
||||||
return $true
|
return $true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1877,11 +1967,17 @@ function Get-Lockout4740EventInfo {
|
|||||||
function Get-ExchangeActiveSyncIpsFromIisLog {
|
function Get-ExchangeActiveSyncIpsFromIisLog {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)][string]$SamAccountName,
|
[Parameter(Mandatory = $true)][string]$SamAccountName,
|
||||||
[string]$DomainNetBios = ""
|
[string]$DomainNetBios = "",
|
||||||
|
[Parameter(Mandatory = $true)][datetime]$ReferenceTime
|
||||||
)
|
)
|
||||||
if ([string]::IsNullOrWhiteSpace($ExchangeIisLogPath)) { return @() }
|
if ([string]::IsNullOrWhiteSpace($ExchangeIisLogPath)) { return @() }
|
||||||
|
$minutes = [int]$ExchangeIisLogMinutesBeforeLockout
|
||||||
|
if ($minutes -lt 1) { $minutes = 1 }
|
||||||
|
$windowStart = $ReferenceTime.AddMinutes(-$minutes)
|
||||||
|
$windowEnd = $ReferenceTime.AddMinutes(2)
|
||||||
|
|
||||||
$logDir = $ExchangeIisLogPath.TrimEnd('\')
|
$logDir = $ExchangeIisLogPath.TrimEnd('\')
|
||||||
$logFile = Join-Path $logDir ("u_ex" + (Get-Date).ToUniversalTime().ToString("yyMMdd") + ".log")
|
$logFile = Join-Path $logDir ("u_ex" + $ReferenceTime.ToUniversalTime().ToString("yyMMdd") + ".log")
|
||||||
if (-not (Test-Path -LiteralPath $logFile)) {
|
if (-not (Test-Path -LiteralPath $logFile)) {
|
||||||
Write-Log "IIS: файл лога не найден: $logFile"
|
Write-Log "IIS: файл лога не найден: $logFile"
|
||||||
return @()
|
return @()
|
||||||
@@ -1905,8 +2001,24 @@ function Get-ExchangeActiveSyncIpsFromIisLog {
|
|||||||
try {
|
try {
|
||||||
$lines = Get-Content -LiteralPath $logFile -Tail $ExchangeIisLogTailLines -ErrorAction Stop
|
$lines = Get-Content -LiteralPath $logFile -Tail $ExchangeIisLogTailLines -ErrorAction Stop
|
||||||
foreach ($line in $lines) {
|
foreach ($line in $lines) {
|
||||||
|
if ($line.StartsWith('#')) { continue }
|
||||||
if ($line -notlike '*401 *' -or $line -notlike '*ActiveSync*') { continue }
|
if ($line -notlike '*401 *' -or $line -notlike '*ActiveSync*') { continue }
|
||||||
if ($line -notlike "*$userPattern1*" -and $line -notlike "*$userPattern2*") { continue }
|
if ($line -notlike "*$userPattern1*" -and $line -notlike "*$userPattern2*") { continue }
|
||||||
|
|
||||||
|
$lineTime = $null
|
||||||
|
if ($line -match '^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})') {
|
||||||
|
try {
|
||||||
|
$lineTime = [datetime]::ParseExact(
|
||||||
|
"$($Matches[1]) $($Matches[2])",
|
||||||
|
'yyyy-MM-dd HH:mm:ss',
|
||||||
|
$null
|
||||||
|
)
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
if ($null -ne $lineTime -and ($lineTime -lt $windowStart -or $lineTime -gt $windowEnd)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if ($line -notmatch '(?:\d{1,3}\.){3}\d{1,3}') { continue }
|
if ($line -notmatch '(?:\d{1,3}\.){3}\d{1,3}') { continue }
|
||||||
$ip = $Matches[0]
|
$ip = $Matches[0]
|
||||||
if ($excludeHosts -contains $ip) { continue }
|
if ($excludeHosts -contains $ip) { continue }
|
||||||
@@ -1949,7 +2061,7 @@ function Format-Lockout4740TelegramMessage {
|
|||||||
$message += ('• {0} ({1})' -f (ConvertTo-TelegramHtml $ip), $netType) + "`r`n"
|
$message += ('• {0} ({1})' -f (ConvertTo-TelegramHtml $ip), $netType) + "`r`n"
|
||||||
}
|
}
|
||||||
} elseif (-not [string]::IsNullOrWhiteSpace($ExchangeIisLogPath)) {
|
} elseif (-not [string]::IsNullOrWhiteSpace($ExchangeIisLogPath)) {
|
||||||
$message += "`r`n<i>IP в логах IIS ActiveSync для этого пользователя не найдены.</i>`r`n"
|
$message += "`r`n<i>IP в IIS ActiveSync не найдены (окно $ExchangeIisLogMinutesBeforeLockout мин до блокировки, 401).</i>`r`n"
|
||||||
}
|
}
|
||||||
|
|
||||||
return $message
|
return $message
|
||||||
@@ -1981,12 +2093,15 @@ function Start-LoginMonitor {
|
|||||||
if ($lockout4740Enabled) {
|
if ($lockout4740Enabled) {
|
||||||
Write-Log "Мониторинг блокировок AD (4740) включён на этом КД ($LockoutMonitorDomainController)."
|
Write-Log "Мониторинг блокировок AD (4740) включён на этом КД ($LockoutMonitorDomainController)."
|
||||||
if (-not [string]::IsNullOrWhiteSpace($ExchangeIisLogPath)) {
|
if (-not [string]::IsNullOrWhiteSpace($ExchangeIisLogPath)) {
|
||||||
Write-Log "Обогащение: IIS ActiveSync — $ExchangeIisLogPath"
|
Write-Log "Обогащение: IIS ActiveSync — $ExchangeIisLogPath (окно ${ExchangeIisLogMinutesBeforeLockout} мин до 4740)"
|
||||||
}
|
}
|
||||||
} elseif (-not [string]::IsNullOrWhiteSpace($LockoutMonitorDomainController)) {
|
} elseif (-not [string]::IsNullOrWhiteSpace($LockoutMonitorDomainController)) {
|
||||||
Write-Log "Мониторинг 4740 задан для КД '$LockoutMonitorDomainController', но этот узел — $env:COMPUTERNAME (блокировки не отслеживаются)."
|
Write-Log "Мониторинг 4740 задан для КД '$LockoutMonitorDomainController', но этот узел — $env:COMPUTERNAME (блокировки не отслеживаются)."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$script:MonitorStartedAt = Get-Date
|
||||||
|
$script:HeartbeatStaleAlertActive = $false
|
||||||
|
|
||||||
Cleanup-OldLogs
|
Cleanup-OldLogs
|
||||||
Send-Heartbeat -IsStartup
|
Send-Heartbeat -IsStartup
|
||||||
Enable-SecurityAudit
|
Enable-SecurityAudit
|
||||||
@@ -2010,8 +2125,9 @@ function Start-LoginMonitor {
|
|||||||
|
|
||||||
while ($true) {
|
while ($true) {
|
||||||
try {
|
try {
|
||||||
# ignore.lst: сверка mtime и лог при изменении файла (не только при событиях 4624/4625).
|
# ignore.lst: сверка mtime и лог при изменении файла.
|
||||||
[void](Get-RdpMonitorIgnoreListEntries)
|
[void](Get-RdpMonitorIgnoreListEntries)
|
||||||
|
Test-AndSendHeartbeatStaleAlert
|
||||||
|
|
||||||
$events = Get-WinEvent -FilterHashtable @{
|
$events = Get-WinEvent -FilterHashtable @{
|
||||||
LogName = 'Security'
|
LogName = 'Security'
|
||||||
@@ -2138,7 +2254,13 @@ function Start-LoginMonitor {
|
|||||||
if ([string]::IsNullOrWhiteSpace($lo.Username)) { continue }
|
if ([string]::IsNullOrWhiteSpace($lo.Username)) { continue }
|
||||||
|
|
||||||
$domainForIis = if ([string]::IsNullOrWhiteSpace($lo.Domain)) { $NetBiosDomainName } else { $lo.Domain }
|
$domainForIis = if ([string]::IsNullOrWhiteSpace($lo.Domain)) { $NetBiosDomainName } else { $lo.Domain }
|
||||||
$iisIps = @(Get-ExchangeActiveSyncIpsFromIisLog -SamAccountName $lo.Username -DomainNetBios $domainForIis)
|
$iisIps = @(Get-ExchangeActiveSyncIpsFromIisLog -SamAccountName $lo.Username `
|
||||||
|
-DomainNetBios $domainForIis -ReferenceTime $lo.TimeCreated)
|
||||||
|
|
||||||
|
if (Should-IgnoreLockout4740Event -Username $lo.Username -IisClientIps $iisIps) {
|
||||||
|
Write-Log "Skip 4740 (ignore.lst): User=$($lo.Username)"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
$msg = Format-Lockout4740TelegramMessage -Username $lo.Username -Domain $lo.Domain `
|
$msg = Format-Lockout4740TelegramMessage -Username $lo.Username -Domain $lo.Domain `
|
||||||
-TimeCreated $lo.TimeCreated -IisClientIps $iisIps
|
-TimeCreated $lo.TimeCreated -IisClientIps $iisIps
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ PowerShell-набор для мониторинга входов в Windows с
|
|||||||
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 не активен.
|
7. (Опционально) Мониторинг блокировок AD на КД — **`$LockoutMonitorDomainController`**, **`$NetBiosDomainName`**, **`$ExchangeIisLogPath`**, **`$ExchangeIisLogMinutesBeforeLockout`** (по умолчанию 30), **`$ExchangeServerHostForIisExclude`**. В оповещении: пользователь из 4740 и IP из IIS за окно до блокировки. В **`ignore.lst`** префикс **`4740:`** или **`all:`** — см. **`ignore.lst.example`**.
|
||||||
|
8. Heartbeat: при отсутствии обновления **`Logs\last_heartbeat.txt`** дольше **`$HeartbeatStaleAlertMultiplier` × `$HeartbeatInterval`** (по умолчанию 2×1 ч) — оповещение в Telegram/Email.
|
||||||
|
|
||||||
## 2) Ручной запуск
|
## 2) Ручной запуск
|
||||||
|
|
||||||
@@ -89,9 +90,9 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-logi
|
|||||||
|
|
||||||
## 7) Подавление уведомлений Security: `ignore.lst`
|
## 7) Подавление уведомлений Security: `ignore.lst`
|
||||||
|
|
||||||
В каталоге установки можно положить файл **`C:\ProgramData\RDP-login-monitor\ignore.lst`** (рядом с **`Login_Monitor.ps1`**). Правила из списка проверяются **только** для Telegram-уведомлений по событиям **`4624`/`4625`** журнала Security (успех/неудача входа). Жёстко заданные в скрипте исключения (`ExcludedUsers`, локальный IP, сервисные учётные записи и т.д.) по-прежнему действуют для всех типов событий; **`ignore.lst`** добавляет к ним **дополнительные** совпадения именно для **4624/4625**.
|
В каталоге установки можно положить файл **`C:\ProgramData\RDP-login-monitor\ignore.lst`** (рядом с **`Login_Monitor.ps1`**). По умолчанию правила относятся к **`4624`/`4625`**; префикс **`4740:`** (или **`lockout:`**, **`блокир:`**) — только к блокировкам учётной записи; **`all:`** — и входы, и **4740**. Для **4740** тип **`ip:`** сравнивается с IP из IIS ActiveSync. Жёсткие исключения в скрипте по-прежнему для всех типов событий, кроме **4740** (там только `ignore.lst` и встроенные проверки пользователя).
|
||||||
|
|
||||||
События **RD Gateway (`302`/`303`)**, **RCM `1149`**, ежедневный отчёт и heartbeat **этим файлом не настраиваются** (для `1149` список не используется, даже если формально вызывается общая функция фильтрации).
|
События **RD Gateway (`302`/`303`)**, **RCM `1149`**, ежедневный отчёт и heartbeat **этим файлом не настраиваются**.
|
||||||
|
|
||||||
### Как читается файл
|
### Как читается файл
|
||||||
|
|
||||||
@@ -101,7 +102,17 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-logi
|
|||||||
- Строка с **`:`**: берётся **первая** двоеточие — всё слева (после обрезки пробелов) определяет тип правила, всё справа — значение. Если справа пусто, строка игнорируется.
|
- Строка с **`:`**: берётся **первая** двоеточие — всё слева (после обрезки пробелов) определяет тип правила, всё справа — значение. Если справа пусто, строка игнорируется.
|
||||||
- Строка **без** **`:`**: целиком трактуется как правило типа «любое совпадение» (см. ниже).
|
- Строка **без** **`:`**: целиком трактуется как правило типа «любое совпадение» (см. ниже).
|
||||||
|
|
||||||
### Типы правил (левая часть до первого `:`)
|
### Префикс области (в самом начале строки, до типа правила)
|
||||||
|
|
||||||
|
| Префикс | События |
|
||||||
|
| --- | --- |
|
||||||
|
| *(нет)* | **4624**, **4625** |
|
||||||
|
| `4740:`, `lockout:`, `блокир:` | **4740** |
|
||||||
|
| `all:`, `*:` | **4624**, **4625**, **4740** |
|
||||||
|
|
||||||
|
Пример: `4740:user:svc_sync` — не слать оповещение о блокировке этой УЗ.
|
||||||
|
|
||||||
|
### Типы правил (левая часть до первого `:` после префикса области)
|
||||||
|
|
||||||
| Левая часть (фрагменты совпадают как regex, без учёта регистра) | Поле события |
|
| Левая часть (фрагменты совпадают как regex, без учёта регистра) | Поле события |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
|||||||
+20
-14
@@ -2,23 +2,29 @@
|
|||||||
# C:\ProgramData\RDP-login-monitor\ignore.lst
|
# C:\ProgramData\RDP-login-monitor\ignore.lst
|
||||||
#
|
#
|
||||||
# Каждая непустая строка — одно правило. Строки с # или ; в начале — комментарии.
|
# Каждая непустая строка — одно правило. Строки с # или ; в начале — комментарии.
|
||||||
# Подавляет только уведомления Security 4624/4625 (не RD Gateway, не RCM 1149).
|
|
||||||
#
|
#
|
||||||
# Форматы:
|
# Область действия (префикс в начале строки, необязателен):
|
||||||
|
# (по умолчанию) — только Security 4624/4625
|
||||||
|
# 4740: — только блокировка учётной записи (4740); для IP — любой IP из IIS
|
||||||
|
# all: — и 4624/4625, и 4740
|
||||||
|
#
|
||||||
|
# Форматы правила (после префикса области):
|
||||||
# user:domain\user
|
# user:domain\user
|
||||||
# user:user
|
# user:user
|
||||||
# workstation:IVANOV
|
# workstation:IVANOV (не для 4740)
|
||||||
# ip:111.222.333.444
|
# ip:111.222.333.444
|
||||||
# Можно вставить «как в Telegram» (берётся значение после первого «:»):
|
|
||||||
# 👤 Пользователь: user
|
|
||||||
# 🖥️ Рабочая станция (клиент из события): IVANOV
|
|
||||||
# 🌐 IP адрес: 111.222.333.444
|
|
||||||
#
|
#
|
||||||
# Строка без префикса:
|
# Строка без префикса типа:
|
||||||
# IVANOV — совпадение с именем рабочей станции ИЛИ с пользователем (sam) ИЛИ с IP (если строка — IPv4)
|
# IVANOV — рабочая станция / пользователь / IP (IPv4)
|
||||||
# 111.222.333.444 — только IP (в реальной конфигурации укажите действительный IPv4 клиента)
|
# domain\user — пользователь
|
||||||
# domain\user — пользователь целиком
|
|
||||||
|
|
||||||
# user:domain\user
|
# --- только входы 4624/4625 ---
|
||||||
# workstation:IVANOV
|
# user:domain\service_account
|
||||||
# ip:111.222.333.444
|
# ip:192.168.1.100
|
||||||
|
|
||||||
|
# --- только блокировки 4740 ---
|
||||||
|
# 4740:user:test.user
|
||||||
|
# 4740:ip:203.0.113.50
|
||||||
|
|
||||||
|
# --- все перечисленные события ---
|
||||||
|
# all:user:domain\noise_account
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
1.5.2
|
1.5.3
|
||||||
|
|||||||
Reference in New Issue
Block a user