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",