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
12 changed files with 2102 additions and 190 deletions
+195
View File
@@ -0,0 +1,195 @@
# Развёртывание RDP Login Monitor в домене
Монитор ставится в **`C:\ProgramData\RDP-login-monitor\`**, задачи планировщика создаёт сам **`Login_Monitor.ps1`** (параметр `-InstallTasks`). Доставку по сети выполняет **`Deploy-LoginMonitor.ps1`**.
## Файлы на файловой шаре
Создайте каталог, доступный **конечным компьютерам** на чтение (часто учётная запись компьютера домена), например:
`\\dc.contoso.local\NETLOGON\RDP-login-monitor\`
Внутри должны лежать **три файла** (имена фиксированы):
| Файл | Назначение |
|------|------------|
| `Login_Monitor.ps1` | Основной скрипт (токен/chat id plain или DPAPI в параметрах). |
| `version.txt` | **Одна строка** — номер версии пакета на шаре (см. раздел «Версии» ниже). |
| `Deploy-LoginMonitor.ps1` | Установщик: сравнивает версию, копирует монитор, вызывает `-InstallTasks`, при необходимости запускает процесс монитора. |
При выпуске новой сборки обновляйте на шаре **`Login_Monitor.ps1`** и при необходимости **`version.txt`** (логика описана в разделе «Версии»).
## Как это работает
1. **`Deploy-LoginMonitor.ps1`** определяет корень дистрибутива:
- параметр **`-SourceShareRoot`** `\\server\share\RDP-login-monitor`, **или**
- если скрипт запущен по UNC, берётся **родительская папка** этого файла (удобно вызывать шару без параметров).
2. Читается **`version.txt`** на шаре и сравнивается с локальной меткой **`C:\ProgramData\RDP-login-monitor\deployed_version.txt`**. Если метки ещё нет — для сравнения подтягивается **`$ScriptVersion`** из уже установленного **`Login_Monitor.ps1`**.
3. Если версия на шаре **совпадает** с зафиксированной локально — выход без копирования (быстро, можно при каждой загрузке).
4. Если версия на шаре **новее** — останавливаются процессы монитора с каноническим путём → копируется **`Login_Monitor.ps1`** → выполняется **`Login_Monitor.ps1 -InstallTasks`** → записывается **`deployed_version.txt`** → запускается монитор (если не указан **`-SkipStartMonitorAfterUpdate`**).
5. Если версия на шаре **старее** локальной — обновление **не выполняется** (защита от отката), пока не указан **`-AllowDowngrade`**.
Лог установки: **`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`** создаёт две задачи:
| Имя | Назначение |
|-----|------------|
| **`RDP-Login-Monitor`** | Запуск основного монитора при старте ОС (`Register-ScheduledTask`, триггер «При запуске компьютера»). |
| **`RDP-Login-Monitor-Watchdog`** | Тот же файл **`Login_Monitor.ps1`** с аргументом **`-Watchdog`**: короткая проверка «жив ли монитор», при необходимости поднимает процесс. |
Watchdog регистрируется через **`schtasks.exe /Create /SC MINUTE /MO 5`** (а не через CIM-триггеры PowerShell): на части ОС у объектов триггера нет настраиваемых **`RepetitionInterval`** / длительность «раз и навсегда» режется планировщиком — из‑за этого раньше вторая задача могла не создаваться.
Перед созданием выполняется **`schtasks /Delete … /F`** (если задачи ещё не было, сообщение об ошибке подавляется — это нормально).
Сразу после регистрации задач вызывается **`schtasks /Run`** и для **основной задачи**, и для **watchdog**: первый запуск не ждёт перезагрузку и ближайшее 5‑минутное окно.
Если watchdog срабатывает, а основной монитор «не поднимается», смотрите **`Logs\watchdog.log`** (сообщение о PID и предупреждение, если процесс сразу завершился) и **конец `Logs\login_monitor.log`** — частая причина (исправлено в **1.3.5+**): пустой **`$PSCommandPath`** у процесса, запущенного через **`Start-Process`**; скрипт теперь подставляет путь через **`$PSScriptRoot`**.
Один экземпляр монитора фиксируется **файлом блокировки** в **`C:\ProgramData\RDP-login-monitor\.login_monitor_single_instance.lock`** (не Global mutex): так и **SYSTEM** (задача планировщика), и **интерактивный администратор** могут корректно запускать скрипт вручную без «Отказано в доступе» к mutex (версия **1.3.6+**).
Проверка:
```powershell
Get-ScheduledTask -TaskName 'RDP-Login-Monitor','RDP-Login-Monitor-Watchdog' -ErrorAction SilentlyContinue
schtasks /Query /TN "RDP-Login-Monitor-Watchdog"
```
Логи: **`...\Logs\login_monitor.log`**, **`...\Logs\watchdog.log`**.
## Запуск Deploy и политики с файловой шары
Если при запуске **`Deploy-LoginMonitor.ps1`** или **`Login_Monitor.ps1`** с UNC по **FQDN** (`\\dc.domain.local\...`) PowerShell сообщает про **цифровую подпись** / политику выполнения, чаще всего помогает путь по **короткому имени** контроллера: **`\\DC01\NETLOGON\...`** (клиент относит UNC к интрасети иначе). Параллельно убедитесь, что команда реально с **`powershell.exe -ExecutionPolicy Bypass -File "..."`**.
## Версии: `version.txt` и `$ScriptVersion`
| Что | Роль |
|-----|------|
| **`version.txt` на шаре** | Единственный источник для **`Deploy-LoginMonitor.ps1`**: решение «класть ли новый файл на компьютер». Поднимайте номер **всякий раз**, когда нужно, чтобы доменные машины забрали **новую копию** скрипта с шары — в том числе при мелких правках **без** изменения «видимой» версии в логах. |
| **`$ScriptVersion` в `Login_Monitor.ps1`** | Версия для **логов и Telegram** (что видит администратор). Меняйте при **значимых** релизах; для полной ясности можно держать ту же строку, что и в **`version.txt`**. |
**Типичные сценарии:**
- Крупный релиз: обновили **`Login_Monitor.ps1`**, подняли **`$ScriptVersion`** (например `1.4.0`) и записали то же в **`version.txt`** на шаре.
- Мелкая правка на шаре (опечатка, узкий фикс), не хотите путать отчёты по версии в логах: поднимите только **`version.txt`** (например с `1.3.0` на **`1.3.0.1`** — поддерживаются четырёхкомпонентные номера .NET **Version**). **`$ScriptVersion`** можно не трогать; на клиентах после деплоя в логах по-прежнему будет старая «человеческая» версия, но файл будет актуальным.
- Идеально поддерживать синхронность **`version.txt`** и **`$ScriptVersion`**, когда правки крупные и версия в логах должна совпадать с дистрибутивом.
Итого: **обновляться «по сети» обязан именно `version.txt`**; строка **`$ScriptVersion`** нужна для прозрачности в мониторинге и может совпадать с шарой или отставать по «маркетингу», если вы сознательно крутите только patch в **`version.txt`**.
## Групповая политика (GPO): проверенная схема
Ниже шаги, которые были проверены на практике для запуска деплоя при старте компьютера.
1. Положите три файла на шару (см. выше), например:
`\\B26\NETLOGON\RDP-login-monitor\`
2. Создайте/настройте GPO и привяжите её к OU с **компьютерами** (не пользователями).
3. В GPO откройте:
**Конфигурация компьютера****Политики****Конфигурация Windows****Сценарии (запуск/завершение)****Автозагрузка**.
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
```
Проверка:
```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"
```
## Ручная проверка
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "\\...\Deploy-LoginMonitor.ps1"
```
- **`-WhatIf`** — только сообщение в лог, без копирования.
- **`-SkipStartMonitorAfterUpdate`** — после обновления не запускать процесс монитора (остаются задачи планировщика и следующая загрузка / watchdog).
## Безопасность и замечания
- ACL на шару: чтение только нужным **компьютерам** / группам; файл **`Login_Monitor.ps1`** может содержать токен — ограничивайте доступ.
- DPAPI-секреты привязаны к машине: шифровать на каждой цели или использовать plain на закрытой шаре (см. комментарии в **`Login_Monitor.ps1`**).
- Deploy при ошибках пишет в **`deploy.log`** и завершается с кодом **0**, чтобы не блокировать загрузку ОС; проблемы смотрите по логу на ПК.
+280
View File
@@ -0,0 +1,280 @@
<#
.SYNOPSIS
Доставка Login_Monitor.ps1 с файловой шары по версии (домен: ПК и серверы).
.DESCRIPTION
Читает version.txt на шаре, сравнивает с локальной меткой (или с версией в установленном скрипте).
При необходимости копирует Login_Monitor.ps1 в C:\ProgramData\RDP-login-monitor\, регистрирует задачи (-InstallTasks),
перезапускает процесс монитора.
Предназначен для GPO «Сценарий запуска компьютера» (SYSTEM); можно запускать вручную от администратора.
СТРУКТУРА НА ШАРЕ (пример):
\\dc\share\RDP-login-monitor\Login_Monitor.ps1
\\dc\share\RDP-login-monitor\version.txt — одна строка, например: 1.3.0
\\dc\share\RDP-login-monitor\Deploy-LoginMonitor.ps1
Если Deploy-LoginMonitor.ps1 запускают с этой шары, параметр -SourceShareRoot можно не указывать —
корень шары берётся из расположения этого файла.
.NOTES
Лог: C:\ProgramData\RDP-login-monitor\Logs\deploy.log
#>
[CmdletBinding()]
param(
# UNC-каталог, где лежат Login_Monitor.ps1 и version.txt. Пусто = родительский каталог этого скрипта.
[string]$SourceShareRoot = "",
[switch]$WhatIf,
# После обновления не стартовать монитор (только файлы и задачи).
[switch]$SkipStartMonitorAfterUpdate,
# Разрешить установку более старой версии с шары (по умолчанию откаты блокируются).
[switch]$AllowDowngrade
)
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"
$PsExe = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
$Utf8Bom = New-Object System.Text.UTF8Encoding $true
function Write-DeployLog {
param([string]$Message)
$line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - $Message" + [Environment]::NewLine
try {
$dir = Split-Path $DeployLogPath -Parent
if (-not (Test-Path -LiteralPath $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
}
[System.IO.File]::AppendAllText($DeployLogPath, $line, $Utf8Bom)
} catch { }
if ($Host.Name -eq 'ConsoleHost' -or $Host.Name -eq 'Windows PowerShell ISE Host') {
Write-Host $line.TrimEnd("`r`n")
}
}
function Resolve-SourceShareRoot {
if (-not [string]::IsNullOrWhiteSpace($SourceShareRoot)) {
return [System.IO.Path]::GetFullPath($SourceShareRoot.TrimEnd('\'))
}
$here = $PSCommandPath
if ([string]::IsNullOrWhiteSpace($here)) { $here = $MyInvocation.MyCommand.Path }
if ([string]::IsNullOrWhiteSpace($here)) {
throw "Укажите -SourceShareRoot или запускайте Deploy-LoginMonitor.ps1 с путём к файлу (например с UNC-шары)."
}
return [System.IO.Path]::GetFullPath((Split-Path -Parent $here))
}
function Read-VersionLineFromFile {
param([string]$Path)
if (-not (Test-Path -LiteralPath $Path)) { return $null }
$raw = (Get-Content -LiteralPath $Path -TotalCount 3 -ErrorAction Stop | Where-Object { $_ -match '\S' }) | Select-Object -First 1
if ($null -eq $raw) { return $null }
return ([string]$raw).Trim() -replace '^v', ''
}
function Get-LocalDeployedVersion {
$fromStamp = Read-VersionLineFromFile -Path $VersionStampPath
if (-not [string]::IsNullOrWhiteSpace($fromStamp)) { return $fromStamp }
if (-not (Test-Path -LiteralPath $LocalScript)) { return $null }
try {
$head = Get-Content -LiteralPath $LocalScript -TotalCount 120 -ErrorAction Stop
foreach ($ln in $head) {
if ($ln -match '^\s*\$ScriptVersion\s*=\s*["'']([0-9]+(?:\.[0-9]+){0,3})["'']') {
return $Matches[1]
}
}
} catch { }
return $null
}
function Normalize-VersionOrNull {
param([string]$Text)
if ([string]::IsNullOrWhiteSpace($Text)) { return $null }
$t = $Text.Trim() -replace '^v', ''
try {
return [version]$t
} catch {
return $null
}
}
function Compare-VersionStrings {
param([string]$Left, [string]$Right)
$a = Normalize-VersionOrNull -Text $Left
$b = Normalize-VersionOrNull -Text $Right
if ($null -eq $a -or $null -eq $b) { return $null }
return $a.CompareTo($b)
}
function Get-ScriptPathFromCommandLine {
param([string]$CommandLine)
if ([string]::IsNullOrWhiteSpace($CommandLine)) { return $null }
$m = [regex]::Match($CommandLine, '(?i)-File\s+"([^"]+)"')
if ($m.Success) {
try { return [System.IO.Path]::GetFullPath($m.Groups[1].Value) } catch { return $null }
}
$m2 = [regex]::Match($CommandLine, '(?i)-File\s+(\S+)')
if ($m2.Success) {
try { return [System.IO.Path]::GetFullPath($m2.Groups[1].Value) } catch { return $null }
}
return $null
}
function Test-CommandLineIsWatchdog {
param([string]$CommandLine)
return ($CommandLine -match '(?i)(^|\s)-Watchdog(\s|$)')
}
function Stop-RdpLoginMonitorMainProcesses {
$canonical = [System.IO.Path]::GetFullPath($LocalScript)
try {
$procs = Get-CimInstance Win32_Process -Filter "Name = 'powershell.exe' OR Name = 'pwsh.exe'" -ErrorAction Stop
foreach ($proc in $procs) {
$cl = [string]$proc.CommandLine
if ($cl -notmatch 'Login_Monitor\.ps1') { continue }
if (Test-CommandLineIsWatchdog -CommandLine $cl) { continue }
$sp = Get-ScriptPathFromCommandLine -CommandLine $cl
if ($null -eq $sp) { continue }
if ([System.IO.Path]::GetFullPath($sp) -ne $canonical) { continue }
if ([int]$proc.ProcessId -eq $PID) { continue }
Write-DeployLog "Останавливаю процесс монитора PID $($proc.ProcessId) перед обновлением файла."
Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue
}
} catch {
Write-DeployLog "Предупреждение при остановке монитора: $($_.Exception.Message)"
}
}
# --- main ---
try {
$shareRoot = Resolve-SourceShareRoot
$sourceScript = Join-Path $shareRoot $ScriptName
$sourceVersionFile = Join-Path $shareRoot $VersionFileName
Write-DeployLog "Deploy: корень дистрибутива: $shareRoot"
if (-not (Test-Path -LiteralPath $sourceScript)) {
Write-DeployLog "ОШИБКА: на шаре нет файла: $sourceScript"
exit 0
}
if (-not (Test-Path -LiteralPath $sourceVersionFile)) {
Write-DeployLog "ОШИБКА: на шаре нет version.txt: $sourceVersionFile (добавьте одну строку с версией, как в Login_Monitor.ps1)."
exit 0
}
$shareVerRaw = Read-VersionLineFromFile -Path $sourceVersionFile
if ([string]::IsNullOrWhiteSpace($shareVerRaw)) {
Write-DeployLog "ОШИБКА: version.txt пустой или не читается: $sourceVersionFile"
exit 0
}
$localVerRaw = Get-LocalDeployedVersion
Write-DeployLog "Версия на шаре: $shareVerRaw; локально установлено: $(if ($localVerRaw) { $localVerRaw } else { '(нет)' })."
$cmp = Compare-VersionStrings -Left $shareVerRaw -Right $localVerRaw
if ($null -ne $cmp) {
if ($cmp -eq 0) {
Write-DeployLog "Актуально, копирование не требуется."
exit 0
}
if ($cmp -lt 0 -and -not $AllowDowngrade) {
Write-DeployLog "На шаре версия старше локальной — пропуск (используйте -AllowDowngrade для отката)."
exit 0
}
}
if (-not (Test-Path -LiteralPath $InstallRoot)) {
if ($WhatIf) {
Write-DeployLog "[WhatIf] Создать каталог $InstallRoot"
} else {
New-Item -ItemType Directory -Path $InstallRoot -Force | Out-Null
}
}
if ($WhatIf) {
Write-DeployLog "[WhatIf] Скопировать $sourceScript -> $LocalScript; InstallTasks; версия $shareVerRaw"
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
Write-DeployLog "Файл скопирован: $LocalScript"
$installArgs = @(
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $LocalScript, '-InstallTasks'
)
$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)."
}
[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
) -WindowStyle Hidden
Write-DeployLog "Запущен процесс монитора (новый файл)."
} else {
Write-DeployLog "Запуск монитора пропущен (-SkipStartMonitorAfterUpdate); поднимется при следующей загрузке или watchdog."
}
exit 0
} catch {
Write-DeployLog "КРИТИЧЕСКАЯ ОШИБКА: $($_.Exception.Message)"
exit 0
}
+11
View File
@@ -0,0 +1,11 @@
# Запуск: от администратора на ТОМ ЖЕ компьютере, где будет Login_Monitor.ps1.
# Результат (Base64) вставьте в $TelegramBotTokenProtectedB64 / $TelegramChatIDProtectedB64.
param(
[Parameter(Mandatory = $true)][string]$PlainText
)
Add-Type -AssemblyName System.Security
$bytes = [Text.Encoding]::UTF8.GetBytes($PlainText)
$protected = [System.Security.Cryptography.ProtectedData]::Protect(
$bytes, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine
)
[Convert]::ToBase64String($protected)
+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
+86
View File
@@ -0,0 +1,86 @@
#Requires -RunAsAdministrator
<#
.SYNOPSIS
Регистрирует в Планировщике заданий основной монитор и watchdog (как в README).
.DESCRIPTION
Запускайте из повышенной PowerShell. Пути по умолчанию — D:\Soft\.
Watchdog использует Watchdog_RDP_Monitor.ps1 из репозитория (проверка процесса и heartbeat).
#>
[CmdletBinding()]
param(
[string]$InstallRoot = "D:\Soft",
[string]$MainTaskName = "RDP Login Monitor",
[string]$WatchdogTaskName = "RDP Login Monitor Watchdog",
[int]$WatchdogRepeatMinutes = 5,
[int]$MainStartupRandomDelayMinutes = 1,
[int]$WatchdogStartupRandomDelayMinutes = 2
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$LoginScriptPath = Join-Path $InstallRoot "Login_Monitor.ps1"
$WatchdogScriptPath = Join-Path $InstallRoot "Watchdog_RDP_Monitor.ps1"
$LogsDir = Join-Path $InstallRoot "Logs"
if (-not (Test-Path -LiteralPath $LoginScriptPath)) {
throw "Не найден основной скрипт: $LoginScriptPath"
}
if (-not (Test-Path -LiteralPath $WatchdogScriptPath)) {
throw "Не найден watchdog: $WatchdogScriptPath"
}
if (-not (Test-Path -LiteralPath $LogsDir)) {
New-Item -ItemType Directory -Path $LogsDir -Force | Out-Null
}
$principal = New-ScheduledTaskPrincipal `
-UserId "NT AUTHORITY\SYSTEM" `
-LogonType ServiceAccount `
-RunLevel Highest
# --- Задание 1: основной монитор ---
$mainArgs = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$LoginScriptPath`""
$mainAction = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument $mainArgs
$mainTrigger = New-ScheduledTaskTrigger -AtStartup -RandomDelay (New-TimeSpan -Minutes $MainStartupRandomDelayMinutes)
$mainSettings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-StartWhenAvailable `
-MultipleInstances IgnoreNew
Register-ScheduledTask `
-TaskName $MainTaskName `
-Action $mainAction `
-Trigger $mainTrigger `
-Principal $principal `
-Settings $mainSettings `
-Force | Out-Null
Write-Host "Создано задание: $MainTaskName" -ForegroundColor Cyan
# --- Задание 2: watchdog (старт + периодический запуск, как в README) ---
$wdArgs = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$WatchdogScriptPath`""
$wdAction = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument $wdArgs
$wdTriggerStartup = New-ScheduledTaskTrigger -AtStartup -RandomDelay (New-TimeSpan -Minutes $WatchdogStartupRandomDelayMinutes)
$repeatDuration = New-TimeSpan -Days 3650
$anchor = (Get-Date).AddMinutes([Math]::Max(3, $WatchdogRepeatMinutes))
$wdTriggerRepeat = New-ScheduledTaskTrigger -Once -At $anchor `
-RepetitionInterval (New-TimeSpan -Minutes $WatchdogRepeatMinutes) `
-RepetitionDuration $repeatDuration
$wdSettings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-StartWhenAvailable `
-MultipleInstances IgnoreNew
Register-ScheduledTask `
-TaskName $WatchdogTaskName `
-Action $wdAction `
-Trigger @($wdTriggerStartup, $wdTriggerRepeat) `
-Principal $principal `
-Settings $wdSettings `
-Force | Out-Null
Write-Host "Создано задание: $WatchdogTaskName (триггеры: при старте ОС и каждые $WatchdogRepeatMinutes мин.)" -ForegroundColor Cyan
Write-Host "Готово. При необходимости сразу запустите: Start-ScheduledTask -TaskName '$MainTaskName'" -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.
+1188 -134
View File
File diff suppressed because it is too large Load Diff
+100 -54
View File
@@ -1,84 +1,130 @@
# RDP Login Monitor
# RDP Login Monitor
PowerShell-набор для мониторинга входов в Windows с отправкой уведомлений в Telegram:
PowerShell-набор для мониторинга входов в Windows с отправкой уведомлений в Telegram.
- `Login_Monitor.ps1` — основной монитор (Security `4624/4625`, RD Gateway `302/303`, ротация логов, heartbeat, ежедневный отчет).
- `Watchdog_RDP_Monitor.ps1` — watchdog: проверяет, жив ли основной скрипт, и перезапускает его при падении/зависании heartbeat.
## Актуальная схема (рекомендуется)
- Базовый путь установки: **`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.
## Что изменилось (важное)
- **Кодировка `.ps1`**: в репозитории добавлены `.editorconfig` и `.gitattributes`, чтобы `*.ps1` по умолчанию сохранялись как **UTF-8 with BOM** и с **CRLF** (это сильно снижает “кракозябры” и ошибки парсинга PowerShell).
- **Кодировка логов**: `login_monitor.log` / `watchdog.log` пишутся как **UTF-8 с BOM** (и при необходимости BOM добавляется к уже существующему файлу), чтобы в **FAR/старых просмотрщиках** не было ситуации “в консоли нормально, а в файле РЈРІРµ…” из‑за неверной авто-кодировки.
- **`auditpol` на русской Windows**: настройка/проверка аудита опирается на категорию **`Вход/выход`** и подкатегории **`Вход в систему` / `Выход из системы`** (ожидается строка **`Успех и сбой`**). Это устраняет ошибки вида `0x00000057` из‑за несуществующего на RU ОС имени `Logon`.
- **Стабильность**: `auditpol` запускается через `cmd.exe` с перехватом `stdout+stderr`, чтобы не ломать выполнение при `$ErrorActionPreference = "Stop"`.
## 1) Подготовка
1. Скопируйте `Login_Monitor.ps1` в `D:\Soft\Login_Monitor.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)
### Задание №1: основной монитор
Текущая схема: вручную задачи в GUI создавать не нужно.
Создайте задачу `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`
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-login-monitor\Login_Monitor.ps1" -InstallTasks
```
### Задание №2: watchdog
Скрипт сам зарегистрирует `RDP-Login-Monitor` и `RDP-Login-Monitor-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`
<<<<<<< HEAD
=======
>>>>>>> 68f8bcb (Add editor/gitattributes defaults for PowerShell encoding)
+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`
+23 -2
View File
@@ -18,15 +18,36 @@ param(
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$script:Utf8BomEncoding = New-Object System.Text.UTF8Encoding $true
$script:WatchdogLogBomChecked = $false
function Ensure-FileStartsWithUtf8Bom {
param([Parameter(Mandatory = $true)][string]$Path)
if (-not (Test-Path -LiteralPath $Path)) { return }
$bytes = [System.IO.File]::ReadAllBytes($Path)
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { return }
$bom = [byte[]](0xEF, 0xBB, 0xBF)
$combined = New-Object byte[] ($bom.Length + $bytes.Length)
[Buffer]::BlockCopy($bom, 0, $combined, 0, $bom.Length)
if ($bytes.Length -gt 0) {
[Buffer]::BlockCopy($bytes, 0, $combined, $bom.Length, $bytes.Length)
}
[System.IO.File]::WriteAllBytes($Path, $combined)
}
function Write-WatchdogLog {
param([string]$Message)
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$line = "$ts - $Message"
$line = "$ts - $Message" + [Environment]::NewLine
$dir = Split-Path -Parent $WatchdogLog
if ($dir -and -not (Test-Path $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
}
Add-Content -Path $WatchdogLog -Value $line -Encoding UTF8
if (-not $script:WatchdogLogBomChecked) {
Ensure-FileStartsWithUtf8Bom -Path $WatchdogLog
$script:WatchdogLogBomChecked = $true
}
[System.IO.File]::AppendAllText($WatchdogLog, $line, $script:Utf8BomEncoding)
}
function Get-MainScriptProcesses {
+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
View File
@@ -0,0 +1 @@
1.4.3