Compare commits

..

28 Commits

Author SHA1 Message Date
PTah 74ebbbe161 Start main task immediately after InstallTasks and include script version in startup notification.
Also bump script/version.txt to 1.3.11 for rollout tracking.

Made-with: Cursor
2026-04-29 16:45:13 +10:00
PTah 5d9176f958 Singleton lock: DeleteOnClose, Ctrl+C propagates, release in finally (v1.3.9)
Made-with: Cursor
2026-04-29 16:23:16 +10:00
PTah eff265ded3 v1.3.5: путь скрипта при Start-Process, диагностика watchdog, DEPLOY.md
Made-with: Cursor
2026-04-29 16:13:14 +10:00
PTah 39f0e6098c Watchdog: schtasks /Run после установки; DEPLOY/README про задачи и UNC; версия 1.3.4
Made-with: Cursor
2026-04-29 16:07:35 +10:00
PTah fcff8311d4 Добавлен доменный деплой (Deploy-LoginMonitor.ps1, version.txt), DEPLOY.md, правки README и комментарий к версии в Login_Monitor.ps1
Made-with: Cursor
2026-04-29 15:14:41 +10:00
PTah 8de023f59e Workstation vs server: ProductType; WKS uses LT10 + RCM 1149; server keeps 2/3/10 (v1.2.0)
Made-with: Cursor
2026-04-29 14:30:28 +10:00
PTah e14e95922a auditpol: detect RU only from /list, not culture; fall back to EN (Hyper-V); v1.1.3
Made-with: Cursor
2026-04-27 10:47:16 +10:00
PTah 3f97da0c2e Fix Uc RU category string: add missing 0x0445 in vyhod (auditpol 0x57); v1.1.2
Made-with: Cursor
2026-04-27 10:29:10 +10:00
PTah d0b02042a4 Revert "Use ASCII-only source for Telegram and logs; remove emoji (v1.2.0)"
This reverts commit b16ebb028b.
2026-04-27 10:25:55 +10:00
PTah b16ebb028b Use ASCII-only source for Telegram and logs; remove emoji (v1.2.0)
Prevents mojibake/parse errors when the .ps1 is saved with wrong encoding after download. RU auditpol subcategory names remain via Uc(); Telegram messages and Write-Log are English.

Made-with: Cursor
2026-04-27 10:24:09 +10:00
PTah 66437bf381 Harden auditpol block: build RU category strings via Uc, English log lines, bump to 1.1.1
Avoids mojibake/parse errors when the script is downloaded with broken UTF-8; keep Windows RU subcategory names as Unicode code points.

Made-with: Cursor
2026-04-27 10:19:09 +10:00
PTah b9027009b8 Add script version to startup log and admin error
Made-with: Cursor
2026-04-27 10:07:23 +10:00
PTah 0d52083a58 Fix scheduling checks and harden Telegram message formatting.
Correct daily rotation/report trigger logic to run after the configured slot once per period, avoid duplicate stop notifications on fatal errors, and HTML-escape dynamic Telegram fields to prevent markup breakage.

