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 <cursoragent@cursor.com>
This commit is contained in:
2026-06-24 14:37:37 +10:00
parent 6330f31162
commit 8b3e43f30d
7 changed files with 83 additions and 6 deletions
+1 -1
View File
@@ -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`
## Возможности
+1 -1
View File
@@ -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
+13
View File
@@ -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(
+10 -2
View File
@@ -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")
+1 -1
View File
@@ -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}"
+56
View File
@@ -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
+1 -1
View File
@@ -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}`;