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:
PTah
2026-05-19 20:46:35 +10:00
parent b7786e8690
commit 52b1723137
4 changed files with 183 additions and 44 deletions
+145 -23
View File
@@ -71,7 +71,7 @@ $script:MonitorSingletonLockStream = $null
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
$ScriptVersion = "1.5.2"
$ScriptVersion = "1.5.3"
# Логи (все под InstallRoot)
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
@@ -82,8 +82,9 @@ $MaxBackupDays = 31
$LogRotationHour = 0
$LogRotationMinute = 0
# Heartbeat (только файл)
# Heartbeat (файл; при отсутствии обновления > HeartbeatStaleAlertMultiplier × интервал — оповещение)
$HeartbeatInterval = 3600
$HeartbeatStaleAlertMultiplier = 2
$HeartbeatFile = Join-Path $script:InstallRoot "Logs\last_heartbeat.txt"
$DeployUpdateMarkerFile = Join-Path $script:InstallRoot "deploy_last_update.txt"
# Построчные правила подавления уведомлений Security 4624/4625 (см. ignore.lst.example в репозитории).
@@ -157,6 +158,8 @@ $NetBiosDomainName = ""
$ExchangeIisLogPath = ""
$ExchangeServerHostForIisExclude = ""
$ExchangeIisLogTailLines = 5000
# Окно поиска IP в IIS: только строки за N минут до события 4740 (локальное время сервера IIS).
$ExchangeIisLogMinutesBeforeLockout = 30
# Очередь оповещений: telegram, email (или tg, mail). Пусто = авто: настроенные каналы, порядок telegram → email.
$NotifyOrder = ""
@@ -796,6 +799,8 @@ try {
$script:IsWorkstation = $false
$script:OsInstallKindLabel = ""
$script:MonitorStartedAt = $null
$script:HeartbeatStaleAlertActive = $false
function Enable-SecurityAudit {
Write-Log "Checking security audit (auditpol) settings..."
@@ -1136,7 +1141,7 @@ function Send-Heartbeat {
}
$ignoreEntries = @(Get-RdpMonitorIgnoreListEntries)
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) {
$v = ConvertTo-TelegramHtml ([string]$e.Value)
$kindLabel = switch ($e.Kind) {
@@ -1153,6 +1158,7 @@ function Send-Heartbeat {
}
$notifyChain = Get-NotifyChainHuman
$message += "`r`n📢 <b>Каналы уведомлений:</b> $(ConvertTo-TelegramHtml $notifyChain)"
$message += "`r`n💓 <b>Heartbeat:</b> файл каждые $HeartbeatInterval с; оповещение, если нет обновления > $($HeartbeatStaleAlertMultiplier)× интервал."
if (Test-RDSDeploymentPresent) {
$message += "`r`n🔐 <b>RDS (хост сессий):</b> обнаружены компоненты RDS помимо чистого шлюза — в мониторинг входят входы по RDP/RDS на этом узле (Security 4624/4625, типы входа по настройке скрипта)."
}
@@ -1176,6 +1182,55 @@ function Send-Heartbeat {
Write-Log "Отправлено уведомление о запуске скрипта (каналы: $notifyChain)"
} else {
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.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 ':') {
return [pscustomobject]@{ Kind = 'Any'; Value = $line }
return [pscustomobject]@{ Kind = 'Any'; Value = $line; Scopes = $scopes }
}
$idx = $line.IndexOf(':')
@@ -1357,16 +1422,16 @@ function Parse-RdpMonitorIgnoreListLine {
if ([string]::IsNullOrWhiteSpace($right)) { return $null }
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)') {
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)') {
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 {
@@ -1393,11 +1458,12 @@ function Get-RdpMonitorIgnoreListEntries {
$nUser = @($arr | Where-Object { $_.Kind -eq 'User' }).Count
$nWks = @($arr | Where-Object { $_.Kind -eq 'Workstation' }).Count
$nAny = @($arr | Where-Object { $_.Kind -eq 'Any' }).Count
$n4740 = @($arr | Where-Object { $_.Scopes -contains '4740' }).Count
$nTotal = $arr.Count
if ($nTotal -eq 0) {
Write-Log "ignore.lst обновлён: список правил пуст, игнорирование по файлу для Security 4624/4625 не задаётся."
Write-Log "ignore.lst обновлён: список правил пуст."
} 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
} catch {
@@ -1410,13 +1476,25 @@ function Get-RdpMonitorIgnoreListEntries {
function Test-RdpMonitorIgnoreListMatch {
param(
[Parameter(Mandatory = $true)][string]$EventId,
[string]$Username,
[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 }
$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) {
$v = [string]$e.Value
if ([string]::IsNullOrWhiteSpace($v)) { continue }
@@ -1426,19 +1504,20 @@ function Test-RdpMonitorIgnoreListMatch {
if (Test-RdpMonitorUsernameMatchesToken -Username $Username -Token $v) { return $true }
}
'Workstation' {
if ($EventId -eq '4740') { continue }
if (-not [string]::IsNullOrWhiteSpace($ComputerName) -and $ComputerName -ne '-' -and ($ComputerName -ieq $v)) {
return $true
}
}
'Ip' {
if (-not [string]::IsNullOrWhiteSpace($SourceIP) -and $SourceIP -ne '-' -and ($SourceIP -ieq $v)) {
return $true
foreach ($checkIp in $ipsToCheck) {
if ($checkIp -ieq $v) { return $true }
}
}
'Any' {
if (Test-RdpMonitorStringLooksLikeIPv4 $v) {
if (-not [string]::IsNullOrWhiteSpace($SourceIP) -and $SourceIP -ne '-' -and ($SourceIP -ieq $v)) {
return $true
foreach ($checkIp in $ipsToCheck) {
if ($checkIp -ieq $v) { return $true }
}
continue
}
@@ -1446,9 +1525,11 @@ function Test-RdpMonitorIgnoreListMatch {
if (Test-RdpMonitorUsernameMatchesToken -Username $Username -Token $v) { return $true }
continue
}
if ($EventId -ne '4740') {
if (-not [string]::IsNullOrWhiteSpace($ComputerName) -and $ComputerName -ne '-' -and ($ComputerName -ieq $v)) {
return $true
}
}
if (Test-RdpMonitorUsernameMatchesToken -Username $Username -Token $v) { return $true }
}
}
@@ -1456,6 +1537,14 @@ function Test-RdpMonitorIgnoreListMatch {
return $false
}
function Should-IgnoreLockout4740Event {
param(
[string]$Username,
[string[]]$IisClientIps = @()
)
return Test-RdpMonitorIgnoreListMatch -EventId '4740' -Username $Username -AdditionalIps $IisClientIps
}
function Should-IgnoreEvent {
param(
[string]$Username,
@@ -1514,7 +1603,8 @@ function Should-IgnoreEvent {
}
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
}
}
@@ -1877,11 +1967,17 @@ function Get-Lockout4740EventInfo {
function Get-ExchangeActiveSyncIpsFromIisLog {
param(
[Parameter(Mandatory = $true)][string]$SamAccountName,
[string]$DomainNetBios = ""
[string]$DomainNetBios = "",
[Parameter(Mandatory = $true)][datetime]$ReferenceTime
)
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('\')
$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)) {
Write-Log "IIS: файл лога не найден: $logFile"
return @()
@@ -1905,8 +2001,24 @@ function Get-ExchangeActiveSyncIpsFromIisLog {
try {
$lines = Get-Content -LiteralPath $logFile -Tail $ExchangeIisLogTailLines -ErrorAction Stop
foreach ($line in $lines) {
if ($line.StartsWith('#')) { continue }
if ($line -notlike '*401 *' -or $line -notlike '*ActiveSync*') { 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 }
$ip = $Matches[0]
if ($excludeHosts -contains $ip) { continue }
@@ -1949,7 +2061,7 @@ function Format-Lockout4740TelegramMessage {
$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"
$message += "`r`n<i>IP в IIS ActiveSync не найдены (окно $ExchangeIisLogMinutesBeforeLockout мин до блокировки, 401).</i>`r`n"
}
return $message
@@ -1981,12 +2093,15 @@ function Start-LoginMonitor {
if ($lockout4740Enabled) {
Write-Log "Мониторинг блокировок AD (4740) включён на этом КД ($LockoutMonitorDomainController)."
if (-not [string]::IsNullOrWhiteSpace($ExchangeIisLogPath)) {
Write-Log "Обогащение: IIS ActiveSync — $ExchangeIisLogPath"
Write-Log "Обогащение: IIS ActiveSync — $ExchangeIisLogPath (окно ${ExchangeIisLogMinutesBeforeLockout} мин до 4740)"
}
} elseif (-not [string]::IsNullOrWhiteSpace($LockoutMonitorDomainController)) {
Write-Log "Мониторинг 4740 задан для КД '$LockoutMonitorDomainController', но этот узел — $env:COMPUTERNAME (блокировки не отслеживаются)."
}
$script:MonitorStartedAt = Get-Date
$script:HeartbeatStaleAlertActive = $false
Cleanup-OldLogs
Send-Heartbeat -IsStartup
Enable-SecurityAudit
@@ -2010,8 +2125,9 @@ function Start-LoginMonitor {
while ($true) {
try {
# ignore.lst: сверка mtime и лог при изменении файла (не только при событиях 4624/4625).
# ignore.lst: сверка mtime и лог при изменении файла.
[void](Get-RdpMonitorIgnoreListEntries)
Test-AndSendHeartbeatStaleAlert
$events = Get-WinEvent -FilterHashtable @{
LogName = 'Security'
@@ -2138,7 +2254,13 @@ function Start-LoginMonitor {
if ([string]::IsNullOrWhiteSpace($lo.Username)) { continue }
$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 `
-TimeCreated $lo.TimeCreated -IisClientIps $iisIps
+15 -4
View File
@@ -35,7 +35,8 @@ PowerShell-набор для мониторинга входов в Windows с
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 не активен.
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) Ручной запуск
@@ -89,9 +90,9 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-logi
## 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, без учёта регистра) | Поле события |
| --- | --- |
+20 -14
View File
@@ -2,23 +2,29 @@
# 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:user
# workstation:IVANOV
# workstation:IVANOV (не для 4740)
# ip:111.222.333.444
# Можно вставить «как в Telegram» (берётся значение после первого «:»):
# 👤 Пользователь: user
# 🖥️ Рабочая станция (клиент из события): IVANOV
# 🌐 IP адрес: 111.222.333.444
#
# Строка без префикса:
# IVANOV — совпадение с именем рабочей станции ИЛИ с пользователем (sam) ИЛИ с IP (если строка — IPv4)
# 111.222.333.444только IP (в реальной конфигурации укажите действительный IPv4 клиента)
# domain\user — пользователь целиком
# Строка без префикса типа:
# IVANOV — рабочая станция / пользователь / IP (IPv4)
# domain\user пользователь
# user:domain\user
# workstation:IVANOV
# ip:111.222.333.444
# --- только входы 4624/4625 ---
# user:domain\service_account
# ip:192.168.1.100
# --- только блокировки 4740 ---
# 4740:user:test.user
# 4740:ip:203.0.113.50
# --- все перечисленные события ---
# all:user:domain\noise_account
+1 -1
View File
@@ -1 +1 @@
1.5.2
1.5.3