Made-with: Cursor
2026-04-27 10:01:42 +10:00
PTah ab0504c3f9 Add Install-ScheduledTasks.ps1 for Task Scheduler registration; document in README
Made-with: Cursor
2026-04-13 11:29:21 +10:00
PTah d8ab28098e README: remove internal PMG/LDAP filter details from public docs
Made-with: Cursor
2026-04-13 11:23:44 +10:00
PTah 4386a58942 Fix daily report: force array for unique users (PS 5.1 Count on scalar)
Made-with: Cursor
2026-04-13 11:12:44 +10:00
PTah d951f54874 Telegram Security events: show log server vs workstation (4624/4625)
Made-with: Cursor
2026-04-13 10:59:02 +10:00
PTah 4bef73001f Clarify startup Telegram: Gateway targets internal PCs; exclude Gateway-only from RDS host line
Made-with: Cursor
2026-04-13 10:53:10 +10:00
PTah 4f6385d483 Telegram startup: mention RDS and RD Gateway when applicable
Made-with: Cursor
2026-04-13 10:49:30 +10:00
PTah 0ce6b8abde Use UTF-8 BOM for logs; document FAR encoding
Made-with: Cursor
2026-04-13 10:33:11 +10:00
Луценко Андрей Анатольевич 6072d70e59 Document recent monitoring and auditpol improvements 2026-04-11 22:16:13 +10:00
Луценко Андрей Анатольевич 7b9d7dcd40 Improve auditpol parsing for RU logon/logoff category output 2026-04-11 22:14:31 +10:00
Луценко Андрей Анатольевич b39d9c0deb Fix auditpol localization (RU logon subcategory) and reduce noise 2026-04-11 21:57:16 +10:00
Луценко Андрей Анатольевич 0e5196e769 Fix auditpol for RU subcategory and safer stderr handling 2026-04-11 21:48:49 +10:00
Луценко Андрей Анатольевич 578230f915 Update scripts 2026-04-11 21:23:34 +10:00
Луценко Андрей Анатольевич a337e72a4d Add editor/gitattributes defaults for PowerShell encoding 2026-04-11 21:23:27 +10:00
PapaTramp 50a5f87603 Update README.md 2026-04-09 16:42:00 +10:00
Луценко Андрей Анатольевич 2ae691f83c Add login monitor, watchdog and Task Scheduler docs 2026-04-09 16:36:27 +10:00
9 changed files with 109 additions and 759 deletions
+12 -90
View File
@@ -34,13 +34,6 @@
Лог установки: **`C:\ProgramData\RDP-login-monitor\Logs\deploy.log`**. Лог установки: **`C:\ProgramData\RDP-login-monitor\Logs\deploy.log`**.
## Опционально на клиенте: `ignore.lst`
На каждом компьютере/сервере можно положить файл **`C:\ProgramData\RDP-login-monitor\ignore.lst`** (в том же каталоге, что и **`Login_Monitor.ps1`**). По строкам этого файла монитор **не отправляет в Telegram** отдельные события **Security `4624`/`4625`** (например, шум от известной рабочей станции, тестового пользователя или фиксированного IP).
- **`Deploy-LoginMonitor.ps1` с шары `ignore.lst` не доставляет** — при необходимости создавайте файл локально или копируйте своим способом.
- Примеры синтаксиса — в репозитории в файле **`ignore.lst.example`**.
## Задачи планировщика после `-InstallTasks` ## Задачи планировщика после `-InstallTasks`
Параметр **`Login_Monitor.ps1 -InstallTasks`** создаёт две задачи: Параметр **`Login_Monitor.ps1 -InstallTasks`** создаёт две задачи:
@@ -54,7 +47,7 @@ Watchdog регистрируется через **`schtasks.exe /Create /SC MIN
Перед созданием выполняется **`schtasks /Delete … /F`** (если задачи ещё не было, сообщение об ошибке подавляется — это нормально). Перед созданием выполняется **`schtasks /Delete … /F`** (если задачи ещё не было, сообщение об ошибке подавляется — это нормально).
Сразу после регистрации задач вызывается **`schtasks /Run`** и для **основной задачи**, и для **watchdog**: первый запуск не ждёт перезагрузку и ближайшее 5‑минутное окно. Сразу после успешного **`/Create`** вызывается **`schtasks /Run`** для watchdog: первый прогон не ждёт следующего 5‑минутного окна расписания.
Если watchdog срабатывает, а основной монитор «не поднимается», смотрите **`Logs\watchdog.log`** (сообщение о PID и предупреждение, если процесс сразу завершился) и **конец `Logs\login_monitor.log`** — частая причина (исправлено в **1.3.5+**): пустой **`$PSCommandPath`** у процесса, запущенного через **`Start-Process`**; скрипт теперь подставляет путь через **`$PSScriptRoot`**. Если watchdog срабатывает, а основной монитор «не поднимается», смотрите **`Logs\watchdog.log`** (сообщение о PID и предупреждение, если процесс сразу завершился) и **конец `Logs\login_monitor.log`** — частая причина (исправлено в **1.3.5+**): пустой **`$PSCommandPath`** у процесса, запущенного через **`Start-Process`**; скрипт теперь подставляет путь через **`$PSScriptRoot`**.
@@ -88,96 +81,25 @@ schtasks /Query /TN "RDP-Login-Monitor-Watchdog"
Итого: **обновляться «по сети» обязан именно `version.txt`**; строка **`$ScriptVersion`** нужна для прозрачности в мониторинге и может совпадать с шарой или отставать по «маркетингу», если вы сознательно крутите только patch в **`version.txt`**. Итого: **обновляться «по сети» обязан именно `version.txt`**; строка **`$ScriptVersion`** нужна для прозрачности в мониторинге и может совпадать с шарой или отставать по «маркетингу», если вы сознательно крутите только patch в **`version.txt`**.
## Групповая политика (GPO): проверенная схема ## Групповая политика (GPO)
Ниже шаги, которые были проверены на практике для запуска деплоя при старте компьютера. 1. Положите три файла на шару (см. выше).
2. **Конфигурация компьютера****Политики****Windows****Сценарии (запуск/завершение)****Автозагрузка**.
3. Добавьте сценарий PowerShell с **путём к установщику на шаре**, например:
1. Положите три файла на шару (см. выше), например: `\\contoso.local\NETLOGON\RDP-login-monitor\Deploy-LoginMonitor.ps1`
`\\B26\NETLOGON\RDP-login-monitor\` Параметры можно оставить пустыми, если рядом на шаре лежат **`Login_Monitor.ps1`** и **`version.txt`**.
2. Создайте/настройте GPO и привяжите её к OU с **компьютерами** (не пользователями). Скрипт выполняется от **SYSTEM** при старте ОС — этого достаточно для записи в **`ProgramData`** и регистрации задач.
3. В GPO откройте: Альтернатива: команда
**Конфигурация компьютера****Политики****Конфигурация Windows****Сценарии (запуск/завершение)****Автозагрузка**.
4. На вкладке **«Сценарии PowerShell»** добавьте скрипт: ```text
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "\\...\Deploy-LoginMonitor.ps1"
`\\B26\NETLOGON\RDP-login-monitor\Deploy-LoginMonitor.ps1`
Параметры можно оставить пустыми, если рядом на шаре лежат `Login_Monitor.ps1` и `version.txt`.
5. Security Filtering:
- можно убрать `Authenticated Users`,
- добавить группу компьютеров (например `B26\RDP-Login`),
- убедиться, что у неё есть права **Read** + **Apply group policy**.
6. Проверьте, что в группе действительно состоит объект компьютера, например `FVG-PC$`.
7. Проверьте доступ на шару/NTFS для контекста компьютера (обычно через `Domain Computers` или целевую группу), потому что startup-скрипт выполняется от имени **SYSTEM**.
### Важные нюансы
- Кнопка **«Показать файлы»** в свойствах Startup показывает папку GPO в `SYSVOL` (`...\Policies\{GUID}\Machine\Scripts\Startup`).
- Если вы указали в настройке сценария **UNC путь в NETLOGON**, ваш `.ps1` в этой папке `SYSVOL` не появится — это нормально.
- После изменения membership компьютера в security-группе часто требуется **перезагрузка** (не только `gpupdate /force`), чтобы обновился токен компьютера.
### Как запускать без ожидания перезагрузки
- Startup-сценарий GPO штатно отрабатывает при загрузке ОС, но сам `Deploy-LoginMonitor.ps1` после `-InstallTasks` теперь инициирует немедленный `schtasks /Run`:
- `RDP-Login-Monitor`
- `RDP-Login-Monitor-Watchdog`
- Поэтому после фактического запуска deploy монитор и watchdog стартуют сразу.
### Серверы без перезагрузок: периодический Deploy
Если сервер перезагружается редко, одного Startup-сценария недостаточно для своевременного обновления. В этом случае добавьте отдельную задачу, которая периодически запускает `Deploy-LoginMonitor.ps1` с шары.
Если серверов немного, можно обойтись без отдельной задачи: при выпуске новой версии выполнять `Deploy-LoginMonitor.ps1` вручную на нужных серверах.
Готовый скрипт в репозитории:
- `Install-DeployScheduledTask.ps1`
Этот вариант удобен для большого парка серверов, когда обновления нужно подтягивать автоматически и без ручного обхода.
Пример (каждые 60 минут и сразу запустить):
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\Install-DeployScheduledTask.ps1" `
-TaskName "RDP-Login-Monitor-Deploy" `
-DeployScriptPath "\\B26\NETLOGON\RDP-login-monitor\Deploy-LoginMonitor.ps1" `
-RepeatMinutes 60 `
-RunNow
``` ```
Проверка: в сценарии **cmd** / отдельном **bat**.
```powershell
schtasks /Query /TN "RDP-Login-Monitor-Deploy" /V /FO LIST
```
Важно: эта задача отвечает только за доставку обновлений (через `version.txt`). За «живость» процесса по-прежнему отвечает `RDP-Login-Monitor-Watchdog`.
Пример ручного запуска (для небольшого количества серверов):
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "\\B26\NETLOGON\RDP-login-monitor\Deploy-LoginMonitor.ps1"
```
### Диагностика GPO/Startup
- События применения политики и startup-сценариев:
- `Event Viewer -> Applications and Services Logs -> Microsoft -> Windows -> GroupPolicy -> Operational`
- Логи на клиенте:
- `C:\ProgramData\RDP-login-monitor\Logs\deploy.log`
- `C:\ProgramData\RDP-login-monitor\Logs\login_monitor.log`
- `C:\ProgramData\RDP-login-monitor\Logs\watchdog.log`
- Быстрая ручная проверка deploy:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "\\B26\NETLOGON\RDP-login-monitor\Deploy-LoginMonitor.ps1"
```
## Ручная проверка ## Ручная проверка
+1 -42
View File
@@ -33,22 +33,9 @@ param(
Set-StrictMode -Version Latest Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
function Test-DeployRunningElevated {
try {
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
# LocalSystem (GPO startup / задачи SYSTEM): не всегда даёт true на BuiltInRole::Administrator.
if ($null -ne $id.User -and $id.User.Value -eq 'S-1-5-18') { return $true }
$p = New-Object Security.Principal.WindowsPrincipal($id)
return $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
} catch {
return $false
}
}
$InstallRoot = [System.IO.Path]::GetFullPath("$env:ProgramData\RDP-login-monitor") $InstallRoot = [System.IO.Path]::GetFullPath("$env:ProgramData\RDP-login-monitor")
$LocalScript = Join-Path $InstallRoot "Login_Monitor.ps1" $LocalScript = Join-Path $InstallRoot "Login_Monitor.ps1"
$VersionStampPath = Join-Path $InstallRoot "deployed_version.txt" $VersionStampPath = Join-Path $InstallRoot "deployed_version.txt"
$DeployUpdateMarkerPath = Join-Path $InstallRoot "deploy_last_update.txt"
$DeployLogPath = Join-Path $InstallRoot "Logs\deploy.log" $DeployLogPath = Join-Path $InstallRoot "Logs\deploy.log"
$ScriptName = "Login_Monitor.ps1" $ScriptName = "Login_Monitor.ps1"
$VersionFileName = "version.txt" $VersionFileName = "version.txt"
@@ -218,11 +205,6 @@ try {
exit 0 exit 0
} }
if (-not (Test-DeployRunningElevated)) {
Write-DeployLog "ОШИБКА: запустите Deploy из повышенной консоли PowerShell («Запуск от имени администратора»). Без этого дочерний Login_Monitor.ps1 -InstallTasks завершится с кодом 1 (нет прав на регистрацию задач)."
exit 0
}
Stop-RdpLoginMonitorMainProcesses Stop-RdpLoginMonitorMainProcesses
Copy-Item -LiteralPath $sourceScript -Destination $LocalScript -Force Copy-Item -LiteralPath $sourceScript -Destination $LocalScript -Force
@@ -231,23 +213,9 @@ try {
$installArgs = @( $installArgs = @(
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $LocalScript, '-InstallTasks' '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $LocalScript, '-InstallTasks'
) )
$instOut = Join-Path $InstallRoot "Logs\deploy_installtasks_stdout.log" $p = Start-Process -FilePath $PsExe -ArgumentList $installArgs -Wait -PassThru -WindowStyle Hidden
$instErr = Join-Path $InstallRoot "Logs\deploy_installtasks_stderr.log"
$p = Start-Process -FilePath $PsExe -ArgumentList $installArgs -Wait -PassThru -WindowStyle Hidden `
-RedirectStandardOutput $instOut -RedirectStandardError $instErr
if ($p.ExitCode -ne 0) { if ($p.ExitCode -ne 0) {
Write-DeployLog "Предупреждение: InstallTasks завершился с кодом $($p.ExitCode)." Write-DeployLog "Предупреждение: InstallTasks завершился с кодом $($p.ExitCode)."
foreach ($pair in @(@($instOut, 'stdout'), @($instErr, 'stderr'))) {
$lp = $pair[0]
$lbl = $pair[1]
if (Test-Path -LiteralPath $lp) {
$tail = Get-Content -LiteralPath $lp -Tail 40 -ErrorAction SilentlyContinue
if ($tail) {
Write-DeployLog "InstallTasks $lbl (хвост): $($tail -join ' | ')"
}
}
}
Write-DeployLog "Подсказка: полный вывод в Logs\deploy_installtasks_*.log; также см. login_monitor.log за это время."
} else { } else {
Write-DeployLog "InstallTasks выполнен (код 0)." Write-DeployLog "InstallTasks выполнен (код 0)."
} }
@@ -255,15 +223,6 @@ try {
[System.IO.File]::WriteAllText($VersionStampPath, "$shareVerRaw`r`n", $Utf8Bom) [System.IO.File]::WriteAllText($VersionStampPath, "$shareVerRaw`r`n", $Utf8Bom)
Write-DeployLog "Записана метка версии: $VersionStampPath" Write-DeployLog "Записана метка версии: $VersionStampPath"
$updStamp = Get-Date -Format "dd.MM.yyyy HH:mm:ss"
$updMarker = @(
"Version=$shareVerRaw"
"UpdatedAt=$updStamp"
"PendingStartupNotice=1"
) -join "`r`n"
[System.IO.File]::WriteAllText($DeployUpdateMarkerPath, "$updMarker`r`n", $Utf8Bom)
Write-DeployLog "Записана метка обновления: $DeployUpdateMarkerPath (Version=$shareVerRaw; UpdatedAt=$updStamp)."
if (-not $SkipStartMonitorAfterUpdate) { if (-not $SkipStartMonitorAfterUpdate) {
Start-Process -FilePath $PsExe -ArgumentList @( Start-Process -FilePath $PsExe -ArgumentList @(
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $LocalScript '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $LocalScript
-45
View File
@@ -1,45 +0,0 @@
#Requires -RunAsAdministrator
[CmdletBinding()]
param(
[string]$TaskName = "RDP-Login-Monitor-Deploy",
[string]$DeployScriptPath = "\\B26\NETLOGON\RDP-login-monitor\Deploy-LoginMonitor.ps1",
[ValidateRange(5, 1440)][int]$RepeatMinutes = 60,
[switch]$RunNow
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
if ([string]::IsNullOrWhiteSpace($DeployScriptPath)) {
throw "DeployScriptPath не задан."
}
$psExe = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
if (-not (Test-Path -LiteralPath $psExe)) {
throw "Не найден powershell.exe по пути: $psExe"
}
$taskCmd = "`"$psExe`" -NoProfile -ExecutionPolicy Bypass -File `"$DeployScriptPath`""
$schtasksExe = Join-Path $env:SystemRoot "System32\schtasks.exe"
Write-Host "Создаю/обновляю задачу: $TaskName" -ForegroundColor Cyan
Write-Host "Команда: $taskCmd" -ForegroundColor DarkGray
Write-Host "Периодичность: каждые $RepeatMinutes мин." -ForegroundColor DarkGray
& $schtasksExe /Create /F /TN $TaskName /RU SYSTEM /RL HIGHEST /SC MINUTE /MO $RepeatMinutes /TR $taskCmd | Out-Host
if ($LASTEXITCODE -ne 0) {
throw "schtasks /Create вернул код $LASTEXITCODE"
}
Write-Host "Задача создана/обновлена: $TaskName" -ForegroundColor Green
if ($RunNow) {
Write-Host "Запускаю задачу немедленно..." -ForegroundColor Cyan
& $schtasksExe /Run /TN $TaskName | Out-Host
if ($LASTEXITCODE -ne 0) {
throw "schtasks /Run вернул код $LASTEXITCODE"
}
}
Write-Host "Готово." -ForegroundColor Green
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 Andrey Lutsenko (PapaTramp)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+34 -321
View File
@@ -69,7 +69,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.4.3" $ScriptVersion = "1.3.11"
# Логи (все под InstallRoot) # Логи (все под InstallRoot)
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log" $LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
@@ -83,11 +83,6 @@ $LogRotationMinute = 0
# Heartbeat (только файл) # Heartbeat (только файл)
$HeartbeatInterval = 3600 $HeartbeatInterval = 3600
$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"
# Построчные правила подавления уведомлений Security 4624/4625 (см. ignore.lst.example в репозитории).
$script:IgnoreListPath = Join-Path $script:InstallRoot "ignore.lst"
$script:IgnoreListCache = $null
$script:IgnoreListCacheStampUtc = $null
# Ежедневный отчет # Ежедневный отчет
$DailyReportHour = 9 $DailyReportHour = 9
@@ -491,24 +486,6 @@ function Start-RdpMonitorWatchdogMain {
exit 0 exit 0
} }
# Watchdog и InstallTasks — до DPAPI/Telegram: Deploy вызывает -InstallTasks без зависимости от секретов на машине.
if ($Watchdog) {
Start-RdpMonitorWatchdogMain
exit 0
}
if ($InstallTasks) {
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Log "InstallTasks: нужны права администратора."
exit 1
}
Register-RdpMonitorScheduledTasksCore
Write-Log "InstallTasks: задачи планировщика обновлены."
exit 0
}
# --- Учётные данные Telegram (открытый текст или DPAPI Base64) --- # --- Учётные данные Telegram (открытый текст или DPAPI Base64) ---
if (-not [string]::IsNullOrWhiteSpace($TelegramBotTokenProtectedB64)) { if (-not [string]::IsNullOrWhiteSpace($TelegramBotTokenProtectedB64)) {
try { try {
@@ -529,6 +506,23 @@ if (-not [string]::IsNullOrWhiteSpace($TelegramChatIDProtectedB64)) {
if ($TelegramBotToken -eq '<TELEGRAM_BOT_TOKEN>') { $TelegramBotToken = "" } if ($TelegramBotToken -eq '<TELEGRAM_BOT_TOKEN>') { $TelegramBotToken = "" }
if ($TelegramChatID -eq '<TELEGRAM_CHAT_ID>') { $TelegramChatID = "" } if ($TelegramChatID -eq '<TELEGRAM_CHAT_ID>') { $TelegramChatID = "" }
if ($Watchdog) {
Start-RdpMonitorWatchdogMain
exit 0
}
if ($InstallTasks) {
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Log "InstallTasks: нужны права администратора."
exit 1
}
Register-RdpMonitorScheduledTasksCore
Write-Log "InstallTasks: задачи планировщика обновлены."
exit 0
}
function ConvertTo-TelegramHtml { function ConvertTo-TelegramHtml {
param([string]$Text) param([string]$Text)
if ($null -eq $Text) { return '' } if ($null -eq $Text) { return '' }
@@ -889,53 +883,9 @@ function Send-Heartbeat {
$timestamp = Get-Date -Format "dd.MM.yyyy HH:mm:ss" $timestamp = Get-Date -Format "dd.MM.yyyy HH:mm:ss"
$hHost = (ConvertTo-TelegramHtml $env:COMPUTERNAME) $hHost = (ConvertTo-TelegramHtml $env:COMPUTERNAME)
function Get-DeployUpdateMarker {
$info = [pscustomobject]@{
Version = $null
UpdatedAt = $null
PendingStartupNotice = $false
}
if (-not (Test-Path -LiteralPath $DeployUpdateMarkerFile)) { return $info }
try {
$lines = @((Get-Content -LiteralPath $DeployUpdateMarkerFile -ErrorAction Stop) | Where-Object { $_ -match '\S' })
foreach ($ln in $lines) {
$parts = $ln -split '=', 2
if ($parts.Count -ne 2) { continue }
$k = ($parts[0]).Trim()
$v = ($parts[1]).Trim()
switch ($k) {
'Version' { $info.Version = $v }
'UpdatedAt' { $info.UpdatedAt = $v }
'PendingStartupNotice' { $info.PendingStartupNotice = ($v -eq '1' -or $v -ieq 'true') }
}
}
} catch { }
return $info
}
function Set-DeployUpdateMarkerPendingOff {
param([pscustomobject]$Marker)
try {
$ver = if ([string]::IsNullOrWhiteSpace($Marker.Version)) { '' } else { $Marker.Version }
$upd = if ([string]::IsNullOrWhiteSpace($Marker.UpdatedAt)) { '' } else { $Marker.UpdatedAt }
$content = @(
"Version=$ver"
"UpdatedAt=$upd"
"PendingStartupNotice=0"
) -join "`r`n"
Write-TextFileUtf8Bom -Path $DeployUpdateMarkerFile -Text $content
} catch { }
}
if ($IsStartup) { if ($IsStartup) {
$message = "<b>✅ Мониторинг логинов ЗАПУЩЕН</b>`r`n" $message = "<b>✅ Мониторинг логинов ЗАПУЩЕН</b>`r`n"
$message += "🏷️ Версия скрипта: $(ConvertTo-TelegramHtml $ScriptVersion)" $message += "🏷️ Версия скрипта: $(ConvertTo-TelegramHtml $ScriptVersion)`r`n"
$upd = Get-DeployUpdateMarker
if ($upd.PendingStartupNotice -and $upd.Version -eq $ScriptVersion -and -not [string]::IsNullOrWhiteSpace($upd.UpdatedAt)) {
$message += " (обновлён $(ConvertTo-TelegramHtml $upd.UpdatedAt))"
Set-DeployUpdateMarkerPendingOff -Marker $upd
}
$message += "`r`n"
$message += "🖥️ Сервер: $hHost`r`n" $message += "🖥️ Сервер: $hHost`r`n"
$message += "🕐 Время запуска: $timestamp" $message += "🕐 Время запуска: $timestamp"
if ($script:OsInstallKindLabel) { if ($script:OsInstallKindLabel) {
@@ -946,23 +896,6 @@ function Send-Heartbeat {
} else { } else {
$message += "`r`n📌 <b>Режим:</b> сервер — Security 4624/4625, типы входа 2, 3, 10." $message += "`r`n📌 <b>Режим:</b> сервер — Security 4624/4625, типы входа 2, 3, 10."
} }
$ignoreEntries = @(Get-RdpMonitorIgnoreListEntries)
if ($ignoreEntries.Count -gt 0) {
$message += "`r`n🚫 <b>Игнорируются:</b> Security 4624/4625 по правилам ignore.lst`r`n"
foreach ($e in $ignoreEntries) {
$v = ConvertTo-TelegramHtml ([string]$e.Value)
$kindLabel = switch ($e.Kind) {
'User' { 'Пользователь' }
'Workstation' { 'Рабочая станция' }
'Ip' { 'IP' }
'Any' { 'Универсальное правило' }
default { 'Правило' }
}
$message += ('• {0}: {1}' -f $kindLabel, $v) + "`r`n"
}
} else {
$message += "`r`n🚫 <b>Игнорируются:</b> не задано (ignore.lst отсутствует или пуст)."
}
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, типы входа по настройке скрипта)."
} }
@@ -1127,137 +1060,6 @@ function Get-LogonTypeName {
} }
} }
function Test-RdpMonitorUsernameMatchesToken {
param([string]$Username, [string]$Token)
if ([string]::IsNullOrWhiteSpace($Username) -or [string]::IsNullOrWhiteSpace($Token)) { return $false }
if ($Username -ieq $Token) { return $true }
if ($Token.Contains('\')) {
return ($Username -ieq $Token)
}
$i = $Username.LastIndexOf('\')
if ($i -ge 0 -and $i -lt ($Username.Length - 1)) {
$sam = $Username.Substring($i + 1)
if ($sam -ieq $Token) { return $true }
}
return $false
}
function Parse-RdpMonitorIgnoreListLine {
param([string]$RawLine)
$line = ([string]$RawLine).Trim()
if ($line.Length -eq 0) { return $null }
if ($line[0] -eq '#' -or $line[0] -eq ';') { return $null }
if ($line.StartsWith([char]0xFEFF)) { $line = $line.TrimStart([char]0xFEFF) }
if ($line -notmatch ':') {
return [pscustomobject]@{ Kind = 'Any'; Value = $line }
}
$idx = $line.IndexOf(':')
$left = $line.Substring(0, $idx).Trim()
$right = $line.Substring($idx + 1).Trim()
if ([string]::IsNullOrWhiteSpace($right)) { return $null }
if ($left -match '(?i)(рабоч|workstation|wks)') {
return [pscustomobject]@{ Kind = 'Workstation'; Value = $right }
}
if ($left -match '(?i)(польз|username|subject|account|target\s*user|\buser\b)') {
return [pscustomobject]@{ Kind = 'User'; Value = $right }
}
if ($left -match '(?i)(\bip\b|ip\s*адрес|ipaddress|адрес\s*ip)') {
return [pscustomobject]@{ Kind = 'Ip'; Value = $right }
}
return [pscustomobject]@{ Kind = 'Any'; Value = $right }
}
function Get-RdpMonitorIgnoreListEntries {
if (-not (Test-Path -LiteralPath $script:IgnoreListPath)) {
$script:IgnoreListCache = @()
$script:IgnoreListCacheStampUtc = $null
return $script:IgnoreListCache
}
try {
$fi = Get-Item -LiteralPath $script:IgnoreListPath -ErrorAction Stop
$stamp = $fi.LastWriteTimeUtc
if ($null -ne $script:IgnoreListCache -and $script:IgnoreListCacheStampUtc -eq $stamp) {
return $script:IgnoreListCache
}
$entries = [System.Collections.Generic.List[object]]::new()
foreach ($ln in (Get-Content -LiteralPath $script:IgnoreListPath -Encoding UTF8 -ErrorAction Stop)) {
$e = Parse-RdpMonitorIgnoreListLine -RawLine $ln
if ($null -ne $e) { $entries.Add($e) | Out-Null }
}
$script:IgnoreListCache = @($entries)
$script:IgnoreListCacheStampUtc = $stamp
$arr = $script:IgnoreListCache
$nIp = @($arr | Where-Object { $_.Kind -eq 'Ip' }).Count
$nUser = @($arr | Where-Object { $_.Kind -eq 'User' }).Count
$nWks = @($arr | Where-Object { $_.Kind -eq 'Workstation' }).Count
$nAny = @($arr | Where-Object { $_.Kind -eq 'Any' }).Count
$nTotal = $arr.Count
if ($nTotal -eq 0) {
Write-Log "ignore.lst обновлён: список правил пуст, игнорирование по файлу для Security 4624/4625 не задаётся."
} else {
Write-Log ("ignore.lst обновлён: добавлено игнорирование событий 4624/4625 по IP ({0}), пользователю ({1}), рабочей станции ({2}); универсальных правил ({3}). Всего записей: {4}." -f $nIp, $nUser, $nWks, $nAny, $nTotal)
}
return $script:IgnoreListCache
} catch {
Write-Log "Предупреждение: не удалось прочитать ignore.lst: $($_.Exception.Message)"
$script:IgnoreListCache = @()
$script:IgnoreListCacheStampUtc = $null
return $script:IgnoreListCache
}
}
function Test-RdpMonitorIgnoreListMatch {
param(
[string]$Username,
[string]$ComputerName,
[string]$SourceIP
)
$entries = @(Get-RdpMonitorIgnoreListEntries)
if ($entries.Count -eq 0) { return $false }
foreach ($e in $entries) {
$v = [string]$e.Value
if ([string]::IsNullOrWhiteSpace($v)) { continue }
switch ($e.Kind) {
'User' {
if (Test-RdpMonitorUsernameMatchesToken -Username $Username -Token $v) { return $true }
}
'Workstation' {
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
}
}
'Any' {
if (Test-RdpMonitorStringLooksLikeIPv4 $v) {
if (-not [string]::IsNullOrWhiteSpace($SourceIP) -and $SourceIP -ne '-' -and ($SourceIP -ieq $v)) {
return $true
}
continue
}
if ($v.Contains('\')) {
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 (Test-RdpMonitorUsernameMatchesToken -Username $Username -Token $v) { return $true }
}
}
}
return $false
}
function Should-IgnoreEvent { function Should-IgnoreEvent {
param( param(
[string]$Username, [string]$Username,
@@ -1315,12 +1117,6 @@ function Should-IgnoreEvent {
if ($ComputerName -like $pattern) { return $true } if ($ComputerName -like $pattern) { return $true }
} }
if ($EventID -in 4624, 4625) {
if (Test-RdpMonitorIgnoreListMatch -Username $Username -ComputerName $ComputerName -SourceIP $SourceIP) {
return $true
}
}
return $false return $false
} }
@@ -1418,13 +1214,6 @@ function Test-RDGatewayLog {
return $false return $false
} }
function Test-RdpMonitorStringLooksLikeIPv4 {
param([string]$Value)
if ([string]::IsNullOrWhiteSpace($Value)) { return $false }
$t = $Value.Trim()
return [bool]($t -match '^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$')
}
function Get-RDGatewayEventInfo { function Get-RDGatewayEventInfo {
param($Event) param($Event)
$eventData = @{ $eventData = @{
@@ -1436,91 +1225,21 @@ function Get-RDGatewayEventInfo {
ErrorCode = "N/A" ErrorCode = "N/A"
} }
try { try {
$map = Get-EventDataMap -Event $Event switch ($Event.Id) {
$lc = @{} 302 {
foreach ($k in $map.Keys) { if ($Event.Properties.Count -gt 0) { $eventData.Username = $Event.Properties[0].Value }
try { $lc[$k.ToLowerInvariant()] = $map[$k] } catch { } if ($Event.Properties.Count -gt 1) { $eventData.ExternalIP = $Event.Properties[1].Value }
} if ($Event.Properties.Count -gt 2) { $eventData.InternalIP = $Event.Properties[2].Value }
function Get-RdpGwMapVal([string[]]$Keys) { if ($Event.Properties.Count -gt 3) { $eventData.Protocol = $Event.Properties[3].Value }
foreach ($key in $Keys) { $eventData.ErrorCode = "0"
$lk = $key.ToLowerInvariant()
if (-not $lc.ContainsKey($lk)) { continue }
$v = $lc[$lk]
if (-not [string]::IsNullOrWhiteSpace($v)) { return [string]$v }
} }
return $null 303 {
} if ($Event.Properties.Count -gt 0) { $eventData.Username = $Event.Properties[0].Value }
if ($Event.Properties.Count -gt 1) { $eventData.ExternalIP = $Event.Properties[1].Value }
$u = Get-RdpGwMapVal @('username','user','authusername') if ($Event.Properties.Count -gt 2) { $eventData.InternalIP = $Event.Properties[2].Value }
$ext = Get-RdpGwMapVal @('clientaddress','clientip','ipaddress','address','remoteaddress','sourceaddress') if ($Event.Properties.Count -gt 3) { $eventData.Protocol = $Event.Properties[3].Value }
$int = Get-RdpGwMapVal @('targetserver','targetname','resource','server','internaladdress','destinationaddress','targetaddress','devicename','computername') if ($Event.Properties.Count -gt 4) { $eventData.ErrorCode = $Event.Properties[4].Value }
$proto = Get-RdpGwMapVal @('protocol','transport','connectionprotocol','tunneltype')
$err = Get-RdpGwMapVal @('errorcode','statuscode','error','resultcode','status')
if ($u) { $eventData.Username = $u }
if ($ext) { $eventData.ExternalIP = $ext }
if ($int) { $eventData.InternalIP = $int }
if ($proto) { $eventData.Protocol = $proto }
if ($err) { $eventData.ErrorCode = $err }
$fillFromProps = ($lc.Count -eq 0) -or (
(($eventData.InternalIP -eq 'N/A' -or [string]::IsNullOrWhiteSpace($eventData.InternalIP)) -and (
($eventData.Protocol -eq 'N/A' -or [string]::IsNullOrWhiteSpace($eventData.Protocol)) -or
(Test-RdpMonitorStringLooksLikeIPv4 $eventData.Protocol)
))
)
if ($fillFromProps) {
switch ($Event.Id) {
302 {
if ($Event.Properties.Count -gt 0) { $eventData.Username = [string]$Event.Properties[0].Value }
if ($Event.Properties.Count -gt 1) { $eventData.ExternalIP = [string]$Event.Properties[1].Value }
if ($Event.Properties.Count -gt 2) {
$p2 = [string]$Event.Properties[2].Value
$p3 = if ($Event.Properties.Count -gt 3) { [string]$Event.Properties[3].Value } else { '' }
$p4 = if ($Event.Properties.Count -gt 4) { [string]$Event.Properties[4].Value } else { '' }
if ([string]::IsNullOrWhiteSpace($p2) -and (Test-RdpMonitorStringLooksLikeIPv4 $p3)) {
$eventData.InternalIP = $p3.Trim()
if (-not [string]::IsNullOrWhiteSpace($p4)) {
$eventData.Protocol = $p4.Trim()
}
} else {
$eventData.InternalIP = $p2.Trim()
if (-not [string]::IsNullOrWhiteSpace($p3)) { $eventData.Protocol = $p3.Trim() }
}
}
$eventData.ErrorCode = "0"
}
303 {
if ($Event.Properties.Count -gt 0) { $eventData.Username = [string]$Event.Properties[0].Value }
if ($Event.Properties.Count -gt 1) { $eventData.ExternalIP = [string]$Event.Properties[1].Value }
if ($Event.Properties.Count -gt 2) {
$p2 = [string]$Event.Properties[2].Value
$p3 = if ($Event.Properties.Count -gt 3) { [string]$Event.Properties[3].Value } else { '' }
if ([string]::IsNullOrWhiteSpace($p2) -and (Test-RdpMonitorStringLooksLikeIPv4 $p3)) {
$eventData.InternalIP = $p3.Trim()
if ($Event.Properties.Count -gt 4) { $eventData.ErrorCode = [string]$Event.Properties[4].Value }
if ($Event.Properties.Count -gt 5) { $eventData.Protocol = [string]$Event.Properties[5].Value }
} else {
$eventData.InternalIP = $p2.Trim()
if (-not [string]::IsNullOrWhiteSpace($p3)) { $eventData.Protocol = $p3.Trim() }
if ($Event.Properties.Count -gt 4) { $eventData.ErrorCode = [string]$Event.Properties[4].Value }
}
}
}
} }
} elseif ($Event.Id -eq 302 -and $eventData.ErrorCode -eq 'N/A') {
$eventData.ErrorCode = "0"
}
if ($Event.Id -eq 303 -and ($eventData.ErrorCode -eq 'N/A' -or [string]::IsNullOrWhiteSpace($eventData.ErrorCode)) -and $Event.Properties.Count -gt 4) {
$eventData.ErrorCode = [string]$Event.Properties[4].Value
}
if (($eventData.InternalIP -eq 'N/A' -or [string]::IsNullOrWhiteSpace($eventData.InternalIP)) -and
(Test-RdpMonitorStringLooksLikeIPv4 $eventData.Protocol)) {
$eventData.InternalIP = $eventData.Protocol.Trim()
$eventData.Protocol = 'N/A'
} }
} catch { } catch {
Write-Log "Ошибка при извлечении RD Gateway события: $($_.Exception.Message)" Write-Log "Ошибка при извлечении RD Gateway события: $($_.Exception.Message)"
@@ -1546,14 +1265,8 @@ function Format-RDGatewayEvent {
$hTime = (ConvertTo-TelegramHtml ($TimeCreated.ToString('dd.MM.yyyy HH:mm:ss'))) $hTime = (ConvertTo-TelegramHtml ($TimeCreated.ToString('dd.MM.yyyy HH:mm:ss')))
$message = "<b>" $message = "<b>"
if ($EventID -eq 302) { $message += "🖥️ ПОДКЛЮЧЕНИЕ ЧЕРЕЗ RD GATEWAY" } if ($EventID -eq 302) { $message += "🖥️ УСПЕШНОЕ ПОДКЛЮЧЕНИЕ ЧЕРЕЗ RD GATEWAY" }
elseif ($EventID -eq 303) { elseif ($EventID -eq 303) { $message += "❌ НЕУДАЧНОЕ ПОДКЛЮЧЕНИЕ ЧЕРЕЗ RD GATEWAY" }
if ($ErrorCode -ne "0" -and $ErrorCode -ne "N/A" -and -not [string]::IsNullOrWhiteSpace($ErrorCode)) {
$message += "⚠️ СЕАНС ЧЕРЕЗ RD GATEWAY ЗАВЕРШЁН С ОШИБКОЙ"
} else {
$message += "ℹ️ СЕАНС ЧЕРЕЗ RD GATEWAY ЗАВЕРШЁН"
}
}
else { $message += "⚠️ СОБЫТИЕ RD GATEWAY" } else { $message += "⚠️ СОБЫТИЕ RD GATEWAY" }
$message += "</b>`r`n" $message += "</b>`r`n"
+61 -87
View File
@@ -1,17 +1,14 @@
# RDP Login Monitor # RDP Login Monitor
PowerShell-набор для мониторинга входов в Windows с отправкой уведомлений в Telegram. PowerShell-набор для мониторинга входов в Windows с отправкой уведомлений в Telegram.
## Актуальная схема (рекомендуется) ## Актуальная схема (рекомендуется)
- Базовый путь установки: **`C:\ProgramData\RDP-login-monitor\`**. - **`Login_Monitor.ps1`** — единый монитор: журнал Security (`4624`/`4625`), при необходимости RD Gateway (`302`/`303`), рабочие станции и серверы, ротация логов, heartbeat, отчёты. Устанавливается в **`C:\ProgramData\RDP-login-monitor\`**, задачи **`RDP-Login-Monitor`** и **`RDP-Login-Monitor-Watchdog`** создаются параметром **`-InstallTasks`** (watchdog через **`schtasks`**, см. **DEPLOY.md**).
- Основной скрипт: **`Login_Monitor.ps1`** — журнал Security **`4624`/`4625`** (логика зависит от типа ОС: рабочая станция или сервер/КД), при наличии журнала — **Remote Connection Manager `1149`** (часто актуально для РС с RDP), при роли **RD Gateway****`302`/`303`**, **ежедневный отчёт** в Telegram (активные сессии через `quser`), **heartbeat**, **ротация логов**, уведомления в Telegram. - **`Deploy-LoginMonitor.ps1`** + **`version.txt`** — доставка с файловой шары по версии (домен, GPO «автозагрузка» компьютера). Подробности, структура шары и правила версий: **[DEPLOY.md](DEPLOY.md)**.
- Установка задач: запуск **`Login_Monitor.ps1 -InstallTasks`** создаёт: - **`Encrypt-DpapiForRdpMonitor.ps1`** — опционально, для DPAPI-строк токена/chat id на конкретном ПК.
- `RDP-Login-Monitor` (основной монитор),
- `RDP-Login-Monitor-Watchdog` (контроль процесса каждые 5 минут). Разделы ниже про **`D:\Soft\`**, **`Watchdog_RDP_Monitor.ps1`** и **`Install-ScheduledTasks.ps1`** относятся к **старой схеме** и оставлены для совместимости; новые установки ориентируйте на **`ProgramData`** и **DEPLOY.md**.
- Доменная доставка и обновления: **`Deploy-LoginMonitor.ps1`** + **`version.txt`** с шары `NETLOGON`. После успешного деплоя в приветственном сообщении Telegram может появиться отметка об обновлении (файл **`deploy_last_update.txt`** рядом с логами).
- Для полной инструкции по деплою/GPO используйте **[DEPLOY.md](DEPLOY.md)**.
- **`Encrypt-DpapiForRdpMonitor.ps1`** — опционально для подготовки DPAPI-строк токена/chat id.
## Что изменилось (важное) ## Что изменилось (важное)
@@ -22,109 +19,86 @@ PowerShell-набор для мониторинга входов в Windows с
## 1) Подготовка ## 1) Подготовка
1. Подготовьте папку установки: 1. Скопируйте в `D:\Soft\` как минимум `Login_Monitor.ps1` и `Watchdog_RDP_Monitor.ps1` (для установки из скрипта — ещё `Install-ScheduledTasks.ps1`), либо измените пути в параметрах установщика / в watchdog.
- `C:\ProgramData\RDP-login-monitor\` 2. Откройте `Login_Monitor.ps1` и задайте:
2. Скопируйте в неё как минимум: - `$TelegramBotToken`
- `Login_Monitor.ps1` - `$TelegramChatID`
- (для доменного развёртывания отдельно на шаре) `Deploy-LoginMonitor.ps1` и `version.txt`. 3. Убедитесь, что существует `D:\Soft\Logs\` (скрипт сам создаст при запуске).
3. Откройте `Login_Monitor.ps1` и задайте токен/чат: 4. Запускайте от имени администратора (нужен доступ к журналу `Security`).
- `$TelegramBotToken` или `...ProtectedB64`
- `$TelegramChatID` или `...ProtectedB64`
4. Запускайте с правами администратора (чтение `Security` журнала и регистрация задач).
5. Логи и служебные файлы будут в:
- `C:\ProgramData\RDP-login-monitor\Logs\`
6. (Опционально) Подавление части алертов по списку — см. раздел **«7) ignore.lst»** ниже.
## 2) Ручной запуск ## 2) Ручной запуск
Используйте этот вариант для быстрой проверки старта/логики без установки задач планировщика.
```powershell ```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-login-monitor\Login_Monitor.ps1" powershell.exe -NoProfile -ExecutionPolicy Bypass -File "D:\Soft\Login_Monitor.ps1"
``` ```
Примечание: при ручном запуске монитор работает в текущей сессии до остановки (например, `Ctrl+C`).
## 3) Запуск через Планировщик заданий (Task Scheduler) ## 3) Запуск через Планировщик заданий (Task Scheduler)
Текущая схема: вручную задачи в GUI создавать не нужно. Задания можно завести **вручную в GUI** (ниже) или **одной командой из PowerShell** — см. `Install-ScheduledTasks.ps1` (в начале файла стоит `#Requires -RunAsAdministrator`). Скрипт регистрирует **`RDP Login Monitor`** (старт ОС, запуск `Login_Monitor.ps1`) и **`RDP Login Monitor Watchdog`** (старт ОС + повтор каждые 5 минут, запуск `Watchdog_RDP_Monitor.ps1` из того же каталога). Пути по умолчанию: `D:\Soft\`. Параметры: `-InstallRoot`, `-MainTaskName`, `-WatchdogTaskName`, `-WatchdogRepeatMinutes`, задержки случайного старта и т.д.
Достаточно запустить: Пример после копирования файлов в `D:\Soft\`:
```powershell ```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-login-monitor\Login_Monitor.ps1" -InstallTasks powershell.exe -NoProfile -ExecutionPolicy Bypass -File "D:\Soft\Install-ScheduledTasks.ps1"
``` ```
Скрипт сам зарегистрирует `RDP-Login-Monitor` и `RDP-Login-Monitor-Watchdog`, а также запросит немедленный первый запуск задач. Это не заменяет настройку токена Telegram в `Login_Monitor.ps1`, а только регистрирует задания.
Для доменной установки/обновления с шары вручную ничего в планировщике на клиенте настраивать не требуется: используйте `Deploy-LoginMonitor.ps1` (подробно в `DEPLOY.md`). ### Задание №1: основной монитор (вручную)
Создайте задачу `RDP Login Monitor`:
- **General**
- `Run whether user is logged on or not`
- `Run with highest privileges`
- `Configure for`: ваша версия Windows Server/Windows
- **Triggers**
- `At startup` (при запуске системы)
- **Actions**
- Program/script: `powershell.exe`
- Add arguments:
```text
-NoProfile -ExecutionPolicy Bypass -File "D:\Soft\Login_Monitor.ps1"
```
- **Settings**
- `If the task is already running`: `Do not start a new instance`
### Задание №2: watchdog (вручную)
Создайте задачу `RDP Login Monitor Watchdog`:
- **General**
- `Run whether user is logged on or not`
- `Run with highest privileges`
- **Triggers**
- `At startup`
- Дополнительно: `Repeat task every 5 minutes` (или отдельный триггер по расписанию каждые 5 минут)
- **Actions**
- Program/script: `powershell.exe`
- Add arguments:
```text
-NoProfile -ExecutionPolicy Bypass -File "D:\Soft\Watchdog_RDP_Monitor.ps1"
```
- **Settings**
- `If the task is already running`: `Do not start a new instance`
> Если watchdog лежит не в `D:\Soft\`, поправьте путь в аргументах или параметр `-MainScriptPath`.
## 4) Что проверять после запуска ## 4) Что проверять после запуска
- Логи: - Логи:
- `C:\ProgramData\RDP-login-monitor\Logs\login_monitor.log` - `D:\Soft\Logs\login_monitor.log`
- `C:\ProgramData\RDP-login-monitor\Logs\watchdog.log` - `D:\Soft\Logs\watchdog.log`
- Heartbeat: - Heartbeat:
- `C:\ProgramData\RDP-login-monitor\Logs\last_heartbeat.txt` обновляется по интервалу **`$HeartbeatInterval`** (по умолчанию раз в час). - `D:\Soft\Logs\last_heartbeat.txt` обновляется примерно раз в час (по `$HeartbeatInterval`).
- Ежедневный отчёт: после первого прохождения дневного слота (по умолчанию **09:00**, задаётся **`$DailyReportHour`** / **`$DailyReportMinute`** в `Login_Monitor.ps1`) в Telegram уходит сводка по **`quser`**; метка последнего отчёта — `Logs\last_daily_report.txt`.
- Telegram при старте: при установленном **RD Session Host** (или аналогичных компонентах RDS, не только шлюз) — строка про входы по RDP/RDS на этом сервере; при доступном журнале **RD Gateway** — отдельная строка про подключения к **внутренним целевым ПК** через шлюз (302/303). Узел только с ролью RD Gateway не дублирует формулировку «хост сессий». - Telegram при старте: при установленном **RD Session Host** (или аналогичных компонентах RDS, не только шлюз) — строка про входы по RDP/RDS на этом сервере; при доступном журнале **RD Gateway** — отдельная строка про подключения к **внутренним целевым ПК** через шлюз (302/303). Узел только с ролью RD Gateway не дублирует формулировку «хост сессий».
## 5) Автоматический перезапуск при падении ## 5) Автоматический перезапуск при падении
Режим `-Watchdog` внутри `Login_Monitor.ps1` делает: `Watchdog_RDP_Monitor.ps1` делает:
- проверяет, есть ли процесс `powershell.exe/pwsh.exe` с `Login_Monitor.ps1` в командной строке; - проверяет, есть ли процесс `powershell.exe/pwsh.exe` с `Login_Monitor.ps1` в командной строке;
- если процесса нет — запускает монитор; - если процесса нет — запускает монитор;
- если монитор уже есть — не дублирует экземпляр. - если heartbeat старше `$HeartbeatStaleMinutes` (по умолчанию 90 минут) — перезапускает монитор.
## 6) Дополнительные параметры и прочие файлы
- **`-SkipScheduledTaskMaintenance`**: при обычном запуске монитора не выполнять проверку/пересоздание задач планировщика (если регистрацию задач ведёте только через **`-InstallTasks`** или вручную).
- **`Install-DeployScheduledTask.ps1`** — helper для периодического запуска **`Deploy-LoginMonitor.ps1`** с шары (см. **[DEPLOY.md](DEPLOY.md)**).
- **`Watchdog_RDP_Monitor.ps1`** и **`Install-ScheduledTasks.ps1`** — **альтернативная** схема с отдельным watchdog-файлом и путями по умолчанию **`D:\Soft`**. Для новых установок рекомендуется встроенный режим **`-Watchdog`** в **`Login_Monitor.ps1`** и задачи **`RDP-Login-Monitor`** / **`RDP-Login-Monitor-Watchdog`**.
- **`ignore.lst.example`** в репозитории — образец файла **`ignore.lst`** для подавления отдельных уведомлений Security (см. раздел 7).
## 7) Подавление уведомлений Security: `ignore.lst`
В каталоге установки можно положить файл **`C:\ProgramData\RDP-login-monitor\ignore.lst`** (рядом с **`Login_Monitor.ps1`**). Правила из списка проверяются **только** для Telegram-уведомлений по событиям **`4624`/`4625`** журнала Security (успех/неудача входа). Жёстко заданные в скрипте исключения (`ExcludedUsers`, локальный IP, сервисные учётные записи и т.д.) по-прежнему действуют для всех типов событий; **`ignore.lst`** добавляет к ним **дополнительные** совпадения именно для **4624/4625**.
События **RD Gateway (`302`/`303`)**, **RCM `1149`**, ежедневный отчёт и heartbeat **этим файлом не настраиваются** (для `1149` список не используется, даже если формально вызывается общая функция фильтрации).
### Как читается файл
- Чтение выполняется по мере обработки событий; содержимое **кэшируется в памяти**. Если **`LastWriteTimeUtc`** файла изменился (редактирование и сохранение), список **перечитывается автоматически** — перезапуск монитора не обязателен.
- Кодировка: **UTF-8** (`Get-Content -Encoding UTF8`). Строка может начинаться с BOM — он отбрасывается при разборе.
- Пустые строки пропускаются. Строки, начинающиеся с **`#`** или **`;`**, считаются комментариями.
- Строка с **`:`**: берётся **первая** двоеточие — всё слева (после обрезки пробелов) определяет тип правила, всё справа — значение. Если справа пусто, строка игнорируется.
- Строка **без** **`:`**: целиком трактуется как правило типа «любое совпадение» (см. ниже).
### Типы правил (левая часть до первого `:`)
| Левая часть (фрагменты совпадают как regex, без учёта регистра) | Поле события |
| --- | --- |
| `рабоч`, `workstation`, `wks` | имя рабочей станции (**WorkstationName** и аналоги в XML события) |
| `польз`, `username`, `subject`, `account`, `target user`, целое слово `user` | имя пользователя (**TargetUserName** и др.) |
| `ip`, `ip адрес`, `ipaddress`, `адрес ip` | IP источника (**IpAddress** и др.), только если в событии есть непустой IP |
Если левая часть **не** подошла ни к одному типу, но двоеточие есть — используется режим как в разборе строк Telegram: тип **«любое»**, значение — **только правая часть** (метка слева отбрасывается).
### Совпадение для типа «любое» (строка без `:` или «неизвестная» метка слева от `:`)
Проверка по очереди:
1. Если значение похоже на **IPv4** — сравнивается с IP источника в событии (точное совпадение, без учёта регистра для текста не применимо).
2. Если значение содержит **`\`** — сравнивается с **учётной записью**: полное совпадение с `DOMAIN\user` **или** совпадение с **SAM** после последнего `\` (как `DOMAIN\IVANOV` при правиле `IVANOV`).
3. Иначе сначала полное совпадение с **именем рабочей станции**, затем с **учётной записью** по тем же правилам, что в п.2.
Для явных типов **User** / **Workstation** / **Ip** используется только соответствующее поле (для пользователя — те же правила полного имени и SAM, что в п.2).
### Примеры и поставка
- Расширенные примеры строк — в **`ignore.lst.example`** в корне репозитория (скопируйте на сервер как **`ignore.lst`** и отредактируйте).
- **`Deploy-LoginMonitor.ps1`** этот файл **не копирует**: правила обычно разные на каждой машине; при необходимости создайте `ignore.lst` вручную или через вашу систему конфигурации.
## Ключевые слова (для поиска репозитория)
`rdp`, `rd-gateway`, `rdp-gateway`, `rds`, `remote-desktop`, `windows-security-log`, `eventlog`, `event-id-4624`, `event-id-4625`, `event-id-302`, `event-id-303`, `powershell`, `telegram-bot`, `watchdog`, `gpo`, `netlogon`, `domain-deployment`, `windows-server`, `monitoring`
-128
View File
@@ -1,128 +0,0 @@
# RDP Login Monitor
PowerShell toolkit for monitoring Windows logons with Telegram notifications.
## Recommended layout
- 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, **daily report** to Telegram (active sessions via `quser`), **heartbeat**, **log rotation**, Telegram alerts.
- Scheduled tasks: run **`Login_Monitor.ps1 -InstallTasks`** to register:
- `RDP-Login-Monitor` (main monitor),
- `RDP-Login-Monitor-Watchdog` (process health check every 5 minutes).
- Domain delivery and upgrades: **`Deploy-LoginMonitor.ps1`** + **`version.txt`** on a share such as `NETLOGON`. After a successful deploy, the startup Telegram message may include an update note (file **`deploy_last_update.txt`** next to logs).
- Full deploy/GPO guidance: **[DEPLOY.md](DEPLOY.md)**.
- **`Encrypt-DpapiForRdpMonitor.ps1`** — optional helper to prepare DPAPI-protected Base64 for the bot token / chat id.
## Notable behavior
- **`.ps1` encoding**: `.editorconfig` and `.gitattributes` encourage **`*.ps1`** as **UTF-8 with BOM** and **CRLF**, reducing mojibake and PowerShell parse issues.
- **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.
- **Stability**: `auditpol` is invoked via `cmd.exe` with merged stdout/stderr so `$ErrorActionPreference = 'Stop'` does not abort on stderr-only output.
## 1) Preparation
1. Create the install folder:
- `C:\ProgramData\RDP-login-monitor\`
2. Copy at least:
- `Login_Monitor.ps1`
- (for domain rollout on a share) `Deploy-LoginMonitor.ps1` and `version.txt`.
3. Edit `Login_Monitor.ps1` and set the bot token / chat:
- `$TelegramBotToken` or `$TelegramBotTokenProtectedB64`
- `$TelegramChatID` or `$TelegramChatIDProtectedB64`
4. Run elevated (Security log access and task registration).
5. Logs and auxiliary files:
- `C:\ProgramData\RDP-login-monitor\Logs\`
6. (Optional) Suppress some Security alerts via **`ignore.lst`** — see **section 7** below.
## 2) Manual run
Use this to validate startup/logic without registering scheduled tasks.
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-login-monitor\Login_Monitor.ps1"
```
The monitor keeps running in the session until you stop it (for example `Ctrl+C`).
## 3) Task Scheduler
You do not need to create tasks manually in the GUI.
Run:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-login-monitor\Login_Monitor.ps1" -InstallTasks
```
The script registers `RDP-Login-Monitor` and `RDP-Login-Monitor-Watchdog`, then triggers an immediate first run.
For domain deployment from a share you do not configure the scheduler on clients by hand — use `Deploy-LoginMonitor.ps1` (see `DEPLOY.md`).
## 4) Post-install checks
- Logs:
- `C:\ProgramData\RDP-login-monitor\Logs\login_monitor.log`
- `C:\ProgramData\RDP-login-monitor\Logs\watchdog.log`
- Heartbeat:
- `C:\ProgramData\RDP-login-monitor\Logs\last_heartbeat.txt` updates on **`$HeartbeatInterval`** (hourly by default).
- Daily report: after the first daily window (default **09:00**, controlled by **`$DailyReportHour`** / **`$DailyReportMinute`** in `Login_Monitor.ps1`), Telegram receives a `quser` summary; last run marker: `Logs\last_daily_report.txt`.
- Startup Telegram message: with **RD Session Host** (or broader RDS session components, not gateway-only) you get the RDS/RDP session-host line; when the **RD Gateway** log is available you get a separate line about connections to **internal targets** through the gateway (302/303). A gateway-only node does not duplicate the “session host” wording.
## 5) Automatic restart on failure
`-Watchdog` inside `Login_Monitor.ps1`:
- looks for `powershell.exe` / `pwsh.exe` whose command line references `Login_Monitor.ps1`;
- if the main monitor is missing, starts it;
- if the monitor is already running, does not spawn a second instance.
## 6) Extra parameters and other repo files
- **`-SkipScheduledTaskMaintenance`**: during normal monitor startup, skip verification/recreation of scheduled tasks (if you manage tasks only via **`-InstallTasks`** or manually).
- **`Install-DeployScheduledTask.ps1`** — helper to run **`Deploy-LoginMonitor.ps1`** from a share on a schedule (see **[DEPLOY.md](DEPLOY.md)**).
- **`Watchdog_RDP_Monitor.ps1`** and **`Install-ScheduledTasks.ps1`** — **alternate** layout with a separate watchdog script and default paths under **`D:\Soft`**. For new installs, prefer the built-in **`-Watchdog`** in **`Login_Monitor.ps1`** and tasks **`RDP-Login-Monitor`** / **`RDP-Login-Monitor-Watchdog`**.
- **`ignore.lst.example`** in the repo is a template for **`ignore.lst`** to suppress selected Security notifications (see section 7).
## 7) Suppressing Security alerts: `ignore.lst`
Place a file **`C:\ProgramData\RDP-login-monitor\ignore.lst`** next to **`Login_Monitor.ps1`**. Rules in this list are evaluated **only** for Telegram notifications from Security events **`4624`/`4625`** (successful/failed logons). Built-in exclusions in the script (`ExcludedUsers`, loopback IPs, service-style accounts, etc.) still apply to all event paths; **`ignore.lst`** adds **extra** matches **for 4624/4625 only**.
**RD Gateway (`302`/`303`)**, **RCM `1149`**, the daily report, and heartbeat **are not controlled** by this file (for `1149` the list is not applied, even though a shared filter function runs).
### How the file is loaded
- The file is read during event processing; entries are **cached in memory**. If **`LastWriteTimeUtc`** changes (save after edit), the list is **reloaded automatically** — no monitor restart required.
- Encoding: **UTF-8** (`Get-Content -Encoding UTF8`). A UTF-8 BOM at the start of a line is stripped when parsing.
- Blank lines are skipped. Lines starting with **`#`** or **`;`** are comments.
- If the line contains **`:`**, the **first** colon splits the line: the left part (trimmed) selects the rule kind, the right part is the value. If the right part is empty, the line is ignored.
- If there is **no** colon, the whole trimmed line is one “match-any” value (see below).
### Rule kinds (left part before the first `:`)
| Left part (case-insensitive regex fragments) | Event field |
| --- | --- |
| `рабоч`, `workstation`, `wks` | workstation name (**WorkstationName** and similar XML fields) |
| `польз`, `username`, `subject`, `account`, `target user`, whole word `user` | user name (**TargetUserName**, etc.) |
| `ip`, `ip адрес`, `ipaddress`, `адрес ip` | source IP (**IpAddress**, etc.) when present |
If the left part matches none of the above but a colon exists, the parser behaves like the Telegram line splitter: kind is **Any**, value is **only the right part** (the label on the left is discarded).
### “Any” matching (no colon, or unknown label before `:`)
Evaluated in order:
1. If the value looks like **IPv4**, it is compared to the event source IP (exact match).
2. If the value contains **`\`**, it is matched against the **account**: full `DOMAIN\user` equality **or** SAM after the last `\` (e.g. rule `IVANOV` matches `DOMAIN\IVANOV`).
3. Otherwise: exact match against **workstation name** first, then the same account rules as in step 2.
Explicit **User** / **Workstation** / **Ip** kinds only compare their respective field (user rules use the same full-name and SAM behavior as step 2).
### Examples and deployment
- See **`ignore.lst.example`** in the repo; copy it to the server as **`ignore.lst`** and edit.
- **`Deploy-LoginMonitor.ps1`** does **not** copy this file — rules are usually host-specific; create **`ignore.lst`** manually or via your configuration tooling.
## Keywords (for discovery)
`rdp`, `rd-gateway`, `rdp-gateway`, `rds`, `remote-desktop`, `windows-security-log`, `eventlog`, `event-id-4624`, `event-id-4625`, `event-id-302`, `event-id-303`, `powershell`, `telegram-bot`, `watchdog`, `gpo`, `netlogon`, `domain-deployment`, `windows-server`, `monitoring`
-24
View File
@@ -1,24 +0,0 @@
# Файл ignore.lst (UTF-8): положите рядом с Login_Monitor.ps1 в каталоге установки:
# C:\ProgramData\RDP-login-monitor\ignore.lst
#
# Каждая непустая строка — одно правило. Строки с # или ; в начале — комментарии.
# Подавляет только уведомления Security 4624/4625 (не RD Gateway, не RCM 1149).
#
# Форматы:
# user:domain\user
# user:user
# workstation:IVANOV
# 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 — пользователь целиком
# user:domain\user
# workstation:IVANOV
# ip:111.222.333.444
+1 -1
View File
@@ -1 +1 @@
1.4.3 1.3.11