diff --git a/Login_Monitor.ps1 b/Login_Monitor.ps1
index ab9e5e6..e829bca 100644
--- a/Login_Monitor.ps1
+++ b/Login_Monitor.ps1
@@ -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🚫 Игнорируются: Security 4624/4625 по правилам ignore.lst`r`n"
+ $message += "`r`n🚫 Игнорируются: по правилам 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📢 Каналы уведомлений: $(ConvertTo-TelegramHtml $notifyChain)"
+ $message += "`r`n💓 Heartbeat: файл каждые $HeartbeatInterval с; оповещение, если нет обновления > $($HeartbeatStaleAlertMultiplier)× интервал."
if (Test-RDSDeploymentPresent) {
$message += "`r`n🔐 RDS (хост сессий): обнаружены компоненты 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 = "⚠️ Heartbeat монитора не обновлялся`r`n"
+ $msg += "🖥️ Сервер: $hHost`r`n"
+ $msg += "⏱️ Порог: $hThreshold с ($HeartbeatStaleAlertMultiplier × интервал $HeartbeatInterval с)`r`n"
+ $msg += "📄 Последний heartbeat: $hLast`r`n"
+ $msg += "Проверьте процесс Login_Monitor.ps1 и задачи планировщика RDP-Login-Monitor / Watchdog."
+
+ 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,8 +1525,10 @@ function Test-RdpMonitorIgnoreListMatch {
if (Test-RdpMonitorUsernameMatchesToken -Username $Username -Token $v) { return $true }
continue
}
- if (-not [string]::IsNullOrWhiteSpace($ComputerName) -and $ComputerName -ne '-' -and ($ComputerName -ieq $v)) {
- return $true
+ 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`nIP в логах IIS ActiveSync для этого пользователя не найдены.`r`n"
+ $message += "`r`nIP в IIS ActiveSync не найдены (окно $ExchangeIisLogMinutesBeforeLockout мин до блокировки, 401).`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
diff --git a/README.md b/README.md
index 571ddbc..80a6c59 100644
--- a/README.md
+++ b/README.md
@@ -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, без учёта регистра) | Поле события |
| --- | --- |
diff --git a/ignore.lst.example b/ignore.lst.example
index 5ea7f28..83132d6 100644
--- a/ignore.lst.example
+++ b/ignore.lst.example
@@ -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
diff --git a/version.txt b/version.txt
index 4cda8f1..8af85be 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-1.5.2
+1.5.3