diff --git a/app/(dashboard)/waf-events/WafEventsClient.tsx b/app/(dashboard)/waf-events/WafEventsClient.tsx
index 5348a3df..93e04086 100644
--- a/app/(dashboard)/waf-events/WafEventsClient.tsx
+++ b/app/(dashboard)/waf-events/WafEventsClient.tsx
@@ -2,8 +2,19 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
-import { Chip, Stack, TextField, Tooltip, Typography } from "@mui/material";
+import {
+ Box,
+ Chip,
+ Divider,
+ Drawer,
+ IconButton,
+ Stack,
+ TextField,
+ Tooltip,
+ Typography,
+} from "@mui/material";
import SearchIcon from "@mui/icons-material/Search";
+import CloseIcon from "@mui/icons-material/Close";
import { DataTable } from "@/src/components/ui/DataTable";
import type { WafEvent } from "@/src/lib/models/waf-events";
@@ -29,11 +40,107 @@ function SeverityChip({ severity }: { severity: string | null }) {
return ;
}
+function DetailRow({ label, children }: { label: string; children: React.ReactNode }) {
+ return (
+
+
+ {label}
+
+ {children}
+
+ );
+}
+
+function WafEventDrawer({ event, onClose }: { event: WafEvent | null; onClose: () => void }) {
+ // Parse rawData safely — render as text only, never as HTML
+ let parsedRaw: unknown = null;
+ if (event?.rawData) {
+ try { parsedRaw = JSON.parse(event.rawData); } catch { parsedRaw = event.rawData; }
+ }
+
+ return (
+
+ {event && (
+
+ {/* Header */}
+
+
+
+ WAF Event
+
+
+
+
+
+
+ {/* Fields */}
+
+ {new Date(event.ts * 1000).toLocaleString()}
+
+
+
+ {event.host || "—"}
+
+
+
+
+ {event.clientIp}
+ {event.countryCode && }
+
+
+
+
+
+ {event.method} {event.uri}
+
+
+
+
+ {event.ruleId ?? "—"}
+
+
+
+ {event.ruleMessage ?? "—"}
+
+
+
+
+ {/* Raw audit log — rendered as plain text, never as HTML */}
+
+ {parsedRaw !== null ? (
+
+ {JSON.stringify(parsedRaw, null, 2)}
+
+ ) : (
+ —
+ )}
+
+
+ )}
+
+ );
+}
+
export default function WafEventsClient({ events, pagination, initialSearch }: Props) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const [searchTerm, setSearchTerm] = useState(initialSearch);
+ const [selected, setSelected] = useState(null);
useEffect(() => { setSearchTerm(initialSearch); }, [initialSearch]);
const debounceRef = useRef | null>(null);
@@ -175,7 +282,10 @@ export default function WafEventsClient({ events, pagination, initialSearch }: P
keyField="id"
emptyMessage="No WAF events found. Enable the WAF in Settings and send some traffic to see events here."
pagination={pagination}
+ onRowClick={setSelected}
/>
+
+ setSelected(null)} />
);
}
diff --git a/src/components/ui/DataTable.tsx b/src/components/ui/DataTable.tsx
index 7f5dcd81..ab60d48d 100644
--- a/src/components/ui/DataTable.tsx
+++ b/src/components/ui/DataTable.tsx
@@ -29,6 +29,7 @@ type DataTableProps = {
keyField: keyof T;
emptyMessage?: string;
loading?: boolean;
+ onRowClick?: (row: T) => void;
pagination?: {
total: number;
page: number;
@@ -69,6 +70,7 @@ export function DataTable({
keyField,
emptyMessage = "No data available",
loading = false,
+ onRowClick,
pagination,
}: DataTableProps) {
return (
@@ -97,7 +99,11 @@ export function DataTable({
) : (
data.map((row) => (
-
+ onRowClick(row) : undefined}
+ sx={onRowClick ? { cursor: "pointer", "&:hover": { bgcolor: "action.hover" } } : undefined}
+ >
{columns.map((col) => (
{col.render ? col.render(row) : (row as Record)[col.id] as ReactNode}
diff --git a/src/lib/models/waf-events.ts b/src/lib/models/waf-events.ts
index 61d457e8..fd34793f 100644
--- a/src/lib/models/waf-events.ts
+++ b/src/lib/models/waf-events.ts
@@ -13,6 +13,7 @@ export type WafEvent = {
ruleId: number | null;
ruleMessage: string | null;
severity: string | null;
+ rawData: string | null;
};
function buildSearch(search?: string) {
@@ -53,5 +54,6 @@ export async function listWafEvents(limit = 50, offset = 0, search?: string): Pr
ruleId: r.ruleId ?? null,
ruleMessage: r.ruleMessage ?? null,
severity: r.severity ?? null,
+ rawData: r.rawData ?? null,
}));
}