feat(phase-27): add advanced ATT&CK Navigator-style heatmap with layers, filters and export (T-221 to T-223)

This commit is contained in:
2026-02-09 17:16:59 +01:00
parent 57b47c296d
commit a911ddeb52
14 changed files with 2024 additions and 171 deletions

View File

@@ -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 (
<div className="w-72 rounded-lg border border-gray-700 bg-gray-900 p-3 shadow-xl">
{/* Header */}
<div className="mb-2 border-b border-gray-800 pb-2">
<p className="font-mono text-sm font-bold text-white">
{technique.techniqueID}
</p>
{technique.tactic && (
<p className="mt-0.5 text-[10px] uppercase tracking-wider text-gray-500">
{technique.tactic.replace(/-/g, " ")}
</p>
)}
</div>
{/* Status & Score */}
<div className="space-y-1.5 text-xs">
<div className="flex items-center justify-between">
<span className="text-gray-400">Status:</span>
<span className={`font-medium ${status.color}`}>{status.label}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-400">Score:</span>
<span className="font-medium text-white">{technique.score}/100</span>
</div>
{/* Score bar */}
<div className="h-1.5 w-full overflow-hidden rounded-full bg-gray-800">
<div
className="h-full rounded-full transition-all"
style={{
width: `${technique.score}%`,
backgroundColor: technique.color || "#666",
}}
/>
</div>
{testsCount !== null && (
<div className="flex items-center justify-between">
<span className="text-gray-400">Tests:</span>
<span className="text-gray-200">{testsCount} validated</span>
</div>
)}
{detectionRules !== null && (
<div className="flex items-center justify-between">
<span className="text-gray-400">Detection Rules:</span>
<span className="text-gray-200">{detectionRules} available</span>
</div>
)}
{totalRules !== null && (
<div className="flex items-center justify-between">
<span className="text-gray-400">Rules:</span>
<span className="text-gray-200">
{evaluatedRules || 0} evaluated / {totalRules} total
</span>
</div>
)}
{campaignTests !== null && (
<div className="flex items-center justify-between">
<span className="text-gray-400">Campaign Tests:</span>
<span className="text-gray-200">{campaignTests}</span>
</div>
)}
{lastValidated && (
<div className="flex items-center justify-between">
<span className="text-gray-400">Last validated:</span>
<span className="text-gray-200">{lastValidated}</span>
</div>
)}
</div>
{/* Comment */}
{technique.comment && (
<p className="mt-2 border-t border-gray-800 pt-2 text-[10px] leading-relaxed text-gray-500">
{technique.comment}
</p>
)}
</div>
);
}