+
+
+ Filters:
+
+
+ {/* Platform checkboxes */}
+
+ {PLATFORMS.map((platform) => (
+
+ ))}
+
+
+ {/* Tactic multi-select */}
+
+
+
+
+ {/* Selected tactic pills */}
+ {selectedTactics.length > 0 && (
+
+ {selectedTactics.map((tactic) => (
+
+ ))}
+
+ )}
+
+ {/* Min score slider */}
+
+ Min Score:
+ onMinScoreChange(parseInt(e.target.value, 10))}
+ className="h-1 w-20 cursor-pointer accent-cyan-500"
+ />
+
+ {minScore}
+
+
+
+ {/* Clear all */}
+ {hasActiveFilters && (
+
+ )}
+
+ );
+}
diff --git a/frontend/src/components/heatmap/HeatmapLayerSelector.tsx b/frontend/src/components/heatmap/HeatmapLayerSelector.tsx
new file mode 100644
index 0000000..ff0deb9
--- /dev/null
+++ b/frontend/src/components/heatmap/HeatmapLayerSelector.tsx
@@ -0,0 +1,120 @@
+import { useState, useEffect } from "react";
+import { useQuery } from "@tanstack/react-query";
+import { Shield, User, Search, ClipboardList } from "lucide-react";
+import { getThreatActors, type ThreatActorSummary } from "../../api/threat-actors";
+import { listCampaigns, type CampaignSummary } from "../../api/campaigns";
+
+export type LayerType = "coverage" | "threat-actor" | "detection-rules" | "campaign";
+
+interface HeatmapLayerSelectorProps {
+ activeLayer: LayerType;
+ onLayerChange: (layer: LayerType) => void;
+ selectedActorId: string | null;
+ onActorChange: (actorId: string | null) => void;
+ selectedCampaignId: string | null;
+ onCampaignChange: (campaignId: string | null) => void;
+}
+
+const LAYERS: {
+ id: LayerType;
+ label: string;
+ icon: React.FC<{ className?: string }>;
+}[] = [
+ { id: "coverage", label: "Coverage", icon: Shield },
+ { id: "threat-actor", label: "Threat Actor", icon: User },
+ { id: "detection-rules", label: "Detection Rules", icon: Search },
+ { id: "campaign", label: "Campaign", icon: ClipboardList },
+];
+
+export default function HeatmapLayerSelector({
+ activeLayer,
+ onLayerChange,
+ selectedActorId,
+ onActorChange,
+ selectedCampaignId,
+ onCampaignChange,
+}: HeatmapLayerSelectorProps) {
+ // Fetch actors for dropdown
+ const { data: actorsData } = useQuery({
+ queryKey: ["threat-actors-selector"],
+ queryFn: () => getThreatActors({ limit: 200 }),
+ enabled: activeLayer === "threat-actor",
+ });
+
+ // Fetch campaigns for dropdown
+ const { data: campaignsData } = useQuery({
+ queryKey: ["campaigns-selector"],
+ queryFn: () => listCampaigns({ limit: 200 }),
+ enabled: activeLayer === "campaign",
+ });
+
+ const actors: ThreatActorSummary[] = actorsData?.items || [];
+ const campaigns: CampaignSummary[] = campaignsData?.items || [];
+
+ // Auto-select first actor/campaign if none selected
+ useEffect(() => {
+ if (activeLayer === "threat-actor" && !selectedActorId && actors.length > 0) {
+ onActorChange(actors[0].id);
+ }
+ }, [activeLayer, actors, selectedActorId, onActorChange]);
+
+ useEffect(() => {
+ if (activeLayer === "campaign" && !selectedCampaignId && campaigns.length > 0) {
+ onCampaignChange(campaigns[0].id);
+ }
+ }, [activeLayer, campaigns, selectedCampaignId, onCampaignChange]);
+
+ return (
+
+
{legend.label}:
+
+ {/* Gradient bar */}
+
+
c.color).join(", ")})`,
+ }}
+ />
+
+
+ {/* Individual labels */}
+ {legend.colors.map((item) => (
+
+ ))}
+
+ );
+}
diff --git a/frontend/src/components/heatmap/HeatmapTooltip.tsx b/frontend/src/components/heatmap/HeatmapTooltip.tsx
new file mode 100644
index 0000000..e6d0770
--- /dev/null
+++ b/frontend/src/components/heatmap/HeatmapTooltip.tsx
@@ -0,0 +1,109 @@
+import type { HeatmapTechnique } from "../../api/heatmap";
+
+interface HeatmapTooltipProps {
+ technique: HeatmapTechnique;
+}
+
+export default function HeatmapTooltip({ technique }: HeatmapTooltipProps) {
+ const getMeta = (name: string): string | null => {
+ const item = technique.metadata.find((m) => m.name === name);
+ return item?.value ?? null;
+ };
+
+ const testsCount = getMeta("tests_count");
+ const detectionRules = getMeta("detection_rules");
+ const totalRules = getMeta("total_rules");
+ const evaluatedRules = getMeta("evaluated_rules");
+ const lastValidated = getMeta("last_validated");
+ const campaignTests = getMeta("campaign_tests");
+
+ // Determine status label from score
+ const getStatusLabel = (score: number): { label: string; color: string } => {
+ if (score >= 100) return { label: "Validated", color: "text-green-400" };
+ if (score >= 60) return { label: "Partial", color: "text-yellow-400" };
+ if (score >= 30) return { label: "In Progress", color: "text-blue-400" };
+ if (score > 0) return { label: "Not Covered", color: "text-red-400" };
+ return { label: "Not Evaluated", color: "text-gray-400" };
+ };
+
+ const status = getStatusLabel(technique.score);
+
+ return (
+
+ {/* Header */}
+
+
+ {technique.techniqueID}
+
+ {technique.tactic && (
+
+ {technique.tactic.replace(/-/g, " ")}
+
+ )}
+
+
+ {/* Status & Score */}
+
+
+ Status:
+ {status.label}
+
+
+ Score:
+ {technique.score}/100
+
+
+ {/* Score bar */}
+
+
+ {testsCount !== null && (
+
+ Tests:
+ {testsCount} validated
+
+ )}
+ {detectionRules !== null && (
+
+ Detection Rules:
+ {detectionRules} available
+
+ )}
+ {totalRules !== null && (
+
+ Rules:
+
+ {evaluatedRules || 0} evaluated / {totalRules} total
+
+
+ )}
+ {campaignTests !== null && (
+
+ Campaign Tests:
+ {campaignTests}
+
+ )}
+ {lastValidated && (
+
+ Last validated:
+ {lastValidated}
+
+ )}
+
+
+ {/* Comment */}
+ {technique.comment && (
+
+ {technique.comment}
+
+ )}
+
+ );
+}
diff --git a/frontend/src/pages/MatrixPage.tsx b/frontend/src/pages/MatrixPage.tsx
index 6457b4d..aaf1178 100644
--- a/frontend/src/pages/MatrixPage.tsx
+++ b/frontend/src/pages/MatrixPage.tsx
@@ -1,197 +1,287 @@
-import { useState, useMemo } from "react";
+import { useState, useMemo, useCallback } from "react";
import { useQuery } from "@tanstack/react-query";
-import { Loader2, AlertCircle, Filter, X } from "lucide-react";
-import { getTechniques, type TechniqueSummary } from "../api/techniques";
-import AttackMatrix from "../components/AttackMatrix";
-import type { TechniqueStatus } from "../types/models";
+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 STATUS_OPTIONS: { value: TechniqueStatus | "all"; label: string; color: string }[] = [
- { value: "all", label: "All Statuses", color: "text-gray-400" },
- { value: "validated", label: "Validated", color: "text-green-400" },
- { value: "partial", label: "Partial", color: "text-yellow-400" },
- { value: "in_progress", label: "In Progress", color: "text-blue-400" },
- { value: "not_covered", label: "Not Covered", color: "text-red-400" },
- { value: "not_evaluated", label: "Not Evaluated", color: "text-gray-400" },
+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 PLATFORM_OPTIONS = ["all", "windows", "linux", "macos", "cloud", "network"] as const;
+type ZoomLevel = "compact" | "normal" | "expanded";
export default function MatrixPage() {
- const [statusFilter, setStatusFilter] = useState
("all");
- const [platformFilter, setPlatformFilter] = useState("all");
- const [tacticFilter, setTacticFilter] = useState("all");
+ 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: techniques,
+ data: layerData,
isLoading,
error,
- } = useQuery({
- queryKey: ["techniques"],
- queryFn: () => getTechniques(),
+ } = 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),
});
- // Extract unique tactics from techniques
- const availableTactics = useMemo(() => {
- if (!techniques) return [];
- const tactics = new Set();
- for (const tech of techniques) {
- if (tech.tactic) {
- tech.tactic.split(",").forEach((t) => tactics.add(t.trim().toLowerCase()));
+ 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);
}
- return Array.from(tactics).sort();
- }, [techniques]);
-
- // Apply filters
- const filteredTechniques = useMemo(() => {
- if (!techniques) return [];
-
- return techniques.filter((tech: TechniqueSummary) => {
- // Status filter
- if (statusFilter !== "all" && tech.status_global !== statusFilter) {
- return false;
- }
-
- // Tactic filter
- if (tacticFilter !== "all") {
- const techTactics = tech.tactic?.split(",").map((t) => t.trim().toLowerCase()) || [];
- if (!techTactics.includes(tacticFilter)) {
- return false;
- }
- }
-
- // Platform filter is handled client-side since we don't have platform in summary
- // For now we show all - platform filtering would need the full technique data
-
- return true;
- });
- }, [techniques, statusFilter, tacticFilter]);
-
- const hasActiveFilters = statusFilter !== "all" || tacticFilter !== "all" || platformFilter !== "all";
-
- const clearFilters = () => {
- setStatusFilter("all");
- setPlatformFilter("all");
- setTacticFilter("all");
};
- if (isLoading) {
- return (
-
-
-
- );
- }
+ // Zoom controls
+ const zoomIn = () => {
+ if (zoom === "compact") setZoom("normal");
+ else if (zoom === "normal") setZoom("expanded");
+ };
- if (error) {
- return (
-
-
-
Failed to load techniques
-
- );
- }
+ const zoomOut = () => {
+ if (zoom === "expanded") setZoom("normal");
+ else if (zoom === "normal") setZoom("compact");
+ };
return (
-
+
{/* Header */}
ATT&CK Matrix
- Interactive MITRE ATT&CK coverage matrix — click any technique for details
+ Advanced heatmap with multiple layers, filters, and ATT&CK Navigator export
- {/* Filters */}
-
-
-
-
Filters:
+ {/* Toolbar: Layer Selector + Filters + Export + Zoom */}
+
+
+ {/* Layer selector */}
+
+
+ {/* Right side: Export + Zoom */}
+
+ {/* Export dropdown */}
+
+
+ {showExportMenu && (
+
+
+
+
+ )}
+
+
+ {/* Zoom controls */}
+
+
+
+ {zoom}
+
+
+
+
- {/* Status filter */}
-
-
- {/* Tactic filter */}
-
-
- {/* Platform filter */}
-
-
- {hasActiveFilters && (
-
- )}
-
-
- Showing {filteredTechniques.length} of {techniques?.length || 0} techniques
-
+ {/* Filters */}
+
- {/* Matrix */}
-
+ {/* 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 */}
-
-
Legend:
- {STATUS_OPTIONS.filter((s) => s.value !== "all").map((status) => (
-
- ))}
-
+
);
}