From 629aabf31134ce429ffcaedf475d3c2f0aa4976d Mon Sep 17 00:00:00 2001 From: David Alberto Adler Date: Thu, 21 May 2026 22:42:10 +0100 Subject: [PATCH 1/8] feat(dashboard): detection rules section under Secure Add a Detection Rules page that lists built-in rules by category (matching the policy form groupings) and allows authoring custom regex-based rules. Severity overrides and custom rules are stored client-side until the server endpoints land. Custom rules show up as a selectable section in the policy creation form. --- .../dashboard/src/components/app-sidebar.tsx | 13 +- .../src/pages/security/DetectionRules.tsx | 788 ++++++++++++++++++ .../src/pages/security/PolicyCenter.tsx | 144 +++- .../pages/security/detection-rules-data.ts | 349 ++++++++ client/dashboard/src/routes.tsx | 8 + 5 files changed, 1296 insertions(+), 6 deletions(-) create mode 100644 client/dashboard/src/pages/security/DetectionRules.tsx create mode 100644 client/dashboard/src/pages/security/detection-rules-data.ts diff --git a/client/dashboard/src/components/app-sidebar.tsx b/client/dashboard/src/components/app-sidebar.tsx index dffb603ebd..d1fce80a2d 100644 --- a/client/dashboard/src/components/app-sidebar.tsx +++ b/client/dashboard/src/components/app-sidebar.tsx @@ -88,9 +88,11 @@ export function AppSidebar({ ...props }: React.ComponentProps) { const observeActive = [routes.insights, routes.logs].some((r) => r.active); - const securityActive = [routes.riskOverview, routes.policyCenter].some( - (r) => r.active, - ); + const securityActive = [ + routes.riskOverview, + routes.detectionRules, + routes.policyCenter, + ].some((r) => r.active); let activeGroup: string | undefined; switch (true) { @@ -123,6 +125,7 @@ export function AppSidebar({ ...props }: React.ComponentProps) { routes.insights, routes.logs, routes.riskOverview, + routes.detectionRules, routes.policyCenter, routes.settings, ]; @@ -212,6 +215,10 @@ export function AppSidebar({ ...props }: React.ComponentProps) { item={routes.riskOverview} scope="project:read" /> + + + + + + + + + + + ); +} + +function DetectionRulesContent() { + const { + severityOverrides, + customRules, + setSeverityOverride, + addCustomRule, + updateCustomRule, + removeCustomRule, + } = useDetectionRulesStore(); + + const [createOpen, setCreateOpen] = useState(false); + const [selected, setSelected] = useState(null); + const [expanded, setExpanded] = useState( + "secrets", + ); + + return ( + + Detection Rules + + Built-in detection rules grouped by category. Click a rule to view its + description and override the default severity, or add your own custom + regex rule. + + + + + +
+ {customRules.length > 0 && ( + + setExpanded(expanded === "custom" ? null : "custom") + } + onSelect={(rule) => setSelected({ kind: "custom", rule })} + /> + )} + + setExpanded(expanded === cat ? null : cat)} + onSelect={(rule) => setSelected({ kind: "builtin", rule })} + /> +
+
+ + setSelected(null)} + onOverrideSeverity={setSeverityOverride} + onUpdateCustomRule={updateCustomRule} + onDeleteCustomRule={(id) => { + removeCustomRule(id); + setSelected(null); + toast.success("Custom detection rule deleted"); + }} + /> + + r.id)} + onCreate={(rule) => { + addCustomRule(rule); + setCreateOpen(false); + toast.success(`Created custom rule ${rule.id}`); + }} + /> +
+ ); +} + +/* -------------------------------------------------------------------------- */ +/* Custom rules section */ +/* -------------------------------------------------------------------------- */ + +function CustomRulesSection({ + rules, + expanded, + onToggle, + onSelect, +}: { + rules: CustomDetectionRule[]; + expanded: boolean; + onToggle: () => void; + onSelect: (rule: CustomDetectionRule) => void; +}) { + const meta = RULE_CATEGORY_META.custom; + return ( +
+ + Custom + +
+ + {expanded && ( +
+ {rules.map((rule) => ( + onSelect(rule)} + /> + ))} +
+ )} +
+
+ ); +} + +/* -------------------------------------------------------------------------- */ +/* Builtin rules section */ +/* -------------------------------------------------------------------------- */ + +function BuiltinRulesSection({ + severityOverrides, + expanded, + onToggle, + onSelect, +}: { + severityOverrides: Record; + expanded: RuleCategory | "custom" | null; + onToggle: (cat: RuleCategory) => void; + onSelect: (rule: BuiltinRule) => void; +}) { + return ( +
+ + Built-in + +
+ {BUILTIN_CATEGORY_ORDER.map((cat) => { + const meta = RULE_CATEGORY_META[cat]; + const rules = BUILTIN_RULES_BY_CATEGORY[cat]; + const isExpanded = expanded === cat; + return ( +
+ onToggle(cat)} + count={rules.length} + /> + {isExpanded && rules.length > 0 && ( +
+ {rules.map((rule) => { + const severity = resolveSeverity( + rule.id, + rule.defaultSeverity, + severityOverrides, + ); + const isOverridden = + severityOverrides[rule.id] !== undefined && + severityOverrides[rule.id] !== rule.defaultSeverity; + return ( + onSelect(rule)} + /> + ); + })} +
+ )} +
+ ); + })} +
+
+ ); +} + +/* -------------------------------------------------------------------------- */ +/* Shared row components */ +/* -------------------------------------------------------------------------- */ + +function CategoryHeader({ + icon, + label, + description, + expanded, + count, + onClick, +}: { + icon: IconName; + label: string; + description: string; + expanded: boolean; + count: number; + onClick: () => void; +}) { + return ( + + ); +} + +function RuleRow({ + title, + subtitle, + severity, + overridden, + onClick, +}: { + title: string; + subtitle: string; + severity: SeverityLevel; + overridden?: boolean; + onClick: () => void; +}) { + return ( + + ); +} + +export function SeverityBadge({ severity }: { severity: SeverityLevel }) { + const meta = SEVERITY_META[severity]; + return ( + + {meta.label} + + ); +} + +/* -------------------------------------------------------------------------- */ +/* Rule detail sheet */ +/* -------------------------------------------------------------------------- */ + +function RuleDetailSheet({ + selection, + severityOverrides, + onClose, + onOverrideSeverity, + onUpdateCustomRule, + onDeleteCustomRule, +}: { + selection: SelectedRule | null; + severityOverrides: Record; + onClose: () => void; + onOverrideSeverity: (ruleId: string, severity: SeverityLevel | null) => void; + onUpdateCustomRule: ( + id: string, + patch: Partial>, + ) => void; + onDeleteCustomRule: (id: string) => void; +}) { + return ( + !open && onClose()}> + + {selection?.kind === "builtin" && ( + + onOverrideSeverity(selection.rule.id, severity) + } + /> + )} + {selection?.kind === "custom" && ( + onUpdateCustomRule(selection.rule.id, patch)} + onDelete={() => onDeleteCustomRule(selection.rule.id)} + /> + )} + + + ); +} + +function BuiltinRuleDetail({ + rule, + override, + onOverride, +}: { + rule: BuiltinRule; + override: SeverityLevel | undefined; + onOverride: (severity: SeverityLevel | null) => void; +}) { + const meta = RULE_CATEGORY_META[rule.category]; + const effective = override ?? rule.defaultSeverity; + return ( + <> + + {rule.title} + + {rule.id} + + +
+ +
+ + {meta.label} +
+
+ + +

{rule.description}

+
+ + +
+ + + {SEVERITY_META[rule.defaultSeverity].description} + +
+
+ + +
+ + {override !== undefined && override !== rule.defaultSeverity && ( + + )} +
+

+ Overrides change how findings for this rule render in dashboards and + risk reports. +

+
+
+ + ); +} + +function CustomRuleDetail({ + rule, + onUpdate, + onDelete, +}: { + rule: CustomDetectionRule; + onUpdate: ( + patch: Partial>, + ) => void; + onDelete: () => void; +}) { + const [title, setTitle] = useState(rule.title); + const [description, setDescription] = useState(rule.description); + const [regex, setRegex] = useState(rule.regex); + const [severity, setSeverity] = useState(rule.severity); + + const regexError = useMemo(() => validateRegex(regex), [regex]); + const dirty = + title !== rule.title || + description !== rule.description || + regex !== rule.regex || + severity !== rule.severity; + + return ( + <> + + {rule.title || rule.id} + + {rule.id} + + +
+
+ + +
+ +
+ +