import { useState, useMemo, useCallback } from "react"; import { useQuery } from "@tanstack/react-query"; import { useNavigate } from "react-router-dom"; import { Loader2, AlertCircle, Download, ZoomIn, ZoomOut } from "lucide-react"; import { getHeatmapCoverage, getHeatmapThreatActor, getHeatmapDetectionRules, getHeatmapCampaign, exportNavigatorJSON, type HeatmapLayer, type HeatmapFilters as HeatmapFilterParams, } from "../api/heatmap"; import AdvancedHeatmap from "../components/heatmap/AdvancedHeatmap"; import HeatmapLayerSelector, { type LayerType, } from "../components/heatmap/HeatmapLayerSelector"; import HeatmapFiltersComponent from "../components/heatmap/HeatmapFilters"; import HeatmapLegend from "../components/heatmap/HeatmapLegend"; 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", ]; type ZoomLevel = "compact" | "normal" | "expanded"; export default function MatrixPage() { const navigate = useNavigate(); // Layer selection state const [activeLayer, setActiveLayer] = useState("coverage"); const [selectedActorId, setSelectedActorId] = useState(null); const [selectedCampaignId, setSelectedCampaignId] = useState(null); // Filter state const [platforms, setPlatforms] = useState([]); const [selectedTactics, setSelectedTactics] = useState([]); const [minScore, setMinScore] = useState(0); // Zoom const [zoom, setZoom] = useState("normal"); // Export dropdown const [showExportMenu, setShowExportMenu] = useState(false); // Build filter params const filterParams: HeatmapFilterParams = useMemo( () => ({ platforms: platforms.length > 0 ? platforms.join(",") : undefined, tactics: selectedTactics.length > 0 ? selectedTactics.join(",") : undefined, min_score: minScore > 0 ? minScore : undefined, }), [platforms, selectedTactics, minScore], ); // Build query key based on active layer + selection const queryKey = useMemo(() => { const base = ["heatmap", activeLayer, filterParams]; if (activeLayer === "threat-actor") return [...base, selectedActorId]; if (activeLayer === "campaign") return [...base, selectedCampaignId]; return base; }, [activeLayer, filterParams, selectedActorId, selectedCampaignId]); // Fetch the active layer data const { data: layerData, isLoading, error, } = useQuery({ queryKey, queryFn: () => { switch (activeLayer) { case "coverage": return getHeatmapCoverage(filterParams); case "threat-actor": if (!selectedActorId) return Promise.resolve({ name: "", versions: { attack: "", navigator: "", layer: "" }, domain: "", description: "", filters: { platforms: [] }, gradient: { colors: [], minValue: 0, maxValue: 0 }, techniques: [] } as HeatmapLayer); return getHeatmapThreatActor(selectedActorId, filterParams); case "detection-rules": return getHeatmapDetectionRules(filterParams); case "campaign": if (!selectedCampaignId) return Promise.resolve({ name: "", versions: { attack: "", navigator: "", layer: "" }, domain: "", description: "", filters: { platforms: [] }, gradient: { colors: [], minValue: 0, maxValue: 0 }, techniques: [] } as HeatmapLayer); return getHeatmapCampaign(selectedCampaignId, filterParams); default: return getHeatmapCoverage(filterParams); } }, enabled: activeLayer === "coverage" || activeLayer === "detection-rules" || (activeLayer === "threat-actor" && !!selectedActorId) || (activeLayer === "campaign" && !!selectedCampaignId), }); const techniques = layerData?.techniques || []; // Handle cell click - navigate to technique detail const handleCellClick = useCallback( (techniqueId: string) => { navigate(`/techniques/${techniqueId}`); }, [navigate], ); // Handle export const handleExport = async (type: "download" | "url") => { setShowExportMenu(false); const layerId = activeLayer === "threat-actor" ? selectedActorId ?? undefined : activeLayer === "campaign" ? selectedCampaignId ?? undefined : undefined; if (type === "download") { try { const blob = await exportNavigatorJSON(activeLayer, layerId, filterParams); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `aegis_${activeLayer}_layer.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch { console.error("Failed to export Navigator JSON"); } } else { // Copy Navigator URL const navigatorUrl = `https://mitre-attack.github.io/attack-navigator/#layerURL=${encodeURIComponent( window.location.origin + `/api/v1/heatmap/export-navigator?layer=${activeLayer}${layerId ? `&layer_id=${layerId}` : ""}` )}`; navigator.clipboard.writeText(navigatorUrl); } }; // Zoom controls const zoomIn = () => { if (zoom === "compact") setZoom("normal"); else if (zoom === "normal") setZoom("expanded"); }; const zoomOut = () => { if (zoom === "expanded") setZoom("normal"); else if (zoom === "normal") setZoom("compact"); }; return (
{/* Header */}

ATT&CK Matrix

Advanced heatmap with multiple layers, filters, and ATT&CK Navigator export

{/* Toolbar: Layer Selector + Filters + Export + Zoom */}
{/* Layer selector */} {/* Right side: Export + Zoom */}
{/* Export dropdown */}
{showExportMenu && (
)}
{/* Zoom controls */}
{zoom}
{/* Filters */}
{/* Stats bar */} {layerData && (
Layer: {layerData.name} Techniques:{" "} {techniques.filter((t) => t.enabled).length} active {" "} / {techniques.length} total
)} {/* Loading / Error / Heatmap */} {isLoading ? (
) : error ? (

Failed to load heatmap data

) : ( )} {/* Legend */}
); }