Compare commits
7 Commits
c67bd2595c
...
599b152296
| Author | SHA1 | Date | |
|---|---|---|---|
| 599b152296 | |||
| c8dc681289 | |||
| c4c0556a2d | |||
| 2f44ddc29d | |||
| 6d160cef16 | |||
| 2e262ae878 | |||
| 6f08af7365 |
@@ -682,6 +682,49 @@ function Sync-RdpMonitorSettingsHeartbeatInterval {
|
||||
return $true
|
||||
}
|
||||
|
||||
function Test-RdpMonitorSettingsNeedsHeartbeatStaleAlertMultiplier {
|
||||
param([string]$SettingsPath)
|
||||
if (-not (Test-Path -LiteralPath $SettingsPath)) { return $true }
|
||||
$c = Get-RdpMonitorSettingsRaw -Path $SettingsPath
|
||||
if ([string]::IsNullOrWhiteSpace($c)) { return $true }
|
||||
return ($c -notmatch '(?m)^\s*\$HeartbeatStaleAlertMultiplier\s*=\s*\d+')
|
||||
}
|
||||
|
||||
function Sync-RdpMonitorSettingsHeartbeatStaleAlertMultiplierIfMissing {
|
||||
param(
|
||||
[string]$LocalSettings,
|
||||
[int]$TargetMultiplier = 2
|
||||
)
|
||||
|
||||
if (-not (Test-RdpMonitorSettingsNeedsHeartbeatStaleAlertMultiplier -SettingsPath $LocalSettings)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $LocalSettings)) { return $false }
|
||||
|
||||
$c = Get-RdpMonitorSettingsRaw -Path $LocalSettings
|
||||
$block = @(
|
||||
'# --- Heartbeat stale alert: множитель × HeartbeatInterval ---'
|
||||
"`$HeartbeatStaleAlertMultiplier = $TargetMultiplier"
|
||||
) -join "`r`n"
|
||||
|
||||
$bak = "$LocalSettings.bak.$(Get-Date -Format 'yyyyMMdd_HHmmss')"
|
||||
Copy-Item -LiteralPath $LocalSettings -Destination $bak -Force
|
||||
|
||||
$insertAfterHeartbeat = '(?m)^(\s*\$HeartbeatInterval\s*=\s*\d+\s*)$'
|
||||
if (-not [string]::IsNullOrWhiteSpace($c) -and $c -match $insertAfterHeartbeat) {
|
||||
$newContent = [regex]::Replace($c, $insertAfterHeartbeat, ('$1' + "`r`n" + $block), 1)
|
||||
} elseif (-not [string]::IsNullOrWhiteSpace($c)) {
|
||||
$newContent = ($c.TrimEnd() + "`r`n`r`n" + $block + "`r`n")
|
||||
} else {
|
||||
$newContent = ($block + "`r`n")
|
||||
}
|
||||
|
||||
[System.IO.File]::WriteAllText($LocalSettings, $newContent.TrimEnd() + "`r`n", $Utf8Bom)
|
||||
Write-DeployLog "login_monitor.settings.ps1: добавлен `$HeartbeatStaleAlertMultiplier = $TargetMultiplier (резервная копия: $bak)"
|
||||
return $true
|
||||
}
|
||||
|
||||
function Test-RdpMonitorSettingsNeedsStartupRebootDetectMinutes {
|
||||
param([string]$SettingsPath)
|
||||
if (-not (Test-Path -LiteralPath $SettingsPath)) { return $true }
|
||||
@@ -736,6 +779,7 @@ function Invoke-RdpMonitorSettingsPostPatches {
|
||||
if (Sync-RdpMonitorSettingsWinRmInboundBlock -LocalSettings $LocalSettings) { $changed = $true }
|
||||
if (Sync-RdpMonitorSettingsMaxBackupDaysIfMissing -LocalSettings $LocalSettings) { $changed = $true }
|
||||
if (Sync-RdpMonitorSettingsGetInventoryIfMissing -LocalSettings $LocalSettings) { $changed = $true }
|
||||
if (Sync-RdpMonitorSettingsHeartbeatStaleAlertMultiplierIfMissing -LocalSettings $LocalSettings) { $changed = $true }
|
||||
if (Sync-RdpMonitorSettingsStartupRebootDetectMinutesIfMissing -LocalSettings $LocalSettings) { $changed = $true }
|
||||
return $changed
|
||||
}
|
||||
|
||||
@@ -203,6 +203,21 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass `
|
||||
|
||||
**`Deploy-LoginMonitor.ps1`** пишет **`restart.request`**, ждёт до **35 с**, затем **`Stop-Process -Force`** только при таймауте.
|
||||
|
||||
## Обновление через SAC (WinRM)
|
||||
|
||||
Альтернатива NETLOGON/GPO: кнопка **«Обновить через WinRM»** на карточке хоста в SAC (сервер ≥ 0.20.15).
|
||||
|
||||
| Этап | Где | Действие |
|
||||
|------|-----|----------|
|
||||
| 1 | SAC | `git fetch` RDP-login-monitor → zip (`.ps1` с UTF-8 BOM) |
|
||||
| 2 | Клиент (WinRM) | `Invoke-WebRequest` → `/api/v1/agent/rdp-bundle/<token>` |
|
||||
| 3 | Клиент | Распаковка в `C:\ProgramData\RDP-login-monitor\_sac_staging` |
|
||||
| 4 | Клиент | `Deploy-LoginMonitor.ps1 -SourceShareRoot _sac_staging` |
|
||||
|
||||
**Требования:** domain admin в SAC; `SAC_PUBLIC_URL` доступен с ПК; `rdp_git_repo_url` в настройках обновлений. **`Deploy-LoginMonitor.ps1` на шаре NETLOGON для этого пути не обязателен** — скрипт приходит в zip.
|
||||
|
||||
Лог операции — в модалке SAC сразу при старте. См. [agent-control-plane.md](https://git.kalinamall.ru/PapaTramp/security-alert-center/src/branch/main/docs/agent-control-plane.md) §4.3.
|
||||
|
||||
## Версии
|
||||
|
||||
| Файл | Роль |
|
||||
|
||||
+9
-3
@@ -1,4 +1,4 @@
|
||||
<#
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Мониторинг логинов/попыток входа с уведомлениями в Telegram
|
||||
.DESCRIPTION
|
||||
@@ -90,7 +90,7 @@ $script:SkipLogDetailLimit = 15
|
||||
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
|
||||
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
|
||||
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
|
||||
$ScriptVersion = "2.0.38-SAC"
|
||||
$ScriptVersion = "2.1.2-SAC"
|
||||
|
||||
# Логи (все под InstallRoot)
|
||||
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
||||
@@ -103,12 +103,13 @@ $MaxBackupDays = 31
|
||||
|
||||
# Heartbeat (файл; при отсутствии обновления > HeartbeatStaleAlertMultiplier × интервал — оповещение)
|
||||
$HeartbeatInterval = 14400
|
||||
$HeartbeatStaleAlertMultiplier = 2
|
||||
# Окно (мин) для определения «старт после перезагрузки ОС» (LastBootUpTime + System 41/1074/6005/6008/6009).
|
||||
$StartupRebootDetectMinutes = 5
|
||||
# Инвентаризация железа/ПО для SAC (agent.inventory); интервал опроса, сек (12 ч).
|
||||
$InventoryIntervalSec = 43200
|
||||
$GetInventory = 1
|
||||
$HeartbeatStaleAlertMultiplier = 2
|
||||
$SacCommandPollIntervalSec = 60
|
||||
# Один RDP-вход часто даёт 2+ события 4624 с одним временем — не слать дубли в Telegram/SAC.
|
||||
$LoginSuccessNotifyDedupSeconds = 90
|
||||
$HeartbeatFile = Join-Path $script:InstallRoot "Logs\last_heartbeat.txt"
|
||||
@@ -4404,6 +4405,7 @@ function Start-LoginMonitor {
|
||||
$adminShareMonitoringEnabled = Test-MonitorFeatureEnabled -Value $EnableAdminShareMonitoring
|
||||
|
||||
$nextHeartbeatTime = (Get-Date).AddSeconds($HeartbeatInterval)
|
||||
$script:NextSacCommandPollTime = Get-Date
|
||||
$nextInventoryTime = (Get-Date).AddSeconds($InventoryIntervalSec)
|
||||
$nextRotationCheck = Check-AndRotateLog
|
||||
$nextReportCheck = Check-AndSendDailyReport
|
||||
@@ -4934,6 +4936,10 @@ function Start-LoginMonitor {
|
||||
}
|
||||
if ($script:SacClientLoaded) {
|
||||
Invoke-SacFlushSpool -MaxFiles 5 | Out-Null
|
||||
if ((Get-SacNormalizedMode) -ne 'off' -and $now -ge $script:NextSacCommandPollTime) {
|
||||
Invoke-SacProcessPendingCommands | Out-Null
|
||||
$script:NextSacCommandPollTime = $now.AddSeconds($SacCommandPollIntervalSec)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if ($_.Exception -is [System.Management.Automation.PipelineStoppedException]) { throw }
|
||||
|
||||
@@ -1,234 +1,61 @@
|
||||
# RDP Login Monitor
|
||||
# RDP Login Monitor
|
||||
|
||||
PowerShell-набор для мониторинга входов в Windows с уведомлениями в Telegram и/или Email (SMTP).
|
||||
**Версия:** `2.1.2-SAC` (`$ScriptVersion` + `version.txt`)
|
||||
|
||||
## Быстрый старт (1 команда с шары)
|
||||
PowerShell-мониторинг Windows: RDP/RDS, RD Gateway, WinRM, admin share, блокировки УЗ, heartbeat, отчёты.
|
||||
|
||||
Для **первичной установки на новый сервер** используйте только deploy-скрипт с шары:
|
||||
## Установка (домен)
|
||||
|
||||
```powershell
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "\\<DC>\NETLOGON\RDP-login-monitor\Deploy-LoginMonitor.ps1"
|
||||
```
|
||||
|
||||
Скрипт сам:
|
||||
- создаст `C:\ProgramData\RDP-login-monitor\`;
|
||||
- скопирует `Login_Monitor.ps1`, `Sac-Client.ps1` и служебные файлы;
|
||||
- создаст/обновит задачи `RDP-Login-Monitor` и `RDP-Login-Monitor-Watchdog`;
|
||||
- запустит монитор;
|
||||
- при необходимости создаст/дополнит `login_monitor.settings.ps1`;
|
||||
- проверит WinRM Operational и попробует включить канал.
|
||||
Каталог: `C:\ProgramData\RDP-login-monitor\` · настройки: `login_monitor.settings.ps1` (не перезаписываются при деплое).
|
||||
|
||||
## Обновление через SAC (WinRM)
|
||||
|
||||
С карточки хоста в SAC: **«Обновить через WinRM»** (SAC ≥ 0.20.15). Сервер тянет репозиторий с git, отдаёт zip по HTTPS; на ПК **не нужны** git и NETLOGON.
|
||||
|
||||
1. Staging: `C:\ProgramData\RDP-login-monitor\_sac_staging`
|
||||
2. `Deploy-LoginMonitor.ps1 -SourceShareRoot` (тот же алгоритм, что при GPO)
|
||||
3. Нужны: WinRM, domain admin в SAC, доступ ПК к `SAC_PUBLIC_URL`, `rdp_git_repo_url` в **Настройки → Обновления агентов**
|
||||
|
||||
Подробнее: [security-alert-center — agent-control-plane](https://git.kalinamall.ru/PapaTramp/security-alert-center/src/branch/main/docs/agent-control-plane.md) §4.3.
|
||||
|
||||
## События в SAC
|
||||
|
||||
| Источник | Тип SAC |
|
||||
|----------|---------|
|
||||
| Security 4624/4625 | `rdp.login.*` |
|
||||
| Security 5140 | `smb.admin_share.access` |
|
||||
| WinRM Operational 91 | `winrm.session.started` |
|
||||
| RD Gateway 302/303 | `rdg.connection.*` → flap 302→303 в SAC |
|
||||
| Security 4740 (КД) | `auth.account.locked` |
|
||||
| Агент | `agent.heartbeat`, `report.daily.rdp`, `agent.inventory` |
|
||||
|
||||
**SAC:** `Sac-Client.ps1`, `$UseSAC` — [agent-integration.md](https://git.kalinamall.ru/PapaTramp/security-alert-center/src/branch/main/docs/agent-integration.md). Poll команд `qwinsta`/`logoff` (≥ 2.1.0-SAC).
|
||||
|
||||
## Ключевые файлы
|
||||
|
||||
| Файл | Назначение |
|
||||
|------|------------|
|
||||
| `Login_Monitor.ps1` | Основной цикл |
|
||||
| `Sac-Client.ps1` | Отправка в SAC |
|
||||
| `Deploy-LoginMonitor.ps1` | NETLOGON, задачи, WinRM self-heal |
|
||||
| `login_monitor.settings.ps1` | Секреты и параметры (локально) |
|
||||
|
||||
## Быстрые проверки
|
||||
|
||||
Проверка после установки (1 команда):
|
||||
```powershell
|
||||
Get-Content "C:\ProgramData\RDP-login-monitor\Logs\login_monitor.log" -Tail 60
|
||||
Login_Monitor.ps1 -CheckSac
|
||||
```
|
||||
|
||||
## Актуальная схема (рекомендуется)
|
||||
## Документация
|
||||
|
||||
- Базовый путь установки: **`C:\ProgramData\RDP-login-monitor\`**.
|
||||
- Основной скрипт: **`Login_Monitor.ps1`** — журнал Security **`4624`/`4625`** (логика зависит от типа ОС: рабочая станция или сервер/КД), при всплеске **`4625`** — **агрегированные оповещения** (два порога: IP+пользователь и только IP), при наличии журнала — **Remote Connection Manager `1149`** (часто актуально для РС с RDP), **RDS Shadow Control** (`20506`/`20507`/`20510`, severity warning), **WinRM inbound / Enter-PSSession** (Operational `91`, severity warning), **admin share `C$`/`ADMIN$`** (Security **5140**, severity warning), при роли **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/Email) может появиться отметка об обновлении (файл **`deploy_last_update.txt`** рядом с логами).
|
||||
- Документация по развёртыванию: **[Docs/README.md](Docs/README.md)** (RDP-монитор, Exchange, NETLOGON).
|
||||
- **`Encrypt-DpapiForRdpMonitor.ps1`** — опционально для подготовки DPAPI-строк токена/chat id и пароля SMTP (`$MailSmtpPasswordProtectedB64` в файле настроек).
|
||||
- **Локальные настройки RDP-монитора:** **`login_monitor.settings.ps1`** в каталоге установки (образец **`login_monitor.settings.example.ps1`**). При автообновлении **`Login_Monitor.ps1`** с шары файл настроек **не перезаписывается** (как **`exchange_monitor.settings.ps1`** для Exchange).
|
||||
- **Security Alert Center (SAC):** модуль **`Sac-Client.ps1`** (копируется вместе с `Login_Monitor.ps1`). Режимы **`$UseSAC`**: `off` | `exclusive` | `dual` | `fallback` — контракт в репозитории **security-alert-center** (`docs/agent-integration.md`). Версия релиза: **`$ScriptVersion`** и **`version.txt`** (сейчас **2.0.20-SAC**); **`Sac-Client.ps1`** передаёт ту же версию в SAC (`product_version`).
|
||||
|
||||
### Highlights (события в SAC / Telegram)
|
||||
|
||||
| Источник | Что ловится | Тип SAC (пример) |
|
||||
|----------|-------------|------------------|
|
||||
| Security 4624/4625 | RDP/RDS входы | `rdp.login.success` / `rdp.login.failed` |
|
||||
| Security **5140** | Доступ к **`C$`**, **`ADMIN$`** | `smb.admin_share.access` |
|
||||
| WinRM Operational 91 | Enter-PSSession / PS Remoting | `winrm.session.started` |
|
||||
| RCM Operational | RDS Shadow Control | `rdp.shadow.control.*` |
|
||||
| RD Gateway | 302/303 | `rdg.connection.*` |
|
||||
| Security 4740 | Блокировка УЗ (на заданном КД) | `auth.account.locked` |
|
||||
| Агент | Heartbeat, отчёт, lifecycle | `agent.heartbeat`, `report.daily.rdp` |
|
||||
|
||||
## Что изменилось (важное)
|
||||
|
||||
- **Локальные настройки RDP-монитора** вынесены в **`login_monitor.settings.ps1`** (образец **`login_monitor.settings.example.ps1`**). При автообновлении **`Login_Monitor.ps1`** с шары секреты и параметры КД **не слетают**. Deploy при отсутствии settings создаёт файл один раз из example на шаре.
|
||||
- **Кодировка `.ps1`**: в репозитории добавлены `.editorconfig` и `.gitattributes`, чтобы `*.ps1` по умолчанию сохранялись как **UTF-8 with BOM** и с **CRLF** (это сильно снижает “кракозябры” и ошибки парсинга PowerShell).
|
||||
- **Кодировка логов**: `login_monitor.log` / `watchdog.log` пишутся как **UTF-8 с BOM** (и при необходимости BOM добавляется к уже существующему файлу), чтобы в **FAR/старых просмотрщиках** не было ситуации “в консоли нормально, а в файле РЈРІРµ…” из‑за неверной авто-кодировки.
|
||||
- **`auditpol` на русской Windows**: настройка/проверка аудита опирается на категорию **`Вход/выход`** и подкатегории **`Вход в систему` / `Выход из системы`** (ожидается **`Успех и сбой`** или **`Успех и отказ`** — формулировка зависит от сборки ОС). Это устраняет ошибки вида `0x00000057` из‑за несуществующего на RU ОС имени `Logon`.
|
||||
- **Стабильность**: `auditpol` вызывается по полному пути `%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)**. GPO **`Deploy-LoginMonitor.ps1`** на MailServer ставит тот же RDP-монитор и дописывает WinRM/4624 noise в **`login_monitor.settings.ps1`**; **`Deploy-DomainMonitors.ps1 -Target Exchange`** — отдельно.
|
||||
- **Admin share 5140 (2.0.19+):** Security **5140** для **`C$`/`ADMIN$`** (audit File Share); SAC `smb.admin_share.access`; подавление в `ignore.lst` (`smb:` / `5140:`).
|
||||
- **WinRM / Enter-PSSession (2.0.4+):** улучшен парсинг события `Microsoft-Windows-WinRM/Operational` (`ID=91`) — корректно извлекаются `user` и `clientIP` из EventData/Message fallback, событие отправляется в SAC как `winrm.session.started`.
|
||||
- **Deploy WinRM self-heal (2.0.3+):** `Deploy-LoginMonitor.ps1` проверяет/дописывает обязательный блок WinRM inbound в `login_monitor.settings.ps1`, диагностирует доступность канала `Microsoft-Windows-WinRM/Operational` и пытается включить его через `wevtutil`.
|
||||
- **Диагностика skip WinRM (2.0.5+):** в логах пишется точная причина `Skip WinRM 91` (`empty-user`, `excluded-user-pattern`, `machine-account`, `local-or-linklocal-ip`, `ignore-list-match`), что упрощает troubleshooting.
|
||||
- **WinRM Exchange strict (2.0.23+):** на сервере с ролью Exchange — WinRM **91** без user в EventData/Properties не алертится; корреляция Security **4624** только при `LogonProcess WinRM` (отсекает ложные связки с Outlook/почтовым LT3). Причины skip: `exchange-winrm-no-user-in-event`, `exchange-winrm-unresolved-uri-no-ip`. Deploy дописывает `$WinRmExchangeStrictMode = 1`.
|
||||
|
||||
## 1) Подготовка
|
||||
|
||||
1. Подготовьте папку установки:
|
||||
- `C:\ProgramData\RDP-login-monitor\`
|
||||
2. Скопируйте в неё как минимум:
|
||||
- `Login_Monitor.ps1`
|
||||
- `Sac-Client.ps1`
|
||||
- `login_monitor.settings.example.ps1` → переименуйте в **`login_monitor.settings.ps1`** и задайте параметры (см. п. 3)
|
||||
- (для доменного развёртывания отдельно на шаре) `Deploy-LoginMonitor.ps1`, `version.txt` и `login_monitor.settings.example.ps1`
|
||||
3. Настройте **`C:\ProgramData\RDP-login-monitor\login_monitor.settings.ps1`** (не редактируйте секреты в `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`)
|
||||
- **SAC (опционально):** `$UseSAC`, `$SacUrl`, `$SacApiKey` — см. блок в **`login_monitor.settings.example.ps1`**. Проверка: `Login_Monitor.ps1 -CheckSac`
|
||||
- **IP хоста для SAC (опционально):** `$ServerIPv4` — явный IPv4 для `host.ipv4`; если не задан, берётся автоопределение
|
||||
- **Exchange-шум 4624 (опционально):** `${Ignore4624-LT3-EmptyIP-Event} = $true` — подавляет `4624` c `LogonType=3` и `IP='-'` (часто Outlook/SMTP-клиенты на почтовом сервере)
|
||||
- Пошаговое обновление по домену: **[Docs/deploy-rdp-login-monitor.md](Docs/deploy-rdp-login-monitor.md)** (раздел «Обновление на любой Windows-машине»)
|
||||
4. Запускайте с правами администратора (чтение `Security` журнала и регистрация задач).
|
||||
5. Логи и служебные файлы будут в:
|
||||
- `C:\ProgramData\RDP-login-monitor\Logs\`
|
||||
6. (Опционально) Подавление части алертов по списку — см. раздел **«7) ignore.lst»** ниже.
|
||||
7. (Опционально) Мониторинг блокировок AD на КД — в **`login_monitor.settings.ps1`**: **`$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) Ручной запуск
|
||||
|
||||
Используйте этот вариант для быстрой проверки старта/логики без установки задач планировщика.
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-login-monitor\Login_Monitor.ps1"
|
||||
```
|
||||
|
||||
Примечание: при ручном запуске монитор работает в текущей сессии до остановки (например, `Ctrl+C`).
|
||||
|
||||
## 3) Запуск через Планировщик заданий (Task Scheduler)
|
||||
|
||||
Текущая схема: вручную задачи в GUI создавать не нужно.
|
||||
|
||||
Достаточно запустить:
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-login-monitor\Login_Monitor.ps1" -InstallTasks
|
||||
```
|
||||
|
||||
Скрипт сам зарегистрирует `RDP-Login-Monitor` и `RDP-Login-Monitor-Watchdog`, а также запросит немедленный первый запуск задач.
|
||||
|
||||
Для доменной установки/обновления с шары вручную ничего в планировщике на клиенте настраивать не требуется: используйте `Deploy-LoginMonitor.ps1` (подробно в [Docs/deploy-rdp-login-monitor.md](Docs/deploy-rdp-login-monitor.md)).
|
||||
|
||||
## 4) Что проверять после запуска
|
||||
|
||||
- Логи:
|
||||
- `C:\ProgramData\RDP-login-monitor\Logs\login_monitor.log`
|
||||
- `C:\ProgramData\RDP-login-monitor\Logs\watchdog.log`
|
||||
- Heartbeat:
|
||||
- `C:\ProgramData\RDP-login-monitor\Logs\last_heartbeat.txt` обновляется по интервалу **`$HeartbeatInterval`** (по умолчанию раз в час).
|
||||
- Ежедневный отчёт: после первого прохождения дневного слота (по умолчанию **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 не дублирует формулировку «хост сессий».
|
||||
- **Дубли Telegram на один RDP-вход:** Windows часто пишет **несколько 4624** с одним временем; с версии **1.2.18-SAC** второе уведомление за **`$LoginSuccessNotifyDedupSeconds`** (90 с) подавляется (`Notify dedup 4624` в логе).
|
||||
- **В логе нет `Notify`, но 4624 в Security есть:** монитор обрабатывает только события **после** своего `StartTime` (окно опроса ~10 с при старте). Ищите строки **`Skip 4624:`** (фильтр LogonType / ignore.lst). Диагностика: **`tools\Show-Rdp4624Recent.ps1`**.
|
||||
|
||||
## 4.1) Контрольный список стабилизации (5 минут)
|
||||
|
||||
После деплоя на сервер быстро проверьте, что WinRM/Enter-PSSession действительно проходит весь путь до SAC:
|
||||
|
||||
1. **WinRM Operational включён**
|
||||
```powershell
|
||||
Get-WinEvent -ListLog 'Microsoft-Windows-WinRM/Operational' |
|
||||
Select-Object LogName, IsEnabled, RecordCount
|
||||
```
|
||||
2. **Тестовый вход с другой машины**
|
||||
```powershell
|
||||
Enter-PSSession -ComputerName <SERVER> -Credential <DOMAIN\USER>
|
||||
```
|
||||
3. **На целевом сервере есть событие ID=91**
|
||||
```powershell
|
||||
Get-WinEvent -LogName 'Microsoft-Windows-WinRM/Operational' -MaxEvents 50 |
|
||||
Where-Object Id -eq 91 |
|
||||
Select-Object TimeCreated, Id, Message
|
||||
```
|
||||
4. **В логе агента нет ложного skip**
|
||||
```powershell
|
||||
Get-Content 'C:\ProgramData\RDP-login-monitor\Logs\login_monitor.log' -Tail 120 |
|
||||
Select-String -Pattern 'WinRM 91|Skip WinRM|reason='
|
||||
```
|
||||
Для версии `2.0.5-SAC+` при skip указывается конкретная причина: `empty-user`, `excluded-user-pattern`, `machine-account`, `local-or-linklocal-ip`, `ignore-list-match`.
|
||||
5. **Событие ушло в SAC**
|
||||
- В UI SAC (`/events`) есть запись типа `winrm.session.started` от нужного хоста.
|
||||
|
||||
Минимальная настройка шумоподавления (если нужно):
|
||||
- не используйте `all:` в `ignore.lst` для рабочих админских учёток;
|
||||
- для WinRM подавление задавайте адресно: `winrm:user:DOMAIN\\service` или `winrm:ip:x.x.x.x`;
|
||||
- при проверке troubleshooting временно отключайте лишние исключения и проверяйте повторно.
|
||||
|
||||
## 5) Автоматический перезапуск при падении
|
||||
|
||||
Режим `-Watchdog` внутри `Login_Monitor.ps1` делает:
|
||||
|
||||
- проверяет, есть ли процесс `powershell.exe/pwsh.exe` с `Login_Monitor.ps1` в командной строке;
|
||||
- если процесса нет — запускает монитор;
|
||||
- если монитор уже есть — не дублирует экземпляр.
|
||||
|
||||
## 6) Дополнительные параметры и прочие файлы
|
||||
|
||||
- **`-SkipScheduledTaskMaintenance`**: при обычном запуске монитора не выполнять проверку/пересоздание задач планировщика (если регистрацию задач ведёте только через **`-InstallTasks`** или вручную).
|
||||
- **`Install-DeployScheduledTask.ps1`** — helper для периодического запуска **`Deploy-LoginMonitor.ps1`** с шары (см. **[DEPLOY.md](DEPLOY.md)**).
|
||||
- **`Watchdog_RDP_Monitor.ps1`** и **`Install-ScheduledTasks.ps1`** — **альтернативная** схема с отдельным watchdog-файлом и путями по умолчанию **`D:\Soft`**. Для новых установок рекомендуется встроенный режим **`-Watchdog`** в **`Login_Monitor.ps1`** и задачи **`RDP-Login-Monitor`** / **`RDP-Login-Monitor-Watchdog`**.
|
||||
- **`ignore.lst.example`** в репозитории — образец файла **`ignore.lst`** для подавления отдельных уведомлений Security (см. раздел 7).
|
||||
- **`Diagnose-RdpLoginMonitor.ps1`** — сбор диагностики после RDP-входа (Security 4624/4778, хвост `login_monitor.log`, симуляция фильтров монитора). Отчёт в **`Logs\diagnose_*.txt`**. Запуск:
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-login-monitor\Diagnose-RdpLoginMonitor.ps1" -MinutesBack 15 -ExpectedUser "ваш_логин"
|
||||
```
|
||||
- **`login_monitor.settings.example.ps1`** — образец **`login_monitor.settings.ps1`** (Telegram, SMTP, 4740, локальные IP-исключения). Deploy при первой установке может создать `login_monitor.settings.ps1` из example автоматически.
|
||||
|
||||
## 7) Подавление уведомлений Security: `ignore.lst`
|
||||
|
||||
В каталоге установки можно положить файл **`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 **этим файлом не настраиваются**.
|
||||
|
||||
### Как читается файл
|
||||
|
||||
- Чтение выполняется по мере обработки событий; содержимое **кэшируется в памяти**. Если **`LastWriteTimeUtc`** файла изменился (редактирование и сохранение), список **перечитывается автоматически** — перезапуск монитора не обязателен.
|
||||
- Кодировка: **UTF-8** (`Get-Content -Encoding UTF8`). Строка может начинаться с BOM — он отбрасывается при разборе.
|
||||
- Пустые строки пропускаются. Строки, начинающиеся с **`#`** или **`;`**, считаются комментариями.
|
||||
- Строка с **`:`**: берётся **первая** двоеточие — всё слева (после обрезки пробелов) определяет тип правила, всё справа — значение. Если справа пусто, строка игнорируется.
|
||||
- Строка **без** **`:`**: целиком трактуется как правило типа «любое совпадение» (см. ниже).
|
||||
|
||||
### Префикс области (в самом начале строки, до типа правила)
|
||||
|
||||
| Префикс | События |
|
||||
| --- | --- |
|
||||
| *(нет)* | **4624**, **4625** |
|
||||
| `4740:`, `lockout:`, `блокир:` | **4740** |
|
||||
| `all:`, `*:` | **4624**, **4625**, **4740** |
|
||||
|
||||
Пример: `4740:user:svc_sync` — не слать оповещение о блокировке этой УЗ.
|
||||
|
||||
### Типы правил (левая часть до первого `:` после префикса области)
|
||||
|
||||
| Левая часть (фрагменты совпадают как regex, без учёта регистра) | Поле события |
|
||||
| --- | --- |
|
||||
| `рабоч`, `workstation`, `wks` | имя рабочей станции (**WorkstationName** и аналоги в XML события) |
|
||||
| `польз`, `username`, `subject`, `account`, `target user`, целое слово `user` | имя пользователя (**TargetUserName** и др.) |
|
||||
| `ip`, `ip адрес`, `ipaddress`, `адрес ip` | IP источника (**IpAddress** и др.), только если в событии есть непустой IP |
|
||||
|
||||
Если левая часть **не** подошла ни к одному типу, но двоеточие есть — используется режим как в разборе строк Telegram: тип **«любое»**, значение — **только правая часть** (метка слева отбрасывается).
|
||||
|
||||
### Совпадение для типа «любое» (строка без `:` или «неизвестная» метка слева от `:`)
|
||||
|
||||
Проверка по очереди:
|
||||
|
||||
1. Если значение похоже на **IPv4** — сравнивается с IP источника в событии (точное совпадение, без учёта регистра для текста не применимо).
|
||||
2. Если значение содержит **`\`** — сравнивается с **учётной записью**: полное совпадение с `DOMAIN\user` **или** совпадение с **SAM** после последнего `\` (как `DOMAIN\IVANOV` при правиле `IVANOV`).
|
||||
3. Иначе сначала полное совпадение с **именем рабочей станции**, затем с **учётной записью** по тем же правилам, что в п.2.
|
||||
|
||||
Для явных типов **User** / **Workstation** / **Ip** используется только соответствующее поле (для пользователя — те же правила полного имени и SAM, что в п.2).
|
||||
|
||||
### Примеры и поставка
|
||||
|
||||
- Расширенные примеры строк — в **`ignore.lst.example`** в корне репозитория (скопируйте на сервер как **`ignore.lst`** и отредактируйте).
|
||||
- **`Deploy-LoginMonitor.ps1`** **`ignore.lst`** и **`login_monitor.settings.ps1`** **не копирует и не перезаписывает** — правила и секреты локальны; при отсутствии settings Deploy создаёт его один раз из **`login_monitor.settings.example.ps1`** на шаре.
|
||||
|
||||
## Ключевые слова (для поиска репозитория)
|
||||
|
||||
`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`
|
||||
- [Docs/README.md](Docs/README.md) — развёртывание, Exchange
|
||||
- [security-alert-center](https://git.kalinamall.ru/PapaTramp/security-alert-center) — сервер, RDG flap, qwinsta через SAC UI/Seaca
|
||||
|
||||
## English
|
||||
|
||||
See repository docs; agent contract is in SAC `docs/agent-integration.md`.
|
||||
|
||||
+227
-1
@@ -1,4 +1,4 @@
|
||||
<#
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Клиент Security Alert Center для RDP-login-monitor.
|
||||
.DESCRIPTION
|
||||
@@ -853,6 +853,232 @@ function Invoke-SacFlushSpool {
|
||||
}
|
||||
}
|
||||
|
||||
function Get-SacAgentCommandsUrl {
|
||||
$base = Get-SacBaseUrl
|
||||
if ([string]::IsNullOrWhiteSpace($base)) { return $null }
|
||||
return "$base/api/v1/agent/commands"
|
||||
}
|
||||
|
||||
function Get-SacAgentCommandResultUrl {
|
||||
param([Parameter(Mandatory = $true)][string]$CommandId)
|
||||
$base = Get-SacBaseUrl
|
||||
if ([string]::IsNullOrWhiteSpace($base)) { return $null }
|
||||
return "$base/api/v1/agent/commands/$CommandId/result"
|
||||
}
|
||||
|
||||
function Invoke-SacHttpGet {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Uri,
|
||||
[int]$TimeoutSec = 12
|
||||
)
|
||||
Invoke-SacTlsPrep
|
||||
try {
|
||||
$resp = Invoke-WebRequest -Uri $Uri -Method Get -UseBasicParsing -TimeoutSec $TimeoutSec `
|
||||
-Headers @{ Authorization = "Bearer $SacApiKey" }
|
||||
return @{ StatusCode = [int]$resp.StatusCode; Content = [string]$resp.Content }
|
||||
} catch {
|
||||
$code = 0
|
||||
$content = ''
|
||||
if ($_.Exception.Response) {
|
||||
$code = [int]$_.Exception.Response.StatusCode
|
||||
try {
|
||||
$reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
|
||||
$content = $reader.ReadToEnd()
|
||||
$reader.Close()
|
||||
} catch { }
|
||||
}
|
||||
return @{ StatusCode = $code; Content = $content; Error = $_.Exception.Message }
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-SacHttpPostJson {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Uri,
|
||||
[Parameter(Mandatory = $true)][string]$JsonBody,
|
||||
[int]$TimeoutSec = 12
|
||||
)
|
||||
Invoke-SacTlsPrep
|
||||
$bytes = Get-SacUtf8Bytes -Text $JsonBody
|
||||
try {
|
||||
$resp = Invoke-WebRequest -Uri $Uri -Method Post -UseBasicParsing -TimeoutSec $TimeoutSec `
|
||||
-Headers @{ Authorization = "Bearer $SacApiKey" } `
|
||||
-ContentType 'application/json; charset=utf-8' `
|
||||
-Body $bytes
|
||||
return @{ StatusCode = [int]$resp.StatusCode; Content = [string]$resp.Content }
|
||||
} catch {
|
||||
$code = 0
|
||||
$content = ''
|
||||
if ($_.Exception.Response) {
|
||||
$code = [int]$_.Exception.Response.StatusCode
|
||||
try {
|
||||
$reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
|
||||
$content = $reader.ReadToEnd()
|
||||
$reader.Close()
|
||||
} catch { }
|
||||
}
|
||||
return @{ StatusCode = $code; Content = $content; Error = $_.Exception.Message }
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-SacCaptureProcess {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$CommandLine
|
||||
)
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = 'cmd.exe'
|
||||
$psi.Arguments = "/c $CommandLine"
|
||||
$psi.UseShellExecute = $false
|
||||
$psi.RedirectStandardOutput = $true
|
||||
$psi.RedirectStandardError = $true
|
||||
$psi.CreateNoWindow = $true
|
||||
$p = New-Object System.Diagnostics.Process
|
||||
$p.StartInfo = $psi
|
||||
[void]$p.Start()
|
||||
$stdout = $p.StandardOutput.ReadToEnd()
|
||||
$stderr = $p.StandardError.ReadToEnd()
|
||||
if (-not $p.WaitForExit(60000)) {
|
||||
try { $p.Kill() } catch { }
|
||||
return @{ ExitCode = -1; Stdout = $stdout; Stderr = 'timeout' }
|
||||
}
|
||||
return @{ ExitCode = $p.ExitCode; Stdout = $stdout; Stderr = $stderr }
|
||||
}
|
||||
|
||||
function Invoke-SacRunWithRunAs {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$CommandLine,
|
||||
$RunAs = $null
|
||||
)
|
||||
if ($null -eq $RunAs -or [string]::IsNullOrWhiteSpace($RunAs.user)) {
|
||||
return (Invoke-SacCaptureProcess -CommandLine $CommandLine)
|
||||
}
|
||||
$user = [string]$RunAs.user
|
||||
$password = [string]$RunAs.password
|
||||
if ([string]::IsNullOrWhiteSpace($password)) {
|
||||
return (Invoke-SacCaptureProcess -CommandLine $CommandLine)
|
||||
}
|
||||
$taskName = "SacCmd_$([guid]::NewGuid().ToString('N').Substring(0, 12))"
|
||||
$outFile = Join-Path $env:TEMP "$taskName.out.txt"
|
||||
$errFile = Join-Path $env:TEMP "$taskName.err.txt"
|
||||
if (Test-Path -LiteralPath $outFile) { Remove-Item -LiteralPath $outFile -Force -ErrorAction SilentlyContinue }
|
||||
if (Test-Path -LiteralPath $errFile) { Remove-Item -LiteralPath $errFile -Force -ErrorAction SilentlyContinue }
|
||||
$wrapped = "/c `"$CommandLine > `"$outFile`" 2> `"$errFile`"`""
|
||||
try {
|
||||
$action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument $wrapped
|
||||
Register-ScheduledTask -TaskName $taskName -Action $action -User $user -Password $password `
|
||||
-RunLevel Highest -Force | Out-Null
|
||||
Start-ScheduledTask -TaskName $taskName
|
||||
$deadline = (Get-Date).AddSeconds(45)
|
||||
do {
|
||||
Start-Sleep -Milliseconds 300
|
||||
$state = (Get-ScheduledTask -TaskName $taskName).State
|
||||
} while ($state -eq 'Running' -and (Get-Date) -lt $deadline)
|
||||
$stdout = if (Test-Path -LiteralPath $outFile) { Get-Content -LiteralPath $outFile -Raw -ErrorAction SilentlyContinue } else { '' }
|
||||
$stderr = if (Test-Path -LiteralPath $errFile) { Get-Content -LiteralPath $errFile -Raw -ErrorAction SilentlyContinue } else { '' }
|
||||
return @{ ExitCode = 0; Stdout = [string]$stdout; Stderr = [string]$stderr }
|
||||
} catch {
|
||||
return @{ ExitCode = 1; Stdout = ''; Stderr = $_.Exception.Message }
|
||||
} finally {
|
||||
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath $outFile, $errFile -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-SacAgentCommand {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]$Command,
|
||||
$RunAs = $null
|
||||
)
|
||||
$type = [string]$Command.type
|
||||
switch ($type) {
|
||||
'qwinsta' {
|
||||
return (Invoke-SacRunWithRunAs -CommandLine 'qwinsta.exe' -RunAs $RunAs)
|
||||
}
|
||||
'logoff' {
|
||||
$sid = $null
|
||||
if ($Command.params -and $Command.params.session_id) {
|
||||
$sid = [int]$Command.params.session_id
|
||||
}
|
||||
if (-not $sid) {
|
||||
return @{ ExitCode = 1; Stdout = ''; Stderr = 'missing session_id' }
|
||||
}
|
||||
$line = "logoff.exe $sid /v"
|
||||
return (Invoke-SacRunWithRunAs -CommandLine $line -RunAs $RunAs)
|
||||
}
|
||||
default {
|
||||
return @{ ExitCode = 1; Stdout = ''; Stderr = "unknown command type: $type" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Submit-SacAgentCommandResult {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$CommandId,
|
||||
[Parameter(Mandatory = $true)][string]$Status,
|
||||
[string]$Stdout = '',
|
||||
[string]$Stderr = ''
|
||||
)
|
||||
$url = Get-SacAgentCommandResultUrl -CommandId $CommandId
|
||||
if ([string]::IsNullOrWhiteSpace($url)) { return $false }
|
||||
$body = @{
|
||||
status = $Status
|
||||
stdout = $Stdout
|
||||
stderr = $Stderr
|
||||
} | ConvertTo-Json -Compress
|
||||
$timeout = if ($SacTimeoutSec) { [int]$SacTimeoutSec } else { 12 }
|
||||
$resp = Invoke-SacHttpPostJson -Uri $url -JsonBody $body -TimeoutSec $timeout
|
||||
return ($resp.StatusCode -in 200, 201)
|
||||
}
|
||||
|
||||
function Invoke-SacProcessPendingCommands {
|
||||
if ((Get-SacNormalizedMode) -eq 'off') { return 0 }
|
||||
if (-not (Test-SacConfigured)) { return 0 }
|
||||
|
||||
$pollUrl = Get-SacAgentCommandsUrl
|
||||
if ([string]::IsNullOrWhiteSpace($pollUrl)) { return 0 }
|
||||
|
||||
$agentId = Get-SacAgentInstanceId
|
||||
$timeout = if ($SacTimeoutSec) { [int]$SacTimeoutSec } else { 12 }
|
||||
$uri = "${pollUrl}?agent_instance_id=$([uri]::EscapeDataString($agentId))"
|
||||
$get = Invoke-SacHttpGet -Uri $uri -TimeoutSec $timeout
|
||||
if ($get.StatusCode -ne 200) {
|
||||
if ($get.StatusCode -gt 0) {
|
||||
Write-SacLog "WARN: SAC agent commands poll HTTP $($get.StatusCode)"
|
||||
}
|
||||
return 0
|
||||
}
|
||||
$parsed = $null
|
||||
try {
|
||||
$parsed = $get.Content | ConvertFrom-Json
|
||||
} catch {
|
||||
Write-SacLog "WARN: SAC agent commands poll: invalid JSON"
|
||||
return 0
|
||||
}
|
||||
$commands = @($parsed.commands)
|
||||
if ($commands.Count -eq 0) { return 0 }
|
||||
|
||||
$done = 0
|
||||
foreach ($cmd in $commands) {
|
||||
$cmdId = [string]$cmd.id
|
||||
if ([string]::IsNullOrWhiteSpace($cmdId)) { continue }
|
||||
Write-SacLog "SAC agent command: $cmdId type=$($cmd.type)"
|
||||
$runAs = $cmd.run_as
|
||||
$result = Invoke-SacAgentCommand -Command $cmd -RunAs $runAs
|
||||
$status = if ([int]$result.ExitCode -eq 0) { 'completed' } else { 'failed' }
|
||||
$stderr = [string]$result.Stderr
|
||||
if ([int]$result.ExitCode -ne 0 -and [string]::IsNullOrWhiteSpace($stderr)) {
|
||||
$stderr = "exit code $($result.ExitCode)"
|
||||
}
|
||||
if (Submit-SacAgentCommandResult -CommandId $cmdId -Status $status `
|
||||
-Stdout ([string]$result.Stdout) -Stderr $stderr) {
|
||||
$done++
|
||||
Write-SacLog "SAC agent command result submitted: $cmdId status=$status"
|
||||
} else {
|
||||
Write-SacLog "WARN: SAC agent command result failed: $cmdId"
|
||||
}
|
||||
}
|
||||
return $done
|
||||
}
|
||||
|
||||
function Test-SacConnection {
|
||||
Write-Host 'SAC check (rdp-login-monitor)'
|
||||
Write-Host "UseSAC=$(Get-SacNormalizedMode)"
|
||||
|
||||
@@ -46,6 +46,10 @@ $DailyReportEnabled = 1
|
||||
|
||||
# --- Heartbeat SAC (agent.heartbeat): интервал в секундах; 14400 = 4 ч ---
|
||||
$HeartbeatInterval = 14400
|
||||
# Оповещение, если last_heartbeat.txt не обновлялся > множитель × интервал (2 × 4 ч = 8 ч)
|
||||
$HeartbeatStaleAlertMultiplier = 2
|
||||
# Poll SAC на команды qwinsta/logoff (сек); см. security-alert-center/docs/agent-control-plane.md
|
||||
# $SacCommandPollIntervalSec = 60
|
||||
|
||||
# Окно (мин): LastBootUpTime + System 41/1074/6005/6008/6009 → «старт после перезагрузки ОС»
|
||||
$StartupRebootDetectMinutes = 5
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2.0.38-SAC
|
||||
2.1.2-SAC
|
||||
|
||||
Reference in New Issue
Block a user