diff --git a/backend/app/services/telegram_templates.py b/backend/app/services/telegram_templates.py index 3906550..9c64648 100644 --- a/backend/app/services/telegram_templates.py +++ b/backend/app/services/telegram_templates.py @@ -168,6 +168,48 @@ def format_sudo_html(event: Event) -> str: return msg.rstrip() +def format_rdp_shadow_html(event: Event) -> str: + details = _details_dict(event) + action = _detail(details, "shadow_action", default="") + if action == "control_stopped" or event.type.endswith(".stopped"): + header = "🎭 RDS SHADOW CONTROL — остановлено" + elif action == "control_permission" or event.type.endswith(".permission"): + header = "🎭 RDS SHADOW CONTROL — разрешение" + else: + header = "🎭 RDS SHADOW CONTROL — начато" + + msg = f"{header}\n" + msg += _line("🏢", "Сервер", host_label(event.host)) + msg += _line("👤", "Администратор", html_escape(_detail(details, "shadower_user", "user"))) + msg += _line("🎯", "Сессия пользователя", html_escape(_detail(details, "target_user", default="-"))) + sid = _detail(details, "session_id", "target_session_id", default="") + if sid != "-": + msg += _line("🔢", "Session ID", html_escape(sid)) + msg += _line("🕐", "Время", format_time(event.occurred_at)) + win_id = _detail(details, "event_id_windows", default="") + if win_id != "-": + msg += f"🔢 Event ID: {html_escape(win_id)} (RemoteConnectionManager)" + return msg.rstrip() + + +def format_winrm_session_html(event: Event) -> str: + details = _details_dict(event) + msg = "⚠️ WinRM / Enter-PSSession — удалённая shell\n" + msg += _line("🏢", "Сервер", host_label(event.host)) + msg += _line("👤", "Пользователь", html_escape(_detail(details, "user", "username"))) + ip = _detail(details, "source_ip", "ip_address", "ip") + if ip != "-": + msg += _line("🌐", "IP источника", html_escape(ip)) + uri = _detail(details, "resource_uri", default="") + if uri != "-": + msg += _line("🔗", "ResourceUri", html_escape(uri)) + msg += _line("🕐", "Время", format_time(event.occurred_at)) + win_id = _detail(details, "event_id_windows", default="") + if win_id != "-": + msg += f"🔢 Event ID: {html_escape(win_id)} (WinRM Operational)" + return msg.rstrip() + + def format_generic_event_html(event: Event) -> str: sev = event.severity.upper() msg = f"🚨 SAC: {html_escape(event.title)}\n" @@ -216,6 +258,10 @@ def format_event_telegram_html(event: Event) -> str: return format_sudo_html(event) if event.type.startswith("rdg."): return _format_rdg_html(event) + if event.type.startswith("rdp.shadow."): + return format_rdp_shadow_html(event) + if event.type.startswith("winrm."): + return format_winrm_session_html(event) return format_generic_event_html(event) @@ -249,6 +295,15 @@ def format_problem_telegram_html(problem: Problem, event: Event | None = None) - msg += f"\n{html_escape(problem.title)}\n{html_escape(problem.summary)}" if event is not None: msg += f"\n\nТриггер: {html_escape(event.type)} ({html_escape(event.severity)})" - if event.type in ("rdp.login.success", "rdp.login.failed", "ssh.login.success", "ssh.login.failed"): + if event.type in ( + "rdp.login.success", + "rdp.login.failed", + "ssh.login.success", + "ssh.login.failed", + "rdp.shadow.control.started", + "rdp.shadow.control.stopped", + "rdp.shadow.control.permission", + "winrm.session.started", + ): msg += "\n" + format_event_telegram_html(event) return msg.rstrip() diff --git a/backend/tests/test_telegram_templates.py b/backend/tests/test_telegram_templates.py index 904e34a..297f628 100644 --- a/backend/tests/test_telegram_templates.py +++ b/backend/tests/test_telegram_templates.py @@ -67,6 +67,60 @@ def test_rdp_success_template(): assert "Сеть/RDP" in text +def test_rdp_shadow_control_template(): + host = Host(hostname="RDS01", display_name="RDS Farm", ipv4="10.0.0.5", os_family="windows") + event = Event( + event_id="00000000-0000-4000-8000-000000000701", + host_id=1, + host=host, + occurred_at=datetime(2026, 5, 30, 10, 0, 0, tzinfo=timezone.utc), + category="session", + type="rdp.shadow.control.started", + severity="warning", + title="RDS Shadow Control 20506", + summary="Shadow admin -> user", + details={ + "event_id_windows": 20506, + "shadower_user": "DOMAIN\\admin", + "target_user": "DOMAIN\\user", + "session_id": "3", + "shadow_action": "control_started", + }, + payload={}, + ) + text = format_event_telegram_html(event) + assert "SHADOW CONTROL" in text + assert "DOMAIN" in text + assert "20506" in text + assert "RDS Farm" in text + + +def test_winrm_session_template(): + host = Host(hostname="SRV01", display_name="App Server", os_family="windows") + event = Event( + event_id="00000000-0000-4000-8000-000000000702", + host_id=1, + host=host, + occurred_at=datetime(2026, 5, 30, 11, 0, 0, tzinfo=timezone.utc), + category="session", + type="winrm.session.started", + severity="warning", + title="WinRM shell", + summary="WinRM 91", + details={ + "event_id_windows": 91, + "user": "DOMAIN\\admin", + "source_ip": "192.168.160.3", + "resource_uri": "http://schemas.microsoft.com/powershell/Microsoft.PowerShell", + }, + payload={}, + ) + text = format_event_telegram_html(event) + assert "Enter-PSSession" in text or "WinRM" in text + assert "192.168.160.3" in text + assert "91" in text + + def test_ssh_failed_template(): event = Event( event_id="00000000-0000-4000-8000-000000000503", diff --git a/docs/agent-integration.md b/docs/agent-integration.md index 51a9427..a6af1b7 100644 --- a/docs/agent-integration.md +++ b/docs/agent-integration.md @@ -183,6 +183,10 @@ Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 |---------|--------|------------| | 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** | | 4648 | `auth.explicit.credentials` | warning | | RD Gateway 302 | `rdg.connection.success` | info | | RD Gateway 303 | `rdg.connection.failed` | warning | @@ -192,9 +196,13 @@ Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 **Дополнительные поля:** - `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` - `gateway_target`, `gateway_error_code` - `filtered_out`, `filter_reason` +**Переключатели в `login_monitor.settings.ps1` (≥ 1.2.23-SAC):** `$EnableRcmShadowControlMonitoring`, `$EnableWinRmInboundMonitoring` (по умолчанию `1`). Подавление: `ignore.lst` с префиксами `shadow:` и `winrm:`. + --- ## 3.3. Человекочитаемое имя хоста (`host.display_name`) diff --git a/docs/event-schema-v1.json b/docs/event-schema-v1.json index d5d103c..35631ba 100644 --- a/docs/event-schema-v1.json +++ b/docs/event-schema-v1.json @@ -94,6 +94,10 @@ "session.logind.new", "rdp.login.success", "rdp.login.failed", + "rdp.shadow.control.started", + "rdp.shadow.control.stopped", + "rdp.shadow.control.permission", + "winrm.session.started", "rdg.connection.success", "report.daily.ssh", "agent.heartbeat", diff --git a/schemas/event-schema-v1.json b/schemas/event-schema-v1.json index d5d103c..35631ba 100644 --- a/schemas/event-schema-v1.json +++ b/schemas/event-schema-v1.json @@ -94,6 +94,10 @@ "session.logind.new", "rdp.login.success", "rdp.login.failed", + "rdp.shadow.control.started", + "rdp.shadow.control.stopped", + "rdp.shadow.control.permission", + "winrm.session.started", "rdg.connection.success", "report.daily.ssh", "agent.heartbeat",