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:
109
frontend/src/components/heatmap/HeatmapTooltip.tsx
Normal file
109
frontend/src/components/heatmap/HeatmapTooltip.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user