From 77d3e35c630e0cc6b653c35808ec460e0fd9d963 Mon Sep 17 00:00:00 2001 From: fuomag9 <1580624+fuomag9@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:21:08 +0100 Subject: [PATCH] feat: clickable WAF event rows with detail drawer - WafEvent model: expose rawData field from DB - DataTable: add optional onRowClick prop with hover cursor - WafEventsClient: clicking a row opens a right-side drawer showing all event fields plus the raw Coraza audit JSON (pretty-printed) Safety: rawData is rendered via JSON.stringify into a
 element,
never via dangerouslySetInnerHTML, so attack payloads are displayed
as inert text.

Co-Authored-By: Claude Sonnet 4.6 
---
 .../waf-events/WafEventsClient.tsx            | 112 +++++++++++++++++-
 src/components/ui/DataTable.tsx               |   8 +-
 src/lib/models/waf-events.ts                  |   2 +
 3 files changed, 120 insertions(+), 2 deletions(-)

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,
   }));
 }