diff --git a/app/(dashboard)/waf-events/WafEventsClient.tsx b/app/(dashboard)/waf-events/WafEventsClient.tsx index efcb5392..f3b81a96 100644 --- a/app/(dashboard)/waf-events/WafEventsClient.tsx +++ b/app/(dashboard)/waf-events/WafEventsClient.tsx @@ -18,6 +18,7 @@ import { Tooltip, Typography, } from "@mui/material"; + import SearchIcon from "@mui/icons-material/Search"; import CloseIcon from "@mui/icons-material/Close"; import BlockIcon from "@mui/icons-material/Block"; @@ -35,6 +36,7 @@ type Props = { pagination: { total: number; page: number; perPage: number }; initialSearch: string; globalExcluded: number[]; + globalExcludedMessages: Record; globalWafEnabled: boolean; hostWafMap: Record; }; @@ -222,10 +224,12 @@ function WafEventDrawer({ function GlobalSuppressedRules({ excluded, + messages, wafEnabled, onRemove, }: { excluded: number[]; + messages: Record; wafEnabled: boolean; onRemove: (ruleId: number) => void; }) { @@ -265,18 +269,46 @@ function GlobalSuppressedRules({ Open a WAF event and click "Suppress Globally" to add one. ) : ( - + {excluded.map((id) => ( - handleRemove(id)} - deleteIcon={} - disabled={pending} - sx={{ fontFamily: "monospace", fontWeight: 600 }} - color="error" - variant="outlined" - /> + sx={{ + display: "flex", + alignItems: "center", + gap: 2, + px: 2, + py: 1.5, + borderRadius: 1.5, + border: "1px solid", + borderColor: "divider", + bgcolor: "action.hover", + }} + > + + + Rule {id} + + + {messages[id] ?? "No description available — rule has not triggered yet"} + + + + handleRemove(id)} + disabled={pending} + color="error" + sx={{ flexShrink: 0 }} + > + + + + ))} )} @@ -295,7 +327,7 @@ function GlobalSuppressedRules({ ); } -export default function WafEventsClient({ events, pagination, initialSearch, globalExcluded, globalWafEnabled, hostWafMap }: Props) { +export default function WafEventsClient({ events, pagination, initialSearch, globalExcluded, globalExcludedMessages, globalWafEnabled, hostWafMap }: Props) { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); @@ -407,16 +439,7 @@ export default function WafEventsClient({ events, pagination, initialSearch, glo setTab(v)} sx={{ borderBottom: 1, borderColor: "divider" }}> - - Suppressed Rules - {localGlobalExcluded.length > 0 && ( - - )} - - } - /> + {tab === 0 && ( @@ -453,6 +476,7 @@ export default function WafEventsClient({ events, pagination, initialSearch, glo {tab === 1 && ( setLocalGlobalExcluded((prev) => prev.filter((id) => id !== ruleId))} /> diff --git a/app/(dashboard)/waf-events/page.tsx b/app/(dashboard)/waf-events/page.tsx index a7220bcb..ede283e7 100644 --- a/app/(dashboard)/waf-events/page.tsx +++ b/app/(dashboard)/waf-events/page.tsx @@ -1,7 +1,7 @@ export const dynamic = 'force-dynamic'; import WafEventsClient from "./WafEventsClient"; -import { listWafEvents, countWafEvents } from "@/src/lib/models/waf-events"; +import { listWafEvents, countWafEvents, getWafRuleMessages } from "@/src/lib/models/waf-events"; import { getWafSettings } from "@/src/lib/settings"; import { listProxyHosts } from "@/src/lib/models/proxy-hosts"; import { requireAdmin } from "@/src/lib/auth"; @@ -26,6 +26,9 @@ export default async function WafEventsPage({ searchParams }: PageProps) { listProxyHosts(), ]); + const globalExcludedIds = globalWaf?.excluded_rule_ids ?? []; + const globalExcludedMessages = await getWafRuleMessages(globalExcludedIds); + const hostWafMap: Record = {}; for (const host of hosts) { const ids = host.waf?.excluded_rule_ids ?? []; @@ -39,7 +42,8 @@ export default async function WafEventsPage({ searchParams }: PageProps) { events={events} pagination={{ total, page, perPage: PER_PAGE }} initialSearch={search ?? ""} - globalExcluded={globalWaf?.excluded_rule_ids ?? []} + globalExcluded={globalExcludedIds} + globalExcludedMessages={globalExcludedMessages} globalWafEnabled={globalWaf?.enabled ?? false} hostWafMap={hostWafMap} /> diff --git a/src/lib/models/waf-events.ts b/src/lib/models/waf-events.ts index 7a118ed3..04df07de 100644 --- a/src/lib/models/waf-events.ts +++ b/src/lib/models/waf-events.ts @@ -1,6 +1,6 @@ import db from "../db"; import { wafEvents } from "../db/schema"; -import { desc, like, or, count, and, gte, lte, sql } from "drizzle-orm"; +import { desc, like, or, count, and, gte, lte, sql, inArray } from "drizzle-orm"; export type WafEvent = { id: number; @@ -61,6 +61,22 @@ export async function getTopWafRules(from: number, to: number, limit = 10): Prom .map((r) => ({ ruleId: r.ruleId, count: r.count, message: r.message ?? null })); } +export async function getWafRuleMessages(ruleIds: number[]): Promise> { + if (ruleIds.length === 0) return {}; + const rows = await db + .select({ + ruleId: wafEvents.ruleId, + message: sql`MAX(${wafEvents.ruleMessage})`, + }) + .from(wafEvents) + .where(inArray(wafEvents.ruleId, ruleIds)) + .groupBy(wafEvents.ruleId); + return Object.fromEntries( + rows.filter((r): r is typeof r & { ruleId: number } => r.ruleId != null) + .map((r) => [r.ruleId, r.message ?? null]) + ); +} + export async function listWafEvents(limit = 50, offset = 0, search?: string): Promise { const rows = await db .select()