Categorize audit log by threat level with filtering UI
This commit is contained in:
@@ -60,11 +60,44 @@ def system_stats():
|
|||||||
def audit_log(lines: int = Query(100, le=500)):
|
def audit_log(lines: int = Query(100, le=500)):
|
||||||
log_path = os.path.join(VOLUME_ROOT, "docker/nas-dashboard/audit.log")
|
log_path = os.path.join(VOLUME_ROOT, "docker/nas-dashboard/audit.log")
|
||||||
if not os.path.exists(log_path):
|
if not os.path.exists(log_path):
|
||||||
return {"lines": []}
|
return {"entries": []}
|
||||||
with open(log_path, "rb") as f:
|
with open(log_path, "rb") as f:
|
||||||
f.seek(0, 2)
|
f.seek(0, 2)
|
||||||
size = f.tell()
|
size = f.tell()
|
||||||
buf = min(size, lines * 200)
|
buf = min(size, lines * 200)
|
||||||
f.seek(max(0, size - buf))
|
f.seek(max(0, size - buf))
|
||||||
data = f.read().decode(errors="replace").splitlines()
|
raw = f.read().decode(errors="replace").splitlines()[-lines:]
|
||||||
return {"lines": data[-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"
|
||||||
|
|||||||
@@ -114,14 +114,20 @@
|
|||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let auditLines = $state([]);
|
let auditEntries = $state([]);
|
||||||
let auditLoading = $state(false);
|
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() {
|
async function loadAuditLog() {
|
||||||
auditLoading = true;
|
auditLoading = true;
|
||||||
try {
|
try {
|
||||||
const res = await get("/system/audit-log?lines=100");
|
const res = await get("/system/audit-log?lines=200");
|
||||||
auditLines = res.lines.reverse();
|
auditEntries = res.entries.reverse();
|
||||||
} catch {}
|
} catch {}
|
||||||
auditLoading = false;
|
auditLoading = false;
|
||||||
}
|
}
|
||||||
@@ -206,12 +212,30 @@
|
|||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h3 class="text-sm font-semibold text-surface-700">Audit Log</h3>
|
<h3 class="text-sm font-semibold text-surface-700">Audit Log</h3>
|
||||||
<button onclick={loadAuditLog} disabled={auditLoading} class="px-3 py-1.5 text-xs font-medium text-primary-600 bg-primary-50 hover:bg-primary-100 rounded-lg transition-colors disabled:opacity-50">
|
<button onclick={loadAuditLog} disabled={auditLoading} class="px-3 py-1.5 text-xs font-medium text-primary-600 bg-primary-50 hover:bg-primary-100 rounded-lg transition-colors disabled:opacity-50">
|
||||||
{auditLoading ? "Loading..." : auditLines.length ? "Refresh" : "Load"}
|
{auditLoading ? "Loading..." : auditEntries.length ? "Refresh" : "Load"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{#if auditLines.length > 0}
|
{#if auditEntries.length > 0}
|
||||||
<div class="max-h-80 overflow-auto rounded-lg bg-surface-950 p-3">
|
<div class="flex gap-1.5 mb-3">
|
||||||
<pre class="text-xs text-surface-300 font-mono whitespace-pre leading-5">{auditLines.join('\n')}</pre>
|
{#each ["all", "high", "medium", "low", "info"] as lvl}
|
||||||
|
<button onclick={() => auditFilter = lvl} class="px-2.5 py-1 text-xs rounded-md transition-colors {auditFilter === lvl ? 'bg-surface-800 text-white' : 'bg-surface-100 text-surface-500 hover:bg-surface-200'}">
|
||||||
|
{lvl === "all" ? "All" : levelLabels[lvl] || lvl}
|
||||||
|
{#if lvl !== "all"}
|
||||||
|
<span class="ml-1 opacity-60">{auditEntries.filter(e => e.level === lvl).length}</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div class="max-h-80 overflow-auto rounded-lg bg-surface-950 p-3 space-y-0.5">
|
||||||
|
{#each filteredEntries as e}
|
||||||
|
<div class="text-xs font-mono flex gap-2 leading-5">
|
||||||
|
<span class="{levelColors[e.level]} w-8 shrink-0">{levelLabels[e.level]}</span>
|
||||||
|
<span class="text-surface-500 shrink-0">{e.ts.replace('T', ' ')}</span>
|
||||||
|
<span class="text-surface-400 shrink-0 w-10">{e.status}</span>
|
||||||
|
<span class="text-surface-300 shrink-0">{e.user}</span>
|
||||||
|
<span class="text-surface-500">{e.method} {e.path}</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else if !auditLoading}
|
{:else if !auditLoading}
|
||||||
<p class="text-sm text-surface-400">Click Load to view recent audit log entries</p>
|
<p class="text-sm text-surface-400">Click Load to view recent audit log entries</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user