import { useMemo } from "react"; import TechniqueCell from "./TechniqueCell"; import type { TechniqueSummary } from "../api/techniques"; interface AttackMatrixProps { techniques: TechniqueSummary[]; } // MITRE ATT&CK Enterprise Tactics in order const TACTIC_ORDER = [ "reconnaissance", "resource-development", "initial-access", "execution", "persistence", "privilege-escalation", "defense-evasion", "credential-access", "discovery", "lateral-movement", "collection", "command-and-control", "exfiltration", "impact", ]; const formatTacticName = (tactic: string): string => { return tactic .split("-") .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(" "); }; export default function AttackMatrix({ techniques }: AttackMatrixProps) { // Group techniques by tactic const groupedByTactic = useMemo(() => { const groups: Record = {}; for (const tech of techniques) { // A technique can belong to multiple tactics (comma-separated) const tactics = tech.tactic ? tech.tactic.split(",").map((t) => t.trim().toLowerCase()) : ["unknown"]; for (const tactic of tactics) { if (!groups[tactic]) { groups[tactic] = []; } groups[tactic].push(tech); } } // Sort techniques within each tactic by mitre_id for (const tactic of Object.keys(groups)) { groups[tactic].sort((a, b) => a.mitre_id.localeCompare(b.mitre_id)); } return groups; }, [techniques]); // Get ordered tactics that have techniques const orderedTactics = useMemo(() => { const tacticSet = new Set(Object.keys(groupedByTactic)); const ordered = TACTIC_ORDER.filter((t) => tacticSet.has(t)); // Add any unknown tactics at the end const remaining = Array.from(tacticSet).filter((t) => !TACTIC_ORDER.includes(t)); return [...ordered, ...remaining]; }, [groupedByTactic]); if (techniques.length === 0) { return (

No techniques found matching your filters

); } return (
{orderedTactics.map((tactic) => (
{/* Tactic header */}

{formatTacticName(tactic)}

{groupedByTactic[tactic]?.length || 0} techniques

{/* Technique cells */}
{groupedByTactic[tactic]?.map((tech) => ( ))}
))}
); }