From 4a666c2914cde124a63a7daeef5aeced3a519a5e Mon Sep 17 00:00:00 2001 From: "Gan, Jimmy" Date: Sun, 22 Feb 2026 12:38:59 +0800 Subject: [PATCH] Categorize audit log by threat level with filtering UI --- dashboard/backend/routers/system.py | 39 +++++++++++++++++-- dashboard/frontend/src/routes/Settings.svelte | 38 ++++++++++++++---- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/dashboard/backend/routers/system.py b/dashboard/backend/routers/system.py index 8395543..23c05d3 100644 --- a/dashboard/backend/routers/system.py +++ b/dashboard/backend/routers/system.py @@ -60,11 +60,44 @@ def system_stats(): def audit_log(lines: int = Query(100, le=500)): log_path = os.path.join(VOLUME_ROOT, "docker/nas-dashboard/audit.log") if not os.path.exists(log_path): - return {"lines": []} + return {"entries": []} with open(log_path, "rb") as f: f.seek(0, 2) size = f.tell() buf = min(size, lines * 200) f.seek(max(0, size - buf)) - data = f.read().decode(errors="replace").splitlines() - return {"lines": data[-lines:]} + raw = f.read().decode(errors="replace").splitlines()[-lines:] + + entries = [] + for line in raw: + parts = line.split(" ", 6) + if len(parts) < 7: + continue + ts, ip, user, method, path, status_str, dur = parts + status = int(status_str) if status_str.isdigit() else 0 + level = _classify(method, path, status, user) + if level == "noise": + continue + entries.append({"ts": ts, "ip": ip, "user": user, "method": method, "path": path, "status": status, "duration": dur, "level": level}) + return {"entries": entries} + + +def _classify(method, path, status, user): + # High — failed auth attempts + if "/auth/login" in path and status in (401, 403): + return "high" + if "/auth/passkey/login/verify" in path and status == 400: + return "high" + # Medium — 403/404 on sensitive paths, or any 5xx + if status >= 500: + return "medium" + if status == 403 and "/files/" in path: + return "medium" + # Low — normal auth activity + if "/auth/" in path and status == 200: + return "low" + # Noise — health checks, static assets + if path in ("/api/health",) or path.startswith("/assets/") or path == "/favicon.ico" or path == "/": + return "noise" + # Info — everything else + return "info" diff --git a/dashboard/frontend/src/routes/Settings.svelte b/dashboard/frontend/src/routes/Settings.svelte index 8114324..acbe893 100644 --- a/dashboard/frontend/src/routes/Settings.svelte +++ b/dashboard/frontend/src/routes/Settings.svelte @@ -114,14 +114,20 @@ saving = false; } - let auditLines = $state([]); + let auditEntries = $state([]); let auditLoading = $state(false); + let auditFilter = $state("all"); + + const levelColors = { high: "text-rose-400", medium: "text-amber-400", low: "text-emerald-400", info: "text-surface-400" }; + const levelLabels = { high: "HIGH", medium: "MED", low: "LOW", info: "INFO" }; + + let filteredEntries = $derived(auditFilter === "all" ? auditEntries : auditEntries.filter(e => e.level === auditFilter)); async function loadAuditLog() { auditLoading = true; try { - const res = await get("/system/audit-log?lines=100"); - auditLines = res.lines.reverse(); + const res = await get("/system/audit-log?lines=200"); + auditEntries = res.entries.reverse(); } catch {} auditLoading = false; } @@ -206,12 +212,30 @@

Audit Log

- {#if auditLines.length > 0} -
-
{auditLines.join('\n')}
+ {#if auditEntries.length > 0} +
+ {#each ["all", "high", "medium", "low", "info"] as lvl} + + {/each} +
+
+ {#each filteredEntries as e} +
+ {levelLabels[e.level]} + {e.ts.replace('T', ' ')} + {e.status} + {e.user} + {e.method} {e.path} +
+ {/each}
{:else if !auditLoading}

Click Load to view recent audit log entries