From 8b3e43f30d85f8054fe1ac0854bf1c137309fbb4 Mon Sep 17 00:00:00 2001 From: PTah Date: Wed, 24 Jun 2026 14:37:37 +1000 Subject: [PATCH] fix: event terminate-session 500 on rdp.login.success (0.3.7) Use extract_event_actor_user instead of missing ORM field; map WinRM/SSH errors to HTTP 400. Co-authored-by: Cursor --- README.md | 2 +- README_en.md | 2 +- backend/app/api/v1/events.py | 13 +++++++ backend/app/services/host_sessions.py | 12 +++++- backend/app/version.py | 2 +- backend/tests/test_host_sessions.py | 56 +++++++++++++++++++++++++++ frontend/src/version.ts | 2 +- 7 files changed, 83 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b1533e1..85bdf59 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ | **security-alert-center** | Сервер SAC (Ubuntu 24.04) | | [seaca](https://git.kalinamall.ru/PapaTramp/seaca) | Android-клиент | -**Версия:** `0.3.6` · **Деплой:** `sudo /opt/sac-deploy.sh` +**Версия:** `0.3.7` · **Деплой:** `sudo /opt/sac-deploy.sh` ## Возможности diff --git a/README_en.md b/README_en.md index 0a0075f..da84fdb 100644 --- a/README_en.md +++ b/README_en.md @@ -13,7 +13,7 @@ Self-hosted hub for security events from Linux and Windows agents: ingest, corre | **security-alert-center** | SAC server (Ubuntu 24.04) | | [seaca](https://git.kalinamall.ru/PapaTramp/seaca) | Android client | -**Version:** `0.3.6` · **Deploy:** `sudo /opt/sac-deploy.sh` +**Version:** `0.3.7` · **Deploy:** `sudo /opt/sac-deploy.sh` ## Features diff --git a/backend/app/api/v1/events.py b/backend/app/api/v1/events.py index ad006a5..926d831 100644 --- a/backend/app/api/v1/events.py +++ b/backend/app/api/v1/events.py @@ -26,7 +26,12 @@ from app.services.host_sessions import ( terminate_session_for_event, ) from app.services.linux_admin_settings import get_effective_linux_admin_config +from app.services.ssh_connect import ( + HostNotLinuxError as SshHostNotLinuxError, + HostTargetMissingError as SshHostTargetMissingError, +) from app.services.win_admin_settings import get_effective_win_admin_config +from app.services.winrm_connect import HostNotWindowsError, HostTargetMissingError from app.services.ingest import ingest_event from app.services.event_summary import event_to_summary from app.services.event_type_visibility import get_hidden_event_types, visibility_type_filter @@ -263,6 +268,14 @@ def post_event_terminate_session( ) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc + except HostNotWindowsError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except HostTargetMissingError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except SshHostNotLinuxError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except SshHostTargetMissingError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc if hasattr(result, "exit_code"): return EventSessionTerminateResponse( diff --git a/backend/app/services/host_sessions.py b/backend/app/services/host_sessions.py index 509a1f5..a394938 100644 --- a/backend/app/services/host_sessions.py +++ b/backend/app/services/host_sessions.py @@ -7,6 +7,7 @@ import re from dataclasses import dataclass from app.models import Event, Host +from app.services.event_actor_user import extract_event_actor_user from app.services.linux_admin_settings import LinuxAdminConfig from app.services.ssh_connect import ( HostNotLinuxError as SshHostNotLinuxError, @@ -69,6 +70,13 @@ def _details_dict(event: Event) -> dict: return raw if isinstance(raw, dict) else {} +def _event_login_user(event: Event) -> str: + actor = extract_event_actor_user(event.type, event.details) + if actor: + return actor.strip() + return str(_details_dict(event).get("user") or "").strip() + + def event_session_id(event: Event) -> str | None: details = _details_dict(event) for key in ("session_id", "sid"): @@ -422,7 +430,7 @@ def terminate_session_for_event( sid = (session_id or event_session_id(event) or "").strip() if is_linux_host(host): if not sid: - user = (event.actor_user or _details_dict(event).get("user") or "").strip() + user = _event_login_user(event) if user: return _terminate_linux_user_sessions(host, linux_cfg, user) raise ValueError("session_id is required for this event") @@ -430,7 +438,7 @@ def terminate_session_for_event( if is_windows_host(host): if not sid: - user = (event.actor_user or _details_dict(event).get("user") or "").strip() + user = _event_login_user(event) sessions, qwinsta = list_windows_sessions(host, win_cfg) if not qwinsta or not qwinsta.ok: raise ValueError(qwinsta.message if qwinsta else "qwinsta failed") diff --git a/backend/app/version.py b/backend/app/version.py index 7c5a79b..6433570 100644 --- a/backend/app/version.py +++ b/backend/app/version.py @@ -1,5 +1,5 @@ """Единый источник версии SAC (API, health, логи, OpenAPI).""" APP_NAME = "Security Alert Center" -APP_VERSION = "0.3.6" +APP_VERSION = "0.3.7" APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}" diff --git a/backend/tests/test_host_sessions.py b/backend/tests/test_host_sessions.py index 551a83d..5d2b24f 100644 --- a/backend/tests/test_host_sessions.py +++ b/backend/tests/test_host_sessions.py @@ -1,12 +1,20 @@ """Tests for host session list/parse helpers.""" +from types import SimpleNamespace + from app.services.host_sessions import ( + HostSessionRow, + _event_login_user, event_supports_session_terminate, filter_logind_session_rows, parse_loginctl_sessions, parse_loginctl_sessions_json, parse_qwinsta_sessions, + terminate_session_for_event, ) +from app.services.linux_admin_settings import LinuxAdminConfig +from app.services.win_admin_settings import WinAdminConfig +from app.services.winrm_connect import WinRmCmdResult def test_parse_loginctl_sessions(): @@ -74,3 +82,51 @@ def test_event_supports_session_terminate_types(): self.host = HostStub() assert event_supports_session_terminate(EventStub("ssh.login.success")) is True + + +def test_event_login_user_without_orm_actor_user_attr(): + event = SimpleNamespace( + type="rdp.login.success", + details={"user": "papatramp"}, + ) + assert _event_login_user(event) == "papatramp" + + +def test_terminate_session_for_event_windows_no_actor_user_attr(monkeypatch): + host = SimpleNamespace( + os_family="windows", + product="rdp-login-monitor", + hostname="srv01", + ipv4="10.0.0.1", + display_name=None, + inventory={}, + ) + event = SimpleNamespace( + type="rdp.login.success", + details={"user": "papatramp"}, + host=host, + ) + rows = [HostSessionRow(session_id="2", user="B26\\papatramp", state="Active")] + + def fake_list(_host, _cfg): + return rows, WinRmCmdResult(ok=True, message="ok", target="srv01") + + def fake_term(_host, _cfg, sid): + assert sid == "2" + return WinRmCmdResult(ok=True, message="logged off", target="srv01") + + monkeypatch.setattr( + "app.services.host_sessions.list_windows_sessions", + fake_list, + ) + monkeypatch.setattr( + "app.services.host_sessions.terminate_windows_session", + fake_term, + ) + + result = terminate_session_for_event( + event, + linux_cfg=LinuxAdminConfig(user="", password="", source="test"), + win_cfg=WinAdminConfig(user="B26\\admin", password="x", source="test"), + ) + assert result.ok is True diff --git a/frontend/src/version.ts b/frontend/src/version.ts index 252474c..2d47a6f 100644 --- a/frontend/src/version.ts +++ b/frontend/src/version.ts @@ -1,4 +1,4 @@ /** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */ export const APP_NAME = "Security Alert Center"; -export const APP_VERSION = "0.3.6"; +export const APP_VERSION = "0.3.7"; export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;