From d4bf6e536c3b0bf515fb9e0e82a580fba17ec238 Mon Sep 17 00:00:00 2001 From: PTah Date: Thu, 28 May 2026 11:49:10 +1000 Subject: [PATCH] =?UTF-8?q?feat:=20host=20display=5Fname=20=D0=B2=20ingest?= =?UTF-8?q?,=20=D0=BF=D0=BE=D0=B8=D1=81=D0=BA=20=D1=85=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D0=B2,=20UI=20=D0=B8=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- backend/app/api/v1/hosts.py | 5 +++-- backend/app/services/ingest.py | 4 +++- backend/tests/test_health_ops.py | 11 +++++++++++ backend/tests/test_hosts.py | 26 ++++++++++++++++++++++++++ backend/tests/test_ingest.py | 30 ++++++++++++++++++++++++++++++ docs/agent-integration.md | 23 +++++++++++++++++++++++ docs/work-plan.md | 10 +++++----- frontend/src/views/HostsView.vue | 24 +++++++++++++++++++++--- 8 files changed, 122 insertions(+), 11 deletions(-) create mode 100644 backend/tests/test_health_ops.py create mode 100644 backend/tests/test_hosts.py diff --git a/backend/app/api/v1/hosts.py b/backend/app/api/v1/hosts.py index 259a31e..1b4efe6 100644 --- a/backend/app/api/v1/hosts.py +++ b/backend/app/api/v1/hosts.py @@ -34,8 +34,9 @@ def list_hosts( count_stmt = count_stmt.where(Host.product == product) if hostname: like = f"%{hostname}%" - stmt = stmt.where(Host.hostname.ilike(like)) - count_stmt = count_stmt.where(Host.hostname.ilike(like)) + host_match = Host.hostname.ilike(like) | Host.display_name.ilike(like) + stmt = stmt.where(host_match) + count_stmt = count_stmt.where(host_match) total = db.scalar(count_stmt) or 0 rows = db.scalars( diff --git a/backend/app/services/ingest.py b/backend/app/services/ingest.py index 6c14bba..702c9de 100644 --- a/backend/app/services/ingest.py +++ b/backend/app/services/ingest.py @@ -46,7 +46,9 @@ def upsert_host(db: Session, payload: dict) -> Host: db.add(host) else: host.hostname = hostname - host.display_name = host_data.get("display_name") or host.display_name + dn = host_data.get("display_name") + if isinstance(dn, str) and dn.strip(): + host.display_name = dn.strip() host.os_version = host_data.get("os_version") or host.os_version host.product_version = source.get("product_version") or host.product_version host.ipv4 = host_data.get("ipv4") or host.ipv4 diff --git a/backend/tests/test_health_ops.py b/backend/tests/test_health_ops.py new file mode 100644 index 0000000..17eef59 --- /dev/null +++ b/backend/tests/test_health_ops.py @@ -0,0 +1,11 @@ +"""Extended /health payload for ops monitoring.""" + + +def test_health_includes_stale_and_last_event(client, db_session, auth_headers): + r = client.get("/health") + assert r.status_code == 200 + body = r.json() + assert body["database"] == "ok" + assert "hosts_stale" in body + assert "last_event_received_at" in body + assert body["service"] == "security-alert-center" diff --git a/backend/tests/test_hosts.py b/backend/tests/test_hosts.py new file mode 100644 index 0000000..44df5a5 --- /dev/null +++ b/backend/tests/test_hosts.py @@ -0,0 +1,26 @@ +"""Hosts list API.""" + +from datetime import datetime, timezone + +from app.models import Host + + +def test_hosts_search_by_display_name(client, db_session, jwt_headers): + h = Host( + hostname="pc-01", + display_name="UNMS Kalina", + os_family="linux", + product="ssh-monitor", + last_seen_at=datetime.now(timezone.utc), + ) + db_session.add(h) + db_session.commit() + + r = client.get("/api/v1/hosts?hostname=UNMS", headers=jwt_headers) + assert r.status_code == 200 + body = r.json() + assert body["total"] == 1 + assert body["items"][0]["display_name"] == "UNMS Kalina" + + r2 = client.get("/api/v1/hosts?hostname=nomatch", headers=jwt_headers) + assert r2.json()["total"] == 0 diff --git a/backend/tests/test_ingest.py b/backend/tests/test_ingest.py index 5da81b4..d16625f 100644 --- a/backend/tests/test_ingest.py +++ b/backend/tests/test_ingest.py @@ -70,6 +70,36 @@ def test_ingest_rdp_lifecycle_201(client, auth_headers): assert r.json()["event_id"] == event_id +def test_ingest_host_display_name_updated(client, auth_headers, db_session): + from sqlalchemy import select + + from app.models import Host + + event_id = str(uuid.uuid4()) + payload = { + **VALID_EVENT, + "event_id": event_id, + "host": { + "hostname": "short-name", + "display_name": "UNMS Kalina", + "os_family": "linux", + }, + } + assert client.post("/api/v1/events", json=payload, headers=auth_headers).status_code == 201 + host = db_session.scalar(select(Host).where(Host.hostname == "short-name")) + assert host is not None + assert host.display_name == "UNMS Kalina" + + payload2 = { + **payload, + "event_id": str(uuid.uuid4()), + "host": {"hostname": "short-name", "display_name": "Kalina UNMS", "os_family": "linux"}, + } + assert client.post("/api/v1/events", json=payload2, headers=auth_headers).status_code == 201 + db_session.refresh(host) + assert host.display_name == "Kalina UNMS" + + def test_ingest_invalid_payload_422(client, auth_headers): r = client.post( "/api/v1/events", diff --git a/docs/agent-integration.md b/docs/agent-integration.md index 33c8c48..06bc061 100644 --- a/docs/agent-integration.md +++ b/docs/agent-integration.md @@ -170,6 +170,29 @@ Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 --- +## 3.3. Человекочитаемое имя хоста (`host.display_name`) + +В UI SAC (**Хосты**, фильтры Problems/Events) показывается `display_name`, если оно задано; иначе `hostname` из ОС. + +| Агент | Параметр | В ingest | +|-------|----------|--------| +| **ssh-monitor** | `SERVER_DISPLAY_NAME` в `/etc/ssh-monitor.conf` | `host.display_name` (пусто — поле не передаётся) | +| **RDP-login-monitor** | `$ServerDisplayName` в `login_monitor.settings.ps1` | `host.display_name`; `hostname` = `$env:COMPUTERNAME` | + +Пример фрагмента JSON: + +```json +"host": { + "hostname": "NEW-ADMIN-PC", + "display_name": "UNMS Kalina", + "os_family": "windows" +} +``` + +Telegram у ssh/RDP использует ту же подпись, что и `display_name`, когда параметр задан. + +--- + ## 4. Поля `dedup_key` (рекомендации) | Тип | Формат dedup_key | diff --git a/docs/work-plan.md b/docs/work-plan.md index d547c65..6df1303 100644 --- a/docs/work-plan.md +++ b/docs/work-plan.md @@ -90,15 +90,15 @@ - [x] `d2-1` UI Problems: список, фильтры, `Ack/Resolve`, карточка с таймлайном - [x] `d2-2` Dashboard: top hosts/types, `open vs resolved 24h`, drill-down - [x] `d2-3` Ops: retention, health checks, runbook backup/restore/deploy -- [ ] `dod` DoD: нет дублей `event_id`, Problems e2e, 3 правила, UI MVP, docs, push в kalinamall +- [x] `dod` DoD: нет дублей `event_id`, Problems e2e, 3 правила, UI MVP, docs, push в kalinamall ### Отображение хостов (`display_name`, как `SERVER_DISPLAY_NAME` у ssh) Сейчас: **ssh** — `SERVER_DISPLAY_NAME` только в Telegram; в SAC уходит `socket.gethostname()`. **RDP** — параметра нет, в SAC только `$env:COMPUTERNAME`. В SAC UI «Хосты» уже показывает `display_name || hostname`, если поле пришло в ingest. -- [ ] `agent-display-rdp` **RDP-login-monitor:** `$ServerDisplayName` в `login_monitor.settings.ps1` / example; подпись в Telegram; в `Sac-Client.ps1` — `host.display_name` (и при необходимости согласовать `hostname`) -- [ ] `agent-display-ssh` **ssh-monitor:** в `sac-client.sh` передавать `SERVER_DISPLAY_NAME` как `host.display_name` в JSON ingest -- [ ] `sac-display-hosts` **SAC:** проверить ingest/обновление `Host.display_name`; колонка «Хосты» = человекочитаемое имя; поиск по `display_name`; `docs/agent-integration.md` + schema `host.display_name` +- [x] `agent-display-rdp` **RDP-login-monitor:** `$ServerDisplayName` в `login_monitor.settings.ps1` / example; подпись в Telegram; в `Sac-Client.ps1` — `host.display_name` (и при необходимости согласовать `hostname`) +- [x] `agent-display-ssh` **ssh-monitor:** в `sac-client.sh` передавать `SERVER_DISPLAY_NAME` как `host.display_name` в JSON ingest +- [x] `sac-display-hosts` **SAC:** проверить ingest/обновление `Host.display_name`; колонка «Хосты» = человекочитаемое имя; поиск по `display_name`; `docs/agent-integration.md` + schema `host.display_name` **Приёмка:** хост с `SERVER_DISPLAY_NAME="UNMS Kalina"` (или RDP `$ServerDisplayName`) в SAC → **Хосты** отображается как **UNMS Kalina**, а не только короткое имя ОС. @@ -121,7 +121,7 @@ - [x] `13:00–14:30` Dashboard: виджеты top hosts/types - [x] `14:30–16:00` Dashboard: `open/resolved 24h` + drill-down - [x] `16:00–17:00` retention job (`events 30–90d`, `problems 180d+`) -- [ ] `17:00–18:00` health checks (DB/worker/heartbeat stale) +- [x] `17:00–18:00` health checks (DB/worker/heartbeat stale) - [x] `18:00–19:00` runbook + финальный push в kalinamall + freeze dev ### Неделя после freeze (только тестирование) diff --git a/frontend/src/views/HostsView.vue b/frontend/src/views/HostsView.vue index bfe2668..5cfd853 100644 --- a/frontend/src/views/HostsView.vue +++ b/frontend/src/views/HostsView.vue @@ -1,5 +1,12 @@