# Интеграция агентов с SAC Контракт между **ssh-monitor**, **RDP-login-monitor** и **Security Alert Center**. Изменения вносятся в репозитории агентов отдельными задачами. --- ## 1. Режим `UseSAC` ### 1.1. Параметры конфигурации **ssh-monitor** (`/etc/ssh-monitor.conf`): ```ini # off | exclusive | dual | fallback UseSAC="exclusive" SAC_URL="https://sac.kalinamall.ru/api/v1/events" SAC_API_KEY="sac_xxxxxxxx" SAC_SPOOL_DIR="/var/lib/ssh-monitor/sac-spool" SAC_SEND_HEARTBEAT="1" SAC_FALLBACK_FAILURES="5" SAC_TIMEOUT_SEC="12" ``` **RDP-login-monitor** (`Login_Monitor.ps1` или отдельный `.conf`): ```powershell $UseSAC = "exclusive" # off | exclusive | dual | fallback $SacUrl = "https://sac.kalinamall.ru/api/v1/events" $SacApiKey = "sac_xxxxxxxx" $SacSpoolDir = "D:\Soft\Logs\sac-spool" ``` ### 1.2. Матрица поведения | Событие | `off` | `exclusive` | `dual` | `fallback` | |---------|-------|-------------|--------|------------| | Auth / sudo / ban / RDP login | Telegram/email с агента | Только SAC JSON | SAC + Telegram | SAC; при сбоях → Telegram | | Daily report | С агента | SAC формирует и шлёт | Оба | SAC, fallback как выше | | Heartbeat | С агента (если включён) | Только SAC (`agent.heartbeat`) | Оба | SAC | | Локальный LOG_FILE | Да | Да | Да | Да | ### 1.3. Проверка при старте - `UseSAC=off` — без изменений: хотя бы один канал `NOTIFY_CHAIN` (как сейчас). - `UseSAC≠off` — обязательны `SAC_URL`, `SAC_API_KEY`; HTTP `GET {base}/health` OK. - `--check-sac` / `Test-SacConnection` — отправка `agent.test`, ожидание **HTTP 201** (повтор с тем же `event_id` — **409**, тоже успех для spool). ### 1.3.1. Telegram и email при `UseSAC=exclusive` При **`exclusive`** агент **не** шлёт Telegram/email — оператору нужны оповещения **из SAC** (`backend/app/services/telegram_notify.py`). На сервере SAC — **`sac-api.env`** или UI **Настройки** → **Правило оповещений**: один порог severity и выбор каналов (Telegram / webhook / email). ```env NOTIFY_MIN_SEVERITY=warning NOTIFY_CHANNELS=telegram,webhook,email TELEGRAM_ENABLED=true TELEGRAM_BOT_TOKEN= TELEGRAM_CHAT_ID= ``` | Severity события | `NOTIFY_MIN_SEVERITY=warning` | `high` | |------------------|-------------------------------|--------| | `info` (успешный RDP/SSH login) | нет | нет | | `warning` (`*.login.failed`, sudo) | **да** | нет | | `high` / `critical` (ban, problem) | **да** | **да** | Events и problems используют **одно** глобальное правило (`notification_policy` в БД, миграция `007`). Telegram из SAC отправляется с **`parse_mode=HTML`** (как у RDP-login-monitor): для `rdp.login.*` — пользователь, IP, `logon_type`, рабочая станция, Event ID Windows; для `ssh.login.*` и `privilege.sudo.command` — поля из `details`. UI **Настройки** (`/settings`) — правило + параметры каналов (JWT admin). После деплоя: `alembic upgrade head`. ### 1.4. Версии и доставка обновлений При **любом** изменении агента, влияющем на SAC или поведение на хосте, поднимайте версию и пушьте в **git.kalinamall.ru**: | Агент | Маркер версии | Как хост узнаёт о новой версии | |-------|---------------|--------------------------------| | **ssh-monitor** | `SSH_MONITOR_VERSION` в `ssh-monitor`; `# SAC client release:` в `sac-client.sh` | `update_ssh_monitor.sh`: `git pull` в клоне → сравнение sha256 `ssh-monitor` и `sac-client.sh` | | **RDP-login-monitor** | `$ScriptVersion` в `Login_Monitor.ps1` и **та же** строка в `version.txt` на шаре NETLOGON | `Deploy-LoginMonitor.ps1`: сверка `version.txt` и SHA256 пакета (`Login_Monitor.ps1`, `Sac-Client.ps1`); при отсутствии SAC в settings — **`UseSAC=dual`** из example; подсказка `# $ServerDisplayName` | **Кириллица в SAC (RDP):** до **1.2.8-SAC** `Invoke-WebRequest` мог слать JSON не в UTF-8 — в UI «Отчёты» summary вида `RDP 24?: ??????` вместо `RDP 24ч: сессий …`. Обновите `Sac-Client.ps1` на всех хостах; старые события в БД не пересчитываются, исправятся только новые ingest после деплоя. **HTTP 422 и spool (RDP):** до **1.2.9-SAC** при отклонении схемой (часто `title` длиннее 256 символов) JSON попадал в `sac-spool` и повторялся бесконечно. С **1.2.9-SAC**: обрезка `title`/`summary`, UTF-8 POST, при 422 файл уходит в `sac-spool/rejected/` (не ретраится). Старые `.json` в spool на хосте удалите или перенесите вручную после обновления. **HTTP 409 и spool (RDP):** в PowerShell `Invoke-WebRequest` часто **бросает исключение** на 409 (и иногда на 201), хотя событие уже в SAC. До **1.2.10-SAC** клиент считал это ошибкой и снова слал тот же `event_id` из spool каждые ~5 с (лог `WARN: SAC POST HTTP 409`). С **1.2.10-SAC** коды **201/409/202** обрабатываются как успех и файл spool удаляется. **HTTP 422 на `agent.lifecycle` (RDP):** часто из‑за битого JSON с кириллицей в `summary` (`ConvertTo-Json` + неверная кодировка POST). С **1.2.11-SAC** — сериализация через `JavaScriptSerializer` (Unicode `\uXXXX`), в лог пишется тело ответа SAC (`schema_errors`). Файлы в `sac-spool/rejected/` после 422 можно удалить. **422 `json_invalid` / `Extra data` (позиция 4):** тело POST было `null` + JSON (утечка в pipeline от `Write-Log`). С **1.2.12-SAC**: `[void](Write-Log)`, одна строка JSON, без `ConvertFrom-Json` перед POST. Только правки `Sac-Client.ps1` / `sac-client.sh`: для RDP достаточно поднять patch в `version.txt` (и при необходимости `$ScriptVersion`); для Linux `sac-client.sh` обновится по checksum даже без смены `SSH_MONITOR_VERSION`, но **рекомендуется** поднимать обе метки для логов. ### 1.5. Правила Problems v1 (SAC backend) | `rule_id` | Условие | Пороги (env) | |-----------|---------|--------------| | `rule:brute_force_burst` | `ssh.login.failed` / `rdp.login.failed`, один `source_ip` | `SAC_BRUTE_FORCE_WINDOW_MINUTES=15`, `SAC_BRUTE_FORCE_THRESHOLD=30` | | `rule:privilege_spike` | `privilege.sudo.command` на хосте | `SAC_PRIVILEGE_SPIKE_WINDOW_MINUTES=10`, `SAC_PRIVILEGE_SPIKE_THRESHOLD=10` | | `rule:host_silence` | был heartbeat, но устарел (`agent.heartbeat`) | `SAC_HEARTBEAT_STALE_MINUTES` (как для UI «Хосты») | Свежий `agent.heartbeat` **автоматически resolve** open/ack проблему `rule:host_silence`. Одиночные `high`/`critical` (ban, mass bruteforce) — как раньше. --- ## 2. Протокол ingest ### 2.1. Запрос ```http POST /api/v1/events HTTP/1.1 Host: sac.kalinamall.ru Content-Type: application/json Authorization: Bearer sac_xxxxxxxx Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 { ... событие по event-schema-v1.json ... } ``` ### 2.2. Ответ | HTTP | Значение | `status` в теле | `created` | |------|----------|-----------------|-----------| | **201** | Событие записано впервые | `created` | `true` | | **409** | Тот же `event_id` уже есть (идемпотентность) | `duplicate` | `false` | | **422** | Ошибка JSON Schema | — | — | Пример **201**: ```json { "status": "created", "event_id": "550e8400-e29b-41d4-a716-446655440000", "created": true, "sac_event_url": "https://sac.kalinamall.ru/api/v1/events/12345", "problem_id": null } ``` `event_id` — обязательный **UUID** (RFC 4122), уникален глобально; в БД индекс `UNIQUE (event_id)`. ### 2.3. Spool при ошибке 1. Записать JSON в `SAC_SPOOL_DIR/{event_id}.json`. 2. **ssh-monitor:** каждую итерацию цикла вызывается `sac_flush_spool` (до 20 файлов). 3. После успеха (**HTTP 201** или **409**) — удалить файл из spool. 4. Лимит размера spool (например 500 MB) — логировать WARN, не удалять без алерта. --- ## 3. Маппинг уведомлений → типы событий ### 3.1. ssh-monitor | Текущий текст (сокращённо) | `type` | `severity` | |----------------------------|--------|------------| | Успешное SSH | `ssh.login.success` | info | | Неудачная SSH | `ssh.login.failed` | warning | | IP заблокирован | `ssh.ip.banned` | high | | Лимит без бана | `ssh.ip.bruteforce.threshold` | warning | | Массовый брутфорс | `ssh.bruteforce.mass` | high | | Sudo | `privilege.sudo.command` | warning–critical* | | logind new/removed/failed | `session.logind.*` | info–warning | | Ежедневный отчёт | `report.daily.ssh` | info | | Heartbeat | `agent.heartbeat` | info | \* critical — по эвристике команды (`useradd`, `passwd`, `rm -rf`, …) в `details.risk_level`. **Дополнительные поля в `details` (exclusive):** - `user`, `source_ip`, `port`, `attempt_number`, `max_attempts` - `sudo`: `run_as`, `command`, `pwd`, `risk_level` - `ban`: `ban_until`, `enable_ip_ban` (SSH / ipset) - RDP-login-monitor: автобан IP в схеме **пока не стандартизирован** (`rdp.ip.banned` зарезервирован в SAC для будущего); в отчёте Windows строка «Активных банов» = 0 или число событий `rdp.ip.banned`, если агент начнёт слать - `brute`: `window_sec`, `fails_in_window` - `whitelist_matched`: boolean ### 3.2. RDP-login-monitor | Событие | `type` | `severity` | |---------|--------|------------| | 4624 успех | `rdp.login.success` | info | | 4625 неудача | `rdp.login.failed` | warning | | RCM **20506** Shadow Control started | `rdp.shadow.control.started` | **warning** | | RCM **20507** Shadow Control stopped | `rdp.shadow.control.stopped` | **warning** | | RCM **20510** Shadow Control permission | `rdp.shadow.control.permission` | **warning** | | WinRM **91** inbound shell (Enter-PSSession) | `winrm.session.started` | **warning** | | Security **5140** admin share (`C$`, `ADMIN$`) | `smb.admin_share.access` | **warning** | | 4648 | `auth.explicit.credentials` | warning | | RD Gateway 302 | `rdg.connection.success` | info | | RD Gateway 303 | `rdg.connection.failed` | warning | | Старт/стоп | `agent.lifecycle` | info | | Отчёт | `report.daily.rdp` | info | | Инвентаризация железа/ПО | `agent.inventory` | info (SAC: **warning**, если изменилось железо) | **Инвентаризация (`agent.inventory`, RDP ≥ 2.0.21-SAC):** агент шлёт снимок в `details.inventory` (CPU, RAM, диски, GPU, Windows, IPv4). Интервал по умолчанию **12 ч**; отключение: `$GetInventory = $false` в `login_monitor.settings.ps1`. SAC хранит последний снимок в `hosts.inventory`; при изменении железа ingest повышает severity до **warning** и шлёт оповещение. **Дополнительные поля:** - `event_id_windows`, `logon_type`, `ip_address`, `workstation_name` - Shadow: `shadower_user`, `target_user`, `session_id`, `shadow_mode`, `shadow_action` - WinRM: `source_ip`, `resource_uri`, `transport` - Admin share 5140: `share_name`, `share_path`, `relative_target`, `access_mask` - `gateway_target`, `gateway_error_code` - `filtered_out`, `filter_reason` **Переключатели в `login_monitor.settings.ps1` (≥ 1.2.23-SAC):** `$EnableRcmShadowControlMonitoring`, `$EnableWinRmInboundMonitoring`, `$EnableAdminShareMonitoring` (по умолчанию `1`; Security **5140**, audit File Share). **`$GetInventory`** (по умолчанию `$true`) — опрос железа/ПО для SAC. Подавление: `ignore.lst` с префиксами `shadow:`, `winrm:`, `smb:` / `5140:`. **Exchange (RDP ≥ 2.0.23-SAC):** на почтовом сервере `$WinRmExchangeStrictMode = 1` — WinRM **91** без user в EventData не уходит в SAC; корреляция **4624** только при `LogonProcess WinRM` (отсекает ложные связки с Outlook/LT3). --- ## 3.3. Человекочитаемое имя хоста (`host.display_name`) В UI SAC (**Хосты**, фильтры Problems/Events) показывается `display_name`, если оно задано; иначе `hostname` из ОС. | Агент | Параметр | В ingest | |-------|----------|--------| | **ssh-monitor** | `SERVER_DISPLAY_NAME` в `/etc/ssh-monitor.conf` | `host.display_name` (пусто — поле не передаётся) | | **RDP-login-monitor** | `$ServerDisplayName` в `login_monitor.settings.ps1` | `host.display_name`; `hostname` = `$env:COMPUTERNAME` | Пример фрагмента JSON: ```json "host": { "hostname": "NEW-ADMIN-PC", "display_name": "UNMS Kalina", "os_family": "windows" } ``` Telegram у ssh/RDP использует ту же подпись, что и `display_name`, когда параметр задан. ### Версии агентов (единый номер) | Репозиторий | Источник версии | SAC `product_version` | |-------------|-----------------|------------------------| | **ssh-monitor** | `SSH_MONITOR_VERSION` в `ssh-monitor` + `version.txt` | из `SSH_MONITOR_VERSION` | | **RDP-login-monitor** | `$ScriptVersion` в `Login_Monitor.ps1` + `version.txt` | из `$ScriptVersion` | Не задавайте разные номера для «скрипта» и «SAC-модуля» — в UI **Хосты** отображается одна версия с ingest. --- ## 4. Поля `dedup_key` (рекомендации) | Тип | Формат dedup_key | |-----|------------------| | ssh.login.success | `{product}\|{host}\|ssh.login.success\|{user}\|{ip}` | | ssh.login.failed | `{product}\|{host}\|ssh.login.failed\|{ip}` (окно в SAC) | | privilege.sudo | `{product}\|{host}\|sudo\|{user}\|{hash(command)}` | | rdp.login.failed | `{product}\|{host}\|rdp.failed\|{ip}\|{user}` | SAC применяет cooldown по `dedup_key` (события, по умолчанию **90 с**) и по `fingerprint` problem (**300 с**). Таблица `notification_cooldown`, env: `SAC_NOTIFY_EVENT_COOLDOWN_SEC`, `SAC_NOTIFY_PROBLEM_COOLDOWN_SEC`. В логах API: `notify cooldown skip`. --- ## 5. Точки встраивания в код агентов ### ssh-monitor - Новая функция `send_sac_event()` вызывается из мест, где сейчас `notify_send "$message"`. - Обёртка `notify_or_sac()`: - `UseSAC=off` → `notify_send` - `exclusive` → только `send_sac_event` (в `summary` — тот же текст) - `dual` → оба - `fallback` → `send_sac_event`; при fail increment counter → при пороге `notify_send` ### RDP-login-monitor - `Send-SacEvent` + замена вызовов `Send-TelegramMessage` для событий мониторинга. - Heartbeat и daily report — отдельные типы в SAC. - При `UseSAC=exclusive` суточный отчёт может формироваться **на SAC** (`generated_by: sac` в `details`), если агент за день не прислал `report.daily.*` — см. `SAC_DAILY_REPORT_*`, job `python -m app.jobs.daily_report`, timer `sac-daily-report.timer`. - **Единый шаблон отчёта** (SSH / Windows): агенты `ssh-monitor` ≥ 1.2.8-SAC, `RDP-login-monitor` ≥ 1.2.21-SAC и SAC используют одинаковые секции в `report_body` / `details.stats`. - **Только SAC, без отчёта с агента:** на агенте `DAILY_REPORT_ENABLED=0` (ssh) или `$DailyReportEnabled = $false` (RDP); на SAC `SAC_DAILY_REPORT_ENABLED=true`, `SAC_DAILY_REPORT_SKIP_IF_AGENT_SENT=true`. --- ## 6. Обратная совместимость - По умолчанию `UseSAC=off` — поведение 100% как сейчас. - `BACKUP_WEBHOOK_URL` в ssh-monitor при `exclusive` не используется для обычных алертов (только emergency в `fallback` — опционально). --- ## 7. Чеклист готовности агента - [x] Параметры конфига задокументированы в README агента (ssh-monitor, RDP-login-monitor) - [x] `--check-sac` / `Test-SacConnection` / `-CheckSac` - [x] Spool и повторная отправка - [ ] Все типы из п. 3 покрыты (RDP: основные; 4648 — при появлении обработки) - [ ] `event_id` UUID на каждое событие - [ ] Секреты не в git --- ## 8. См. также - [event-schema-v1.json](event-schema-v1.json) - [TZ.md](TZ.md) §3.2, §4.8