docs: add Docs/ guides; Exchange Mail Security scripts (v1.6.0)
- Docs: RDP deploy, Exchange mail security, NETLOGON publish - Exchange-MailSecurity.ps1, Notify-Common, Deploy/Install-DomainMonitors - DEPLOY.md redirects to Docs/
This commit is contained in:
@@ -1,226 +1,11 @@
|
||||
# Развёртывание RDP Login Monitor в домене
|
||||
# Развёртывание
|
||||
|
||||
Монитор ставится в **`C:\ProgramData\RDP-login-monitor\`**, задачи планировщика создаёт сам **`Login_Monitor.ps1`** (параметр `-InstallTasks`). Доставку по сети выполняет **`Deploy-LoginMonitor.ps1`**.
|
||||
Документация перенесена в каталог **[Docs/](Docs/README.md)**:
|
||||
|
||||
## Файлы на файловой шаре
|
||||
| Руководство | Файл |
|
||||
|-------------|------|
|
||||
| RDP Login Monitor (NETLOGON, GPO) | [Docs/deploy-rdp-login-monitor.md](Docs/deploy-rdp-login-monitor.md) |
|
||||
| Exchange Mail Security | [Docs/exchange-mail-security.md](Docs/exchange-mail-security.md) |
|
||||
| Публикация на шару | [Docs/deploy-netlogon-publish.md](Docs/deploy-netlogon-publish.md) |
|
||||
|
||||
Создайте каталог, доступный **конечным компьютерам** на чтение (часто учётная запись компьютера домена), например:
|
||||
|
||||
`\\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`** (логика описана в разделе «Версии»).
|
||||
|
||||
На сервере публикации (например DC3): клон в `C:\Soft\Git\RDP-login-monitor`, скрипт **`update-rdp-monitor.ps1`** — при отсутствии remote сам добавит `origin` на `https://git.kalinamall.ru/PapaTramp/RDP-login-monitor.git`, выполнит `git pull` и скопирует три файла в `\\b26\NETLOGON\RDP-login-monitor\`. Лог: `C:\soft\Logs\update-rdp-monitor.log`.
|
||||
|
||||
## Как это работает
|
||||
|
||||
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 и/или 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` не доставляет** — при необходимости создавайте файл локально или копируйте своим способом.
|
||||
|
||||
## Опционально на контроллере домена: блокировки 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`
|
||||
|
||||
Параметр **`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).
|
||||
|
||||
## Внутренний репозиторий (git.kalinamall.ru)
|
||||
|
||||
Рабочая копия **`Login_Monitor.ps1`** для домена может содержать **Telegram token/chat id**, **`$LockoutMonitorDomainController`**, **`$NetBiosDomainName`**, **`$ExchangeIisLogPath`** и **`$NotifyOrder`** в открытом виде.
|
||||
|
||||
- Дистрибутив на шару `NETLOGON` берите из **закрытого** репозитория (`kalinamall` remote), не из публичного GitHub.
|
||||
- В **`origin` (GitHub)** не пушьте файл с секретами: перед `git push origin` верните плейсхолдеры или используйте отдельную ветку только для kalinamall.
|
||||
- После правок поднимайте **`version.txt`** на шаре (сейчас синхронно с **`$ScriptVersion`** в скрипте).
|
||||
|
||||
## Безопасность и замечания
|
||||
|
||||
- ACL на шару: чтение только нужным **компьютерам** / группам; файл **`Login_Monitor.ps1`** может содержать токен — ограничивайте доступ.
|
||||
- DPAPI-секреты привязаны к машине: шифровать на каждой цели (Telegram token/chat id, пароль SMTP) или использовать plain на закрытой шаре (см. комментарии в **`Login_Monitor.ps1`**, helper **`Encrypt-DpapiForRdpMonitor.ps1`**).
|
||||
- Deploy при ошибках пишет в **`deploy.log`** и завершается с кодом **0**, чтобы не блокировать загрузку ОС; проблемы смотрите по логу на ПК.
|
||||
Оглавление: **[Docs/README.md](Docs/README.md)**
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Доставка скриптов доменных мониторов (Exchange / AD) с шары в ProgramData.
|
||||
.PARAMETER Target
|
||||
Exchange — Exchange-MailSecurity.ps1 + Notify-Common.ps1; Ad — зарезервировано.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet('Exchange', 'Ad')]
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Target,
|
||||
[string]$SourceShareRoot = '',
|
||||
[switch]$WhatIf,
|
||||
[switch]$SkipInstallTasks,
|
||||
[switch]$AllowDowngrade
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$InstallRoot = [System.IO.Path]::GetFullPath("$env:ProgramData\RDP-login-monitor")
|
||||
$VersionStampPath = Join-Path $InstallRoot 'deployed_domain_monitors_version.txt'
|
||||
$DeployLogPath = Join-Path $InstallRoot 'Logs\deploy_domain_monitors.log'
|
||||
$VersionFileName = 'version.txt'
|
||||
$Utf8Bom = New-Object System.Text.UTF8Encoding $true
|
||||
$PsExe = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||
|
||||
$ExchangeFiles = @(
|
||||
'Exchange-MailSecurity.ps1',
|
||||
'Notify-Common.ps1',
|
||||
'Install-DomainMonitors.ps1'
|
||||
)
|
||||
|
||||
function Write-DeployLog {
|
||||
param([string]$Message)
|
||||
$line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - [$Target] $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 { }
|
||||
Write-Host $line.TrimEnd("`r`n")
|
||||
}
|
||||
|
||||
function Write-TextFileUtf8Bom {
|
||||
param([string]$Path, [string]$Text)
|
||||
[System.IO.File]::WriteAllText($Path, $Text, $Utf8Bom)
|
||||
}
|
||||
|
||||
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 или запускайте скрипт с 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 Compare-SemVerLike {
|
||||
param([string]$A, [string]$B)
|
||||
try {
|
||||
$va = [version]$A
|
||||
$vb = [version]$B
|
||||
return $va.CompareTo($vb)
|
||||
} catch {
|
||||
return [string]::Compare($A, $B, [StringComparison]::OrdinalIgnoreCase)
|
||||
}
|
||||
}
|
||||
|
||||
$shareRoot = Resolve-SourceShareRoot
|
||||
$filesToCopy = switch ($Target) {
|
||||
'Exchange' { $ExchangeFiles }
|
||||
'Ad' { throw 'Target Ad пока не поддерживается.' }
|
||||
}
|
||||
|
||||
$sourceVersionFile = Join-Path $shareRoot $VersionFileName
|
||||
if (-not (Test-Path -LiteralPath $sourceVersionFile)) {
|
||||
Write-DeployLog "ОШИБКА: нет $sourceVersionFile на шаре."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$shareVer = Read-VersionLineFromFile -Path $sourceVersionFile
|
||||
$localVer = Read-VersionLineFromFile -Path $VersionStampPath
|
||||
Write-DeployLog "Шара version=$shareVer локальная метка=$localVer"
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($localVer)) {
|
||||
$cmp = Compare-SemVerLike -A $shareVer -B $localVer
|
||||
if ($cmp -eq 0) {
|
||||
Write-DeployLog 'Версия совпадает — копирование не требуется.'
|
||||
if (-not $SkipInstallTasks) {
|
||||
Write-DeployLog 'Проверка InstallTasks...'
|
||||
if (-not $WhatIf) {
|
||||
& $PsExe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $InstallRoot 'Install-DomainMonitors.ps1') -Target $Target
|
||||
}
|
||||
}
|
||||
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"; exit 0 }
|
||||
New-Item -ItemType Directory -Path $InstallRoot -Force | Out-Null
|
||||
}
|
||||
|
||||
foreach ($name in $filesToCopy) {
|
||||
$src = Join-Path $shareRoot $name
|
||||
$dst = Join-Path $InstallRoot $name
|
||||
if (-not (Test-Path -LiteralPath $src)) {
|
||||
Write-DeployLog "ОШИБКА: на шаре нет $name"
|
||||
exit 1
|
||||
}
|
||||
if ($WhatIf) {
|
||||
Write-DeployLog "WhatIf: copy $src -> $dst"
|
||||
continue
|
||||
}
|
||||
Copy-Item -LiteralPath $src -Destination $dst -Force
|
||||
Write-DeployLog "Скопировано: $name"
|
||||
}
|
||||
|
||||
if (-not $WhatIf) {
|
||||
Write-TextFileUtf8Bom -Path $VersionStampPath -Text $shareVer
|
||||
if (-not $SkipInstallTasks) {
|
||||
$installer = Join-Path $InstallRoot 'Install-DomainMonitors.ps1'
|
||||
& $PsExe -NoProfile -ExecutionPolicy Bypass -File $installer -Target $Target
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-DeployLog "Install-DomainMonitors завершился с кодом $LASTEXITCODE"
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-DeployLog 'Deploy-DomainMonitors завершён.'
|
||||
exit 0
|
||||
@@ -0,0 +1,13 @@
|
||||
# Документация RDP-login-monitor
|
||||
|
||||
Руководства по развёртыванию и эксплуатации скриптов мониторинга.
|
||||
|
||||
| Документ | Описание |
|
||||
|----------|----------|
|
||||
| [deploy-rdp-login-monitor.md](deploy-rdp-login-monitor.md) | **Login_Monitor.ps1** — NETLOGON, GPO, задачи планировщика, версии, 4740 на КД |
|
||||
| [exchange-mail-security.md](exchange-mail-security.md) | **Exchange-MailSecurity.ps1** — очереди, пересылка на внешние адреса, установка |
|
||||
| [deploy-netlogon-publish.md](deploy-netlogon-publish.md) | Публикация пакета на шару (`update-rdp-monitor.ps1`) |
|
||||
|
||||
Общий каталог установки на серверах: **`C:\ProgramData\RDP-login-monitor\`**.
|
||||
|
||||
Краткий обзор возможностей — в корневом **[README.md](../README.md)**.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Публикация пакета на NETLOGON
|
||||
|
||||
Скрипт **`update-rdp-monitor.ps1`** на сервере публикации (например DC3) выполняет `git pull` и копирует файлы в шару.
|
||||
|
||||
## Параметры по умолчанию
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| `$RepoPath` | `C:\Soft\Git\RDP-login-monitor` |
|
||||
| `$NetlogonDest` | `\\b26\NETLOGON\RDP-login-monitor` |
|
||||
| `$GitUrl` | `https://git.kalinamall.ru/PapaTramp/RDP-login-monitor.git` |
|
||||
| `$GitBranch` | `main` |
|
||||
| `$LogFile` | `C:\soft\Logs\update-rdp-monitor.log` |
|
||||
|
||||
## Копируемые файлы
|
||||
|
||||
- `Login_Monitor.ps1`
|
||||
- `version.txt`
|
||||
- `Deploy-LoginMonitor.ps1`
|
||||
- `Exchange-MailSecurity.ps1`
|
||||
- `Notify-Common.ps1`
|
||||
- `Install-DomainMonitors.ps1`
|
||||
- `Deploy-DomainMonitors.ps1`
|
||||
- `exchange_monitor.settings.example.ps1`
|
||||
|
||||
## Запуск
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\soft\update-rdp-monitor.ps1
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\soft\update-rdp-monitor.ps1 -WhatIf
|
||||
```
|
||||
|
||||
После pull обязательно проверьте **`version.txt`** на шаре — его номер определяет, подтянут ли обновления на клиентах и Exchange.
|
||||
@@ -0,0 +1,118 @@
|
||||
# Развёртывание RDP Login Monitor в домене
|
||||
|
||||
Монитор ставится в **`C:\ProgramData\RDP-login-monitor\`**, задачи планировщика создаёт сам **`Login_Monitor.ps1`** (параметр `-InstallTasks`). Доставку по сети выполняет **`Deploy-LoginMonitor.ps1`**.
|
||||
|
||||
См. также: [exchange-mail-security.md](exchange-mail-security.md) (отдельно, только сервер Exchange).
|
||||
|
||||
## Файлы на файловой шаре
|
||||
|
||||
Создайте каталог, доступный **конечным компьютерам** на чтение (часто учётная запись компьютера домена), например:
|
||||
|
||||
`\\dc.contoso.local\NETLOGON\RDP-login-monitor\`
|
||||
|
||||
Минимум для RDP-монитора на всех ПК/серверах:
|
||||
|
||||
| Файл | Назначение |
|
||||
|------|------------|
|
||||
| `Login_Monitor.ps1` | Основной скрипт (токен/chat id plain или DPAPI в параметрах). |
|
||||
| `version.txt` | **Одна строка** — номер версии пакета на шаре (см. раздел «Версии» ниже). |
|
||||
| `Deploy-LoginMonitor.ps1` | Установщик: сравнивает версию, копирует монитор, вызывает `-InstallTasks`, при необходимости запускает процесс монитора. |
|
||||
|
||||
Полный список файлов для публикации на шару — в [deploy-netlogon-publish.md](deploy-netlogon-publish.md).
|
||||
|
||||
## Как это работает
|
||||
|
||||
1. **`Deploy-LoginMonitor.ps1`** определяет корень дистрибутива:
|
||||
- параметр **`-SourceShareRoot`** `\\server\share\RDP-login-monitor`, **или**
|
||||
- если скрипт запущен по UNC, берётся **родительская папка** этого файла.
|
||||
|
||||
2. Читается **`version.txt`** на шаре и сравнивается с локальной меткой **`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`** — подавление отдельных алертов **4624/4625/4740**. Синтаксис — в **[README.md](../README.md)** (раздел 7) и **`ignore.lst.example`**. Deploy с шары **`ignore.lst` не копирует**.
|
||||
|
||||
## Опционально на КД: блокировки AD (4740)
|
||||
|
||||
Включается только если монитор запущен на КД с именем **`$LockoutMonitorDomainController`**. Параметры задаются **локально** на этом КД после деплоя (`$NetBiosDomainName`, `$ExchangeIisLogPath`, …).
|
||||
|
||||
## Heartbeat и watchdog
|
||||
|
||||
- **`Logs\last_heartbeat.txt`** — обновление по **`$HeartbeatInterval`** (по умолчанию 1 ч).
|
||||
- Нет обновления дольше **`$HeartbeatStaleAlertMultiplier × интервал`** — оповещение.
|
||||
- Задача **`RDP-Login-Monitor-Watchdog`** — каждые 5 мин проверяет процесс и поднимает при падении.
|
||||
|
||||
## Задачи планировщика (`-InstallTasks`)
|
||||
|
||||
| Имя | Назначение |
|
||||
|-----|------------|
|
||||
| **`RDP-Login-Monitor`** | Запуск при старте ОС |
|
||||
| **`RDP-Login-Monitor-Watchdog`** | Контроль процесса каждые 5 мин |
|
||||
|
||||
Проверка:
|
||||
|
||||
```powershell
|
||||
Get-ScheduledTask -TaskName 'RDP-Login-Monitor','RDP-Login-Monitor-Watchdog' -ErrorAction SilentlyContinue
|
||||
```
|
||||
|
||||
Логи: **`login_monitor.log`**, **`watchdog.log`**.
|
||||
|
||||
## GPO (автозагрузка): проверенная схема
|
||||
|
||||
1. Файлы на `\\B26\NETLOGON\RDP-login-monitor\`
|
||||
2. GPO на OU **компьютеров** → **Сценарии PowerShell** автозагрузки → `Deploy-LoginMonitor.ps1`
|
||||
3. Security Filtering: группа компьютеров (например `B26\RDP-Login`), права Read + Apply GPO
|
||||
4. Доступ на шару для **SYSTEM** / Domain Computers
|
||||
5. После смены membership — **перезагрузка** (не только `gpupdate`)
|
||||
|
||||
**Нюансы:** UNC в NETLOGON не копируется в SYSVOL GPO — это нормально. Deploy после `-InstallTasks` делает `schtasks /Run` для немедленного старта монитора.
|
||||
|
||||
### Периодический Deploy на серверах без перезагрузок
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Ручной deploy
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "\\B26\NETLOGON\RDP-login-monitor\Deploy-LoginMonitor.ps1"
|
||||
```
|
||||
|
||||
Параметры: **`-WhatIf`**, **`-SkipStartMonitorAfterUpdate`**, **`-AllowDowngrade`**.
|
||||
|
||||
### Диагностика
|
||||
|
||||
- Group Policy Operational log
|
||||
- `Logs\deploy.log`, `login_monitor.log`, `watchdog.log`
|
||||
|
||||
## Версии: `version.txt` и `$ScriptVersion`
|
||||
|
||||
| Что | Роль |
|
||||
|-----|------|
|
||||
| **`version.txt` на шаре** | Триггер обновления для Deploy |
|
||||
| **`$ScriptVersion` в скрипте** | Версия в логах и Telegram |
|
||||
|
||||
Поднимайте **`version.txt`** при каждой выкладке на NETLOGON.
|
||||
|
||||
## Безопасность
|
||||
|
||||
- Ограничьте ACL на шару (в скрипте могут быть секреты).
|
||||
- DPAPI: **`Encrypt-DpapiForRdpMonitor.ps1`**
|
||||
- Внутренний git: не пушить секреты в публичный GitHub
|
||||
|
||||
## UNC и ExecutionPolicy
|
||||
|
||||
При ошибках подписи с FQDN-шары используйте короткое имя DC: `\\DC01\NETLOGON\...` и **`powershell.exe -ExecutionPolicy Bypass -File "..."`**.
|
||||
@@ -0,0 +1,227 @@
|
||||
# Exchange Mail Security — руководство
|
||||
|
||||
Скрипт **`Exchange-MailSecurity.ps1`** предназначен **только для сервера Microsoft Exchange** с Exchange Management Shell (EMS). Не устанавливается на все компьютеры домена через GPO RDP-монитора.
|
||||
|
||||
## Назначение
|
||||
|
||||
| Функция | Режим | Расписание (задача планировщика) |
|
||||
|---------|--------|----------------------------------|
|
||||
| **Очереди транспорта** | `-Mode Queues` | `RDP-Exchange-TransportQueues` — каждые **10** мин |
|
||||
| **Пересылка на внешние адреса** (BEC) | `-Mode Inbox` | `RDP-Exchange-InboxRules` — ежедневно **02:00** |
|
||||
| **Контроль heartbeat** | `-Watchdog` | `RDP-Exchange-MailSecurity-Watchdog` — каждые **15** мин |
|
||||
|
||||
Каталог установки (общий с RDP-монитором): **`C:\ProgramData\RDP-login-monitor\`**.
|
||||
|
||||
## Что считается «несанкционированной пересылкой»
|
||||
|
||||
Скрипт ищет доставку почты **на домены вне организации** (не из списка принятых доменов Exchange).
|
||||
|
||||
### 1. Правила Inbox (`Get-InboxRule`)
|
||||
|
||||
Для **включённых** правил (`Enabled = $true`) проверяются:
|
||||
|
||||
- `ForwardTo`
|
||||
- `RedirectTo`
|
||||
- `ForwardAsAttachmentTo`
|
||||
|
||||
Если целевой SMTP-адрес указывает на домен **не** из `Get-AcceptedDomain` (типы **Authoritative** и **InternalRelay**) и не в **whitelist** — фиксируется находка.
|
||||
|
||||
**Повышенная важность (Критическая):** правило также включает `DeleteMessage` или `MarkAsRead` (типичный паттерн скрытой пересылки при компрометации).
|
||||
|
||||
### 2. Пересылка на уровне ящика (`Get-Mailbox`)
|
||||
|
||||
Проверяются ящики с заполненными:
|
||||
|
||||
- `ForwardingSmtpAddress`
|
||||
- `ForwardingAddress`
|
||||
|
||||
Это отдельный от Inbox Rule механизм («пересылать всю почту на внешний ящик») — частый вектор BEC.
|
||||
|
||||
### 3. Правила транспорта (`Get-TransportRule`)
|
||||
|
||||
Для **включённых** transport rules проверяются действия с адресами:
|
||||
|
||||
- `RedirectMessageTo`
|
||||
- `BlindCopyTo`
|
||||
- `CopyTo`
|
||||
|
||||
При внешнем SMTP — находка с типом **TransportRule**.
|
||||
|
||||
### Что не отслеживается
|
||||
|
||||
- **Отключённые** Inbox rules с внешней пересылкой
|
||||
- Делегирование, Send As, скрытые EWS-правила без Inbox rule
|
||||
- Пересылка только между **внутренними** accepted domains
|
||||
- Чтение почты злоумышленником **без** настройки forward
|
||||
|
||||
## Очереди транспорта
|
||||
|
||||
`Get-Queue`: если **`MessageCount`** на очереди больше **`$QueueMessageCountThreshold`** (по умолчанию **150**), отправляется оповещение.
|
||||
|
||||
Повтор алерта по той же очереди — не чаще **`$QueueAlertCooldownSeconds`** (по умолчанию **900** с), состояние в `Logs\exchange_queue_alert_state.json`.
|
||||
|
||||
## Оповещения и baseline
|
||||
|
||||
- Каналы: **Telegram** и/или **Email** (модуль **`Notify-Common.ps1`**).
|
||||
- Пересылка: по умолчанию **`$AlertOnlyOnNewForwardingFindings = $true`** — алерт только при **новой** находке относительно `Logs\exchange_forwarding_baseline.json`.
|
||||
- **Первый** полный скан: baseline пуст → алерт по **всем** найденным внешним пересылкам (легитимные партнёры добавьте в whitelist заранее).
|
||||
- **`$NotifyWhenForwardingScanClean = $false`** — не слать ежедневное «всё чисто» (можно включить).
|
||||
|
||||
## Файлы на шаре NETLOGON
|
||||
|
||||
| Файл | Назначение |
|
||||
|------|------------|
|
||||
| `Exchange-MailSecurity.ps1` | Основной скрипт |
|
||||
| `Notify-Common.ps1` | Отправка Telegram/Email |
|
||||
| `Install-DomainMonitors.ps1` | `-Target Exchange` → регистрация задач |
|
||||
| `Deploy-DomainMonitors.ps1` | Копирование с шары |
|
||||
| `exchange_monitor.settings.example.ps1` | Образец настроек (на сервер — вручную) |
|
||||
| `version.txt` | Версия пакета (общая с RDP-монитором) |
|
||||
|
||||
## Установка (пошагово)
|
||||
|
||||
### 1. Публикация на шару
|
||||
|
||||
На сервере публикации: `update-rdp-monitor.ps1` (см. [deploy-netlogon-publish.md](deploy-netlogon-publish.md)).
|
||||
|
||||
### 2. Деплой на Exchange (от администратора)
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "\\B26\NETLOGON\RDP-login-monitor\Deploy-DomainMonitors.ps1" -Target Exchange
|
||||
```
|
||||
|
||||
Скрипт копирует файлы в `C:\ProgramData\RDP-login-monitor\`, вызывает **`Install-DomainMonitors.ps1 -Target Exchange`**, который регистрирует задачи планировщика.
|
||||
|
||||
Метка версии: `deployed_domain_monitors_version.txt`. Лог: `Logs\deploy_domain_monitors.log`.
|
||||
|
||||
### 3. Локальные настройки (один раз)
|
||||
|
||||
```powershell
|
||||
Copy-Item "C:\ProgramData\RDP-login-monitor\exchange_monitor.settings.example.ps1" `
|
||||
"C:\ProgramData\RDP-login-monitor\exchange_monitor.settings.ps1"
|
||||
notepad C:\ProgramData\RDP-login-monitor\exchange_monitor.settings.ps1
|
||||
```
|
||||
|
||||
Задайте:
|
||||
|
||||
- `$TelegramBotToken` / `$TelegramChatID` (или DPAPI `...ProtectedB64`)
|
||||
- при необходимости SMTP
|
||||
- `$ExternalDomainWhitelist` — доверенные внешние домены
|
||||
- для пилота: `$VipMailboxesOnly = $true` и `$VipMailboxes = @('user@domain.ru')`
|
||||
|
||||
Файл **`exchange_monitor.settings.ps1` не перезаписывается** при деплое.
|
||||
|
||||
### 4. Права
|
||||
|
||||
Учётная запись **SYSTEM** (задачи планировщика) должна иметь возможность:
|
||||
|
||||
- загрузить EMS на сервере;
|
||||
- выполнять `Get-Mailbox`, `Get-InboxRule`, `Get-Queue`, `Get-TransportRule`, `Get-AcceptedDomain`.
|
||||
|
||||
На практике на Exchange-сервере под SYSTEM это обычно работает при локальном snap-in; при ошибках RBAC — запуск задачи от служебной УЗ с правами просмотра получателей.
|
||||
|
||||
### 5. Удалённый EMS
|
||||
|
||||
Если скрипт запускается **не** на Exchange, укажите в settings:
|
||||
|
||||
```powershell
|
||||
$ExchangeServerFqdn = 'mail.contoso.local'
|
||||
```
|
||||
|
||||
(подключение через `New-PSSession -ConfigurationName Microsoft.Exchange`).
|
||||
|
||||
## Ручной запуск и проверка
|
||||
|
||||
```powershell
|
||||
# Очереди (быстро)
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-login-monitor\Exchange-MailSecurity.ps1" -Mode Queues
|
||||
|
||||
# Полный скан пересылки (может занять часы на большом количестве ящиков)
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-login-monitor\Exchange-MailSecurity.ps1" -Mode Inbox
|
||||
|
||||
# Watchdog
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-login-monitor\Exchange-MailSecurity.ps1" -Watchdog
|
||||
|
||||
# Переустановка задач
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-login-monitor\Exchange-MailSecurity.ps1" -InstallTasks
|
||||
```
|
||||
|
||||
Проверка задач:
|
||||
|
||||
```powershell
|
||||
schtasks /Query /TN "RDP-Exchange-TransportQueues" /V /FO LIST
|
||||
schtasks /Query /TN "RDP-Exchange-InboxRules" /V /FO LIST
|
||||
schtasks /Query /TN "RDP-Exchange-MailSecurity-Watchdog" /V /FO LIST
|
||||
```
|
||||
|
||||
## Логи и служебные файлы
|
||||
|
||||
| Путь | Назначение |
|
||||
|------|------------|
|
||||
| `Logs\exchange_mail_security.log` | Основной лог |
|
||||
| `Logs\exchange_watchdog.log` | Watchdog |
|
||||
| `Logs\last_exchange_queues_ok.txt` | Успешный скан очередей |
|
||||
| `Logs\last_exchange_inbox_scan_ok.txt` | Успешный скан пересылки |
|
||||
| `Logs\exchange_forwarding_baseline.json` | Baseline находок (id правил/пересылок) |
|
||||
| `Logs\exchange_queue_alert_state.json` | Cooldown алертов по очередям |
|
||||
|
||||
### Watchdog
|
||||
|
||||
- Нет обновления **очередей** > **20** мин (`$QueuesHeartbeatStaleSeconds`) → алерт.
|
||||
- Нет обновления **inbox scan** > **26** ч (`$InboxHeartbeatStaleSeconds`) → алерт.
|
||||
|
||||
## Основные параметры в `Exchange-MailSecurity.ps1`
|
||||
|
||||
| Параметр | По умолчанию | Смысл |
|
||||
|----------|--------------|--------|
|
||||
| `$QueueMessageCountThreshold` | 150 | Порог сообщений в очереди |
|
||||
| `$QueueAlertCooldownSeconds` | 900 | Пауза между алертами по одной очереди |
|
||||
| `$ScanInboxRules` | `$true` | Скан Inbox rules |
|
||||
| `$ScanMailboxForwarding` | `$true` | Скан forwarding на ящике |
|
||||
| `$ScanTransportRules` | `$true` | Скан transport rules |
|
||||
| `$AlertOnlyOnNewForwardingFindings` | `$true` | Только новые находки |
|
||||
| `$VipMailboxesOnly` | `$false` | Ограничить список ящиков |
|
||||
| `$InboxScanBatchSize` | 50 | Пауза каждые N ящиков |
|
||||
| `$InboxScanBatchDelaySeconds` | 3 | Задержка между батчами |
|
||||
| `$ExcludeMailboxPatterns` | HealthMailbox*, … | Исключения |
|
||||
|
||||
Переопределение — в **`exchange_monitor.settings.ps1`**.
|
||||
|
||||
## Формат оповещения (пересылка)
|
||||
|
||||
Пример:
|
||||
|
||||
```text
|
||||
📧 Exchange: пересылка на внешний адрес
|
||||
Тип: InboxRule | MailboxForwarding | TransportRule
|
||||
Ящик: user@kalinamall.ru
|
||||
Правило: …
|
||||
Куда: attacker@gmail.com (внешний)
|
||||
Важность: Критическая | Высокая
|
||||
```
|
||||
|
||||
## Обновление версии
|
||||
|
||||
1. Поднять **`version.txt`** в репозитории и на шаре.
|
||||
2. `update-rdp-monitor.ps1` или ручное копирование.
|
||||
3. На Exchange:
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "\\B26\NETLOGON\RDP-login-monitor\Deploy-DomainMonitors.ps1" -Target Exchange
|
||||
```
|
||||
|
||||
## Устранение неполадок
|
||||
|
||||
| Симптом | Действие |
|
||||
|---------|----------|
|
||||
| «Не удалось загрузить Exchange Management Shell» | Запуск на mailbox/Exchange server; проверить snap-in / `$ExchangeServerFqdn` |
|
||||
| Нет алертов | Проверить `exchange_monitor.settings.ps1`, Telegram/SMTP, лог `exchange_mail_security.log` |
|
||||
| Слишком много алертов в первый день | Заполнить `$ExternalDomainWhitelist`; дождаться baseline; или временно `$AlertOnlyOnNewForwardingFindings = $false` только для теста |
|
||||
| Inbox scan долгий | `$VipMailboxesOnly = $true`; увеличить `$InboxScanBatchDelaySeconds` |
|
||||
| Ошибки Get-InboxRule на ящике | В логе будет «ошибка mailbox» — штатно, скан продолжается |
|
||||
|
||||
## Связанные документы
|
||||
|
||||
- [deploy-rdp-login-monitor.md](deploy-rdp-login-monitor.md) — RDP-монитор на ПК/серверах
|
||||
- [deploy-netlogon-publish.md](deploy-netlogon-publish.md) — выкладка на шару
|
||||
- [README.md](README.md) — оглавление Docs
|
||||
@@ -0,0 +1,733 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Мониторинг Exchange: очереди транспорта, пересылка на внешние адреса (Inbox rules + mailbox forwarding + transport rules).
|
||||
.DESCRIPTION
|
||||
Запуск на сервере с Exchange Management Shell (локальный snap-in или remote).
|
||||
Режимы: -Mode Queues (каждые 10 мин по задаче), -Mode Inbox (ежедневный скан), -Watchdog (проверка heartbeat).
|
||||
Установка задач: -InstallTasks
|
||||
.NOTES
|
||||
Каталог: C:\ProgramData\RDP-login-monitor\
|
||||
Опционально: exchange_monitor.settings.ps1 в том же каталоге (секреты, whitelist).
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet('Queues', 'Inbox', 'Watchdog')]
|
||||
[string]$Mode = 'Queues',
|
||||
[switch]$InstallTasks,
|
||||
[switch]$Watchdog
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# ============================================
|
||||
# КОНФИГУРАЦИЯ
|
||||
# ============================================
|
||||
|
||||
$ScriptVersion = '1.6.0'
|
||||
$script:InstallRoot = [System.IO.Path]::GetFullPath("$env:ProgramData\RDP-login-monitor")
|
||||
$script:CanonicalScriptName = 'Exchange-MailSecurity.ps1'
|
||||
$LogFile = Join-Path $script:InstallRoot 'Logs\exchange_mail_security.log'
|
||||
$WatchdogLogFile = Join-Path $script:InstallRoot 'Logs\exchange_watchdog.log'
|
||||
$QueuesHeartbeatFile = Join-Path $script:InstallRoot 'Logs\last_exchange_queues_ok.txt'
|
||||
$InboxHeartbeatFile = Join-Path $script:InstallRoot 'Logs\last_exchange_inbox_scan_ok.txt'
|
||||
$ForwardingBaselineFile = Join-Path $script:InstallRoot 'Logs\exchange_forwarding_baseline.json'
|
||||
$QueueAlertStateFile = Join-Path $script:InstallRoot 'Logs\exchange_queue_alert_state.json'
|
||||
|
||||
# --- Уведомления (как Login_Monitor.ps1; можно вынести в exchange_monitor.settings.ps1) ---
|
||||
$NotifyOrder = 'tg'
|
||||
$TelegramBotToken = ''
|
||||
$TelegramChatID = ''
|
||||
$TelegramBotTokenProtectedB64 = ''
|
||||
$TelegramChatIDProtectedB64 = ''
|
||||
$MailSmtpHost = ''
|
||||
$MailSmtpPort = 587
|
||||
$MailSmtpUser = ''
|
||||
$MailSmtpPassword = ''
|
||||
$MailSmtpPasswordProtectedB64 = ''
|
||||
$MailFrom = ''
|
||||
$MailTo = ''
|
||||
$MailSmtpStartTls = $true
|
||||
$MailSmtpSsl = $false
|
||||
|
||||
# --- Exchange / сканирование ---
|
||||
$ExchangeServerFqdn = '' # пусто = локальный snap-in на этом сервере
|
||||
$QueueMessageCountThreshold = 150
|
||||
$QueueAlertCooldownSeconds = 900
|
||||
$ExternalDomainWhitelist = @() # partner-bank.ru
|
||||
$AlertOnlyOnNewForwardingFindings = $true
|
||||
$ScanMailboxForwarding = $true
|
||||
$ScanInboxRules = $true
|
||||
$ScanTransportRules = $true
|
||||
$VipMailboxesOnly = $false
|
||||
$VipMailboxes = @() # user@domain.com
|
||||
$ExcludeMailboxPatterns = @('HealthMailbox*', 'DiscoveryMailbox*', 'SystemMailbox*')
|
||||
$InboxScanBatchSize = 50
|
||||
$InboxScanBatchDelaySeconds = 3
|
||||
$MaxMailboxesPerRun = 0 # 0 = без лимита
|
||||
$QueuesHeartbeatStaleSeconds = 1200 # 20 мин
|
||||
$InboxHeartbeatStaleSeconds = 93600 # 26 ч
|
||||
$NotifyWhenForwardingScanClean = $false # true = ежедневное «внешняя пересылка не найдена»
|
||||
|
||||
$script:ScheduledTaskQueues = 'RDP-Exchange-TransportQueues'
|
||||
$script:ScheduledTaskInbox = 'RDP-Exchange-InboxRules'
|
||||
$script:ScheduledTaskWatchdog = 'RDP-Exchange-MailSecurity-Watchdog'
|
||||
|
||||
# ============================================
|
||||
# ИНИЦИАЛИЗАЦИЯ
|
||||
# ============================================
|
||||
|
||||
if (!(Test-Path -LiteralPath $script:InstallRoot)) {
|
||||
New-Item -ItemType Directory -Path $script:InstallRoot -Force | Out-Null
|
||||
}
|
||||
$LogDir = Split-Path $LogFile -Parent
|
||||
if (!(Test-Path $LogDir)) { New-Item -ItemType Directory -Path $LogDir -Force | Out-Null }
|
||||
|
||||
$script:Utf8BomEncoding = New-Object System.Text.UTF8Encoding $true
|
||||
|
||||
$SettingsFile = Join-Path $script:InstallRoot 'exchange_monitor.settings.ps1'
|
||||
if (Test-Path -LiteralPath $SettingsFile) {
|
||||
. $SettingsFile
|
||||
}
|
||||
|
||||
function Write-NotifyLog {
|
||||
param([string]$Message)
|
||||
Write-ExchLog $Message
|
||||
}
|
||||
|
||||
function Write-ExchLog {
|
||||
param([string]$Message)
|
||||
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||
$logMessage = "$timestamp - $Message" + [Environment]::NewLine
|
||||
[System.IO.File]::AppendAllText($LogFile, $logMessage, $script:Utf8BomEncoding)
|
||||
Write-Host ($logMessage.TrimEnd("`r`n"))
|
||||
}
|
||||
|
||||
function Write-WdLog {
|
||||
param([string]$Message)
|
||||
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||
$line = "$timestamp - $Message" + [Environment]::NewLine
|
||||
[System.IO.File]::AppendAllText($WatchdogLogFile, $line, $script:Utf8BomEncoding)
|
||||
Write-Host ($line.TrimEnd("`r`n"))
|
||||
}
|
||||
|
||||
function Test-RunningElevated {
|
||||
try {
|
||||
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
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 }
|
||||
}
|
||||
|
||||
function Get-ExchangeMonitorScriptPath {
|
||||
$p = $PSCommandPath
|
||||
if ([string]::IsNullOrWhiteSpace($p)) { $p = $MyInvocation.MyCommand.Path }
|
||||
if ([string]::IsNullOrWhiteSpace($p)) { return (Join-Path $script:InstallRoot $script:CanonicalScriptName) }
|
||||
return [System.IO.Path]::GetFullPath($p)
|
||||
}
|
||||
|
||||
function Get-ExchangeMonitorPowerShellExe {
|
||||
return "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||
}
|
||||
|
||||
# --- InstallTasks / Watchdog до dot-source уведомлений ---
|
||||
|
||||
function Register-ExchangeMonitorScheduledTasks {
|
||||
$psExe = Get-ExchangeMonitorPowerShellExe
|
||||
$scriptPath = Get-ExchangeMonitorScriptPath
|
||||
$argQueues = "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`" -Mode Queues"
|
||||
$argInbox = "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`" -Mode Inbox"
|
||||
$argWd = "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`" -Watchdog"
|
||||
|
||||
$schtasksExe = Join-Path $env:SystemRoot 'System32\schtasks.exe'
|
||||
$delEa = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'SilentlyContinue'
|
||||
|
||||
foreach ($tn in @($script:ScheduledTaskQueues, $script:ScheduledTaskInbox, $script:ScheduledTaskWatchdog)) {
|
||||
& $schtasksExe /Delete /TN $tn /F 2>$null | Out-Null
|
||||
}
|
||||
$ErrorActionPreference = $delEa
|
||||
|
||||
$trQueues = "`"$psExe`" $argQueues"
|
||||
$outQ = & $schtasksExe /Create /F /RU SYSTEM /RL HIGHEST /SC MINUTE /MO 10 /TN $script:ScheduledTaskQueues /TR $trQueues 2>&1
|
||||
foreach ($line in @($outQ)) { Write-ExchLog "schtasks queues: $line" }
|
||||
|
||||
$trInbox = "`"$psExe`" $argInbox"
|
||||
$outI = & $schtasksExe /Create /F /RU SYSTEM /RL HIGHEST /SC DAILY /ST 02:00 /TN $script:ScheduledTaskInbox /TR $trInbox 2>&1
|
||||
foreach ($line in @($outI)) { Write-ExchLog "schtasks inbox: $line" }
|
||||
|
||||
$trWd = "`"$psExe`" $argWd"
|
||||
$outW = & $schtasksExe /Create /F /RU SYSTEM /RL HIGHEST /SC MINUTE /MO 15 /TN $script:ScheduledTaskWatchdog /TR $trWd 2>&1
|
||||
foreach ($line in @($outW)) { Write-ExchLog "schtasks watchdog: $line" }
|
||||
|
||||
$ErrorActionPreference = 'SilentlyContinue'
|
||||
& $schtasksExe /Run /TN $script:ScheduledTaskQueues 2>&1 | Out-Null
|
||||
& $schtasksExe /Run /TN $script:ScheduledTaskWatchdog 2>&1 | Out-Null
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
Write-ExchLog "InstallTasks: зарегистрированы $($script:ScheduledTaskQueues), $($script:ScheduledTaskInbox), $($script:ScheduledTaskWatchdog)"
|
||||
}
|
||||
|
||||
if ($InstallTasks) {
|
||||
if (-not (Test-RunningElevated)) {
|
||||
Write-Host 'InstallTasks: нужны права администратора.'
|
||||
exit 1
|
||||
}
|
||||
Register-ExchangeMonitorScheduledTasks
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($Watchdog) { $Mode = 'Watchdog' }
|
||||
|
||||
# Уведомления
|
||||
$notifyPath = Join-Path $script:InstallRoot 'Notify-Common.ps1'
|
||||
if (-not (Test-Path -LiteralPath $notifyPath)) {
|
||||
$notifyPath = Join-Path (Split-Path -Parent (Get-ExchangeMonitorScriptPath)) 'Notify-Common.ps1'
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $notifyPath)) {
|
||||
Write-Host "Не найден Notify-Common.ps1 рядом с $script:InstallRoot"
|
||||
exit 1
|
||||
}
|
||||
. $notifyPath
|
||||
|
||||
$tgToken = [ref]$TelegramBotToken
|
||||
$tgChat = [ref]$TelegramChatID
|
||||
$mailPass = [ref]$MailSmtpPassword
|
||||
Initialize-NotifyCredentials -TelegramBotTokenProtectedB64 $TelegramBotTokenProtectedB64 `
|
||||
-TelegramChatIDProtectedB64 $TelegramChatIDProtectedB64 `
|
||||
-TelegramBotToken $tgToken -TelegramChatID $tgChat `
|
||||
-MailSmtpPasswordProtectedB64 $MailSmtpPasswordProtectedB64 -MailSmtpPassword $mailPass
|
||||
if ($TelegramBotToken -eq '<TELEGRAM_BOT_TOKEN>') { $TelegramBotToken = '' }
|
||||
if ($TelegramChatID -eq '<TELEGRAM_CHAT_ID>') { $TelegramChatID = '' }
|
||||
|
||||
# ============================================
|
||||
# EXCHANGE EMS
|
||||
# ============================================
|
||||
|
||||
$script:ExchangeSessionLoaded = $false
|
||||
|
||||
function Import-ExchangeManagementShell {
|
||||
if ($script:ExchangeSessionLoaded) { return $true }
|
||||
|
||||
$snapins = @(Get-PSSnapin -Registered -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.Name -match 'Microsoft\.Exchange\.Management\.PowerShell'
|
||||
})
|
||||
if ($snapins.Count -gt 0) {
|
||||
Add-PSSnapin -Name $snapins[0].Name -ErrorAction Stop
|
||||
$script:ExchangeSessionLoaded = $true
|
||||
Write-ExchLog "Exchange: snap-in $($snapins[0].Name)"
|
||||
return $true
|
||||
}
|
||||
|
||||
$rex = Join-Path ${env:ExchangeInstallPath} 'bin\RemoteExchange.ps1'
|
||||
if ([string]::IsNullOrWhiteSpace(${env:ExchangeInstallPath})) {
|
||||
$candidates = @(
|
||||
'C:\Program Files\Microsoft\Exchange Server\V15\bin\RemoteExchange.ps1',
|
||||
'C:\Program Files\Microsoft\Exchange Server\V14\bin\RemoteExchange.ps1'
|
||||
)
|
||||
foreach ($c in $candidates) {
|
||||
if (Test-Path -LiteralPath $c) { $rex = $c; break }
|
||||
}
|
||||
}
|
||||
if (Test-Path -LiteralPath $rex) {
|
||||
. $rex
|
||||
Import-ExchangeCmdlet -DisableNameVerifying -ErrorAction Stop
|
||||
$script:ExchangeSessionLoaded = $true
|
||||
Write-ExchLog "Exchange: RemoteExchange $rex"
|
||||
return $true
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($ExchangeServerFqdn)) {
|
||||
$uri = "http://$ExchangeServerFqdn/PowerShell/"
|
||||
$session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri $uri -Authentication Kerberos -ErrorAction Stop
|
||||
Import-PSSession $session -DisableNameChecking -AllowClobber -ErrorAction Stop | Out-Null
|
||||
$script:ExchangeSessionLoaded = $true
|
||||
Write-ExchLog "Exchange: PSSession $uri"
|
||||
return $true
|
||||
}
|
||||
|
||||
throw 'Не удалось загрузить Exchange Management Shell (snap-in / RemoteExchange / укажите $ExchangeServerFqdn).'
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# ВНЕШНИЕ ДОМЕНЫ
|
||||
# ============================================
|
||||
|
||||
function Get-InternalAcceptedDomainNames {
|
||||
Import-ExchangeManagementShell
|
||||
$names = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
||||
$domains = Get-AcceptedDomain -ErrorAction Stop
|
||||
foreach ($d in $domains) {
|
||||
if ($d.DomainType -eq 'Authoritative' -or $d.DomainType -eq 'InternalRelay') {
|
||||
$null = $names.Add([string]$d.DomainName)
|
||||
}
|
||||
}
|
||||
return $names
|
||||
}
|
||||
|
||||
function Get-EmailDomainFromAddress {
|
||||
param([string]$Address)
|
||||
if ([string]::IsNullOrWhiteSpace($Address)) { return $null }
|
||||
$a = $Address.Trim().ToLowerInvariant()
|
||||
if ($a -match 'smtp:([^@\s]+@([^@\s;]+))') { return $matches[2] }
|
||||
if ($a -match '([a-z0-9._%+-]+@([a-z0-9.-]+\.[a-z]{2,}))') { return $matches[2] }
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-AddressesFromExchangeProperty {
|
||||
param($PropertyValue)
|
||||
$list = [System.Collections.Generic.List[string]]::new()
|
||||
if ($null -eq $PropertyValue) { return @() }
|
||||
foreach ($item in @($PropertyValue)) {
|
||||
$s = [string]$item
|
||||
if ([string]::IsNullOrWhiteSpace($s)) { continue }
|
||||
if ($s -match 'smtp:([^;\s]+@[^;\s]+)') {
|
||||
$list.Add($matches[1].ToLowerInvariant()) | Out-Null
|
||||
} elseif ($s -match '([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})') {
|
||||
$list.Add($matches[1].ToLowerInvariant()) | Out-Null
|
||||
}
|
||||
}
|
||||
return @($list)
|
||||
}
|
||||
|
||||
function Test-IsExternalSmtpAddress {
|
||||
param(
|
||||
[string]$SmtpAddress,
|
||||
[System.Collections.Generic.HashSet[string]]$InternalDomains,
|
||||
[string[]]$WhitelistDomains
|
||||
)
|
||||
|
||||
$domain = Get-EmailDomainFromAddress -Address $SmtpAddress
|
||||
if ([string]::IsNullOrWhiteSpace($domain)) { return $false }
|
||||
|
||||
foreach ($w in $WhitelistDomains) {
|
||||
if ([string]::IsNullOrWhiteSpace($w)) { continue }
|
||||
if ($domain -eq $w.Trim().ToLowerInvariant()) { return $false }
|
||||
}
|
||||
|
||||
return -not $InternalDomains.Contains($domain)
|
||||
}
|
||||
|
||||
function Test-MailboxExcluded {
|
||||
param([string]$PrimarySmtp)
|
||||
foreach ($pat in $ExcludeMailboxPatterns) {
|
||||
if ([string]::IsNullOrWhiteSpace($pat)) { continue }
|
||||
if ($PrimarySmtp -like $pat) { return $true }
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# FINDINGS
|
||||
# ============================================
|
||||
|
||||
function New-ForwardingFinding {
|
||||
param(
|
||||
[string]$FindingType,
|
||||
[string]$Mailbox,
|
||||
[string]$RuleName,
|
||||
[string]$TargetAddress,
|
||||
[string]$Severity,
|
||||
[hashtable]$Extra = @{}
|
||||
)
|
||||
|
||||
$rulePart = if ([string]::IsNullOrWhiteSpace($RuleName)) { '' } else { $RuleName }
|
||||
$id = '{0}|{1}|{2}|{3}' -f $FindingType, $Mailbox.ToLowerInvariant(), $rulePart, $TargetAddress.ToLowerInvariant()
|
||||
return [pscustomobject]@{
|
||||
Id = $id
|
||||
FindingType = $FindingType
|
||||
Mailbox = $Mailbox
|
||||
RuleName = $RuleName
|
||||
TargetAddress = $TargetAddress
|
||||
Severity = $Severity
|
||||
Extra = $Extra
|
||||
}
|
||||
}
|
||||
|
||||
function Format-ForwardingFindingMessage {
|
||||
param($Finding)
|
||||
|
||||
$hType = ConvertTo-TelegramHtml $Finding.FindingType
|
||||
$hMb = ConvertTo-TelegramHtml $Finding.Mailbox
|
||||
$hRule = ConvertTo-TelegramHtml $Finding.RuleName
|
||||
$hTarget = ConvertTo-TelegramHtml $Finding.TargetAddress
|
||||
$hSev = ConvertTo-TelegramHtml $Finding.Severity
|
||||
$hostName = ConvertTo-TelegramHtml $env:COMPUTERNAME
|
||||
|
||||
$msg = "<b>📧 Exchange: пересылка на внешний адрес</b>`r`n"
|
||||
$msg += "🖥️ Сервер: $hostName`r`n"
|
||||
$msg += "📋 Тип: $hType`r`n"
|
||||
$msg += "📬 Ящик: $hMb`r`n"
|
||||
if (-not [string]::IsNullOrWhiteSpace($Finding.RuleName)) {
|
||||
$msg += "📎 Правило: $hRule`r`n"
|
||||
}
|
||||
$msg += "➡️ Куда: $hTarget (внешний)`r`n"
|
||||
$msg += "⚠️ Важность: $hSev`r`n"
|
||||
|
||||
if ($Finding.Extra.ContainsKey('Enabled')) {
|
||||
$msg += "✅ Включено: $(ConvertTo-TelegramHtml ([string]$Finding.Extra['Enabled']))`r`n"
|
||||
}
|
||||
if ($Finding.Extra.ContainsKey('DeleteMessage')) {
|
||||
$msg += "🗑️ DeleteMessage: $(ConvertTo-TelegramHtml ([string]$Finding.Extra['DeleteMessage']))`r`n"
|
||||
}
|
||||
if ($Finding.Extra.ContainsKey('MarkAsRead')) {
|
||||
$msg += "👁️ MarkAsRead: $(ConvertTo-TelegramHtml ([string]$Finding.Extra['MarkAsRead']))`r`n"
|
||||
}
|
||||
if ($Finding.Extra.ContainsKey('DeliverToMailboxAndForward')) {
|
||||
$msg += "📤 DeliverToMailboxAndForward: $(ConvertTo-TelegramHtml ([string]$Finding.Extra['DeliverToMailboxAndForward']))`r`n"
|
||||
}
|
||||
|
||||
return $msg
|
||||
}
|
||||
|
||||
function Get-ForwardingBaseline {
|
||||
if (-not (Test-Path -LiteralPath $ForwardingBaselineFile)) {
|
||||
return @{ FindingIds = @(); LastScanUtc = $null }
|
||||
}
|
||||
try {
|
||||
$raw = Get-Content -LiteralPath $ForwardingBaselineFile -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
$ids = @()
|
||||
if ($null -ne $raw.FindingIds) { $ids = @($raw.FindingIds) }
|
||||
return @{ FindingIds = $ids; LastScanUtc = $raw.LastScanUtc }
|
||||
} catch {
|
||||
Write-ExchLog "Baseline: не прочитан ($($_.Exception.Message))"
|
||||
return @{ FindingIds = @(); LastScanUtc = $null }
|
||||
}
|
||||
}
|
||||
|
||||
function Save-ForwardingBaseline {
|
||||
param([string[]]$FindingIds)
|
||||
$obj = @{
|
||||
LastScanUtc = (Get-Date).ToUniversalTime().ToString('o')
|
||||
FindingIds = @($FindingIds)
|
||||
}
|
||||
$json = $obj | ConvertTo-Json -Depth 4
|
||||
Write-TextFileUtf8Bom -Path $ForwardingBaselineFile -Text $json
|
||||
}
|
||||
|
||||
function Write-TextFileUtf8Bom {
|
||||
param([string]$Path, [string]$Text)
|
||||
[System.IO.File]::WriteAllText($Path, $Text, $script:Utf8BomEncoding)
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# SCAN: QUEUES
|
||||
# ============================================
|
||||
|
||||
function Get-QueueAlertState {
|
||||
if (-not (Test-Path -LiteralPath $QueueAlertStateFile)) { return @{} }
|
||||
try {
|
||||
$h = @{}
|
||||
(Get-Content -LiteralPath $QueueAlertStateFile -Raw -Encoding UTF8 | ConvertFrom-Json).PSObject.Properties | ForEach-Object {
|
||||
$h[$_.Name] = [datetime]$_.Value
|
||||
}
|
||||
return $h
|
||||
} catch { return @{} }
|
||||
}
|
||||
|
||||
function Save-QueueAlertState {
|
||||
param([hashtable]$State)
|
||||
$obj = @{}
|
||||
foreach ($k in $State.Keys) { $obj[$k] = $State[$k].ToUniversalTime().ToString('o') }
|
||||
Write-TextFileUtf8Bom -Path $QueueAlertStateFile -Text ($obj | ConvertTo-Json)
|
||||
}
|
||||
|
||||
function Invoke-ExchangeQueueScan {
|
||||
Import-ExchangeManagementShell
|
||||
Write-ExchLog "Queues: порог MessageCount > $QueueMessageCountThreshold"
|
||||
|
||||
$queues = @(Get-Queue -ErrorAction Stop)
|
||||
$hot = @($queues | Where-Object { $_.MessageCount -gt $QueueMessageCountThreshold })
|
||||
$state = Get-QueueAlertState
|
||||
$now = Get-Date
|
||||
|
||||
if ($hot.Count -eq 0) {
|
||||
Write-ExchLog "Queues: OK (всего $($queues.Count), выше порога 0)"
|
||||
Write-TextFileUtf8Bom -Path $QueuesHeartbeatFile -Text (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
|
||||
return
|
||||
}
|
||||
|
||||
foreach ($q in $hot) {
|
||||
$qKey = [string]$q.Identity
|
||||
$lastAlert = $null
|
||||
if ($state.ContainsKey($qKey)) { $lastAlert = $state[$qKey] }
|
||||
$cooldownOk = ($null -eq $lastAlert) -or (((Get-Date).ToUniversalTime() - $lastAlert.ToUniversalTime()).TotalSeconds -ge $QueueAlertCooldownSeconds)
|
||||
if (-not $cooldownOk) {
|
||||
Write-ExchLog "Queues: $($q.Identity) count=$($q.MessageCount) — cooldown, пропуск алерта"
|
||||
continue
|
||||
}
|
||||
|
||||
$hId = ConvertTo-TelegramHtml $q.Identity
|
||||
$msg = "<b>📬 Exchange: очередь транспорта</b>`r`n"
|
||||
$msg += "🖥️ Сервер: $(ConvertTo-TelegramHtml $env:COMPUTERNAME)`r`n"
|
||||
$msg += "📋 Очередь: $hId`r`n"
|
||||
$msg += "📊 Сообщений: <b>$($q.MessageCount)</b> (порог $QueueMessageCountThreshold)`r`n"
|
||||
$msg += "📌 Статус: $(ConvertTo-TelegramHtml ([string]$q.Status))`r`n"
|
||||
$msg += "🕐 $(ConvertTo-TelegramHtml (Get-Date -Format 'dd.MM.yyyy HH:mm:ss'))"
|
||||
|
||||
if (Send-MonitorNotification -Message $msg -EmailSubject 'Exchange: transport queue') {
|
||||
$state[$qKey] = $now
|
||||
Write-ExchLog "Queues: алерт отправлен для $($q.Identity)"
|
||||
}
|
||||
}
|
||||
|
||||
Save-QueueAlertState -State $state
|
||||
Write-TextFileUtf8Bom -Path $QueuesHeartbeatFile -Text (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# SCAN: FORWARDING
|
||||
# ============================================
|
||||
|
||||
function Get-MailboxListForScan {
|
||||
Import-ExchangeManagementShell
|
||||
if ($VipMailboxesOnly -and $VipMailboxes.Count -gt 0) {
|
||||
return @($VipMailboxes | ForEach-Object { $_.Trim().ToLowerInvariant() } | Where-Object { $_ })
|
||||
}
|
||||
|
||||
$all = @(Get-Mailbox -ResultSize Unlimited -ErrorAction Stop)
|
||||
$list = [System.Collections.Generic.List[string]]::new()
|
||||
foreach ($mb in $all) {
|
||||
$smtp = [string]$mb.PrimarySmtpAddress
|
||||
if ([string]::IsNullOrWhiteSpace($smtp)) { continue }
|
||||
if (Test-MailboxExcluded -PrimarySmtp $smtp) { continue }
|
||||
$list.Add($smtp.ToLowerInvariant()) | Out-Null
|
||||
if ($MaxMailboxesPerRun -gt 0 -and $list.Count -ge $MaxMailboxesPerRun) { break }
|
||||
}
|
||||
return @($list)
|
||||
}
|
||||
|
||||
function Scan-MailboxForwardingSettings {
|
||||
param(
|
||||
[System.Collections.Generic.HashSet[string]]$InternalDomains,
|
||||
[System.Collections.Generic.List[object]]$Findings
|
||||
)
|
||||
|
||||
if (-not $ScanMailboxForwarding) { return }
|
||||
|
||||
Write-ExchLog 'Forwarding: Get-Mailbox (ForwardingSmtpAddress / ForwardingAddress)'
|
||||
$mbs = Get-Mailbox -ResultSize Unlimited -ErrorAction Stop | Where-Object {
|
||||
$_.ForwardingSmtpAddress -or $_.ForwardingAddress
|
||||
}
|
||||
|
||||
foreach ($mb in $mbs) {
|
||||
$primary = [string]$mb.PrimarySmtpAddress
|
||||
if (Test-MailboxExcluded -PrimarySmtp $primary) { continue }
|
||||
if ($VipMailboxesOnly -and $VipMailboxes.Count -gt 0) {
|
||||
if ($VipMailboxes -notcontains $primary.ToLowerInvariant()) { continue }
|
||||
}
|
||||
|
||||
$targets = @()
|
||||
if ($mb.ForwardingSmtpAddress) {
|
||||
$targets += @($mb.ForwardingSmtpAddress)
|
||||
}
|
||||
if ($mb.ForwardingAddress) {
|
||||
$targets += @((Get-AddressesFromExchangeProperty -PropertyValue $mb.ForwardingAddress))
|
||||
}
|
||||
|
||||
foreach ($t in $targets) {
|
||||
if (-not (Test-IsExternalSmtpAddress -SmtpAddress $t -InternalDomains $InternalDomains -WhitelistDomains $ExternalDomainWhitelist)) {
|
||||
continue
|
||||
}
|
||||
$sev = 'Высокая'
|
||||
$Findings.Add((New-ForwardingFinding -FindingType 'MailboxForwarding' -Mailbox $primary `
|
||||
-RuleName '' -TargetAddress $t -Severity $sev -Extra @{
|
||||
DeliverToMailboxAndForward = [string]$mb.DeliverToMailboxAndForward
|
||||
})) | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Scan-InboxRulesForMailbox {
|
||||
param(
|
||||
[string]$Mailbox,
|
||||
[System.Collections.Generic.HashSet[string]]$InternalDomains,
|
||||
[System.Collections.Generic.List[object]]$Findings
|
||||
)
|
||||
|
||||
$rules = @(Get-InboxRule -Mailbox $Mailbox -ErrorAction SilentlyContinue)
|
||||
foreach ($rule in $rules) {
|
||||
if (-not $rule.Enabled) { continue }
|
||||
|
||||
$props = @(
|
||||
@{ Name = 'ForwardTo'; Value = $rule.ForwardTo }
|
||||
@{ Name = 'RedirectTo'; Value = $rule.RedirectTo }
|
||||
@{ Name = 'ForwardAsAttachmentTo'; Value = $rule.ForwardAsAttachmentTo }
|
||||
)
|
||||
|
||||
foreach ($p in $props) {
|
||||
$addrs = Get-AddressesFromExchangeProperty -PropertyValue $p.Value
|
||||
foreach ($addr in $addrs) {
|
||||
if (-not (Test-IsExternalSmtpAddress -SmtpAddress $addr -InternalDomains $InternalDomains -WhitelistDomains $ExternalDomainWhitelist)) {
|
||||
continue
|
||||
}
|
||||
$sev = 'Высокая'
|
||||
if ($rule.DeleteMessage -or $rule.MarkAsRead) { $sev = 'Критическая' }
|
||||
$Findings.Add((New-ForwardingFinding -FindingType 'InboxRule' -Mailbox $Mailbox `
|
||||
-RuleName [string]$rule.Name -TargetAddress $addr -Severity $sev -Extra @{
|
||||
Enabled = $rule.Enabled
|
||||
DeleteMessage = $rule.DeleteMessage
|
||||
MarkAsRead = $rule.MarkAsRead
|
||||
Property = $p.Name
|
||||
})) | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Scan-TransportRulesExternalForward {
|
||||
param(
|
||||
[System.Collections.Generic.HashSet[string]]$InternalDomains,
|
||||
[System.Collections.Generic.List[object]]$Findings
|
||||
)
|
||||
|
||||
if (-not $ScanTransportRules) { return }
|
||||
|
||||
Write-ExchLog 'Forwarding: Get-TransportRule'
|
||||
$rules = @(Get-TransportRule -ErrorAction SilentlyContinue | Where-Object { $_.State -eq 'Enabled' })
|
||||
foreach ($rule in $rules) {
|
||||
$props = @(
|
||||
$rule.RedirectMessageTo
|
||||
$rule.BlindCopyTo
|
||||
$rule.CopyTo
|
||||
)
|
||||
foreach ($pv in $props) {
|
||||
$addrs = Get-AddressesFromExchangeProperty -PropertyValue $pv
|
||||
foreach ($addr in $addrs) {
|
||||
if (-not (Test-IsExternalSmtpAddress -SmtpAddress $addr -InternalDomains $InternalDomains -WhitelistDomains $ExternalDomainWhitelist)) {
|
||||
continue
|
||||
}
|
||||
$Findings.Add((New-ForwardingFinding -FindingType 'TransportRule' -Mailbox '(transport)' `
|
||||
-RuleName [string]$rule.Name -TargetAddress $addr -Severity 'Высокая' -Extra @{})) | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-ExchangeInboxAndForwardingScan {
|
||||
Write-ExchLog "Inbox/Forwarding scan v$ScriptVersion; каналы: $(Get-NotifyChainHuman)"
|
||||
Import-ExchangeManagementShell
|
||||
$internalDomains = Get-InternalAcceptedDomainNames
|
||||
Write-ExchLog "Accepted domains (internal): $($internalDomains -join ', ')"
|
||||
|
||||
$findings = [System.Collections.Generic.List[object]]::new()
|
||||
|
||||
Scan-MailboxForwardingSettings -InternalDomains $internalDomains -Findings $findings
|
||||
|
||||
if ($ScanInboxRules) {
|
||||
$mailboxes = @(Get-MailboxListForScan)
|
||||
Write-ExchLog "Inbox rules: ящиков к скану: $($mailboxes.Count)"
|
||||
$idx = 0
|
||||
foreach ($mb in $mailboxes) {
|
||||
$idx++
|
||||
try {
|
||||
Scan-InboxRulesForMailbox -Mailbox $mb -InternalDomains $internalDomains -Findings $findings
|
||||
} catch {
|
||||
Write-ExchLog "Inbox rules: ошибка $mb : $($_.Exception.Message)"
|
||||
}
|
||||
if ($idx % $InboxScanBatchSize -eq 0) {
|
||||
Write-ExchLog "Inbox rules: обработано $idx / $($mailboxes.Count)"
|
||||
Start-Sleep -Seconds $InboxScanBatchDelaySeconds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scan-TransportRulesExternalForward -InternalDomains $internalDomains -Findings $findings
|
||||
|
||||
$allIds = @($findings | ForEach-Object { $_.Id })
|
||||
$baseline = Get-ForwardingBaseline
|
||||
$prevSet = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
||||
foreach ($id in $baseline.FindingIds) { $null = $prevSet.Add($id) }
|
||||
|
||||
$newFindings = @($findings | Where-Object { -not $prevSet.Contains($_.Id) })
|
||||
Write-ExchLog "Forwarding: всего $($findings.Count), новых $($newFindings.Count)"
|
||||
|
||||
$toAlert = if ($AlertOnlyOnNewForwardingFindings) { $newFindings } else { @($findings) }
|
||||
foreach ($f in $toAlert) {
|
||||
$body = Format-ForwardingFindingMessage -Finding $f
|
||||
Send-MonitorNotification -Message $body -EmailSubject 'Exchange: external forward' | Out-Null
|
||||
}
|
||||
|
||||
if ($findings.Count -eq 0 -and $NotifyWhenForwardingScanClean) {
|
||||
$summary = "<b>✅ Exchange: скан пересылки</b>`r`nВнешняя пересылка (Inbox / mailbox / transport) не обнаружена.`r`n🖥️ $(ConvertTo-TelegramHtml $env:COMPUTERNAME)"
|
||||
Send-MonitorNotification -Message $summary -EmailSubject 'Exchange: forward scan OK' | Out-Null
|
||||
} elseif ($newFindings.Count -eq 0 -and $AlertOnlyOnNewForwardingFindings) {
|
||||
Write-ExchLog 'Forwarding: изменений нет (только известные находки в baseline)'
|
||||
}
|
||||
|
||||
Save-ForwardingBaseline -FindingIds $allIds
|
||||
Write-TextFileUtf8Bom -Path $InboxHeartbeatFile -Text (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
|
||||
Write-ExchLog 'Inbox/Forwarding scan завершён'
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# WATCHDOG
|
||||
# ============================================
|
||||
|
||||
function Get-HeartbeatAgeSeconds {
|
||||
param([string]$Path)
|
||||
if (-not (Test-Path -LiteralPath $Path)) { return [double]::PositiveInfinity }
|
||||
try {
|
||||
$t = Get-Content -LiteralPath $Path -TotalCount 1 -ErrorAction Stop
|
||||
if ([string]::IsNullOrWhiteSpace($t)) { return [double]::PositiveInfinity }
|
||||
$dt = [datetime]::ParseExact($t.Trim(), 'yyyy-MM-dd HH:mm:ss', $null)
|
||||
return ((Get-Date) - $dt).TotalSeconds
|
||||
} catch { return [double]::PositiveInfinity }
|
||||
}
|
||||
|
||||
function Invoke-ExchangeWatchdog {
|
||||
Write-WdLog "Watchdog Exchange-MailSecurity v$ScriptVersion"
|
||||
$issues = [System.Collections.Generic.List[string]]::new()
|
||||
|
||||
$qAge = Get-HeartbeatAgeSeconds -Path $QueuesHeartbeatFile
|
||||
if ($qAge -gt $QueuesHeartbeatStaleSeconds) {
|
||||
$issues.Add("Очереди: нет успешного скана > $([int]$qAge) с (порог $QueuesHeartbeatStaleSeconds с)") | Out-Null
|
||||
}
|
||||
|
||||
$iAge = Get-HeartbeatAgeSeconds -Path $InboxHeartbeatFile
|
||||
if ($iAge -gt $InboxHeartbeatStaleSeconds) {
|
||||
$issues.Add("Inbox/Forwarding: нет успешного скана > $([int]$iAge) с (порог $InboxHeartbeatStaleSeconds с)") | Out-Null
|
||||
}
|
||||
|
||||
if ($issues.Count -eq 0) {
|
||||
Write-WdLog 'Watchdog: heartbeat OK'
|
||||
exit 0
|
||||
}
|
||||
|
||||
$msg = "<b>⚠️ Exchange Mail Security: watchdog</b>`r`n"
|
||||
$msg += "🖥️ $(ConvertTo-TelegramHtml $env:COMPUTERNAME)`r`n"
|
||||
foreach ($iss in $issues) {
|
||||
$msg += "• $(ConvertTo-TelegramHtml $iss)`r`n"
|
||||
}
|
||||
|
||||
Send-MonitorNotification -Message $msg -EmailSubject 'Exchange monitor: watchdog' | Out-Null
|
||||
Write-WdLog "Watchdog: отправлено оповещение ($($issues.Count) проблем)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# MAIN
|
||||
# ============================================
|
||||
|
||||
if (-not (Test-RunningElevated)) {
|
||||
Write-ExchLog 'ПРЕДУПРЕЖДЕНИЕ: скрипт без прав администратора — EMS/задачи могут не работать.'
|
||||
}
|
||||
|
||||
Write-ExchLog "=== Exchange-MailSecurity v$ScriptVersion Mode=$Mode ==="
|
||||
|
||||
try {
|
||||
switch ($Mode) {
|
||||
'Queues' { Invoke-ExchangeQueueScan }
|
||||
'Inbox' { Invoke-ExchangeInboxAndForwardingScan }
|
||||
'Watchdog' { Invoke-ExchangeWatchdog }
|
||||
default { throw "Неизвестный Mode: $Mode" }
|
||||
}
|
||||
} catch {
|
||||
Write-ExchLog "ОШИБКА: $($_.Exception.Message)"
|
||||
if ($_.ScriptStackTrace) { Write-ExchLog $_.ScriptStackTrace }
|
||||
exit 1
|
||||
}
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,51 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Регистрирует задачи планировщика для доменных мониторов (Exchange; AD — позже).
|
||||
.PARAMETER Target
|
||||
Exchange — Exchange-MailSecurity.ps1; Ad — зарезервировано.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet('Exchange', 'Ad')]
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Target
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$InstallRoot = [System.IO.Path]::GetFullPath("$env:ProgramData\RDP-login-monitor")
|
||||
$PsExe = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||
|
||||
function Test-RunningElevated {
|
||||
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
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)
|
||||
}
|
||||
|
||||
if (-not (Test-RunningElevated)) {
|
||||
throw 'Запустите Install-DomainMonitors.ps1 от имени администратора.'
|
||||
}
|
||||
|
||||
switch ($Target) {
|
||||
'Exchange' {
|
||||
$scriptPath = Join-Path $InstallRoot 'Exchange-MailSecurity.ps1'
|
||||
if (-not (Test-Path -LiteralPath $scriptPath)) {
|
||||
throw "Не найден: $scriptPath"
|
||||
}
|
||||
$notifyPath = Join-Path $InstallRoot 'Notify-Common.ps1'
|
||||
if (-not (Test-Path -LiteralPath $notifyPath)) {
|
||||
throw "Не найден: $notifyPath"
|
||||
}
|
||||
& $PsExe -NoProfile -ExecutionPolicy Bypass -File $scriptPath -InstallTasks
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
Write-Host "Exchange: задачи планировщика зарегистрированы."
|
||||
}
|
||||
'Ad' {
|
||||
Write-Host 'AD-SecurityMonitor.ps1 пока не реализован — используйте Target Exchange.'
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,214 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Общие функции уведомлений (Telegram / SMTP) для скриптов RDP-login-monitor.
|
||||
.DESCRIPTION
|
||||
Dot-source после определения Write-NotifyLog в вызывающем скрипте.
|
||||
Ожидает переменные: $NotifyOrder, $TelegramBotToken, $TelegramChatID,
|
||||
$MailSmtpHost, $MailFrom, $MailTo и др. (см. Login_Monitor.ps1).
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Write-NotifyLog -ErrorAction SilentlyContinue)) {
|
||||
function Write-NotifyLog {
|
||||
param([string]$Message)
|
||||
Write-Host $Message
|
||||
}
|
||||
}
|
||||
|
||||
function Unprotect-RdpMonitorDpapiB64 {
|
||||
param([Parameter(Mandatory = $true)][string]$Base64)
|
||||
Add-Type -AssemblyName System.Security
|
||||
$bytes = [Convert]::FromBase64String($Base64.Trim())
|
||||
$plain = [System.Security.Cryptography.ProtectedData]::Unprotect(
|
||||
$bytes,
|
||||
$null,
|
||||
[System.Security.Cryptography.DataProtectionScope]::LocalMachine
|
||||
)
|
||||
return [Text.Encoding]::UTF8.GetString($plain)
|
||||
}
|
||||
|
||||
function Initialize-NotifyCredentials {
|
||||
param(
|
||||
[string]$TelegramBotTokenProtectedB64 = '',
|
||||
[string]$TelegramChatIDProtectedB64 = '',
|
||||
[ref]$TelegramBotToken,
|
||||
[ref]$TelegramChatID,
|
||||
[string]$MailSmtpPasswordProtectedB64 = '',
|
||||
[ref]$MailSmtpPassword
|
||||
)
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($TelegramBotTokenProtectedB64)) {
|
||||
$TelegramBotToken.Value = Unprotect-RdpMonitorDpapiB64 -Base64 $TelegramBotTokenProtectedB64
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($TelegramChatIDProtectedB64)) {
|
||||
$TelegramChatID.Value = Unprotect-RdpMonitorDpapiB64 -Base64 $TelegramChatIDProtectedB64
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($MailSmtpPasswordProtectedB64)) {
|
||||
$MailSmtpPassword.Value = Unprotect-RdpMonitorDpapiB64 -Base64 $MailSmtpPasswordProtectedB64
|
||||
}
|
||||
}
|
||||
|
||||
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-NotifyLog "NotifyOrder: неизвестный канал '$part'"
|
||||
$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 '' }
|
||||
return [System.Net.WebUtility]::HtmlEncode([string]$Text)
|
||||
}
|
||||
|
||||
function Send-TelegramMessage {
|
||||
param([string]$Message)
|
||||
|
||||
if (-not (Test-NotifyTelegramConfigured)) {
|
||||
Write-NotifyLog "Telegram: не задан токен/chat_id"
|
||||
return $false
|
||||
}
|
||||
|
||||
$uri = "https://api.telegram.org/bot$TelegramBotToken/sendMessage"
|
||||
$body = @{
|
||||
chat_id = $TelegramChatID
|
||||
text = $Message
|
||||
parse_mode = "HTML"
|
||||
}
|
||||
|
||||
try {
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
|
||||
$null = Invoke-RestMethod -Uri $uri -Method Post -Body $body -ErrorAction Stop -TimeoutSec 30
|
||||
return $true
|
||||
} catch {
|
||||
Write-NotifyLog "Ошибка отправки в Telegram: $($_.Exception.Message)"
|
||||
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-NotifyLog "Email: SMTP не настроен"
|
||||
return $false
|
||||
}
|
||||
|
||||
try {
|
||||
$toList = @($MailTo -split '[,;]' | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||
if ($toList.Count -eq 0) { 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-NotifyLog "Ошибка отправки Email: $($_.Exception.Message)"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Send-MonitorNotification {
|
||||
param(
|
||||
[string]$Message,
|
||||
[string]$EmailSubject = "RDP Login Monitor"
|
||||
)
|
||||
|
||||
$channels = @(Get-NotifyOrderChannels)
|
||||
if ($channels.Count -eq 0) {
|
||||
Write-NotifyLog "Оповещение не отправлено: нет каналов"
|
||||
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
|
||||
}
|
||||
@@ -10,7 +10,7 @@ PowerShell-набор для мониторинга входов в Windows с
|
||||
- `RDP-Login-Monitor` (основной монитор),
|
||||
- `RDP-Login-Monitor-Watchdog` (контроль процесса каждые 5 минут).
|
||||
- Доменная доставка и обновления: **`Deploy-LoginMonitor.ps1`** + **`version.txt`** с шары `NETLOGON`. После успешного деплоя в приветственном сообщении (Telegram/Email) может появиться отметка об обновлении (файл **`deploy_last_update.txt`** рядом с логами).
|
||||
- Для полной инструкции по деплою/GPO используйте **[DEPLOY.md](DEPLOY.md)**.
|
||||
- Документация по развёртыванию: **[Docs/README.md](Docs/README.md)** (RDP-монитор, Exchange, NETLOGON).
|
||||
- **`Encrypt-DpapiForRdpMonitor.ps1`** — опционально для подготовки DPAPI-строк токена/chat id и пароля SMTP (`$MailSmtpPasswordProtectedB64`).
|
||||
|
||||
## Что изменилось (важное)
|
||||
@@ -20,6 +20,7 @@ PowerShell-набор для мониторинга входов в Windows с
|
||||
- **`auditpol` на русской Windows**: настройка/проверка аудита опирается на категорию **`Вход/выход`** и подкатегории **`Вход в систему` / `Выход из системы`** (ожидается строка **`Успех и сбой`**). Это устраняет ошибки вида `0x00000057` из‑за несуществующего на RU ОС имени `Logon`.
|
||||
- **Стабильность**: `auditpol` вызывается по полному пути `%SystemRoot%\System32\auditpol.exe` (без зависимости от PATH), stdout+stderr объединяются через `ProcessStartInfo`.
|
||||
- **Агрегация 4625 (брутфорс)**: при включённом `$FailedLogonRateLimitEnabled` — уровень 1: **5** неудачных попыток за **60** с с одного источника для **одной** учётной записи (IP+user); уровень 2: **12** попыток за **60** с с одного IP (несколько логинов). Пока порог не достигнут — поштучные 4625; при всплеске — сводные алерты, одиночные подавляются. Параметры в начале `Login_Monitor.ps1`. Автоблокировка IP не выполняется.
|
||||
- **Exchange Mail Security** (`Exchange-MailSecurity.ps1`): на **сервере Exchange** — очереди, пересылка на внешние адреса, watchdog. Руководство: **[Docs/exchange-mail-security.md](Docs/exchange-mail-security.md)**.
|
||||
|
||||
## 1) Подготовка
|
||||
|
||||
@@ -61,7 +62,7 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-logi
|
||||
|
||||
Скрипт сам зарегистрирует `RDP-Login-Monitor` и `RDP-Login-Monitor-Watchdog`, а также запросит немедленный первый запуск задач.
|
||||
|
||||
Для доменной установки/обновления с шары вручную ничего в планировщике на клиенте настраивать не требуется: используйте `Deploy-LoginMonitor.ps1` (подробно в `DEPLOY.md`).
|
||||
Для доменной установки/обновления с шары вручную ничего в планировщике на клиенте настраивать не требуется: используйте `Deploy-LoginMonitor.ps1` (подробно в [Docs/deploy-rdp-login-monitor.md](Docs/deploy-rdp-login-monitor.md)).
|
||||
|
||||
## 4) Что проверять после запуска
|
||||
|
||||
|
||||
+3
-2
@@ -10,7 +10,7 @@ PowerShell toolkit for monitoring Windows logons with Telegram and/or Email (SMT
|
||||
- `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 notification may include an update note (file **`deploy_last_update.txt`** next to logs).
|
||||
- Full deploy/GPO guidance: **[DEPLOY.md](DEPLOY.md)**.
|
||||
- Deployment docs: **[Docs/README.md](Docs/README.md)** (RDP monitor, Exchange, NETLOGON).
|
||||
- **`Encrypt-DpapiForRdpMonitor.ps1`** — optional helper to prepare DPAPI-protected Base64 for the bot token / chat id and SMTP password (`$MailSmtpPasswordProtectedB64`).
|
||||
|
||||
## Notable behavior
|
||||
@@ -20,6 +20,7 @@ PowerShell toolkit for monitoring Windows logons with Telegram and/or Email (SMT
|
||||
- **`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 full path `%SystemRoot%\System32\auditpol.exe` (no PATH dependency); stdout and stderr are merged via `ProcessStartInfo`.
|
||||
- **`4625` burst alerts**: when `$FailedLogonRateLimitEnabled` is true — tier 1: **5** failures in **60** s per **IP+user**; tier 2: **12** in **60** s per **IP** (spray). Below thresholds, individual `4625` alerts are sent; during a burst, aggregated alerts replace per-event noise. No automatic IP blocking. Tune at the top of `Login_Monitor.ps1`.
|
||||
- **Exchange Mail Security** (`Exchange-MailSecurity.ps1`): Exchange server only — queues, external forwarding, watchdog. See **[Docs/exchange-mail-security.md](Docs/exchange-mail-security.md)**.
|
||||
|
||||
## 1) Preparation
|
||||
|
||||
@@ -61,7 +62,7 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-logi
|
||||
|
||||
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`).
|
||||
For domain deployment from a share you do not configure the scheduler on clients by hand — use `Deploy-LoginMonitor.ps1` (see [Docs/deploy-rdp-login-monitor.md](Docs/deploy-rdp-login-monitor.md)).
|
||||
|
||||
## 4) Post-install checks
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Пример локальных настроек Exchange-MailSecurity.ps1
|
||||
.DESCRIPTION
|
||||
Скопируйте в C:\ProgramData\RDP-login-monitor\exchange_monitor.settings.ps1
|
||||
и задайте секреты / whitelist. Файл не деплоится с шары автоматически.
|
||||
#>
|
||||
|
||||
# Telegram (или DPAPI Base64 с Encrypt-DpapiForRdpMonitor.ps1)
|
||||
$TelegramBotToken = ''
|
||||
$TelegramChatID = ''
|
||||
# $TelegramBotTokenProtectedB64 = ''
|
||||
# $TelegramChatIDProtectedB64 = ''
|
||||
|
||||
# SMTP (опционально)
|
||||
# $MailSmtpHost = 'smtp.example.com'
|
||||
# $MailFrom = 'monitor@example.com'
|
||||
# $MailTo = 'admin@example.com'
|
||||
# $NotifyOrder = 'telegram,email'
|
||||
|
||||
# Внешние домены-партнёры (не считать угрозой)
|
||||
$ExternalDomainWhitelist = @(
|
||||
# 'partner-bank.ru'
|
||||
)
|
||||
|
||||
# Очереди
|
||||
$QueueMessageCountThreshold = 150
|
||||
|
||||
# Скан ящиков: только VIP (для первого пилота)
|
||||
# $VipMailboxesOnly = $true
|
||||
# $VipMailboxes = @('ceo@kalinamall.ru', 'cfo@kalinamall.ru')
|
||||
|
||||
# Удалённый EMS (если скрипт не на Exchange)
|
||||
# $ExchangeServerFqdn = 'fifth.kalinamall.ru'
|
||||
+11
-2
@@ -4,7 +4,7 @@
|
||||
.DESCRIPTION
|
||||
Dlya servera publikatsii (napr. DC3). Ne trebuet imenovaniya remote: pri otsutstvii
|
||||
dobavlyaetsya origin ili vypolnyaetsya git pull po URL.
|
||||
Kopiruyutsya: Login_Monitor.ps1, version.txt, Deploy-LoginMonitor.ps1.
|
||||
Kopiruyutsya: polnyj spisok v Docs/deploy-netlogon-publish.md.
|
||||
.EXAMPLE
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\soft\update-rdp-monitor.ps1
|
||||
.EXAMPLE
|
||||
@@ -20,7 +20,16 @@ param(
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$DistFiles = @('Login_Monitor.ps1', 'version.txt', 'Deploy-LoginMonitor.ps1')
|
||||
$DistFiles = @(
|
||||
'Login_Monitor.ps1',
|
||||
'version.txt',
|
||||
'Deploy-LoginMonitor.ps1',
|
||||
'Exchange-MailSecurity.ps1',
|
||||
'Notify-Common.ps1',
|
||||
'Install-DomainMonitors.ps1',
|
||||
'Deploy-DomainMonitors.ps1',
|
||||
'exchange_monitor.settings.example.ps1'
|
||||
)
|
||||
|
||||
function Write-UpdateLog {
|
||||
param([string]$Message)
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.5.10
|
||||
1.6.0
|
||||
|
||||
Reference in New Issue
Block a user