diff --git a/backend/app/version.py b/backend/app/version.py index 09637d6..4287def 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.9.7" +APP_VERSION = "0.9.8" APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}" diff --git a/frontend/src/components/ReportBodyCard.vue b/frontend/src/components/ReportBodyCard.vue index 965e758..db18c93 100644 --- a/frontend/src/components/ReportBodyCard.vue +++ b/frontend/src/components/ReportBodyCard.vue @@ -11,9 +11,9 @@
  • {{ line }}
  • -
    {{ plainBody }}
    +
    -
    +
    {{ plainBody }}

    {{ summary }}

    @@ -28,6 +28,7 @@ import { computed } from "vue"; import { reportActiveUserLines, + normalizeReportPlainText, reportBodyFromDetails, reportHtmlFromDetails, reportStatEntries, @@ -50,5 +51,8 @@ const htmlContent = computed(() => { return raw ? sanitizeAgentHtml(raw) : null; }); -const plainBody = computed(() => reportBodyFromDetails(props.details)); +const plainBody = computed(() => { + const raw = reportBodyFromDetails(props.details); + return raw ? normalizeReportPlainText(raw) : null; +}); diff --git a/frontend/src/style.css b/frontend/src/style.css index 343e2f8..21c2254 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -378,6 +378,13 @@ pre { .report-body-html .agent-report { white-space: normal; + word-break: break-word; +} + +.report-body-html .agent-report br { + display: block; + margin-bottom: 0.25em; + content: ""; } .report-body-plain { @@ -406,6 +413,43 @@ pre { margin-bottom: 1rem; } +.detail-fields { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 0.85rem 1.25rem; + margin: 0; +} + +.detail-fields-compact { + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); +} + +.detail-field { + min-width: 0; +} + +.detail-field-wide { + grid-column: 1 / -1; +} + +.detail-field dt { + margin: 0 0 0.2rem; + font-size: 0.75rem; + font-weight: 600; + color: #9aa4b2; +} + +.detail-field dd { + margin: 0; + color: #e8edf2; + word-break: break-word; + line-height: 1.45; +} + +.detail-field dd code { + word-break: break-all; +} + .raw-details { margin-top: 1rem; } diff --git a/frontend/src/utils/reportDisplay.ts b/frontend/src/utils/reportDisplay.ts index 8eba0d4..fdd483d 100644 --- a/frontend/src/utils/reportDisplay.ts +++ b/frontend/src/utils/reportDisplay.ts @@ -49,10 +49,27 @@ export interface NormalizedReportStat { value: string | number; } +export const DAILY_REPORT_TYPE_LABELS: Record = { + "report.daily.ssh": "Отчёты ssh клиентов", + "report.daily.rdp": "Отчёты RDP клиентов", +}; + +export function dailyReportTypeLabel(type: string): string { + return DAILY_REPORT_TYPE_LABELS[type] ?? type; +} + export function isDailyReportType(type: string): boolean { return type === "report.daily.ssh" || type === "report.daily.rdp"; } +/** Текст отчёта: literal \\n и
    из legacy-агентов → нормальные переносы. */ +export function normalizeReportPlainText(body: string): string { + let text = body.replace(/\r\n/g, "\n"); + text = text.replace(/\\n/g, "\n"); + text = text.replace(//gi, "\n"); + return text; +} + export function reportPlatform(type: string): "ssh" | "windows" { return type === "report.daily.rdp" ? "windows" : "ssh"; } @@ -66,20 +83,29 @@ export function normalizeReportStats( const platform = stats.platform ?? reportPlatform(type); const out: DailyReportStats = { ...stats, platform }; + const asInt = (v: unknown, fallback = 0): number => { + if (typeof v === "number" && Number.isFinite(v)) return v; + if (typeof v === "string") { + const n = parseInt(v.replace(/[^\d-]/g, ""), 10); + if (Number.isFinite(n)) return n; + } + return fallback; + }; + if (platform === "ssh") { - out.successful_logins = stats.successful_logins ?? stats.successful_ssh ?? 0; - out.failed_logins = stats.failed_logins ?? stats.failed_ssh ?? 0; - out.sudo_commands = stats.sudo_commands ?? 0; + out.successful_logins = asInt(stats.successful_logins, asInt(stats.successful_ssh)); + out.failed_logins = asInt(stats.failed_logins, asInt(stats.failed_ssh)); + out.sudo_commands = asInt(stats.sudo_commands); } else { - out.successful_logins = stats.successful_logins ?? stats.rdp_success ?? 0; - out.failed_logins = stats.failed_logins ?? stats.rdp_failed ?? 0; + out.successful_logins = asInt(stats.successful_logins, asInt(stats.rdp_success)); + out.failed_logins = asInt(stats.failed_logins, asInt(stats.rdp_failed)); if (!out.active_users?.length && stats.unique_users?.length) { out.active_users = stats.unique_users.map((u) => u.startsWith("👤") ? u : `👤 ${u}`, ); } } - out.active_bans = stats.active_bans ?? 0; + out.active_bans = asInt(stats.active_bans); return out; } diff --git a/frontend/src/version.ts b/frontend/src/version.ts index b93357c..3d1c5ce 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.9.7"; +export const APP_VERSION = "0.9.8"; export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`; diff --git a/frontend/src/views/EventDetailView.vue b/frontend/src/views/EventDetailView.vue index 638378c..4f28493 100644 --- a/frontend/src/views/EventDetailView.vue +++ b/frontend/src/views/EventDetailView.vue @@ -1,131 +1,91 @@ - - - - diff --git a/frontend/src/views/HostDetailView.vue b/frontend/src/views/HostDetailView.vue index a0c706e..9369a6f 100644 --- a/frontend/src/views/HostDetailView.vue +++ b/frontend/src/views/HostDetailView.vue @@ -5,30 +5,60 @@