import { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Loader2, CheckCircle, XCircle, MinusCircle, ChevronDown, ChevronRight, ExternalLink, Shield, } from "lucide-react"; import { getDetectionRulesForTest, evaluateDetectionRule, type DetectionRuleItem, } from "../../api/detection-rules"; import type { User } from "../../types/models"; const severityColors: Record = { critical: "bg-red-900/50 text-red-400 border-red-500/30", high: "bg-orange-900/50 text-orange-400 border-orange-500/30", medium: "bg-yellow-900/50 text-yellow-400 border-yellow-500/30", low: "bg-blue-900/50 text-blue-400 border-blue-500/30", informational: "bg-gray-800/50 text-gray-400 border-gray-600/30", }; const sourceColors: Record = { sigma: "bg-purple-900/50 text-purple-400 border-purple-500/30", elastic: "bg-cyan-900/50 text-cyan-400 border-cyan-500/30", splunk: "bg-green-900/50 text-green-400 border-green-500/30", custom: "bg-gray-800/50 text-gray-400 border-gray-600/30", }; interface Props { testId: string; user: User | null; canEdit: boolean; } export default function DetectionRuleChecklist({ testId, user, canEdit }: Props) { const queryClient = useQueryClient(); const [expandedRules, setExpandedRules] = useState>(new Set()); const [editingNotes, setEditingNotes] = useState>({}); const { data, isLoading, error } = useQuery({ queryKey: ["detection-rules-for-test", testId], queryFn: () => getDetectionRulesForTest(testId), enabled: !!testId, }); const evaluateMutation = useMutation({ mutationFn: evaluateDetectionRule, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["detection-rules-for-test", testId] }); }, }); const toggleExpanded = (ruleId: string) => { setExpandedRules((prev) => { const next = new Set(prev); if (next.has(ruleId)) next.delete(ruleId); else next.add(ruleId); return next; }); }; const handleEvaluate = (ruleId: string, triggered: boolean | null) => { evaluateMutation.mutate({ test_id: testId, detection_rule_id: ruleId, triggered, notes: editingNotes[ruleId], }); }; const handleNotesChange = (ruleId: string, notes: string) => { setEditingNotes((prev) => ({ ...prev, [ruleId]: notes })); }; const handleNotesSave = (ruleId: string, triggered: boolean | null) => { evaluateMutation.mutate({ test_id: testId, detection_rule_id: ruleId, triggered: triggered, notes: editingNotes[ruleId] ?? "", }); }; if (isLoading) { return (
); } if (error || !data) { return null; } if (data.rules.length === 0) { return (

No detection rules available for this technique.

); } return (
{/* Summary bar */}
{data.triggered} / {data.total} rules triggered
{data.evaluated > 0 && ( {data.detection_rate}% detection rate )}
{data.evaluated} / {data.total} evaluated
{/* Progress bar */}
{data.total > 0 && ( <>
)}
{/* Rules list */}
{data.rules.map((rule) => { const isExpanded = expandedRules.has(rule.id); const notesDraft = editingNotes[rule.id] ?? rule.notes ?? ""; return (
{/* Rule header */}
{/* Expand toggle */} {/* Status icon */} {rule.triggered === true && } {rule.triggered === false && } {rule.triggered == null && } {/* Rule info */}

{rule.title}

{/* Badges */}
{rule.severity && ( {rule.severity} )} {rule.source}
{/* Evaluate buttons */} {canEdit && (
)}
{/* Expanded content */} {isExpanded && (
{rule.description && (

{rule.description}

)} {/* Rule content */} {rule.rule_content && (

Rule Content ({rule.rule_format})

                        {rule.rule_content}
                      
)} {/* Source link */} {rule.source_url && ( View source )} {/* Notes */} {canEdit ? (
handleNotesChange(rule.id, e.target.value)} placeholder="Add evaluation notes..." className="flex-1 rounded border border-gray-700 bg-gray-900 px-2 py-1.5 text-xs text-gray-200 placeholder-gray-500 focus:border-indigo-500 focus:outline-none" />
) : ( rule.notes && (

Notes

{rule.notes}

) )}
)}
); })}
); }