Compare commits

..

48 Commits

Author SHA1 Message Date
PTah a3a5b45cf8 fix: ignore list startup line use -f format (avoid $kindLabel: parse), bump 1.4.3 2026-05-04 09:42:32 +10:00
PTah 362318707e fix: PowerShell parse error for ignore list line (), bump 1.4.2 2026-05-04 09:40:34 +10:00
PTah 98031a20a1 fix: InstallTasks before DPAPI, deploy elevation and installtasks logs (1.4.1) 2026-05-04 09:38:02 +10:00
PTah b3da911d51 chore: bump version to 1.4.0 2026-05-04 09:33:54 +10:00
PTah 967e470e39 feat: list ignore.lst rules in startup Telegram notice 2026-05-04 09:30:24 +10:00
PTah ace7220752 feat: log ignore.lst reload with rule counts 2026-05-04 09:11:03 +10:00
PTah bdcf2bdc5f docs: expand ignore.lst section in README and README_eng 2026-05-04 09:02:14 +10:00
PTah 57bc617d26 Sanitize ignore.lst.example placeholders
Replace sample workstation, domain user, and IP with generic values.
2026-05-04 08:25:21 +10:00
PTah 17befc867e Document ignore.lst in README and DEPLOY
Describe optional Security 4624/4625 suppression, UTF-8, and that deploy does not ship ignore.lst.
2026-05-04 07:56:49 +10:00
PTah d3154ceb1b Add ignore.lst filtering for Security logon alerts
Support user/workstation/IP rules and Telegram-style lines; add ignore.lst.example; bump to 1.3.14.
2026-05-04 07:55:24 +10:00
Andrey Lutsenko fe600790e8 docs(license): update copyright holder name 2026-05-03 18:55:00 +10:00
Andrey Lutsenko 11736501ec chore: add MIT LICENSE 2026-05-03 18:54:16 +10:00
Andrey Lutsenko e96131d722 docs: refresh README, add English README_eng.md
Document daily report, deploy marker, SkipScheduledTaskMaintenance, and legacy watchdog scripts. Add full English translation.
2026-05-03 18:52:18 +10:00
PTah 99be51467e Refine RD Gateway 303 semantics and show deploy update timestamp 2026-04-30 16:55:30 +10:00
PTah d26784fb7f Refresh README for current ProgramData + Deploy workflow 2026-04-30 16:43:14 +10:00
PTah 02424875fb Add repository search keywords to README 2026-04-30 16:34:26 +10:00
PTah 0e2b3f2534 Clarify manual deploy option for small server fleets 2026-04-30 16:31:33 +10:00
PTah b6ac34a52d Add periodic deploy task installer and docs for rarely rebooted servers 2026-04-30 16:21:42 +10:00
PTah a37a639be1 Fix RD Gateway 302/303 field mapping (XML + updated Properties layout).
Bump version to 1.3.12.
2026-04-30 16:05:21 +10:00
PTah 1c14eec3cd Update DEPLOY.md with a tested GPO startup setup and troubleshooting notes.
Document PowerShell Startup tab usage, security filtering for computer groups, SYSVOL/NETLOGON behavior, reboot expectations, and where to inspect logs.
2026-04-29 17:47:21 +10:00
PTah cc8925f88a 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.
2026-04-29 16:45:13 +10:00
PTah a20055f9d3 Singleton lock: DeleteOnClose, Ctrl+C propagates, release in finally (v1.3.9) 2026-04-29 16:23:16 +10:00
PTah 313cc189e9 v1.3.5: путь скрипта при Start-Process, диагностика watchdog, DEPLOY.md 2026-04-29 16:13:14 +10:00
PTah e0b0fa3d7e Watchdog: schtasks /Run после установки; DEPLOY/README про задачи и UNC; версия 1.3.4 2026-04-29 16:07:35 +10:00
PTah 80f57c5351 Добавлен доменный деплой (Deploy-LoginMonitor.ps1, version.txt), DEPLOY.md, правки README и комментарий к версии в Login_Monitor.ps1 2026-04-29 15:14:41 +10:00
PTah 306b0762a1 Workstation vs server: ProductType; WKS uses LT10 + RCM 1149; server keeps 2/3/10 (v1.2.0) 2026-04-29 14:30:28 +10:00
PTah 6bc5173236 auditpol: detect RU only from /list, not culture; fall back to EN (Hyper-V); v1.1.3 2026-04-27 10:47:16 +10:00
PTah be0c394ac4 Fix Uc RU category string: add missing 0x0445 in vyhod (auditpol 0x57); v1.1.2 2026-04-27 10:29:10 +10:00
PTah 850781a38d 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 b032de9d1c 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.
2026-04-27 10:24:09 +10:00
PTah 4a4a736eb9 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.
2026-04-27 10:19:09 +10:00
PTah 383ffb0bf8 Add script version to startup log and admin error 2026-04-27 10:07:23 +10:00
PTah 3d03d905a1 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.
2026-04-27 10:01:42 +10:00
PTah 4f0b6e219c Add Install-ScheduledTasks.ps1 for Task Scheduler registration; document in README 2026-04-13 11:29:21 +10:00
PTah b164e51965 README: remove internal PMG/LDAP filter details from public docs 2026-04-13 11:23:44 +10:00
PTah 0dce7e7017 Fix daily report: force array for unique users (PS 5.1 Count on scalar) 2026-04-13 11:12:44 +10:00
PTah 9f680dfd89 Telegram Security events: show log server vs workstation (4624/4625) 2026-04-13 10:59:02 +10:00
PTah 685ef5260a Clarify startup Telegram: Gateway targets internal PCs; exclude Gateway-only from RDS host line 2026-04-13 10:53:10 +10:00
PTah 2826e3f9cd Telegram startup: mention RDS and RD Gateway when applicable 2026-04-13 10:49:30 +10:00
PTah 848381e098 Use UTF-8 BOM for logs; document FAR encoding 2026-04-13 10:33:11 +10:00
Луценко Андрей Анатольевич 99ab020347 Document recent monitoring and auditpol improvements 2026-04-11 22:16:13 +10:00
Луценко Андрей Анатольевич 65469cf1d2 Improve auditpol parsing for RU logon/logoff category output 2026-04-11 22:14:31 +10:00
Луценко Андрей Анатольевич 0c9faece92 Fix auditpol localization (RU logon subcategory) and reduce noise 2026-04-11 21:57:16 +10:00
Луценко Андрей Анатольевич d0171daf8e Fix auditpol for RU subcategory and safer stderr handling 2026-04-11 21:48:49 +10:00
Луценко Андрей Анатольевич 16baf12094 Update scripts 2026-04-11 21:23:34 +10:00
Луценко Андрей Анатольевич c5a4c26b1e Add editor/gitattributes defaults for PowerShell encoding 2026-04-11 21:23:27 +10:00
PapaTramp 89acb0a914 Update README.md 2026-04-09 16:42:00 +10:00
Луценко Андрей Анатольевич 21ad87d08d Add login monitor, watchdog and Task Scheduler docs 2026-04-09 16:36:27 +10:00
9 changed files with 759 additions and 109 deletions
+90 -12
View File
@@ -34,6 +34,13 @@
Лог установки: **`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`
Параметр **`Login_Monitor.ps1 -InstallTasks`** создаёт две задачи:
@@ -47,7 +54,7 @@ Watchdog регистрируется через **`schtasks.exe /Create /SC MIN
Перед созданием выполняется **`schtasks /Delete … /F`** (если задачи ещё не было, сообщение об ошибке подавляется — это нормально).
Сразу после успешного **`/Create`** вызывается **`schtasks /Run`** для watchdog: первый прогон не ждёт следующего 5‑минутного окна расписания.
Сразу после регистрации задач вызывается **`schtasks /Run`** и для **основной задачи**, и для **watchdog**: первый запуск не ждёт перезагрузку и ближайшее 5‑минутное окно.
Если watchdog срабатывает, а основной монитор «не поднимается», смотрите **`Logs\watchdog.log`** (сообщение о PID и предупреждение, если процесс сразу завершился) и **конец `Logs\login_monitor.log`** — частая причина (исправлено в **1.3.5+**): пустой **`$PSCommandPath`** у процесса, запущенного через **`Start-Process`**; скрипт теперь подставляет путь через **`$PSScriptRoot`**.
@@ -81,25 +88,96 @@ schtasks /Query /TN "RDP-Login-Monitor-Watchdog"
Итого: **обновляться «по сети» обязан именно `version.txt`**; строка **`$ScriptVersion`** нужна для прозрачности в мониторинге и может совпадать с шарой или отставать по «маркетингу», если вы сознательно крутите только patch в **`version.txt`**.
## Групповая политика (GPO)
## Групповая политика (GPO): проверенная схема
1. Положите три файла на шару (см. выше).
2. **Конфигурация компьютера****Политики****Windows****Сценарии (запуск/завершение)****Автозагрузка**.
3. Добавьте сценарий PowerShell с **путём к установщику на шаре**, например:
Ниже шаги, которые были проверены на практике для запуска деплоя при старте компьютера.
`\\contoso.local\NETLOGON\RDP-login-monitor\Deploy-LoginMonitor.ps1`
1. Положите три файла на шару (см. выше), например:
Параметры можно оставить пустыми, если рядом на шаре лежат **`Login_Monitor.ps1`** и **`version.txt`**.
`\\B26\NETLOGON\RDP-login-monitor\`
Скрипт выполняется от **SYSTEM** при старте ОС — этого достаточно для записи в **`ProgramData`** и регистрации задач.
2. Создайте/настройте GPO и привяжите её к OU с **компьютерами** (не пользователями).
Альтернатива: команда
3. В GPO откройте:
**Конфигурация компьютера****Политики****Конфигурация Windows****Сценарии (запуск/завершение)****Автозагрузка**.
```text
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "\\...\Deploy-LoginMonitor.ps1"
4. На вкладке **«Сценарии PowerShell»** добавьте скрипт:
`\\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"
```
## Ручная проверка
+42 -1
View File
@@ -33,9 +33,22 @@ param(
Set-StrictMode -Version Latest
$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")
$LocalScript = Join-Path $InstallRoot "Login_Monitor.ps1"
$VersionStampPath = Join-Path $InstallRoot "deployed_version.txt"
$DeployUpdateMarkerPath = Join-Path $InstallRoot "deploy_last_update.txt"
$DeployLogPath = Join-Path $InstallRoot "Logs\deploy.log"
$ScriptName = "Login_Monitor.ps1"
$VersionFileName = "version.txt"
@@ -205,6 +218,11 @@ try {
exit 0
}
if (-not (Test-DeployRunningElevated)) {
Write-DeployLog "ОШИБКА: запустите Deploy из повышенной консоли PowerShell («Запуск от имени администратора»). Без этого дочерний Login_Monitor.ps1 -InstallTasks завершится с кодом 1 (нет прав на регистрацию задач)."
exit 0
}
Stop-RdpLoginMonitorMainProcesses
Copy-Item -LiteralPath $sourceScript -Destination $LocalScript -Force
@@ -213,9 +231,23 @@ try {
$installArgs = @(
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $LocalScript, '-InstallTasks'
)
$p = Start-Process -FilePath $PsExe -ArgumentList $installArgs -Wait -PassThru -WindowStyle Hidden
$instOut = Join-Path $InstallRoot "Logs\deploy_installtasks_stdout.log"
$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) {
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 {
Write-DeployLog "InstallTasks выполнен (код 0)."
}
@@ -223,6 +255,15 @@ try {
[System.IO.File]::WriteAllText($VersionStampPath, "$shareVerRaw`r`n", $Utf8Bom)
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) {
Start-Process -FilePath $PsExe -ArgumentList @(
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $LocalScript
+45
View File
@@ -0,0 +1,45 @@
#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
@@ -0,0 +1,21 @@
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.
+321 -34
View File
@@ -69,7 +69,7 @@ $script:MonitorSingletonLockStream = $null
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
$ScriptVersion = "1.3.11"
$ScriptVersion = "1.4.3"
# Логи (все под InstallRoot)
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
@@ -83,6 +83,11 @@ $LogRotationMinute = 0
# Heartbeat (только файл)
$HeartbeatInterval = 3600
$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
@@ -486,6 +491,24 @@ function Start-RdpMonitorWatchdogMain {
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) ---
if (-not [string]::IsNullOrWhiteSpace($TelegramBotTokenProtectedB64)) {
try {
@@ -506,23 +529,6 @@ if (-not [string]::IsNullOrWhiteSpace($TelegramChatIDProtectedB64)) {
if ($TelegramBotToken -eq '<TELEGRAM_BOT_TOKEN>') { $TelegramBotToken = "" }
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 {
param([string]$Text)
if ($null -eq $Text) { return '' }
@@ -883,9 +889,53 @@ function Send-Heartbeat {
$timestamp = Get-Date -Format "dd.MM.yyyy HH:mm:ss"
$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) {
$message = "<b>✅ Мониторинг логинов ЗАПУЩЕН</b>`r`n"
$message += "🏷️ Версия скрипта: $(ConvertTo-TelegramHtml $ScriptVersion)`r`n"
$message += "🏷️ Версия скрипта: $(ConvertTo-TelegramHtml $ScriptVersion)"
$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 += "🕐 Время запуска: $timestamp"
if ($script:OsInstallKindLabel) {
@@ -896,6 +946,23 @@ function Send-Heartbeat {
} else {
$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) {
$message += "`r`n🔐 <b>RDS (хост сессий):</b> обнаружены компоненты RDS помимо чистого шлюза — в мониторинг входят входы по RDP/RDS на этом узле (Security 4624/4625, типы входа по настройке скрипта)."
}
@@ -1060,6 +1127,137 @@ 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 {
param(
[string]$Username,
@@ -1117,6 +1315,12 @@ function Should-IgnoreEvent {
if ($ComputerName -like $pattern) { return $true }
}
if ($EventID -in 4624, 4625) {
if (Test-RdpMonitorIgnoreListMatch -Username $Username -ComputerName $ComputerName -SourceIP $SourceIP) {
return $true
}
}
return $false
}
@@ -1214,6 +1418,13 @@ function Test-RDGatewayLog {
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 {
param($Event)
$eventData = @{
@@ -1225,21 +1436,91 @@ function Get-RDGatewayEventInfo {
ErrorCode = "N/A"
}
try {
switch ($Event.Id) {
302 {
if ($Event.Properties.Count -gt 0) { $eventData.Username = $Event.Properties[0].Value }
if ($Event.Properties.Count -gt 1) { $eventData.ExternalIP = $Event.Properties[1].Value }
if ($Event.Properties.Count -gt 2) { $eventData.InternalIP = $Event.Properties[2].Value }
if ($Event.Properties.Count -gt 3) { $eventData.Protocol = $Event.Properties[3].Value }
$eventData.ErrorCode = "0"
$map = Get-EventDataMap -Event $Event
$lc = @{}
foreach ($k in $map.Keys) {
try { $lc[$k.ToLowerInvariant()] = $map[$k] } catch { }
}
function Get-RdpGwMapVal([string[]]$Keys) {
foreach ($key in $Keys) {
$lk = $key.ToLowerInvariant()
if (-not $lc.ContainsKey($lk)) { continue }
$v = $lc[$lk]
if (-not [string]::IsNullOrWhiteSpace($v)) { return [string]$v }
}
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 }
if ($Event.Properties.Count -gt 2) { $eventData.InternalIP = $Event.Properties[2].Value }
if ($Event.Properties.Count -gt 3) { $eventData.Protocol = $Event.Properties[3].Value }
if ($Event.Properties.Count -gt 4) { $eventData.ErrorCode = $Event.Properties[4].Value }
return $null
}
$u = Get-RdpGwMapVal @('username','user','authusername')
$ext = Get-RdpGwMapVal @('clientaddress','clientip','ipaddress','address','remoteaddress','sourceaddress')
$int = Get-RdpGwMapVal @('targetserver','targetname','resource','server','internaladdress','destinationaddress','targetaddress','devicename','computername')
$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 {
Write-Log "Ошибка при извлечении RD Gateway события: $($_.Exception.Message)"
@@ -1265,8 +1546,14 @@ function Format-RDGatewayEvent {
$hTime = (ConvertTo-TelegramHtml ($TimeCreated.ToString('dd.MM.yyyy HH:mm:ss')))
$message = "<b>"
if ($EventID -eq 302) { $message += "🖥️ УСПЕШНОЕ ПОДКЛЮЧЕНИЕ ЧЕРЕЗ RD GATEWAY" }
elseif ($EventID -eq 303) { $message += "❌ НЕУДАЧНОЕ ПОДКЛЮЧЕНИЕ ЧЕРЕЗ RD GATEWAY" }
if ($EventID -eq 302) { $message += "🖥️ ПОДКЛЮЧЕНИЕ ЧЕРЕЗ RD GATEWAY" }
elseif ($EventID -eq 303) {
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" }
$message += "</b>`r`n"
+87 -61
View File
@@ -1,14 +1,17 @@
# RDP Login Monitor
# RDP Login Monitor
PowerShell-набор для мониторинга входов в Windows с отправкой уведомлений в Telegram.
## Актуальная схема (рекомендуется)
- **`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**).
- **`Deploy-LoginMonitor.ps1`** + **`version.txt`** — доставка с файловой шары по версии (домен, GPO «автозагрузка» компьютера). Подробности, структура шары и правила версий: **[DEPLOY.md](DEPLOY.md)**.
- **`Encrypt-DpapiForRdpMonitor.ps1`** — опционально, для DPAPI-строк токена/chat id на конкретном ПК.
Разделы ниже про **`D:\Soft\`**, **`Watchdog_RDP_Monitor.ps1`** и **`Install-ScheduledTasks.ps1`** относятся к **старой схеме** и оставлены для совместимости; новые установки ориентируйте на **`ProgramData`** и **DEPLOY.md**.
- Базовый путь установки: **`C:\ProgramData\RDP-login-monitor\`**.
- Основной скрипт: **`Login_Monitor.ps1`** — журнал Security **`4624`/`4625`** (логика зависит от типа ОС: рабочая станция или сервер/КД), при наличии журнала — **Remote Connection Manager `1149`** (часто актуально для РС с RDP), при роли **RD Gateway****`302`/`303`**, **ежедневный отчёт** в Telegram (активные сессии через `quser`), **heartbeat**, **ротация логов**, уведомления в Telegram.
- Установка задач: запуск **`Login_Monitor.ps1 -InstallTasks`** создаёт:
- `RDP-Login-Monitor` (основной монитор),
- `RDP-Login-Monitor-Watchdog` (контроль процесса каждые 5 минут).
- Доменная доставка и обновления: **`Deploy-LoginMonitor.ps1`** + **`version.txt`** с шары `NETLOGON`. После успешного деплоя в приветственном сообщении Telegram может появиться отметка об обновлении (файл **`deploy_last_update.txt`** рядом с логами).
- Для полной инструкции по деплою/GPO используйте **[DEPLOY.md](DEPLOY.md)**.
- **`Encrypt-DpapiForRdpMonitor.ps1`** — опционально для подготовки DPAPI-строк токена/chat id.
## Что изменилось (важное)
@@ -19,86 +22,109 @@ PowerShell-набор для мониторинга входов в Windows с
## 1) Подготовка
1. Скопируйте в `D:\Soft\` как минимум `Login_Monitor.ps1` и `Watchdog_RDP_Monitor.ps1` (для установки из скрипта — ещё `Install-ScheduledTasks.ps1`), либо измените пути в параметрах установщика / в watchdog.
2. Откройте `Login_Monitor.ps1` и задайте:
- `$TelegramBotToken`
- `$TelegramChatID`
3. Убедитесь, что существует `D:\Soft\Logs\` (скрипт сам создаст при запуске).
4. Запускайте от имени администратора (нужен доступ к журналу `Security`).
1. Подготовьте папку установки:
- `C:\ProgramData\RDP-login-monitor\`
2. Скопируйте в неё как минимум:
- `Login_Monitor.ps1`
- (для доменного развёртывания отдельно на шаре) `Deploy-LoginMonitor.ps1` и `version.txt`.
3. Откройте `Login_Monitor.ps1` и задайте токен/чат:
- `$TelegramBotToken` или `...ProtectedB64`
- `$TelegramChatID` или `...ProtectedB64`
4. Запускайте с правами администратора (чтение `Security` журнала и регистрация задач).
5. Логи и служебные файлы будут в:
- `C:\ProgramData\RDP-login-monitor\Logs\`
6. (Опционально) Подавление части алертов по списку — см. раздел **«7) ignore.lst»** ниже.
## 2) Ручной запуск
Используйте этот вариант для быстрой проверки старта/логики без установки задач планировщика.
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "D:\Soft\Login_Monitor.ps1"
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-login-monitor\Login_Monitor.ps1"
```
Примечание: при ручном запуске монитор работает в текущей сессии до остановки (например, `Ctrl+C`).
## 3) Запуск через Планировщик заданий (Task Scheduler)
Задания можно завести **вручную в 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`, задержки случайного старта и т.д.
Текущая схема: вручную задачи в GUI создавать не нужно.
Пример после копирования файлов в `D:\Soft\`:
Достаточно запустить:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "D:\Soft\Install-ScheduledTasks.ps1"
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-login-monitor\Login_Monitor.ps1" -InstallTasks
```
Это не заменяет настройку токена Telegram в `Login_Monitor.ps1`, а только регистрирует задания.
Скрипт сам зарегистрирует `RDP-Login-Monitor` и `RDP-Login-Monitor-Watchdog`, а также запросит немедленный первый запуск задач.
### Задание №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`.
Для доменной установки/обновления с шары вручную ничего в планировщике на клиенте настраивать не требуется: используйте `Deploy-LoginMonitor.ps1` (подробно в `DEPLOY.md`).
## 4) Что проверять после запуска
- Логи:
- `D:\Soft\Logs\login_monitor.log`
- `D:\Soft\Logs\watchdog.log`
- `C:\ProgramData\RDP-login-monitor\Logs\login_monitor.log`
- `C:\ProgramData\RDP-login-monitor\Logs\watchdog.log`
- Heartbeat:
- `D:\Soft\Logs\last_heartbeat.txt` обновляется примерно раз в час (по `$HeartbeatInterval`).
- `C:\ProgramData\RDP-login-monitor\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 не дублирует формулировку «хост сессий».
## 5) Автоматический перезапуск при падении
`Watchdog_RDP_Monitor.ps1` делает:
Режим `-Watchdog` внутри `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
@@ -0,0 +1,128 @@
# 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
@@ -0,0 +1,24 @@
# Файл 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.3.11
1.4.3