Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f25ca78ad | |||
| 1b192cea7c | |||
| 2c73526f7c | |||
| 4015f9a7e4 |
@@ -0,0 +1,28 @@
|
||||
---
|
||||
description: Global token-saving, cost control, and context management rules
|
||||
globs: *
|
||||
---
|
||||
|
||||
# Global Optimization & Cost Control
|
||||
|
||||
## ?? Context & Local Data
|
||||
- **Strict file targeting:** Work ONLY with files explicitly provided via `@` or open in the active editor tab. Do not use global codebase search unless requested.
|
||||
- **Local assets only:** Always work with local copies of repositories and dependencies. Never request external web resources or re-download packages without a explicit build error.
|
||||
- **Size Limit:** Do not load files larger than 500 lines into the context unless strictly necessary.
|
||||
|
||||
## ?? Git & Commits Management
|
||||
- **Automatic commits are strictly FORBIDDEN.** Never execute `git commit` or `git push` without an explicit user command.
|
||||
- Only suggest a commit after phrases like: "Ñäåëàé êîììèò", "Çàôèêñèðóé èçìåíåíèÿ", "Push".
|
||||
- Before committing: display `git diff --stat` and wait for explicit user confirmation.
|
||||
- Use Conventional Commits format (`feat:`, `fix:`, `refactor:`, `docs:`).
|
||||
|
||||
## ?? Output Format & Brevity
|
||||
- **Strictly no fluff:** Omit greetings, apologies, and closing pleasantries.
|
||||
- **Diff-style only:** Output only modified code fragments, never duplicate unchanged logic or whole files.
|
||||
- **Extreme brevity:** Explanations must be 1-3 sentences max. Use documentation links instead of long texts.
|
||||
- If a task doesn't require code, respond strictly with text. If in doubt, ask ONE precise clarifying question.
|
||||
|
||||
## ?? Model Routing Reminder
|
||||
- Simple questions, explanations, docs ? Use lightweight models (e.g., `gpt-4o-mini`, `claude-3-haiku`).
|
||||
- Complex code generation, refactoring, and debugging ? Use advanced models (e.g., `claude-3.5-sonnet`).
|
||||
- Do not switch models without an explicit reason.
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
description: Code generation syntax restrictions to minimize output tokens
|
||||
globs: "*.{ts,js,py,cs}"
|
||||
---
|
||||
|
||||
# Syntax Optimization
|
||||
|
||||
- **No JSDoc/Docstrings:** Do NOT write documentation, comments, JSDoc, or docstrings for generated functions unless explicitly asked.
|
||||
- **No logs or prints:** Remove all `console.log`, `print()`, or debugging statements from the final code output.
|
||||
- **Use concise syntax:**
|
||||
- In JavaScript/TypeScript: Use arrow functions, optional chaining (`?.`), nullish coalescing (`??`), and destructuring.
|
||||
- In Python: Use list comprehensions, dict comprehensions, and built-in functions.
|
||||
- **Minimize imports:** Do not output the `import` statements section if the required packages are standard and already exist in the file.
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
description: High-density PowerShell script generation rules for minimal token usage
|
||||
globs: "*.ps1, *.psm1"
|
||||
---
|
||||
|
||||
# PowerShell Token Optimization
|
||||
|
||||
- **Use Short Aliases:** Use short aliases instead of full cmdlet names to drastically cut output tokens:
|
||||
- Use `gc` instead of `Get-Content`
|
||||
- Use `gci` instead of `Get-ChildItem`
|
||||
- Use `%` instead of `ForEach-Object`
|
||||
- Use `?` instead of `Where-Object`
|
||||
- Use `measure` instead of `Measure-Object`
|
||||
- **Pipeline Over Loops:** Prefer pipeline chains (`gci | % { ... }`) over multi-line `foreach ($item in $items) { ... }` blocks.
|
||||
- **No Help/Comments:** Do not generate `.SYNOPSIS`, `.DESCRIPTION`, or comment-based help at the top of scripts.
|
||||
- **Omit Parameter Names:** Drop explicit parameter names where positional arguments are clear (e.g., use `gc file.txt` instead of `Get-Content -Path file.txt`).
|
||||
- **Silent Execution:** Do not add verbose logging, `Write-Host`, or `Write-Output` unless explicitly asked to create UI/logs.
|
||||
- **Preserve CLI Arguments:** Do not duplicate full multi-line `yt-dlp` command-line arguments, format strings, or output templates if they are not the subject of the modifications. Use placeholders or variable references.
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
description: Ultra-dense Python code generation rules to save output tokens
|
||||
globs: "*.py"
|
||||
---
|
||||
|
||||
# Python Token Optimization
|
||||
|
||||
- **Use Syntactic Sugar:** Prioritize list comprehensions, dict comprehensions, and ternary operators (`x if condition else y`) to keep code on a single line.
|
||||
- **Built-in Libraries First:** Use standard libraries (`pathlib`, `json`, `subprocess`, `asyncio`) instead of introducing heavy external dependencies unless already in `requirements.txt`.
|
||||
- **Type Hinting:** Do NOT add type hints (`def func(x: int) -> str:`) unless the existing file strictly uses them. Type hints consume significant tokens.
|
||||
- **No Format Duplication:** When modifying scripts (like video downloaders), provide only the modified function or class method. Never output the `if __name__ == "__main__":` block or argument parsing logic if they haven't changed.
|
||||
- **No Docstrings:** Strictly forbid writing `"""docstrings"""` or `# comments` explaining the logic.
|
||||
- **Preserve yt-dlp Options:** Never rewrite, duplicate, or expand the `ydl_opts` configuration dictionary or custom extraction options. If changes are unrelated to download options, use a placeholder comment like `# ... existing ydl_opts ...` instead of outputting the full dictionary block.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
description: Global token-saving rules for ultra-concise communication and minimal context usage
|
||||
globs: *
|
||||
---
|
||||
|
||||
# Token-Saving Instructions
|
||||
|
||||
## Communication Strategy
|
||||
- **Strictly no fluff:** Omit all greetings, pleasantries, apologies, and concluding remarks.
|
||||
- **Direct answers only:** Start your response immediately with the solution, code block, or direct answer.
|
||||
- **Extreme brevity:** Keep explanations under 2-3 sentences. Use bullet points instead of long paragraphs.
|
||||
|
||||
## Code Generation Guidelines
|
||||
- **Do not restate existing code:** Never copy and paste parts of my existing file just to show where to insert changes.
|
||||
- **Provide diffs only:** Show only the specific lines that need to be changed, added, or deleted. Use brief comments (`// ... existing code ...`) to show placement if necessary.
|
||||
- **No boilerplate:** Do not generate setup code, imports, or boilerplate unless explicitly requested.
|
||||
- **Single-line implementations:** Prefer concise, clean, short code syntax where readable (e.g., arrow functions, ternary operators).
|
||||
|
||||
## Code Review and Verification
|
||||
- Do not explain why the code works unless asked.
|
||||
- If the solution is simple, output *only* the code block and nothing else.
|
||||
- If you need more information, ask a single, precise question. Do not list multiple hypotheticals.
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
description: Bump версии в csproj и тег релиза vX.Y.Z при обновлениях
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Версия сборки и теги релиза
|
||||
|
||||
Единственный источник — `VideoDownloader.App/VideoDownloader.App.csproj`. Синхронно обновляй:
|
||||
|
||||
- `Version` (например `0.7.0`)
|
||||
- `AssemblyVersion` — `x.y.z.0`
|
||||
- `FileVersion` — `x.y.z.0`
|
||||
- `InformationalVersion` остаётся `$(Version)`
|
||||
|
||||
## Обычные правки (patch)
|
||||
|
||||
После осмысленного изменения в `VideoDownloader.App` поднимай **patch** на **+0.0.1** (например `0.7.0` → `0.7.1`). Bump в **том же коммите**, что и изменение, где это уместно.
|
||||
|
||||
## Minor / major
|
||||
|
||||
При заметном релизе можно поднять **minor** или **major** по смыслу (например `0.6.x` → `0.7.0`). Не обязательно делать это при каждой мелкой правке.
|
||||
|
||||
## Тег версии при каждом bump (`vX.Y.Z`) — не забывать
|
||||
|
||||
Ветки вида `v0.7.x` **не используем** — маркер релиза — **легковесный или аннотированный тег** с тем же именем.
|
||||
|
||||
При **любом** повышении `<Version>` в csproj в том же цикле работы:
|
||||
|
||||
1. Закоммитить bump (вместе с изменениями или коммитом `chore: bump version to X.Y.Z`).
|
||||
2. Создать тег на **текущем** `HEAD` (после коммита с bump):
|
||||
`git tag -a vX.Y.Z -m "Release X.Y.Z"` (или `git tag vX.Y.Z` для легковесного тега).
|
||||
Если тег с таким именем уже существовал локально по ошибке: удалить и создать заново на нужном коммите, либо `git tag -f` осознанно.
|
||||
3. Запушить **обязательно**: **`git push origin native-code`** и **`git push origin vX.Y.Z`** (первый релиз с этим номером).
|
||||
Повторный push того же тега на другой коммит на сервере обычно запрещён — bump версии делается новым номером и новым тегом.
|
||||
|
||||
Именование тега: **`v` + полная semver из `<Version>`** (пример: `v0.7.2`), без префикса `release/`.
|
||||
|
||||
**Не пушить** только `native-code` без нового **`v*`** — при bump версии всегда создавай и пушь соответствующий тег.
|
||||
|
||||
### Проверка на GitHub
|
||||
|
||||
Релиз по тегу: вкладка **Releases** или список тегов в репозитории; `git checkout v0.7.2` локально даёт ту же ревизию, что и при сборке с этим bump.
|
||||
|
||||
Если на `origin` ещё есть старая **ветка** с тем же имени, что у тега, удаляй явно: `git push origin --delete refs/heads/vX.Y.Z` (иначе Git может не разрешить неоднозначность `vX.Y.Z`).
|
||||
@@ -0,0 +1,65 @@
|
||||
# Çàâèñèìîñòè è ïàêåòû
|
||||
node_modules/
|
||||
bower_components/
|
||||
jspm_packages/
|
||||
.npm/
|
||||
vendor/
|
||||
.pnpm-store/
|
||||
|
||||
# Âèðòóàëüíûå îêðóæåíèÿ (Python)
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
.env/
|
||||
|
||||
# Ñáîðêà, êýø è àðòåôàêòû
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
.next/
|
||||
.nuxt/
|
||||
.docusaurus/
|
||||
.cache/
|
||||
.sass-cache/
|
||||
.turbo/
|
||||
target/
|
||||
bin/
|
||||
obj/
|
||||
|
||||
# Ñèñòåìíûå ïàïêè IDE è ëîãè
|
||||
.git/
|
||||
.svn/
|
||||
.idea/
|
||||
.vscode/
|
||||
.cursor/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Ìåäèà, øðèôòû è òÿæåëûå áèíàðíûå ôàéëû
|
||||
*.png
|
||||
*.jpg
|
||||
*.jpeg
|
||||
*.gif
|
||||
*.svg
|
||||
*.ico
|
||||
*.mp4
|
||||
*.mp3
|
||||
*.pdf
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
*.exe
|
||||
*.dll
|
||||
*.woff
|
||||
*.woff2
|
||||
*.ttf
|
||||
*.eot
|
||||
|
||||
# Ñãåíåðèðîâàííûå ôàéëû áëîêèðîâîê (îïöèîíàëüíî, ýêîíîìÿò ìíîãî òîêåíîâ)
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
poetry.lock
|
||||
@@ -36,10 +36,31 @@
|
||||
|
||||
## Опционально на клиенте: `ignore.lst`
|
||||
|
||||
На каждом компьютере/сервере можно положить файл **`C:\ProgramData\RDP-login-monitor\ignore.lst`** (в том же каталоге, что и **`Login_Monitor.ps1`**). По строкам этого файла монитор **не отправляет в Telegram** отдельные события **Security `4624`/`4625`** (например, шум от известной рабочей станции, тестового пользователя или фиксированного IP).
|
||||
На каждом компьютере/сервере можно положить файл **`C:\ProgramData\RDP-login-monitor\ignore.lst`** (в том же каталоге, что и **`Login_Monitor.ps1`**). По строкам этого файла монитор **не отправляет оповещения** (Telegram и/или Email — в зависимости от настроенных каналов) для отдельных событий Security:
|
||||
|
||||
- по умолчанию — **`4624`/`4625`** (шум от известной рабочей станции, тестового пользователя, фиксированного IP);
|
||||
- с префиксом **`4740:`** / **`lockout:`** — только блокировки учётной записи (**4740**), в т.ч. по IP из IIS ActiveSync;
|
||||
- с префиксом **`all:`** — и входы, и **4740**.
|
||||
|
||||
Подробный синтаксис — в **[README.md](README.md)** (раздел 7) и **`ignore.lst.example`**.
|
||||
|
||||
- **`Deploy-LoginMonitor.ps1` с шары `ignore.lst` не доставляет** — при необходимости создавайте файл локально или копируйте своим способом.
|
||||
- Примеры синтаксиса — в репозитории в файле **`ignore.lst.example`**.
|
||||
|
||||
## Опционально на контроллере домена: блокировки AD (4740)
|
||||
|
||||
Мониторинг **Security 4740** включается только на том КД, где **установлен и запущен** `Login_Monitor.ps1`, и только если короткое имя узла совпадает с **`$LockoutMonitorDomainController`** в скрипте (после деплоя с шары параметры задаются в локальной копии `Login_Monitor.ps1` на КД).
|
||||
|
||||
Типичная настройка на КД:
|
||||
|
||||
- **`$LockoutMonitorDomainController`** — имя этого контроллера (например `DC01`);
|
||||
- **`$NetBiosDomainName`**, **`$ExchangeIisLogPath`** — каталог IIS-логов Exchange (ActiveSync, строки **401**);
|
||||
- **`$ExchangeIisLogMinutesBeforeLockout`** (по умолчанию 30), **`$ExchangeIisLogTailLines`** (5000), **`$ExchangeServerHostForIisExclude`** — при необходимости.
|
||||
|
||||
Deploy с NETLOGON обновляет только **`Login_Monitor.ps1`**; пути IIS и имя КД обычно прописывают **один раз** в локальном файле на КД (или через GPO/Configuration Manager), не на всех членах домена.
|
||||
|
||||
## Heartbeat и «зависший» монитор
|
||||
|
||||
Файл **`Logs\last_heartbeat.txt`** обновляется по интервалу **`$HeartbeatInterval`** (по умолчанию раз в час). Если обновления нет дольше **`$HeartbeatStaleAlertMultiplier` × $HeartbeatInterval`** (по умолчанию **2×1 ч**), уходит оповещение в настроенные каналы (Telegram/Email). Это не заменяет watchdog-задачу, но сигнализирует, что процесс мог зависнуть без обновления heartbeat.
|
||||
|
||||
## Задачи планировщика после `-InstallTasks`
|
||||
|
||||
@@ -191,5 +212,5 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "\\...\Deploy-LoginMonit
|
||||
## Безопасность и замечания
|
||||
|
||||
- ACL на шару: чтение только нужным **компьютерам** / группам; файл **`Login_Monitor.ps1`** может содержать токен — ограничивайте доступ.
|
||||
- DPAPI-секреты привязаны к машине: шифровать на каждой цели или использовать plain на закрытой шаре (см. комментарии в **`Login_Monitor.ps1`**).
|
||||
- DPAPI-секреты привязаны к машине: шифровать на каждой цели (Telegram token/chat id, пароль SMTP) или использовать plain на закрытой шаре (см. комментарии в **`Login_Monitor.ps1`**, helper **`Encrypt-DpapiForRdpMonitor.ps1`**).
|
||||
- Deploy при ошибках пишет в **`deploy.log`** и завершается с кодом **0**, чтобы не блокировать загрузку ОС; проблемы смотрите по логу на ПК.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Запуск: от администратора на ТОМ ЖЕ компьютере, где будет Login_Monitor.ps1.
|
||||
# Результат (Base64) вставьте в $TelegramBotTokenProtectedB64 / $TelegramChatIDProtectedB64.
|
||||
# Результат (Base64) вставьте в $TelegramBotTokenProtectedB64 / $TelegramChatIDProtectedB64 / $MailSmtpPasswordProtectedB64.
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$PlainText
|
||||
)
|
||||
|
||||
+521
-25
@@ -3,6 +3,7 @@
|
||||
Мониторинг логинов/попыток входа с уведомлениями в Telegram
|
||||
.DESCRIPTION
|
||||
Отслеживает события входа в систему (Security 4624/4625) и события RD Gateway (302/303),
|
||||
на заданном КД — блокировки учётных записей (Security 4740) с IP из логов IIS ActiveSync,
|
||||
отправляет уведомления в Telegram, делает ротацию логов, heartbeat в файл и ежедневный отчет.
|
||||
.NOTES
|
||||
Требуется: PowerShell 5.0+, запуск от администратора.
|
||||
@@ -28,6 +29,7 @@ param(
|
||||
[string]$TelegramChatID = '<TELEGRAM_CHAT_ID>',
|
||||
[string]$TelegramBotTokenProtectedB64 = "",
|
||||
[string]$TelegramChatIDProtectedB64 = "",
|
||||
[string]$MailSmtpPasswordProtectedB64 = "",
|
||||
[switch]$Watchdog,
|
||||
[switch]$InstallTasks,
|
||||
[switch]$SkipScheduledTaskMaintenance
|
||||
@@ -69,7 +71,7 @@ $script:MonitorSingletonLockStream = $null
|
||||
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
|
||||
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
|
||||
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
|
||||
$ScriptVersion = "1.4.3"
|
||||
$ScriptVersion = "1.5.3"
|
||||
|
||||
# Логи (все под InstallRoot)
|
||||
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
||||
@@ -80,8 +82,9 @@ $MaxBackupDays = 31
|
||||
$LogRotationHour = 0
|
||||
$LogRotationMinute = 0
|
||||
|
||||
# Heartbeat (только файл)
|
||||
# Heartbeat (файл; при отсутствии обновления > HeartbeatStaleAlertMultiplier × интервал — оповещение)
|
||||
$HeartbeatInterval = 3600
|
||||
$HeartbeatStaleAlertMultiplier = 2
|
||||
$HeartbeatFile = Join-Path $script:InstallRoot "Logs\last_heartbeat.txt"
|
||||
$DeployUpdateMarkerFile = Join-Path $script:InstallRoot "deploy_last_update.txt"
|
||||
# Построчные правила подавления уведомлений Security 4624/4625 (см. ignore.lst.example в репозитории).
|
||||
@@ -148,6 +151,27 @@ $IgnoreAdvapiNetworkLogonSourceIps = @(
|
||||
)
|
||||
$IgnoreAdvapiNetworkLogonProcessContains = "Advapi"
|
||||
|
||||
# Блокировка учётной записи AD (Security 4740) + IP клиента из логов IIS ActiveSync.
|
||||
# Мониторинг включается только если скрипт запущен на узле, имя которого совпадает с $LockoutMonitorDomainController.
|
||||
$LockoutMonitorDomainController = ""
|
||||
$NetBiosDomainName = ""
|
||||
$ExchangeIisLogPath = ""
|
||||
$ExchangeServerHostForIisExclude = ""
|
||||
$ExchangeIisLogTailLines = 5000
|
||||
# Окно поиска IP в IIS: только строки за N минут до события 4740 (локальное время сервера IIS).
|
||||
$ExchangeIisLogMinutesBeforeLockout = 30
|
||||
|
||||
# Очередь оповещений: telegram, email (или tg, mail). Пусто = авто: настроенные каналы, порядок telegram → email.
|
||||
$NotifyOrder = ""
|
||||
$MailSmtpHost = ""
|
||||
$MailSmtpPort = 587
|
||||
$MailSmtpUser = ""
|
||||
$MailSmtpPassword = ""
|
||||
$MailFrom = ""
|
||||
$MailTo = ""
|
||||
$MailSmtpStartTls = $true
|
||||
$MailSmtpSsl = $false
|
||||
|
||||
# ============================================
|
||||
# ИНИЦИАЛИЗАЦИЯ
|
||||
# ============================================
|
||||
@@ -529,6 +553,70 @@ if (-not [string]::IsNullOrWhiteSpace($TelegramChatIDProtectedB64)) {
|
||||
if ($TelegramBotToken -eq '<TELEGRAM_BOT_TOKEN>') { $TelegramBotToken = "" }
|
||||
if ($TelegramChatID -eq '<TELEGRAM_CHAT_ID>') { $TelegramChatID = "" }
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($MailSmtpPasswordProtectedB64)) {
|
||||
try {
|
||||
$MailSmtpPassword = Unprotect-RdpMonitorDpapiB64 -Base64 $MailSmtpPasswordProtectedB64
|
||||
} catch {
|
||||
Write-Host "Ошибка расшифровки MailSmtpPassword (DPAPI): $($_.Exception.Message)"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
function Test-NotifyTelegramConfigured {
|
||||
return (-not [string]::IsNullOrWhiteSpace($TelegramBotToken)) -and
|
||||
(-not [string]::IsNullOrWhiteSpace($TelegramChatID))
|
||||
}
|
||||
|
||||
function Test-NotifyEmailConfigured {
|
||||
return (-not [string]::IsNullOrWhiteSpace($MailSmtpHost)) -and
|
||||
(-not [string]::IsNullOrWhiteSpace($MailFrom)) -and
|
||||
(-not [string]::IsNullOrWhiteSpace($MailTo))
|
||||
}
|
||||
|
||||
function Get-NotifyOrderChannels {
|
||||
$configured = [System.Collections.Generic.List[string]]::new()
|
||||
if (Test-NotifyTelegramConfigured) { $configured.Add('telegram') | Out-Null }
|
||||
if (Test-NotifyEmailConfigured) { $configured.Add('email') | Out-Null }
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($NotifyOrder)) {
|
||||
return @($configured)
|
||||
}
|
||||
|
||||
$requested = [System.Collections.Generic.List[string]]::new()
|
||||
foreach ($part in ($NotifyOrder -split '[,\s;]+')) {
|
||||
$p = $part.Trim().ToLowerInvariant()
|
||||
if ([string]::IsNullOrWhiteSpace($p)) { continue }
|
||||
$channel = switch -Regex ($p) {
|
||||
'^(tg|telegram)$' { 'telegram' }
|
||||
'^(mail|email|e-mail)$' { 'email' }
|
||||
default {
|
||||
Write-Log "NotifyOrder: неизвестный канал '$part' (ожидается telegram/tg или email/mail)"
|
||||
$null
|
||||
}
|
||||
}
|
||||
if ($null -eq $channel) { continue }
|
||||
if ($configured.Contains($channel) -and -not $requested.Contains($channel)) {
|
||||
$requested.Add($channel) | Out-Null
|
||||
}
|
||||
}
|
||||
return @($requested)
|
||||
}
|
||||
|
||||
function Get-NotifyChainHuman {
|
||||
$channels = @(Get-NotifyOrderChannels)
|
||||
if ($channels.Count -eq 0) {
|
||||
return 'нет (ни Telegram, ни SMTP не настроены)'
|
||||
}
|
||||
$labels = foreach ($ch in $channels) {
|
||||
switch ($ch) {
|
||||
'telegram' { 'Telegram' }
|
||||
'email' { 'Email (SMTP)' }
|
||||
default { $ch }
|
||||
}
|
||||
}
|
||||
return ($labels -join ' → ')
|
||||
}
|
||||
|
||||
function ConvertTo-TelegramHtml {
|
||||
param([string]$Text)
|
||||
if ($null -eq $Text) { return '' }
|
||||
@@ -561,6 +649,10 @@ function Send-TelegramMessage {
|
||||
}
|
||||
|
||||
function Test-TelegramConnection {
|
||||
if (-not (Test-NotifyTelegramConfigured)) {
|
||||
Write-Log "Telegram: канал не настроен, проверка пропущена."
|
||||
return $false
|
||||
}
|
||||
Write-Log "Проверка подключения к Telegram API..."
|
||||
try {
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
|
||||
@@ -577,6 +669,105 @@ function Test-TelegramConnection {
|
||||
return $false
|
||||
}
|
||||
|
||||
function ConvertTo-EmailHtmlBody {
|
||||
param([string]$TelegramHtmlMessage)
|
||||
$inner = [string]$TelegramHtmlMessage
|
||||
if ([string]::IsNullOrEmpty($inner)) { $inner = '' }
|
||||
$inner = $inner -replace "`r`n", "<br>`r`n"
|
||||
return @"
|
||||
<html>
|
||||
<body style="font-family:Segoe UI,Arial,sans-serif;font-size:14px;line-height:1.4;">
|
||||
$inner
|
||||
</body>
|
||||
</html>
|
||||
"@
|
||||
}
|
||||
|
||||
function Send-EmailNotification {
|
||||
param(
|
||||
[string]$Message,
|
||||
[string]$Subject = "RDP Login Monitor"
|
||||
)
|
||||
|
||||
if (-not (Test-NotifyEmailConfigured)) {
|
||||
Write-Log "Email: SMTP не настроен (нужны MailSmtpHost, MailFrom, MailTo)"
|
||||
return $false
|
||||
}
|
||||
|
||||
try {
|
||||
$toList = @($MailTo -split '[,;]' | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||
if ($toList.Count -eq 0) {
|
||||
Write-Log "Email: MailTo пуст или некорректен"
|
||||
return $false
|
||||
}
|
||||
|
||||
$mailParams = @{
|
||||
To = $toList
|
||||
From = $MailFrom.Trim()
|
||||
Subject = $Subject
|
||||
Body = (ConvertTo-EmailHtmlBody -TelegramHtmlMessage $Message)
|
||||
BodyAsHtml = $true
|
||||
SmtpServer = $MailSmtpHost.Trim()
|
||||
Port = [int]$MailSmtpPort
|
||||
Encoding = [System.Text.Encoding]::UTF8
|
||||
ErrorAction = 'Stop'
|
||||
}
|
||||
if ($MailSmtpSsl -or $MailSmtpStartTls) {
|
||||
$mailParams['UseSsl'] = $true
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($MailSmtpUser)) {
|
||||
$securePass = if ([string]::IsNullOrWhiteSpace($MailSmtpPassword)) {
|
||||
New-Object System.Security.SecureString
|
||||
} else {
|
||||
ConvertTo-SecureString $MailSmtpPassword -AsPlainText -Force
|
||||
}
|
||||
$mailParams['Credential'] = New-Object System.Management.Automation.PSCredential($MailSmtpUser.Trim(), $securePass)
|
||||
}
|
||||
|
||||
Send-MailMessage @mailParams
|
||||
return $true
|
||||
} catch {
|
||||
Write-Log "Ошибка отправки Email: $($_.Exception.Message)"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Test-MailSmtpConnection {
|
||||
if (-not (Test-NotifyEmailConfigured)) {
|
||||
Write-Log "Email: канал не настроен, проверка пропущена."
|
||||
return $false
|
||||
}
|
||||
Write-Log "SMTP: $($MailSmtpHost):$MailSmtpPort (STARTTLS=$MailSmtpStartTls, SSL=$MailSmtpSsl), From=$MailFrom, To=$MailTo"
|
||||
if (-not [string]::IsNullOrWhiteSpace($MailSmtpUser) -and [string]::IsNullOrWhiteSpace($MailSmtpPassword)) {
|
||||
Write-Log "SMTP: задан MailSmtpUser, но пароль пуст (возможна ошибка при отправке)."
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
function Send-MonitorNotification {
|
||||
param(
|
||||
[string]$Message,
|
||||
[string]$EmailSubject = "RDP Login Monitor"
|
||||
)
|
||||
|
||||
$channels = @(Get-NotifyOrderChannels)
|
||||
if ($channels.Count -eq 0) {
|
||||
Write-Log "Оповещение не отправлено: нет настроенных каналов (Telegram и/или SMTP)"
|
||||
return $false
|
||||
}
|
||||
|
||||
$anyOk = $false
|
||||
foreach ($ch in $channels) {
|
||||
$ok = switch ($ch) {
|
||||
'telegram' { Send-TelegramMessage -Message $Message }
|
||||
'email' { Send-EmailNotification -Message $Message -Subject $EmailSubject }
|
||||
default { $false }
|
||||
}
|
||||
if ($ok) { $anyOk = $true }
|
||||
}
|
||||
return $anyOk
|
||||
}
|
||||
|
||||
function Test-Administrator {
|
||||
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
|
||||
@@ -608,6 +799,8 @@ try {
|
||||
|
||||
$script:IsWorkstation = $false
|
||||
$script:OsInstallKindLabel = ""
|
||||
$script:MonitorStartedAt = $null
|
||||
$script:HeartbeatStaleAlertActive = $false
|
||||
|
||||
function Enable-SecurityAudit {
|
||||
Write-Log "Checking security audit (auditpol) settings..."
|
||||
@@ -948,7 +1141,7 @@ function Send-Heartbeat {
|
||||
}
|
||||
$ignoreEntries = @(Get-RdpMonitorIgnoreListEntries)
|
||||
if ($ignoreEntries.Count -gt 0) {
|
||||
$message += "`r`n🚫 <b>Игнорируются:</b> Security 4624/4625 по правилам ignore.lst`r`n"
|
||||
$message += "`r`n🚫 <b>Игнорируются:</b> по правилам ignore.lst (4624/4625 и/или 4740)`r`n"
|
||||
foreach ($e in $ignoreEntries) {
|
||||
$v = ConvertTo-TelegramHtml ([string]$e.Value)
|
||||
$kindLabel = switch ($e.Kind) {
|
||||
@@ -963,6 +1156,9 @@ function Send-Heartbeat {
|
||||
} else {
|
||||
$message += "`r`n🚫 <b>Игнорируются:</b> не задано (ignore.lst отсутствует или пуст)."
|
||||
}
|
||||
$notifyChain = Get-NotifyChainHuman
|
||||
$message += "`r`n📢 <b>Каналы уведомлений:</b> $(ConvertTo-TelegramHtml $notifyChain)"
|
||||
$message += "`r`n💓 <b>Heartbeat:</b> файл каждые $HeartbeatInterval с; оповещение, если нет обновления > $($HeartbeatStaleAlertMultiplier)× интервал."
|
||||
if (Test-RDSDeploymentPresent) {
|
||||
$message += "`r`n🔐 <b>RDS (хост сессий):</b> обнаружены компоненты RDS помимо чистого шлюза — в мониторинг входят входы по RDP/RDS на этом узле (Security 4624/4625, типы входа по настройке скрипта)."
|
||||
}
|
||||
@@ -974,10 +1170,67 @@ function Send-Heartbeat {
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
Send-TelegramMessage -Message $message | Out-Null
|
||||
Write-Log "Отправлено уведомление о запуске скрипта"
|
||||
if (Test-Lockout4740MonitoringActive) {
|
||||
$message += "`r`n🔒 <b>Блокировки AD:</b> на этом КД отслеживается Security <b>4740</b> (блокировка учётной записи)."
|
||||
if (-not [string]::IsNullOrWhiteSpace($ExchangeIisLogPath)) {
|
||||
$message += " При событии — IP из логов IIS ActiveSync (<code>ExchangeIisLogPath</code>)."
|
||||
} else {
|
||||
$message += " IP из IIS не заданы (<code>ExchangeIisLogPath</code> пуст)."
|
||||
}
|
||||
}
|
||||
Send-MonitorNotification -Message $message -EmailSubject "RDP Login Monitor: запуск" | Out-Null
|
||||
Write-Log "Отправлено уведомление о запуске скрипта (каналы: $notifyChain)"
|
||||
} else {
|
||||
Write-TextFileUtf8Bom -Path $HeartbeatFile -Text $timestamp
|
||||
$script:HeartbeatStaleAlertActive = $false
|
||||
}
|
||||
}
|
||||
|
||||
function Get-LastHeartbeatTimestamp {
|
||||
if (-not (Test-Path -LiteralPath $HeartbeatFile)) { return $null }
|
||||
try {
|
||||
$txt = (Get-Content -LiteralPath $HeartbeatFile -ErrorAction Stop | Select-Object -First 1)
|
||||
if ([string]::IsNullOrWhiteSpace($txt)) { return $null }
|
||||
return [datetime]::ParseExact($txt.Trim(), 'dd.MM.yyyy HH:mm:ss', $null)
|
||||
} catch {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
function Test-AndSendHeartbeatStaleAlert {
|
||||
if ($null -eq $script:MonitorStartedAt) { return }
|
||||
$thresholdSec = [double]($HeartbeatInterval * $HeartbeatStaleAlertMultiplier)
|
||||
if (((Get-Date) - $script:MonitorStartedAt).TotalSeconds -lt $thresholdSec) { return }
|
||||
|
||||
$lastHb = Get-LastHeartbeatTimestamp
|
||||
$isStale = $false
|
||||
if ($null -eq $lastHb) {
|
||||
$isStale = $true
|
||||
} elseif (((Get-Date) - $lastHb).TotalSeconds -gt $thresholdSec) {
|
||||
$isStale = $true
|
||||
}
|
||||
|
||||
if (-not $isStale) {
|
||||
if ($script:HeartbeatStaleAlertActive) {
|
||||
$script:HeartbeatStaleAlertActive = $false
|
||||
}
|
||||
return
|
||||
}
|
||||
if ($script:HeartbeatStaleAlertActive) { return }
|
||||
|
||||
$hHost = ConvertTo-TelegramHtml $env:COMPUTERNAME
|
||||
$hThreshold = ConvertTo-TelegramHtml ([int]$thresholdSec)
|
||||
$lastTxt = if ($null -eq $lastHb) { 'нет данных' } else { $lastHb.ToString('dd.MM.yyyy HH:mm:ss') }
|
||||
$hLast = ConvertTo-TelegramHtml $lastTxt
|
||||
$msg = "<b>⚠️ Heartbeat монитора не обновлялся</b>`r`n"
|
||||
$msg += "🖥️ Сервер: $hHost`r`n"
|
||||
$msg += "⏱️ Порог: $hThreshold с ($HeartbeatStaleAlertMultiplier × интервал $HeartbeatInterval с)`r`n"
|
||||
$msg += "📄 Последний heartbeat: $hLast`r`n"
|
||||
$msg += "<i>Проверьте процесс Login_Monitor.ps1 и задачи планировщика RDP-Login-Monitor / Watchdog.</i>"
|
||||
|
||||
if (Send-MonitorNotification -Message $msg -EmailSubject 'RDP Login Monitor: нет heartbeat') {
|
||||
$script:HeartbeatStaleAlertActive = $true
|
||||
Write-Log "Отправлено оповещение: heartbeat не обновлялся дольше $thresholdSec с"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -992,7 +1245,7 @@ function Send-StopNotification {
|
||||
$message += "🕐 Время остановки: $timestamp`r`n"
|
||||
$message += "📋 Причина: $hReason"
|
||||
|
||||
Send-TelegramMessage -Message $message | Out-Null
|
||||
Send-MonitorNotification -Message $message -EmailSubject "RDP Login Monitor: остановка" | Out-Null
|
||||
Write-Log "Уведомление об остановке отправлено: $Reason"
|
||||
}
|
||||
|
||||
@@ -1149,8 +1402,18 @@ function Parse-RdpMonitorIgnoreListLine {
|
||||
if ($line[0] -eq '#' -or $line[0] -eq ';') { return $null }
|
||||
if ($line.StartsWith([char]0xFEFF)) { $line = $line.TrimStart([char]0xFEFF) }
|
||||
|
||||
$scopes = @('4624', '4625')
|
||||
if ($line -match '^(?i)(4740|lockout|блокир)\s*:\s*(.+)$') {
|
||||
$scopes = @('4740')
|
||||
$line = $Matches[2].Trim()
|
||||
} elseif ($line -match '^(?i)(all|\*)\s*:\s*(.+)$') {
|
||||
$scopes = @('4624', '4625', '4740')
|
||||
$line = $Matches[2].Trim()
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($line)) { return $null }
|
||||
|
||||
if ($line -notmatch ':') {
|
||||
return [pscustomobject]@{ Kind = 'Any'; Value = $line }
|
||||
return [pscustomobject]@{ Kind = 'Any'; Value = $line; Scopes = $scopes }
|
||||
}
|
||||
|
||||
$idx = $line.IndexOf(':')
|
||||
@@ -1159,16 +1422,16 @@ function Parse-RdpMonitorIgnoreListLine {
|
||||
if ([string]::IsNullOrWhiteSpace($right)) { return $null }
|
||||
|
||||
if ($left -match '(?i)(рабоч|workstation|wks)') {
|
||||
return [pscustomobject]@{ Kind = 'Workstation'; Value = $right }
|
||||
return [pscustomobject]@{ Kind = 'Workstation'; Value = $right; Scopes = $scopes }
|
||||
}
|
||||
if ($left -match '(?i)(польз|username|subject|account|target\s*user|\buser\b)') {
|
||||
return [pscustomobject]@{ Kind = 'User'; Value = $right }
|
||||
return [pscustomobject]@{ Kind = 'User'; Value = $right; Scopes = $scopes }
|
||||
}
|
||||
if ($left -match '(?i)(\bip\b|ip\s*адрес|ipaddress|адрес\s*ip)') {
|
||||
return [pscustomobject]@{ Kind = 'Ip'; Value = $right }
|
||||
return [pscustomobject]@{ Kind = 'Ip'; Value = $right; Scopes = $scopes }
|
||||
}
|
||||
|
||||
return [pscustomobject]@{ Kind = 'Any'; Value = $right }
|
||||
return [pscustomobject]@{ Kind = 'Any'; Value = $right; Scopes = $scopes }
|
||||
}
|
||||
|
||||
function Get-RdpMonitorIgnoreListEntries {
|
||||
@@ -1195,11 +1458,12 @@ function Get-RdpMonitorIgnoreListEntries {
|
||||
$nUser = @($arr | Where-Object { $_.Kind -eq 'User' }).Count
|
||||
$nWks = @($arr | Where-Object { $_.Kind -eq 'Workstation' }).Count
|
||||
$nAny = @($arr | Where-Object { $_.Kind -eq 'Any' }).Count
|
||||
$n4740 = @($arr | Where-Object { $_.Scopes -contains '4740' }).Count
|
||||
$nTotal = $arr.Count
|
||||
if ($nTotal -eq 0) {
|
||||
Write-Log "ignore.lst обновлён: список правил пуст, игнорирование по файлу для Security 4624/4625 не задаётся."
|
||||
Write-Log "ignore.lst обновлён: список правил пуст."
|
||||
} else {
|
||||
Write-Log ("ignore.lst обновлён: добавлено игнорирование событий 4624/4625 по IP ({0}), пользователю ({1}), рабочей станции ({2}); универсальных правил ({3}). Всего записей: {4}." -f $nIp, $nUser, $nWks, $nAny, $nTotal)
|
||||
Write-Log ("ignore.lst обновлён: записей {0} (IP {1}, user {2}, wks {3}, any {4}; затрагивают 4740: {5})." -f $nTotal, $nIp, $nUser, $nWks, $nAny, $n4740)
|
||||
}
|
||||
return $script:IgnoreListCache
|
||||
} catch {
|
||||
@@ -1212,13 +1476,25 @@ function Get-RdpMonitorIgnoreListEntries {
|
||||
|
||||
function Test-RdpMonitorIgnoreListMatch {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$EventId,
|
||||
[string]$Username,
|
||||
[string]$ComputerName,
|
||||
[string]$SourceIP
|
||||
[string]$SourceIP,
|
||||
[string[]]$AdditionalIps = @()
|
||||
)
|
||||
$entries = @(Get-RdpMonitorIgnoreListEntries)
|
||||
$entries = @(Get-RdpMonitorIgnoreListEntries | Where-Object { $_.Scopes -contains $EventId })
|
||||
if ($entries.Count -eq 0) { return $false }
|
||||
|
||||
$ipsToCheck = [System.Collections.Generic.List[string]]::new()
|
||||
if (-not [string]::IsNullOrWhiteSpace($SourceIP) -and $SourceIP -ne '-') {
|
||||
$ipsToCheck.Add($SourceIP.Trim()) | Out-Null
|
||||
}
|
||||
foreach ($ip in $AdditionalIps) {
|
||||
if ([string]::IsNullOrWhiteSpace($ip)) { continue }
|
||||
$t = $ip.Trim()
|
||||
if (-not $ipsToCheck.Contains($t)) { $ipsToCheck.Add($t) | Out-Null }
|
||||
}
|
||||
|
||||
foreach ($e in $entries) {
|
||||
$v = [string]$e.Value
|
||||
if ([string]::IsNullOrWhiteSpace($v)) { continue }
|
||||
@@ -1228,19 +1504,20 @@ function Test-RdpMonitorIgnoreListMatch {
|
||||
if (Test-RdpMonitorUsernameMatchesToken -Username $Username -Token $v) { return $true }
|
||||
}
|
||||
'Workstation' {
|
||||
if ($EventId -eq '4740') { continue }
|
||||
if (-not [string]::IsNullOrWhiteSpace($ComputerName) -and $ComputerName -ne '-' -and ($ComputerName -ieq $v)) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
'Ip' {
|
||||
if (-not [string]::IsNullOrWhiteSpace($SourceIP) -and $SourceIP -ne '-' -and ($SourceIP -ieq $v)) {
|
||||
return $true
|
||||
foreach ($checkIp in $ipsToCheck) {
|
||||
if ($checkIp -ieq $v) { return $true }
|
||||
}
|
||||
}
|
||||
'Any' {
|
||||
if (Test-RdpMonitorStringLooksLikeIPv4 $v) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($SourceIP) -and $SourceIP -ne '-' -and ($SourceIP -ieq $v)) {
|
||||
return $true
|
||||
foreach ($checkIp in $ipsToCheck) {
|
||||
if ($checkIp -ieq $v) { return $true }
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -1248,9 +1525,11 @@ function Test-RdpMonitorIgnoreListMatch {
|
||||
if (Test-RdpMonitorUsernameMatchesToken -Username $Username -Token $v) { return $true }
|
||||
continue
|
||||
}
|
||||
if ($EventId -ne '4740') {
|
||||
if (-not [string]::IsNullOrWhiteSpace($ComputerName) -and $ComputerName -ne '-' -and ($ComputerName -ieq $v)) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
if (Test-RdpMonitorUsernameMatchesToken -Username $Username -Token $v) { return $true }
|
||||
}
|
||||
}
|
||||
@@ -1258,6 +1537,14 @@ function Test-RdpMonitorIgnoreListMatch {
|
||||
return $false
|
||||
}
|
||||
|
||||
function Should-IgnoreLockout4740Event {
|
||||
param(
|
||||
[string]$Username,
|
||||
[string[]]$IisClientIps = @()
|
||||
)
|
||||
return Test-RdpMonitorIgnoreListMatch -EventId '4740' -Username $Username -AdditionalIps $IisClientIps
|
||||
}
|
||||
|
||||
function Should-IgnoreEvent {
|
||||
param(
|
||||
[string]$Username,
|
||||
@@ -1316,7 +1603,8 @@ function Should-IgnoreEvent {
|
||||
}
|
||||
|
||||
if ($EventID -in 4624, 4625) {
|
||||
if (Test-RdpMonitorIgnoreListMatch -Username $Username -ComputerName $ComputerName -SourceIP $SourceIP) {
|
||||
if (Test-RdpMonitorIgnoreListMatch -EventId ([string]$EventID) -Username $Username `
|
||||
-ComputerName $ComputerName -SourceIP $SourceIP) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
@@ -1602,7 +1890,7 @@ function Send-DailyReport {
|
||||
} else {
|
||||
$message += "`r`n<i>Список пользователей недоступен (quser пуст или недостаточно прав).</i>"
|
||||
}
|
||||
Send-TelegramMessage -Message $message | Out-Null
|
||||
Send-MonitorNotification -Message $message -EmailSubject "RDP Login Monitor: ежедневный отчёт" | Out-Null
|
||||
Write-TextFileUtf8Bom -Path $LastReportFile -Text ((Get-Date).ToString("yyyy-MM-dd HH:mm:ss"))
|
||||
Write-Log "Ежедневный отчет отправлен"
|
||||
return $true
|
||||
@@ -1635,6 +1923,150 @@ function Check-AndSendDailyReport {
|
||||
return (Get-NextLocalSlotBoundary -Hour $DailyReportHour -Minute $DailyReportMinute)
|
||||
}
|
||||
|
||||
function Test-Lockout4740MonitoringActive {
|
||||
if ([string]::IsNullOrWhiteSpace($LockoutMonitorDomainController)) { return $false }
|
||||
$configured = ($LockoutMonitorDomainController -split '\.')[0].Trim()
|
||||
$local = ($env:COMPUTERNAME -split '\.')[0].Trim()
|
||||
return ($configured -ieq $local)
|
||||
}
|
||||
|
||||
function Get-Lockout4740EventInfo {
|
||||
param($Event)
|
||||
$info = [pscustomobject]@{
|
||||
TimeCreated = $Event.TimeCreated
|
||||
Username = ""
|
||||
Domain = ""
|
||||
CallerComputer = ""
|
||||
}
|
||||
try {
|
||||
$map = Get-EventDataMap -Event $Event
|
||||
$info.Username = Get-FirstNonEmptyMapValue -DataMap $map -Keys @(
|
||||
'TargetUserName', 'SamAccountName', 'AccountName'
|
||||
)
|
||||
$info.Domain = Get-FirstNonEmptyMapValue -DataMap $map -Keys @(
|
||||
'TargetDomainName', 'TargetAccountDomain', 'AccountDomain'
|
||||
)
|
||||
$info.CallerComputer = Get-FirstNonEmptyMapValue -DataMap $map -Keys @(
|
||||
'CallerComputerName', 'WorkstationName', 'Workstation'
|
||||
)
|
||||
} catch {
|
||||
Write-Log "Ошибка разбора XML 4740: $($_.Exception.Message)"
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($info.Username) -and $Event.Properties.Count -ge 1) {
|
||||
$info.Username = [string]$Event.Properties[0].Value
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($info.Domain) -and $Event.Properties.Count -ge 2) {
|
||||
$info.Domain = [string]$Event.Properties[1].Value
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($info.CallerComputer) -and $Event.Properties.Count -ge 4) {
|
||||
$info.CallerComputer = [string]$Event.Properties[3].Value
|
||||
}
|
||||
return $info
|
||||
}
|
||||
|
||||
function Get-ExchangeActiveSyncIpsFromIisLog {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$SamAccountName,
|
||||
[string]$DomainNetBios = "",
|
||||
[Parameter(Mandatory = $true)][datetime]$ReferenceTime
|
||||
)
|
||||
if ([string]::IsNullOrWhiteSpace($ExchangeIisLogPath)) { return @() }
|
||||
$minutes = [int]$ExchangeIisLogMinutesBeforeLockout
|
||||
if ($minutes -lt 1) { $minutes = 1 }
|
||||
$windowStart = $ReferenceTime.AddMinutes(-$minutes)
|
||||
$windowEnd = $ReferenceTime.AddMinutes(2)
|
||||
|
||||
$logDir = $ExchangeIisLogPath.TrimEnd('\')
|
||||
$logFile = Join-Path $logDir ("u_ex" + $ReferenceTime.ToUniversalTime().ToString("yyMMdd") + ".log")
|
||||
if (-not (Test-Path -LiteralPath $logFile)) {
|
||||
Write-Log "IIS: файл лога не найден: $logFile"
|
||||
return @()
|
||||
}
|
||||
$domainPart = if ([string]::IsNullOrWhiteSpace($DomainNetBios)) { $NetBiosDomainName } else { $DomainNetBios }
|
||||
$userPattern1 = if ([string]::IsNullOrWhiteSpace($domainPart)) {
|
||||
$SamAccountName
|
||||
} else {
|
||||
"User=$domainPart%5C" + $SamAccountName
|
||||
}
|
||||
$userPattern2 = if ([string]::IsNullOrWhiteSpace($domainPart)) {
|
||||
$SamAccountName
|
||||
} else {
|
||||
"$domainPart\" + $SamAccountName
|
||||
}
|
||||
$excludeHosts = @('127.0.0.1', '::1')
|
||||
if (-not [string]::IsNullOrWhiteSpace($ExchangeServerHostForIisExclude)) {
|
||||
$excludeHosts += $ExchangeServerHostForIisExclude.Trim()
|
||||
}
|
||||
$detected = [System.Collections.Generic.List[string]]::new()
|
||||
try {
|
||||
$lines = Get-Content -LiteralPath $logFile -Tail $ExchangeIisLogTailLines -ErrorAction Stop
|
||||
foreach ($line in $lines) {
|
||||
if ($line.StartsWith('#')) { continue }
|
||||
if ($line -notlike '*401 *' -or $line -notlike '*ActiveSync*') { continue }
|
||||
if ($line -notlike "*$userPattern1*" -and $line -notlike "*$userPattern2*") { continue }
|
||||
|
||||
$lineTime = $null
|
||||
if ($line -match '^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})') {
|
||||
try {
|
||||
$lineTime = [datetime]::ParseExact(
|
||||
"$($Matches[1]) $($Matches[2])",
|
||||
'yyyy-MM-dd HH:mm:ss',
|
||||
$null
|
||||
)
|
||||
} catch { }
|
||||
}
|
||||
if ($null -ne $lineTime -and ($lineTime -lt $windowStart -or $lineTime -gt $windowEnd)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ($line -notmatch '(?:\d{1,3}\.){3}\d{1,3}') { continue }
|
||||
$ip = $Matches[0]
|
||||
if ($excludeHosts -contains $ip) { continue }
|
||||
if (-not $detected.Contains($ip)) { $detected.Add($ip) | Out-Null }
|
||||
}
|
||||
} catch {
|
||||
Write-Log "IIS: ошибка чтения $logFile : $($_.Exception.Message)"
|
||||
}
|
||||
return @($detected)
|
||||
}
|
||||
|
||||
function Format-Lockout4740TelegramMessage {
|
||||
param(
|
||||
[string]$Username,
|
||||
[string]$Domain,
|
||||
[datetime]$TimeCreated,
|
||||
[string[]]$IisClientIps = @()
|
||||
)
|
||||
$domainLabel = if ([string]::IsNullOrWhiteSpace($Domain)) { $NetBiosDomainName } else { $Domain }
|
||||
$accountDisplay = if ([string]::IsNullOrWhiteSpace($domainLabel)) {
|
||||
$Username
|
||||
} else {
|
||||
"$domainLabel\$Username"
|
||||
}
|
||||
$hUser = ConvertTo-TelegramHtml $accountDisplay
|
||||
$hTime = ConvertTo-TelegramHtml ($TimeCreated.ToString('dd.MM.yyyy HH:mm:ss'))
|
||||
|
||||
$message = "<b>🔒 Блокировка учётной записи AD (4740)</b>`r`n"
|
||||
$message += "👤 Пользователь: $hUser`r`n"
|
||||
$message += "🕐 Время: $hTime`r`n"
|
||||
|
||||
if ($IisClientIps.Count -gt 0) {
|
||||
$message += "`r`n<b>🌐 IP (попытки ActiveSync, 401):</b>`r`n"
|
||||
foreach ($ip in $IisClientIps) {
|
||||
$netType = if ($ip -like '192.168.*' -or $ip -like '10.*' -or $ip -like '172.1[6-9].*' -or $ip -like '172.2[0-9].*' -or $ip -like '172.3[0-1].*') {
|
||||
'внутренний'
|
||||
} else {
|
||||
'внешний'
|
||||
}
|
||||
$message += ('• {0} ({1})' -f (ConvertTo-TelegramHtml $ip), $netType) + "`r`n"
|
||||
}
|
||||
} elseif (-not [string]::IsNullOrWhiteSpace($ExchangeIisLogPath)) {
|
||||
$message += "`r`n<i>IP в IIS ActiveSync не найдены (окно $ExchangeIisLogMinutesBeforeLockout мин до блокировки, 401).</i>`r`n"
|
||||
}
|
||||
|
||||
return $message
|
||||
}
|
||||
|
||||
function Start-LoginMonitor {
|
||||
param(
|
||||
[int]$MonitorInterval = 5,
|
||||
@@ -1655,6 +2087,20 @@ function Start-LoginMonitor {
|
||||
Write-Log "Режим сервера: Security — LogonType 2, 3, 10"
|
||||
}
|
||||
Write-Log "========================================"
|
||||
Write-Log "Каналы уведомлений: $(Get-NotifyChainHuman)"
|
||||
|
||||
$lockout4740Enabled = Test-Lockout4740MonitoringActive
|
||||
if ($lockout4740Enabled) {
|
||||
Write-Log "Мониторинг блокировок AD (4740) включён на этом КД ($LockoutMonitorDomainController)."
|
||||
if (-not [string]::IsNullOrWhiteSpace($ExchangeIisLogPath)) {
|
||||
Write-Log "Обогащение: IIS ActiveSync — $ExchangeIisLogPath (окно ${ExchangeIisLogMinutesBeforeLockout} мин до 4740)"
|
||||
}
|
||||
} elseif (-not [string]::IsNullOrWhiteSpace($LockoutMonitorDomainController)) {
|
||||
Write-Log "Мониторинг 4740 задан для КД '$LockoutMonitorDomainController', но этот узел — $env:COMPUTERNAME (блокировки не отслеживаются)."
|
||||
}
|
||||
|
||||
$script:MonitorStartedAt = Get-Date
|
||||
$script:HeartbeatStaleAlertActive = $false
|
||||
|
||||
Cleanup-OldLogs
|
||||
Send-Heartbeat -IsStartup
|
||||
@@ -1674,10 +2120,15 @@ function Start-LoginMonitor {
|
||||
$lastCheckTime = (Get-Date).AddSeconds(-10)
|
||||
$lastGatewayCheckTime = (Get-Date).AddSeconds(-10)
|
||||
$lastRcmCheckTime = (Get-Date).AddSeconds(-10)
|
||||
$lastLockout4740CheckTime = (Get-Date).AddSeconds(-10)
|
||||
$monitorEvents = @(4624, 4625, 4648)
|
||||
|
||||
while ($true) {
|
||||
try {
|
||||
# ignore.lst: сверка mtime и лог при изменении файла.
|
||||
[void](Get-RdpMonitorIgnoreListEntries)
|
||||
Test-AndSendHeartbeatStaleAlert
|
||||
|
||||
$events = Get-WinEvent -FilterHashtable @{
|
||||
LogName = 'Security'
|
||||
ID = $monitorEvents
|
||||
@@ -1730,7 +2181,8 @@ function Start-LoginMonitor {
|
||||
-SecurityLogComputerName $event.MachineName
|
||||
|
||||
Write-Log "Notify: ID=$($event.Id) User=$($eventInfo.Username) LT=$($eventInfo.LogonType) IP=$($eventInfo.SourceIP)"
|
||||
Send-TelegramMessage -Message $formattedMessage | Out-Null
|
||||
Send-MonitorNotification -Message $formattedMessage `
|
||||
-EmailSubject "RDP Login Monitor: вход (ID $($event.Id))" | Out-Null
|
||||
}
|
||||
}
|
||||
$lastCheckTime = ($events | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1)
|
||||
@@ -1756,7 +2208,8 @@ function Start-LoginMonitor {
|
||||
-ErrorCode $ei.ErrorCode `
|
||||
-TimeCreated $ei.TimeCreated
|
||||
Write-Log "Notify RDG: ID=$($event.Id) User=$($ei.Username)"
|
||||
Send-TelegramMessage -Message $msg | Out-Null
|
||||
Send-MonitorNotification -Message $msg `
|
||||
-EmailSubject "RDP Login Monitor: RD Gateway ($($event.Id))" | Out-Null
|
||||
}
|
||||
$lastGatewayCheckTime = ($gatewayEvents | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1)
|
||||
}
|
||||
@@ -1780,12 +2233,45 @@ function Start-LoginMonitor {
|
||||
$msg = Format-Rcm1149Event -Username $rcmInfo.Username -ClientIP $rcmInfo.ClientIP `
|
||||
-TimeCreated $rcmInfo.TimeCreated -SecurityLogComputerName $event.MachineName
|
||||
Write-Log "Notify RCM 1149: User=$($rcmInfo.Username) IP=$($rcmInfo.ClientIP)"
|
||||
Send-TelegramMessage -Message $msg | Out-Null
|
||||
Send-MonitorNotification -Message $msg `
|
||||
-EmailSubject "RDP Login Monitor: RDP 1149" | Out-Null
|
||||
}
|
||||
$lastRcmCheckTime = ($rcmEvents | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1)
|
||||
}
|
||||
}
|
||||
|
||||
if ($lockout4740Enabled) {
|
||||
$lockoutEvents = Get-WinEvent -FilterHashtable @{
|
||||
LogName = 'Security'
|
||||
ID = 4740
|
||||
StartTime = $lastLockout4740CheckTime
|
||||
} -ErrorAction SilentlyContinue
|
||||
|
||||
if ($lockoutEvents) {
|
||||
foreach ($event in $lockoutEvents) {
|
||||
if ($event.TimeCreated -le $lastLockout4740CheckTime) { continue }
|
||||
$lo = Get-Lockout4740EventInfo -Event $event
|
||||
if ([string]::IsNullOrWhiteSpace($lo.Username)) { continue }
|
||||
|
||||
$domainForIis = if ([string]::IsNullOrWhiteSpace($lo.Domain)) { $NetBiosDomainName } else { $lo.Domain }
|
||||
$iisIps = @(Get-ExchangeActiveSyncIpsFromIisLog -SamAccountName $lo.Username `
|
||||
-DomainNetBios $domainForIis -ReferenceTime $lo.TimeCreated)
|
||||
|
||||
if (Should-IgnoreLockout4740Event -Username $lo.Username -IisClientIps $iisIps) {
|
||||
Write-Log "Skip 4740 (ignore.lst): User=$($lo.Username)"
|
||||
continue
|
||||
}
|
||||
|
||||
$msg = Format-Lockout4740TelegramMessage -Username $lo.Username -Domain $lo.Domain `
|
||||
-TimeCreated $lo.TimeCreated -IisClientIps $iisIps
|
||||
Write-Log "Notify 4740: User=$($lo.Username) IIS_IPs=$($iisIps -join ', ')"
|
||||
Send-MonitorNotification -Message $msg `
|
||||
-EmailSubject "RDP Login Monitor: блокировка УЗ $($lo.Username)" | Out-Null
|
||||
}
|
||||
$lastLockout4740CheckTime = ($lockoutEvents | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1)
|
||||
}
|
||||
}
|
||||
|
||||
$now = Get-Date
|
||||
if ($now -ge $nextHeartbeatTime) {
|
||||
Send-Heartbeat
|
||||
@@ -1807,7 +2293,17 @@ function Start-LoginMonitor {
|
||||
|
||||
$script:StopNotificationSent = $false
|
||||
try {
|
||||
Test-TelegramConnection | Out-Null
|
||||
$notifyChannels = @(Get-NotifyOrderChannels)
|
||||
if ($notifyChannels.Count -eq 0) {
|
||||
Write-Log "ВНИМАНИЕ: не настроен ни один канал оповещений (Telegram и/или SMTP в конфигурации скрипта)."
|
||||
} else {
|
||||
foreach ($notifyCh in $notifyChannels) {
|
||||
switch ($notifyCh) {
|
||||
'telegram' { Test-TelegramConnection | Out-Null }
|
||||
'email' { Test-MailSmtpConnection | Out-Null }
|
||||
}
|
||||
}
|
||||
}
|
||||
Start-LoginMonitor -MonitorInterval 5 -MonitorInteractiveOnly
|
||||
} catch {
|
||||
if ($_.Exception -is [System.Management.Automation.PipelineStoppedException]) {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
# RDP Login Monitor
|
||||
|
||||
PowerShell-набор для мониторинга входов в Windows с отправкой уведомлений в Telegram.
|
||||
PowerShell-набор для мониторинга входов в Windows с уведомлениями в Telegram и/или Email (SMTP).
|
||||
|
||||
## Актуальная схема (рекомендуется)
|
||||
|
||||
- Базовый путь установки: **`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`** — журнал Security **`4624`/`4625`** (логика зависит от типа ОС: рабочая станция или сервер/КД), при наличии журнала — **Remote Connection Manager `1149`** (часто актуально для РС с RDP), при роли **RD Gateway** — **`302`/`303`**, на **КД, где запущен монитор** (имя совпадает с **`$LockoutMonitorDomainController`**) — **`4740`** (блокировка УЗ + IP из IIS ActiveSync), **ежедневный отчёт** (активные сессии через `quser`), **heartbeat**, **ротация логов**, уведомления в Telegram и/или Email.
|
||||
- Установка задач: запуск **`Login_Monitor.ps1 -InstallTasks`** создаёт:
|
||||
- `RDP-Login-Monitor` (основной монитор),
|
||||
- `RDP-Login-Monitor-Watchdog` (контроль процесса каждые 5 минут).
|
||||
- Доменная доставка и обновления: **`Deploy-LoginMonitor.ps1`** + **`version.txt`** с шары `NETLOGON`. После успешного деплоя в приветственном сообщении Telegram может появиться отметка об обновлении (файл **`deploy_last_update.txt`** рядом с логами).
|
||||
- Доменная доставка и обновления: **`Deploy-LoginMonitor.ps1`** + **`version.txt`** с шары `NETLOGON`. После успешного деплоя в приветственном сообщении (Telegram/Email) может появиться отметка об обновлении (файл **`deploy_last_update.txt`** рядом с логами).
|
||||
- Для полной инструкции по деплою/GPO используйте **[DEPLOY.md](DEPLOY.md)**.
|
||||
- **`Encrypt-DpapiForRdpMonitor.ps1`** — опционально для подготовки DPAPI-строк токена/chat id.
|
||||
- **`Encrypt-DpapiForRdpMonitor.ps1`** — опционально для подготовки DPAPI-строк токена/chat id и пароля SMTP (`$MailSmtpPasswordProtectedB64`).
|
||||
|
||||
## Что изменилось (важное)
|
||||
|
||||
@@ -27,13 +27,16 @@ PowerShell-набор для мониторинга входов в Windows с
|
||||
2. Скопируйте в неё как минимум:
|
||||
- `Login_Monitor.ps1`
|
||||
- (для доменного развёртывания отдельно на шаре) `Deploy-LoginMonitor.ps1` и `version.txt`.
|
||||
3. Откройте `Login_Monitor.ps1` и задайте токен/чат:
|
||||
- `$TelegramBotToken` или `...ProtectedB64`
|
||||
- `$TelegramChatID` или `...ProtectedB64`
|
||||
3. Откройте `Login_Monitor.ps1` и задайте каналы оповещений:
|
||||
- **Telegram:** `$TelegramBotToken` / `$TelegramChatID` или `...ProtectedB64`
|
||||
- **Email (SMTP):** `$MailSmtpHost`, `$MailFrom`, `$MailTo`, `$MailSmtpPort` (по умолчанию 587), при необходимости `$MailSmtpUser` / `$MailSmtpPassword` (или `$MailSmtpPasswordProtectedB64` через DPAPI), `$MailSmtpStartTls` / `$MailSmtpSsl`
|
||||
- **Порядок:** `$NotifyOrder` — пусто = авто (Telegram → Email, только настроенные); иначе `telegram,email` или `email` и т.п. (допускаются `tg`, `mail`)
|
||||
4. Запускайте с правами администратора (чтение `Security` журнала и регистрация задач).
|
||||
5. Логи и служебные файлы будут в:
|
||||
- `C:\ProgramData\RDP-login-monitor\Logs\`
|
||||
6. (Опционально) Подавление части алертов по списку — см. раздел **«7) ignore.lst»** ниже.
|
||||
7. (Опционально) Мониторинг блокировок AD на КД — **`$LockoutMonitorDomainController`** (короткое имя узла, на котором **установлен и запущен** монитор), **`$NetBiosDomainName`**, **`$ExchangeIisLogPath`**, **`$ExchangeIisLogMinutesBeforeLockout`** (по умолчанию 30), **`$ExchangeIisLogTailLines`** (по умолчанию 5000), **`$ExchangeServerHostForIisExclude`**. В оповещении: пользователь из 4740 и IP из IIS за окно до блокировки. В **`ignore.lst`** префикс **`4740:`** или **`all:`** — см. **`ignore.lst.example`**.
|
||||
8. Heartbeat: при отсутствии обновления **`Logs\last_heartbeat.txt`** дольше **`$HeartbeatStaleAlertMultiplier` × `$HeartbeatInterval`** (по умолчанию 2×1 ч) — оповещение в Telegram/Email.
|
||||
|
||||
## 2) Ручной запуск
|
||||
|
||||
@@ -66,7 +69,9 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-logi
|
||||
- `C:\ProgramData\RDP-login-monitor\Logs\watchdog.log`
|
||||
- Heartbeat:
|
||||
- `C:\ProgramData\RDP-login-monitor\Logs\last_heartbeat.txt` обновляется по интервалу **`$HeartbeatInterval`** (по умолчанию раз в час).
|
||||
- Ежедневный отчёт: после первого прохождения дневного слота (по умолчанию **09:00**, задаётся **`$DailyReportHour`** / **`$DailyReportMinute`** в `Login_Monitor.ps1`) в Telegram уходит сводка по **`quser`**; метка последнего отчёта — `Logs\last_daily_report.txt`.
|
||||
- Ежедневный отчёт: после первого прохождения дневного слота (по умолчанию **09:00**, задаётся **`$DailyReportHour`** / **`$DailyReportMinute`** в `Login_Monitor.ps1`) уходит сводка по **`quser`** (Telegram/Email); метка последнего отчёта — `Logs\last_daily_report.txt`.
|
||||
- Stale heartbeat: если **`last_heartbeat.txt`** не обновлялся дольше **`$HeartbeatStaleAlertMultiplier` × `$HeartbeatInterval`** — оповещение в Telegram/Email (см. п. 8 подготовки).
|
||||
- При старте в Telegram/Email: строка **«Каналы уведомлений»** (фактический порядок доставки), плюс режим RDS/4740 по конфигурации.
|
||||
- Telegram при старте: при установленном **RD Session Host** (или аналогичных компонентах RDS, не только шлюз) — строка про входы по RDP/RDS на этом сервере; при доступном журнале **RD Gateway** — отдельная строка про подключения к **внутренним целевым ПК** через шлюз (302/303). Узел только с ролью RD Gateway не дублирует формулировку «хост сессий».
|
||||
|
||||
## 5) Автоматический перезапуск при падении
|
||||
@@ -86,9 +91,9 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-logi
|
||||
|
||||
## 7) Подавление уведомлений Security: `ignore.lst`
|
||||
|
||||
В каталоге установки можно положить файл **`C:\ProgramData\RDP-login-monitor\ignore.lst`** (рядом с **`Login_Monitor.ps1`**). Правила из списка проверяются **только** для Telegram-уведомлений по событиям **`4624`/`4625`** журнала Security (успех/неудача входа). Жёстко заданные в скрипте исключения (`ExcludedUsers`, локальный IP, сервисные учётные записи и т.д.) по-прежнему действуют для всех типов событий; **`ignore.lst`** добавляет к ним **дополнительные** совпадения именно для **4624/4625**.
|
||||
В каталоге установки можно положить файл **`C:\ProgramData\RDP-login-monitor\ignore.lst`** (рядом с **`Login_Monitor.ps1`**). По умолчанию правила относятся к **`4624`/`4625`**; префикс **`4740:`** (или **`lockout:`**, **`блокир:`**) — только к блокировкам учётной записи; **`all:`** — и входы, и **4740**. Для **4740** тип **`ip:`** сравнивается с IP из IIS ActiveSync. Жёсткие исключения в скрипте по-прежнему для всех типов событий, кроме **4740** (там только `ignore.lst` и встроенные проверки пользователя).
|
||||
|
||||
События **RD Gateway (`302`/`303`)**, **RCM `1149`**, ежедневный отчёт и heartbeat **этим файлом не настраиваются** (для `1149` список не используется, даже если формально вызывается общая функция фильтрации).
|
||||
События **RD Gateway (`302`/`303`)**, **RCM `1149`**, ежедневный отчёт и heartbeat **этим файлом не настраиваются**.
|
||||
|
||||
### Как читается файл
|
||||
|
||||
@@ -98,7 +103,17 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-logi
|
||||
- Строка с **`:`**: берётся **первая** двоеточие — всё слева (после обрезки пробелов) определяет тип правила, всё справа — значение. Если справа пусто, строка игнорируется.
|
||||
- Строка **без** **`:`**: целиком трактуется как правило типа «любое совпадение» (см. ниже).
|
||||
|
||||
### Типы правил (левая часть до первого `:`)
|
||||
### Префикс области (в самом начале строки, до типа правила)
|
||||
|
||||
| Префикс | События |
|
||||
| --- | --- |
|
||||
| *(нет)* | **4624**, **4625** |
|
||||
| `4740:`, `lockout:`, `блокир:` | **4740** |
|
||||
| `all:`, `*:` | **4624**, **4625**, **4740** |
|
||||
|
||||
Пример: `4740:user:svc_sync` — не слать оповещение о блокировке этой УЗ.
|
||||
|
||||
### Типы правил (левая часть до первого `:` после префикса области)
|
||||
|
||||
| Левая часть (фрагменты совпадают как regex, без учёта регистра) | Поле события |
|
||||
| --- | --- |
|
||||
@@ -125,6 +140,6 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-logi
|
||||
|
||||
## Ключевые слова (для поиска репозитория)
|
||||
|
||||
`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`
|
||||
`rdp`, `rd-gateway`, `rdp-gateway`, `rds`, `remote-desktop`, `windows-security-log`, `eventlog`, `event-id-4624`, `event-id-4625`, `event-id-4740`, `event-id-302`, `event-id-303`, `account-lockout`, `active-sync`, `exchange`, `iis`, `smtp`, `email`, `powershell`, `telegram-bot`, `watchdog`, `gpo`, `netlogon`, `domain-deployment`, `windows-server`, `monitoring`
|
||||
|
||||
|
||||
|
||||
+28
-13
@@ -1,17 +1,17 @@
|
||||
# RDP Login Monitor
|
||||
|
||||
PowerShell toolkit for monitoring Windows logons with Telegram notifications.
|
||||
PowerShell toolkit for monitoring Windows logons with Telegram and/or Email (SMTP) 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.
|
||||
- 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, on the **DC where the monitor runs** (hostname matches **`$LockoutMonitorDomainController`**) — **`4740`** (account lockout + IPs from IIS ActiveSync), **daily report** (active sessions via `quser`), **heartbeat**, **log rotation**, alerts via Telegram and/or Email.
|
||||
- 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).
|
||||
- Domain delivery and upgrades: **`Deploy-LoginMonitor.ps1`** + **`version.txt`** on a share such as `NETLOGON`. After a successful deploy, the startup notification 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.
|
||||
- **`Encrypt-DpapiForRdpMonitor.ps1`** — optional helper to prepare DPAPI-protected Base64 for the bot token / chat id and SMTP password (`$MailSmtpPasswordProtectedB64`).
|
||||
|
||||
## Notable behavior
|
||||
|
||||
@@ -27,13 +27,16 @@ PowerShell toolkit for monitoring Windows logons with Telegram notifications.
|
||||
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`
|
||||
3. Edit `Login_Monitor.ps1` and configure notification channels:
|
||||
- **Telegram:** `$TelegramBotToken` / `$TelegramChatID` or `...ProtectedB64`
|
||||
- **Email (SMTP):** `$MailSmtpHost`, `$MailFrom`, `$MailTo`, `$MailSmtpPort` (default 587), optionally `$MailSmtpUser` / `$MailSmtpPassword` (or `$MailSmtpPasswordProtectedB64` via DPAPI), `$MailSmtpStartTls` / `$MailSmtpSsl`
|
||||
- **Order:** `$NotifyOrder` — empty = auto (Telegram → Email, configured channels only); otherwise `telegram,email`, `email`, etc. (`tg`, `mail` are accepted)
|
||||
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.
|
||||
7. (Optional) AD account lockout monitoring on a DC — **`$LockoutMonitorDomainController`** (short hostname of the machine **where the monitor is installed and running**), **`$NetBiosDomainName`**, **`$ExchangeIisLogPath`**, **`$ExchangeIisLogMinutesBeforeLockout`** (default 30), **`$ExchangeIisLogTailLines`** (default 5000), **`$ExchangeServerHostForIisExclude`**. Alerts include the user from event **4740** and client IPs from IIS within the time window before lockout. In **`ignore.lst`** use prefix **`4740:`** or **`all:`** — see **`ignore.lst.example`**.
|
||||
8. Heartbeat: if **`Logs\last_heartbeat.txt`** is not updated for longer than **`$HeartbeatStaleAlertMultiplier` × `$HeartbeatInterval`** (default 2×1 h) — alert via Telegram/Email.
|
||||
|
||||
## 2) Manual run
|
||||
|
||||
@@ -66,8 +69,10 @@ For domain deployment from a share you do not configure the scheduler on clients
|
||||
- `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.
|
||||
- Daily report: after the first daily window (default **09:00**, controlled by **`$DailyReportHour`** / **`$DailyReportMinute`** in `Login_Monitor.ps1`), a `quser` summary is sent (Telegram/Email); last run marker: `Logs\last_daily_report.txt`.
|
||||
- Stale heartbeat: if **`last_heartbeat.txt`** is stale beyond **`$HeartbeatStaleAlertMultiplier` × `$HeartbeatInterval`** — alert via Telegram/Email (see preparation step 8).
|
||||
- At startup (Telegram/Email): **notification channels** line (actual delivery order) plus RDS/4740 mode per configuration.
|
||||
- Startup message (Telegram): 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
|
||||
|
||||
@@ -86,9 +91,9 @@ For domain deployment from a share you do not configure the scheduler on clients
|
||||
|
||||
## 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**.
|
||||
Place a file **`C:\ProgramData\RDP-login-monitor\ignore.lst`** next to **`Login_Monitor.ps1`**. By default rules apply to **`4624`/`4625`**; prefix **`4740:`** (or **`lockout:`**) — account lockouts only; **`all:`** — logons and **4740**. For **4740**, rule type **`ip:`** is matched against IPs from IIS ActiveSync. Built-in script exclusions still apply to all event types except **4740** (lockouts use **`ignore.lst`** and built-in user checks 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).
|
||||
**RD Gateway (`302`/`303`)**, **RCM `1149`**, the daily report, and heartbeat **are not controlled** by this file.
|
||||
|
||||
### How the file is loaded
|
||||
|
||||
@@ -98,7 +103,17 @@ Place a file **`C:\ProgramData\RDP-login-monitor\ignore.lst`** next to **`Login_
|
||||
- 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 `:`)
|
||||
### Scope prefix (at the start of the line, before the rule type)
|
||||
|
||||
| Prefix | Events |
|
||||
| --- | --- |
|
||||
| *(none)* | **4624**, **4625** |
|
||||
| `4740:`, `lockout:` | **4740** |
|
||||
| `all:`, `*:` | **4624**, **4625**, **4740** |
|
||||
|
||||
Example: `4740:user:svc_sync` — do not alert on lockout for that account.
|
||||
|
||||
### Rule kinds (left part before the first `:` after the scope prefix)
|
||||
|
||||
| Left part (case-insensitive regex fragments) | Event field |
|
||||
| --- | --- |
|
||||
@@ -125,4 +140,4 @@ Explicit **User** / **Workstation** / **Ip** kinds only compare their respective
|
||||
|
||||
## 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`
|
||||
`rdp`, `rd-gateway`, `rdp-gateway`, `rds`, `remote-desktop`, `windows-security-log`, `eventlog`, `event-id-4624`, `event-id-4625`, `event-id-4740`, `event-id-302`, `event-id-303`, `account-lockout`, `active-sync`, `exchange`, `iis`, `smtp`, `email`, `powershell`, `telegram-bot`, `watchdog`, `gpo`, `netlogon`, `domain-deployment`, `windows-server`, `monitoring`
|
||||
|
||||
+20
-14
@@ -2,23 +2,29 @@
|
||||
# C:\ProgramData\RDP-login-monitor\ignore.lst
|
||||
#
|
||||
# Каждая непустая строка — одно правило. Строки с # или ; в начале — комментарии.
|
||||
# Подавляет только уведомления Security 4624/4625 (не RD Gateway, не RCM 1149).
|
||||
#
|
||||
# Форматы:
|
||||
# Область действия (префикс в начале строки, необязателен):
|
||||
# (по умолчанию) — только Security 4624/4625
|
||||
# 4740: — только блокировка учётной записи (4740); для IP — любой IP из IIS
|
||||
# all: — и 4624/4625, и 4740
|
||||
#
|
||||
# Форматы правила (после префикса области):
|
||||
# user:domain\user
|
||||
# user:user
|
||||
# workstation:IVANOV
|
||||
# workstation:IVANOV (не для 4740)
|
||||
# ip:111.222.333.444
|
||||
# Можно вставить «как в Telegram» (берётся значение после первого «:»):
|
||||
# 👤 Пользователь: user
|
||||
# 🖥️ Рабочая станция (клиент из события): IVANOV
|
||||
# 🌐 IP адрес: 111.222.333.444
|
||||
#
|
||||
# Строка без префикса:
|
||||
# IVANOV — совпадение с именем рабочей станции ИЛИ с пользователем (sam) ИЛИ с IP (если строка — IPv4)
|
||||
# 111.222.333.444 — только IP (в реальной конфигурации укажите действительный IPv4 клиента)
|
||||
# domain\user — пользователь целиком
|
||||
# Строка без префикса типа:
|
||||
# IVANOV — рабочая станция / пользователь / IP (IPv4)
|
||||
# domain\user — пользователь
|
||||
|
||||
# user:domain\user
|
||||
# workstation:IVANOV
|
||||
# ip:111.222.333.444
|
||||
# --- только входы 4624/4625 ---
|
||||
# user:domain\service_account
|
||||
# ip:192.168.1.100
|
||||
|
||||
# --- только блокировки 4740 ---
|
||||
# 4740:user:test.user
|
||||
# 4740:ip:203.0.113.50
|
||||
|
||||
# --- все перечисленные события ---
|
||||
# all:user:domain\noise_account
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.4.3
|
||||
1.5.3
|
||||
|
||||
Reference in New Issue
Block a user