Files
Aegis/frontend/src/components/AttackMatrix.tsx
Kitos cb447f3803 feat: Phase 8 - Frontend main views (T-026 to T-031)
Implement all main frontend views for the MITRE ATT&CK coverage platform:

- T-026: Dashboard with coverage summary cards and tactic breakdown table

- T-027: Interactive ATT&CK matrix with filtering by status, tactic, platform

- T-028: Technique detail page with tests, intel items, and review actions

- T-029: Test creation form with technique selector and validation

- T-030: Test detail page with drag and drop evidence upload and download

- T-031: System admin panel with MITRE sync and intel scan controls

New components: CoverageSummaryCard, TacticCoverageChart, AttackMatrix, TechniqueCell, TestForm, EvidenceUpload, EvidenceList

New API modules: metrics.ts, techniques.ts, tests.ts, evidence.ts, system.ts

All views use TanStack Query for data fetching with proper loading and error states. Role-based UI controls for admin/lead actions.
2026-02-06 16:21:14 +01:00

113 lines
3.4 KiB
TypeScript

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<string, TechniqueSummary[]> = {};
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 (
<div className="rounded-xl border border-gray-800 bg-gray-900 p-8 text-center">
<p className="text-gray-400">No techniques found matching your filters</p>
</div>
);
}
return (
<div className="overflow-x-auto rounded-xl border border-gray-800 bg-gray-900">
<div className="min-w-max p-4">
<div className="flex gap-3">
{orderedTactics.map((tactic) => (
<div key={tactic} className="w-48 flex-shrink-0">
{/* Tactic header */}
<div className="mb-3 rounded-lg bg-gray-800 px-3 py-2">
<h3 className="text-center text-sm font-semibold text-cyan-400">
{formatTacticName(tactic)}
</h3>
<p className="mt-0.5 text-center text-xs text-gray-500">
{groupedByTactic[tactic]?.length || 0} techniques
</p>
</div>
{/* Technique cells */}
<div className="space-y-2">
{groupedByTactic[tactic]?.map((tech) => (
<TechniqueCell
key={`${tactic}-${tech.mitre_id}`}
mitreId={tech.mitre_id}
name={tech.name}
status={tech.status_global}
reviewRequired={tech.review_required}
/>
))}
</div>
</div>
))}
</div>
</div>
</div>
);
}