/** Безопасный вывод HTML из агента (DOMPurify, белый список тегов). */ import DOMPurify from "dompurify"; const ALLOWED_REPORT_TAGS = [ "b", "strong", "i", "em", "u", "code", "pre", "br", "p", "div", "span", "ul", "ol", "li", "hr", ]; export function sanitizeAgentHtml(html: string): string { return DOMPurify.sanitize(html, { ALLOWED_TAGS: ALLOWED_REPORT_TAGS, ALLOWED_ATTR: [], }); } export interface DailyReportStats { platform?: "ssh" | "windows"; successful_logins?: number; failed_logins?: number; sudo_commands?: number; active_bans?: number; top_failed_ips?: string[]; active_users?: string[]; /** legacy */ successful_ssh?: number; failed_ssh?: number; rdp_success?: number; rdp_failed?: number; unique_users?: string[]; active_sessions?: number; active_sessions_rdp?: number; } export interface NormalizedReportStat { key: string; label: string; 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"; } /** Приводит stats агента/SAC к единым полям для карточки. */ export function normalizeReportStats( stats: DailyReportStats | null, type: string, ): DailyReportStats | null { if (!stats) return null; 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 = 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 = 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 = asInt(stats.active_bans); return out; } export function reportStatsFromDetails( details: Record | null, type = "report.daily.ssh", ): DailyReportStats | null { if (!details || typeof details.stats !== "object" || details.stats === null) { return null; } return normalizeReportStats(details.stats as DailyReportStats, type); } export function reportStatEntries( stats: DailyReportStats | null, type: string, ): NormalizedReportStat[] { const s = normalizeReportStats(stats, type); if (!s) return []; const platform = s.platform ?? reportPlatform(type); const entries: NormalizedReportStat[] = [ { key: "successful", label: platform === "ssh" ? "Успешных SSH" : "Успешных RDP", value: s.successful_logins ?? 0, }, { key: "failed", label: platform === "ssh" ? "Неудачных SSH" : "Неудачных RDP", value: s.failed_logins ?? 0, }, { key: "bans", label: "Активных банов", value: s.active_bans ?? 0 }, ]; if (platform === "ssh") { entries.splice(2, 0, { key: "sudo", label: "Команд sudo", value: s.sudo_commands ?? 0, }); } const users = s.active_users ?? []; if (users.length) { entries.push({ key: "active_users", label: "Активные пользователи", value: users.length, }); } else if (typeof s.active_sessions === "number" && s.active_sessions > 0) { entries.push({ key: "active_sessions", label: "Сессий (SSH)", value: s.active_sessions, }); } else if (typeof s.active_sessions_rdp === "number" && s.active_sessions_rdp > 0) { entries.push({ key: "active_sessions_rdp", label: "Сессий (RDP)", value: s.active_sessions_rdp, }); } return entries; } export function reportHtmlFromDetails(details: Record | null): string | null { if (!details) return null; const html = details.report_html; return typeof html === "string" && html.trim() ? html : null; } export function reportBodyFromDetails(details: Record | null): string | null { if (!details) return null; const body = details.report_body; return typeof body === "string" && body.trim() ? body : null; } export function reportActiveUserLines( stats: DailyReportStats | null, type: string, ): string[] { const s = normalizeReportStats(stats, type); if (!s?.active_users?.length) return []; return s.active_users; }