diff --git a/backend/app/api/v1/events.py b/backend/app/api/v1/events.py index 847558b..96e7596 100644 --- a/backend/app/api/v1/events.py +++ b/backend/app/api/v1/events.py @@ -179,19 +179,9 @@ def get_event( ) if event is None: raise HTTPException(status_code=404, detail="Event not found") + base = event_to_summary(event) return EventDetail( - id=event.id, - event_id=event.event_id, - host_id=event.host_id, - hostname=event.host.hostname, - display_name=event.host.display_name, - occurred_at=event.occurred_at, - received_at=event.received_at, - category=event.category, - type=event.type, - severity=event.severity, - title=event.title, - summary=event.summary, + **base.model_dump(), details=event.details, raw=event.raw, dedup_key=event.dedup_key, diff --git a/backend/app/schemas/list_models.py b/backend/app/schemas/list_models.py index 0f9a5f0..253d598 100644 --- a/backend/app/schemas/list_models.py +++ b/backend/app/schemas/list_models.py @@ -54,6 +54,7 @@ class EventSummary(BaseModel): severity: str title: str summary: str + actor_user: str | None = None model_config = {"from_attributes": True} diff --git a/backend/app/services/event_actor_user.py b/backend/app/services/event_actor_user.py new file mode 100644 index 0000000..b697e33 --- /dev/null +++ b/backend/app/services/event_actor_user.py @@ -0,0 +1,49 @@ +"""Имя пользователя для событий подключения/аутентификации (колонка «Пользователь» в UI).""" + +from __future__ import annotations + +from typing import Any + +# События, где в details может быть учётная запись (SSH/RDP/sudo/logind и т.д.). +ACTOR_USER_EVENT_TYPES: frozenset[str] = frozenset( + { + "ssh.login.success", + "ssh.login.failed", + "privilege.sudo.command", + "session.logind.new", + "session.logind.removed", + "session.logind.failed", + "rdp.login.success", + "rdp.login.failed", + "rdp.shadow.control.started", + "rdp.shadow.control.stopped", + "rdp.shadow.control.permission", + "winrm.session.started", + "smb.admin_share.access", + "auth.explicit.credentials", + "rdg.connection.success", + "rdg.connection.disconnected", + "rdg.connection.failed", + } +) + +_SHADOW_DETAIL_KEYS = ("shadower_user", "user", "username") +_DEFAULT_DETAIL_KEYS = ("user", "username") + + +def _pick_detail_value(details: dict[str, Any], keys: tuple[str, ...]) -> str | None: + for key in keys: + val = details.get(key) + if val is not None and str(val).strip() not in ("", "-"): + return str(val).strip() + return None + + +def extract_event_actor_user(event_type: str, details: dict[str, Any] | None) -> str | None: + """Вернуть имя пользователя для auth/connect событий или None (UI покажет «—»).""" + if event_type not in ACTOR_USER_EVENT_TYPES: + return None + if not isinstance(details, dict): + return None + keys = _SHADOW_DETAIL_KEYS if event_type.startswith("rdp.shadow.") else _DEFAULT_DETAIL_KEYS + return _pick_detail_value(details, keys) diff --git a/backend/app/services/event_summary.py b/backend/app/services/event_summary.py index 8a9bd67..44dcef6 100644 --- a/backend/app/services/event_summary.py +++ b/backend/app/services/event_summary.py @@ -1,5 +1,6 @@ from app.models.event import Event from app.schemas.list_models import EventSummary +from app.services.event_actor_user import extract_event_actor_user def event_to_summary(event: Event) -> EventSummary: @@ -18,4 +19,5 @@ def event_to_summary(event: Event) -> EventSummary: severity=event.severity, title=event.title, summary=event.summary, + actor_user=extract_event_actor_user(event.type, event.details), ) diff --git a/backend/tests/test_event_actor_user.py b/backend/tests/test_event_actor_user.py new file mode 100644 index 0000000..3ad0629 --- /dev/null +++ b/backend/tests/test_event_actor_user.py @@ -0,0 +1,67 @@ +"""Actor user extraction for event list UI.""" + +import uuid + +from app.services.event_actor_user import extract_event_actor_user + + +def test_ssh_login_user(): + assert extract_event_actor_user( + "ssh.login.success", + {"user": "deploy", "source_ip": "10.0.0.1"}, + ) == "deploy" + + +def test_rdp_login_username_fallback(): + assert extract_event_actor_user( + "rdp.login.failed", + {"username": "DOMAIN\\admin"}, + ) == "DOMAIN\\admin" + + +def test_sudo_user(): + assert extract_event_actor_user( + "privilege.sudo.command", + {"user": "root", "command": "apt update"}, + ) == "root" + + +def test_shadow_shadower_user(): + assert extract_event_actor_user( + "rdp.shadow.control.started", + {"shadower_user": "admin", "target_user": "user1"}, + ) == "admin" + + +def test_heartbeat_no_user(): + assert extract_event_actor_user("agent.heartbeat", {"user": "ignored"}) is None + + +def test_auth_event_missing_user(): + assert extract_event_actor_user("ssh.login.failed", {}) is None + + +def test_events_list_includes_actor_user(client, auth_headers, jwt_headers): + event_id = str(uuid.uuid4()) + payload = { + "schema_version": "1.0", + "event_id": event_id, + "occurred_at": "2026-05-27T10:00:00+03:00", + "source": {"product": "ssh-monitor", "product_version": "1.2.3-SAC"}, + "host": {"hostname": "actor-host", "os_family": "linux"}, + "category": "auth", + "type": "ssh.login.success", + "severity": "info", + "title": "SSH ok", + "summary": "login", + "details": {"user": "alice", "source_ip": "192.0.2.1"}, + } + assert client.post("/api/v1/events", json=payload, headers=auth_headers).status_code == 201 + + listed = client.get("/api/v1/events?type=ssh.login.success&hostname=actor", headers=jwt_headers) + assert listed.status_code == 200 + items = listed.json()["items"] + assert any(i["actor_user"] == "alice" for i in items) + + dash = client.get("/api/v1/dashboards/recent-events?limit=5", headers=jwt_headers) + assert any(i.get("actor_user") == "alice" for i in dash.json()) diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 1eb2c3c..c12c31b 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -168,6 +168,7 @@ export interface EventSummary { severity: string; title: string; summary: string; + actor_user?: string | null; } export interface EventListResponse { diff --git a/frontend/src/views/DashboardView.vue b/frontend/src/views/DashboardView.vue index d076a6c..3024785 100644 --- a/frontend/src/views/DashboardView.vue +++ b/frontend/src/views/DashboardView.vue @@ -236,6 +236,8 @@
{{ e.type }}