feat: unify SSH and Windows daily report layout in SAC and UI

Agent-style report body for SAC aggregation; shared stats cards; RDP ban note in docs

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-29 17:03:29 +10:00
parent 11195ae64f
commit 7ebbb9a9d1
8 changed files with 469 additions and 105 deletions
+111 -5
View File
@@ -16,24 +16,121 @@ export function sanitizeAgentHtml(html: string): string {
}
export interface DailyReportStats {
successful_ssh?: number;
failed_ssh?: number;
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;
unique_users?: string[];
}
export interface NormalizedReportStat {
key: string;
label: string;
value: string | number;
}
export function isDailyReportType(type: string): boolean {
return type === "report.daily.ssh" || type === "report.daily.rdp";
}
export function reportStatsFromDetails(details: Record<string, unknown> | null): DailyReportStats | null {
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 };
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;
} else {
out.successful_logins = stats.successful_logins ?? stats.rdp_success ?? 0;
out.failed_logins = stats.failed_logins ?? stats.rdp_failed ?? 0;
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;
return out;
}
export function reportStatsFromDetails(
details: Record<string, unknown> | null,
type = "report.daily.ssh",
): DailyReportStats | null {
if (!details || typeof details.stats !== "object" || details.stats === null) {
return null;
}
return details.stats as DailyReportStats;
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<string, unknown> | null): string | null {
@@ -47,3 +144,12 @@ export function reportBodyFromDetails(details: Record<string, unknown> | 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;
}