feat: show RDP session duration in Overview and Events (0.5.16)
Add session_duration_sec to EventSummary and a Duration column (HH:MM:SS / Nd HH:MM:SS).
This commit is contained in:
@@ -13,7 +13,7 @@
|
|||||||
| **security-alert-center** | Сервер SAC (Ubuntu 24.04) |
|
| **security-alert-center** | Сервер SAC (Ubuntu 24.04) |
|
||||||
| [seaca](https://git.kalinamall.ru/PapaTramp/seaca) | Android-клиент |
|
| [seaca](https://git.kalinamall.ru/PapaTramp/seaca) | Android-клиент |
|
||||||
|
|
||||||
**Версия:** `0.5.15` · **Деплой:** `sudo /opt/sac-deploy.sh`
|
**Версия:** `0.5.16` · **Деплой:** `sudo /opt/sac-deploy.sh`
|
||||||
|
|
||||||
## Возможности
|
## Возможности
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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) |
|
| **security-alert-center** | SAC server (Ubuntu 24.04) |
|
||||||
| [seaca](https://git.kalinamall.ru/PapaTramp/seaca) | Android client |
|
| [seaca](https://git.kalinamall.ru/PapaTramp/seaca) | Android client |
|
||||||
|
|
||||||
**Version:** `0.5.15` · **Deploy:** `sudo /opt/sac-deploy.sh`
|
**Version:** `0.5.16` · **Deploy:** `sudo /opt/sac-deploy.sh`
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ class EventSummary(BaseModel):
|
|||||||
title: str
|
title: str
|
||||||
summary: str
|
summary: str
|
||||||
actor_user: str | None = None
|
actor_user: str | None = None
|
||||||
|
session_duration_sec: int | None = None
|
||||||
rdg_flap: bool = False
|
rdg_flap: bool = False
|
||||||
rdg_flap_pair_event_id: int | None = None
|
rdg_flap_pair_event_id: int | None = None
|
||||||
rdg_flap_qwinsta_event_id: int | None = None
|
rdg_flap_qwinsta_event_id: int | None = None
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from app.services.event_actor_user import extract_event_actor_user
|
|||||||
from app.services.host_sessions import event_session_terminated
|
from app.services.host_sessions import event_session_terminated
|
||||||
from app.services.rdg_display import build_rdg_display
|
from app.services.rdg_display import build_rdg_display
|
||||||
from app.services.rdg_session_flap import resolve_rdg_flap_summary
|
from app.services.rdg_session_flap import resolve_rdg_flap_summary
|
||||||
|
from app.services.session_duration import extract_session_duration_sec
|
||||||
|
|
||||||
|
|
||||||
def event_to_summary(event: Event, db: Session | None = None) -> EventSummary:
|
def event_to_summary(event: Event, db: Session | None = None) -> EventSummary:
|
||||||
@@ -44,6 +45,9 @@ def event_to_summary(event: Event, db: Session | None = None) -> EventSummary:
|
|||||||
title=title,
|
title=title,
|
||||||
summary=summary,
|
summary=summary,
|
||||||
actor_user=extract_event_actor_user(event.type, event.details),
|
actor_user=extract_event_actor_user(event.type, event.details),
|
||||||
|
session_duration_sec=extract_session_duration_sec(
|
||||||
|
event.details if isinstance(event.details, dict) else None
|
||||||
|
),
|
||||||
rdg_flap=rdg_flap,
|
rdg_flap=rdg_flap,
|
||||||
rdg_flap_pair_event_id=rdg_flap_pair_event_id,
|
rdg_flap_pair_event_id=rdg_flap_pair_event_id,
|
||||||
rdg_flap_qwinsta_event_id=rdg_flap_qwinsta_event_id,
|
rdg_flap_qwinsta_event_id=rdg_flap_qwinsta_event_id,
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
"""Format and extract RDP/RDG session_duration_sec for UI."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def extract_session_duration_sec(details: dict[str, Any] | None) -> int | None:
|
||||||
|
if not isinstance(details, dict):
|
||||||
|
return None
|
||||||
|
raw = details.get("session_duration_sec")
|
||||||
|
if raw is None or raw == "":
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
value = int(raw)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
if value < 0:
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def format_session_duration(seconds: int) -> str:
|
||||||
|
"""
|
||||||
|
Human-readable session length for lists:
|
||||||
|
- under 24h → HH:MM:SS (05:05:24)
|
||||||
|
- 24h+ → Nд HH:MM:SS (1д 00:01:25)
|
||||||
|
"""
|
||||||
|
if seconds < 0:
|
||||||
|
return "—"
|
||||||
|
days, rem = divmod(int(seconds), 86_400)
|
||||||
|
hours, rem = divmod(rem, 3600)
|
||||||
|
minutes, secs = divmod(rem, 60)
|
||||||
|
clock = f"{hours:02d}:{minutes:02d}:{secs:02d}"
|
||||||
|
if days:
|
||||||
|
return f"{days}д {clock}"
|
||||||
|
return clock
|
||||||
@@ -564,9 +564,11 @@ def _format_rdg_html(event: Event) -> str:
|
|||||||
err = _detail(details, "gateway_error_code", "error_code", default="")
|
err = _detail(details, "gateway_error_code", "error_code", default="")
|
||||||
if err != "-":
|
if err != "-":
|
||||||
msg += _line("⚠️", "Код ошибки", html_escape(err))
|
msg += _line("⚠️", "Код ошибки", html_escape(err))
|
||||||
dur = _detail(details, "session_duration_sec", default="")
|
from app.services.session_duration import extract_session_duration_sec, format_session_duration
|
||||||
if dur not in ("-", "0", ""):
|
|
||||||
msg += _line("⏱️", "Длительность", f"{html_escape(dur)} с")
|
dur_sec = extract_session_duration_sec(details if isinstance(details, dict) else None)
|
||||||
|
if dur_sec is not None and dur_sec > 0:
|
||||||
|
msg += _line("⏱️", "Длительность", html_escape(format_session_duration(dur_sec)))
|
||||||
msg += _line("🕐", "Время", format_time(event.occurred_at))
|
msg += _line("🕐", "Время", format_time(event.occurred_at))
|
||||||
win_id = _detail(details, "event_id_windows", default="")
|
win_id = _detail(details, "event_id_windows", default="")
|
||||||
if win_id != "-":
|
if win_id != "-":
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""Единый источник версии SAC (API, health, логи, OpenAPI)."""
|
"""Единый источник версии SAC (API, health, логи, OpenAPI)."""
|
||||||
|
|
||||||
APP_NAME = "Security Alert Center"
|
APP_NAME = "Security Alert Center"
|
||||||
APP_VERSION = "0.5.15"
|
APP_VERSION = "0.5.16"
|
||||||
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"
|
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
"""Tests for session_duration_sec extract/format."""
|
||||||
|
|
||||||
|
from app.services.session_duration import extract_session_duration_sec, format_session_duration
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_session_duration_sec():
|
||||||
|
assert extract_session_duration_sec({"session_duration_sec": 18324}) == 18324
|
||||||
|
assert extract_session_duration_sec({"session_duration_sec": "42"}) == 42
|
||||||
|
assert extract_session_duration_sec({"session_duration_sec": ""}) is None
|
||||||
|
assert extract_session_duration_sec({}) is None
|
||||||
|
assert extract_session_duration_sec(None) is None
|
||||||
|
assert extract_session_duration_sec({"session_duration_sec": -1}) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_session_duration():
|
||||||
|
assert format_session_duration(0) == "00:00:00"
|
||||||
|
assert format_session_duration(18324) == "05:05:24"
|
||||||
|
assert format_session_duration(2428) == "00:40:28"
|
||||||
|
assert format_session_duration(86400 + 85) == "1д 00:01:25"
|
||||||
|
assert format_session_duration(2 * 86400 + 3661) == "2д 01:01:01"
|
||||||
@@ -192,6 +192,7 @@ export interface EventSummary {
|
|||||||
title: string;
|
title: string;
|
||||||
summary: string;
|
summary: string;
|
||||||
actor_user?: string | null;
|
actor_user?: string | null;
|
||||||
|
session_duration_sec?: number | null;
|
||||||
rdg_flap?: boolean;
|
rdg_flap?: boolean;
|
||||||
rdg_flap_pair_event_id?: number | null;
|
rdg_flap_pair_event_id?: number | null;
|
||||||
rdg_flap_qwinsta_event_id?: number | null;
|
rdg_flap_qwinsta_event_id?: number | null;
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
"""Human-readable session_duration_sec for event tables."""
|
||||||
|
|
||||||
|
export function formatSessionDuration(seconds: number | null | undefined): string {
|
||||||
|
if (seconds == null || !Number.isFinite(seconds) || seconds < 0) {
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
const total = Math.floor(seconds);
|
||||||
|
const days = Math.floor(total / 86_400);
|
||||||
|
const hours = Math.floor((total % 86_400) / 3600);
|
||||||
|
const minutes = Math.floor((total % 3600) / 60);
|
||||||
|
const secs = total % 60;
|
||||||
|
const clock = `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
|
||||||
|
if (days > 0) {
|
||||||
|
return `${days}д ${clock}`;
|
||||||
|
}
|
||||||
|
return clock;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
|
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
|
||||||
export const APP_NAME = "Security Alert Center";
|
export const APP_NAME = "Security Alert Center";
|
||||||
export const APP_VERSION = "0.5.15";
|
export const APP_VERSION = "0.5.16";
|
||||||
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
|
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
|
||||||
|
|||||||
@@ -238,6 +238,8 @@
|
|||||||
|
|
||||||
<th>Пользователь</th>
|
<th>Пользователь</th>
|
||||||
|
|
||||||
|
<th title="Длительность сессии (из session_duration_sec)">Длительность</th>
|
||||||
|
|
||||||
<th>Версия агента</th>
|
<th>Версия агента</th>
|
||||||
|
|
||||||
<th>Severity</th>
|
<th>Severity</th>
|
||||||
@@ -276,6 +278,8 @@
|
|||||||
|
|
||||||
<td>{{ e.actor_user || "—" }}</td>
|
<td>{{ e.actor_user || "—" }}</td>
|
||||||
|
|
||||||
|
<td>{{ formatSessionDuration(e.session_duration_sec) }}</td>
|
||||||
|
|
||||||
<td>{{ e.product_version || "—" }}</td>
|
<td>{{ e.product_version || "—" }}</td>
|
||||||
|
|
||||||
<td :class="'sev-' + e.severity">{{ e.severity }}</td>
|
<td :class="'sev-' + e.severity">{{ e.severity }}</td>
|
||||||
@@ -354,6 +358,7 @@ import RdgQwinstaModal from "../components/RdgQwinstaModal.vue";
|
|||||||
import { useRdgQwinsta } from "../composables/useRdgQwinsta";
|
import { useRdgQwinsta } from "../composables/useRdgQwinsta";
|
||||||
import { formatServerName } from "../utils/hostDisplay";
|
import { formatServerName } from "../utils/hostDisplay";
|
||||||
import { hasRdgFlapUi, rdgQwinstaEventId } from "../utils/rdgFlap";
|
import { hasRdgFlapUi, rdgQwinstaEventId } from "../utils/rdgFlap";
|
||||||
|
import { formatSessionDuration } from "../utils/sessionDuration";
|
||||||
import { markEventSessionTerminatedInList, showEventSessionTerminateButton } from "../utils/sessionActions";
|
import { markEventSessionTerminatedInList, showEventSessionTerminateButton } from "../utils/sessionActions";
|
||||||
|
|
||||||
const { qwinstaLoadingId, qwinstaModal, closeQwinstaModal, runQwinsta, runLogoff } = useRdgQwinsta();
|
const { qwinstaLoadingId, qwinstaModal, closeQwinstaModal, runQwinsta, runLogoff } = useRdgQwinsta();
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
<th>Хост</th>
|
<th>Хост</th>
|
||||||
<th>Имя сервера</th>
|
<th>Имя сервера</th>
|
||||||
<th>Пользователь</th>
|
<th>Пользователь</th>
|
||||||
|
<th title="Длительность сессии (из session_duration_sec)">Длительность</th>
|
||||||
<th>Версия агента</th>
|
<th>Версия агента</th>
|
||||||
<th>Severity</th>
|
<th>Severity</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
@@ -50,6 +51,7 @@
|
|||||||
<td>{{ e.hostname }}</td>
|
<td>{{ e.hostname }}</td>
|
||||||
<td>{{ formatServerName(e.display_name) }}</td>
|
<td>{{ formatServerName(e.display_name) }}</td>
|
||||||
<td>{{ e.actor_user || "—" }}</td>
|
<td>{{ e.actor_user || "—" }}</td>
|
||||||
|
<td>{{ formatSessionDuration(e.session_duration_sec) }}</td>
|
||||||
<td>{{ e.product_version || "—" }}</td>
|
<td>{{ e.product_version || "—" }}</td>
|
||||||
<td :class="'sev-' + e.severity">{{ e.severity }}</td>
|
<td :class="'sev-' + e.severity">{{ e.severity }}</td>
|
||||||
<td><code>{{ e.type }}</code></td>
|
<td><code>{{ e.type }}</code></td>
|
||||||
@@ -120,6 +122,7 @@ import {
|
|||||||
type EventsListState,
|
type EventsListState,
|
||||||
} from "../utils/eventsListQuery";
|
} from "../utils/eventsListQuery";
|
||||||
import { formatServerName } from "../utils/hostDisplay";
|
import { formatServerName } from "../utils/hostDisplay";
|
||||||
|
import { formatSessionDuration } from "../utils/sessionDuration";
|
||||||
import { markEventSessionTerminatedInList, showEventSessionTerminateButton } from "../utils/sessionActions";
|
import { markEventSessionTerminatedInList, showEventSessionTerminateButton } from "../utils/sessionActions";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|||||||
Reference in New Issue
Block a user