diff --git a/frontend/src/components/AddTestToCampaignModal.tsx b/frontend/src/components/AddTestToCampaignModal.tsx index 069dcf9..e009b77 100644 --- a/frontend/src/components/AddTestToCampaignModal.tsx +++ b/frontend/src/components/AddTestToCampaignModal.tsx @@ -22,7 +22,9 @@ import type { Test, TestState, TestTemplateSummary } from "../types/models"; const stateBadge: Record = { draft: "bg-gray-800/50 text-gray-400 border-gray-600/30", red_executing: "bg-orange-900/50 text-orange-400 border-orange-500/30", + red_review: "bg-amber-900/50 text-amber-400 border-amber-500/30", blue_evaluating: "bg-indigo-900/50 text-indigo-400 border-indigo-500/30", + blue_review: "bg-purple-900/50 text-purple-400 border-purple-500/30", in_review: "bg-blue-900/50 text-blue-400 border-blue-500/30", validated: "bg-green-900/50 text-green-400 border-green-500/30", rejected: "bg-red-900/50 text-red-400 border-red-500/30", diff --git a/frontend/src/components/CampaignTimeline.tsx b/frontend/src/components/CampaignTimeline.tsx index 34a7721..7f66401 100644 --- a/frontend/src/components/CampaignTimeline.tsx +++ b/frontend/src/components/CampaignTimeline.tsx @@ -22,7 +22,9 @@ const PHASES = [ const stateColors: Record = { draft: { bg: "bg-gray-800", text: "text-gray-400", border: "border-gray-600" }, red_executing: { bg: "bg-orange-900/50", text: "text-orange-400", border: "border-orange-500/50" }, + red_review: { bg: "bg-amber-900/50", text: "text-amber-400", border: "border-amber-500/50" }, blue_evaluating: { bg: "bg-indigo-900/50", text: "text-indigo-400", border: "border-indigo-500/50" }, + blue_review: { bg: "bg-purple-900/50", text: "text-purple-400", border: "border-purple-500/50" }, in_review: { bg: "bg-blue-900/50", text: "text-blue-400", border: "border-blue-500/50" }, validated: { bg: "bg-green-900/50", text: "text-green-400", border: "border-green-500/50" }, rejected: { bg: "bg-red-900/50", text: "text-red-400", border: "border-red-500/50" }, diff --git a/frontend/src/components/test-detail/ReviewModal.tsx b/frontend/src/components/test-detail/ReviewModal.tsx new file mode 100644 index 0000000..1093e62 --- /dev/null +++ b/frontend/src/components/test-detail/ReviewModal.tsx @@ -0,0 +1,252 @@ +import { useState } from "react"; +import { + CheckCircle, + RotateCcw, + AlertTriangle, + Loader2, + Shield, + ShieldCheck, + FileIcon, + X, +} from "lucide-react"; +import type { Test } from "../../types/models"; + +// ── Props ────────────────────────────────────────────────────────── + +type RedDecision = "approve" | "reopen"; +type BlueDecision = "approve" | "reopen" | "gap"; + +interface ReviewModalProps { + side: "red" | "blue"; + test: Test; + isSubmitting: boolean; + onSubmitRed?: (decision: RedDecision, notes: string) => void; + onSubmitBlue?: (decision: BlueDecision, notes: string, systemGaps: string) => void; + onClose: () => void; +} + +// ── Component ────────────────────────────────────────────────────── + +export default function ReviewModal({ + side, + test, + isSubmitting, + onSubmitRed, + onSubmitBlue, + onClose, +}: ReviewModalProps) { + const isRed = side === "red"; + const [decision, setDecision] = useState(null); + const [notes, setNotes] = useState(""); + const [systemGaps, setSystemGaps] = useState(""); + + const title = isRed ? "Review Red Team Submission" : "Review Blue Team Submission"; + const accent = isRed ? "orange" : "indigo"; + const evidences = isRed ? test.red_evidences || [] : test.blue_evidences || []; + + const requiresNotes = decision === "reopen" && notes.trim().length === 0; + const requiresGaps = decision === "gap" && systemGaps.trim().length === 0; + + const canSubmit = decision !== null && !isSubmitting && !requiresNotes && !requiresGaps; + + const handleSubmit = () => { + if (!decision) return; + if (isRed) { + onSubmitRed?.(decision as RedDecision, notes); + } else { + onSubmitBlue?.(decision as BlueDecision, notes, systemGaps); + } + }; + + return ( +
+
+ {/* Header */} +
+
+ {isRed ? ( + + ) : ( + + )} +

{title}

+
+ +
+ + {/* Body */} +
+ {/* Evidence summary */} +
+

+ {isRed ? "Red" : "Blue"} Team Evidence ({evidences.length}) +

+ {evidences.length > 0 ? ( +
+ {evidences.map((ev) => ( +
+ + {ev.file_name} +
+ ))} +
+ ) : ( +

No evidence files uploaded.

+ )} +
+ + {/* Decision */} +
+

Decision

+
+ + + {!isRed && ( + + )} +
+ {decision === "gap" && ( +

+ Use this when the operator did everything right but Blue Team is missing a + capability (tooling, visibility) needed to detect/block this technique. The test + still proceeds to cross-validation. +

+ )} +
+ + {/* Notes (reopen) */} + {decision === "reopen" && ( +
+ +