110 lines
4.0 KiB
TypeScript
110 lines
4.0 KiB
TypeScript
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>
|
|
);
|
|
}
|