diff --git a/dashboard/backend/routers/security.py b/dashboard/backend/routers/security.py index 5ed3014..812af0c 100644 --- a/dashboard/backend/routers/security.py +++ b/dashboard/backend/routers/security.py @@ -1,7 +1,7 @@ import docker import re import json -from datetime import datetime, timezone, timedelta +from datetime import datetime, timezone, timedelta, time from collections import Counter from fastapi import APIRouter, Query from config import DOCKER_HOST @@ -130,10 +130,60 @@ def _parse_caddy_logs(lines: str, limit: int) -> list[dict]: return entries[-limit:] +def _parse_timestamp(value: str) -> datetime | None: + if not value: + return None + try: + return datetime.strptime(value, "%Y-%m-%d %H:%M:%S").replace(tzinfo=_tz_cst) + except ValueError: + return None + + +def _filter_security_entries( + entries: list[dict], + event_type: str | None, + ip: str | None, + start_date: str | None, + end_date: str | None, +) -> list[dict]: + normalized_ip = ip.strip() if ip else None + start_dt = None + end_dt = None + + if start_date: + start_dt = datetime.combine(datetime.strptime(start_date, "%Y-%m-%d").date(), time.min, tzinfo=_tz_cst) + if end_date: + end_dt = datetime.combine(datetime.strptime(end_date, "%Y-%m-%d").date(), time.max, tzinfo=_tz_cst) + + filtered = [] + for entry in entries: + if event_type and entry.get("type") != event_type: + continue + if normalized_ip and entry.get("ip", "").strip() != normalized_ip: + continue + + if start_dt or end_dt: + entry_ts = _parse_timestamp(entry.get("timestamp", "")) + if entry_ts is None: + continue + if start_dt and entry_ts < start_dt: + continue + if end_dt and entry_ts > end_dt: + continue + + filtered.append(entry) + + return filtered + + @router.get("/logs") def security_logs( tail: int = Query(200, le=2000), limit: int = Query(100, le=500), + type: str | None = Query(None), + ip: str | None = Query(None), + start_date: str | None = Query(None), + end_date: str | None = Query(None), ): """Fetch and parse security-relevant logs from Authelia and Caddy.""" results = [] @@ -153,7 +203,8 @@ def security_logs( # Sort by timestamp descending results.sort(key=lambda x: x.get("timestamp", ""), reverse=True) - return {"logs": results[:limit]} + filtered = _filter_security_entries(results, type, ip, start_date, end_date) + return {"logs": filtered[:limit]} @router.get("/stats") diff --git a/dashboard/frontend/src/routes/Security.svelte b/dashboard/frontend/src/routes/Security.svelte index cea9ae9..843611b 100644 --- a/dashboard/frontend/src/routes/Security.svelte +++ b/dashboard/frontend/src/routes/Security.svelte @@ -6,6 +6,10 @@ let stats = $state(null); let loading = $state(false); let filter = $state("all"); + let ipFilter = $state(""); + let startDate = $state(""); + let endDate = $state(""); + let filterError = $state(""); onMount(loadAll); @@ -23,23 +27,65 @@ } } + function buildLogsQuery() { + const params = new URLSearchParams({ limit: "300" }); + if (filter !== "all") params.set("type", filter); + if (ipFilter.trim()) params.set("ip", ipFilter.trim()); + if (startDate) params.set("start_date", startDate); + if (endDate) params.set("end_date", endDate); + return params.toString(); + } + + function validateFilters() { + if (startDate && endDate && startDate > endDate) { + filterError = "From date must be on or before To date"; + return false; + } + filterError = ""; + return true; + } + async function loadLogs() { + if (!validateFilters()) { + logs = []; + return; + } + try { - const res = await get("/security/logs?limit=300"); + const res = await get(`/security/logs?${buildLogsQuery()}`); logs = res.logs; } catch (e) { console.error("Failed to load security logs:", e); } } - let filteredLogs = $derived( - filter === "all" ? logs : logs.filter(l => l.type === filter) - ); + async function applyTypeFilter(nextFilter) { + filter = nextFilter; + await loadLogs(); + } + + async function applyIpFilter(ip) { + ipFilter = ip; + await loadLogs(); + } + + async function clearFilters() { + filter = "all"; + ipFilter = ""; + startDate = ""; + endDate = ""; + filterError = ""; + await loadLogs(); + } let maxTimelineCount = $derived( stats ? Math.max(1, ...stats.timeline.map(t => t.count)) : 1 ); + let hasActiveFilters = $derived( + filter !== "all" || !!ipFilter.trim() || !!startDate || !!endDate + ); + const typeLabels = { auth_failure: "Auth", suspicious_request: "Request", error: "Error" }; const typeColors = { auth_failure: "text-red-400", suspicious_request: "text-amber-400", error: "text-surface-500" }; @@ -99,7 +145,7 @@
{entry.ip}
+
Filter recent events by type, IP, or date range.
{filterError}
+ {/if} + + {#if logs.length > 0}No security events found
+{hasActiveFilters ? "No security events match the current filters" : "No security events found"}
{/if}