feat(threat-actors): hover tooltip on motivation badges
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

New MotivationBadge component with CSS tooltip showing:
- espionage: goal (intelligence theft), typical behavior, examples
- financial: goal (monetary), typical behavior, examples
- destruction: goal (disrupt/destroy infra), wiper/ICS attacks, examples
- hacktivism: goal (political/ideological), defacement/leaks, examples

Used in ThreatActorsPage (card list) and ThreatActorDetailPage (header).
This commit is contained in:
kitos
2026-06-02 10:50:37 +02:00
parent 61e705ece4
commit a518c06653
3 changed files with 107 additions and 21 deletions

View File

@@ -0,0 +1,103 @@
/**
* MotivationBadge — renders a threat actor motivation pill with a
* CSS-only hover tooltip explaining what the motivation means.
*/
const COLORS: Record<string, string> = {
espionage: "border-purple-500/30 bg-purple-900/50 text-purple-400",
financial: "border-yellow-500/30 bg-yellow-900/50 text-yellow-400",
destruction: "border-red-500/30 bg-red-900/50 text-red-400",
hacktivism: "border-cyan-500/30 bg-cyan-900/50 text-cyan-400",
};
const DEFAULT_COLOR = "border-gray-600/30 bg-gray-800/50 text-gray-400";
interface TooltipLine { label: string; text: string }
const TOOLTIPS: Record<string, { heading: string; lines: TooltipLine[] }> = {
espionage: {
heading: "🕵️ Espionage",
lines: [
{ label: "Goal", text: "Steal sensitive data, intellectual property, or government secrets for intelligence purposes." },
{ label: "Typical", text: "Nation-state APTs, long-duration intrusions, low detection priority." },
{ label: "Example", text: "APT28, APT29, Turla, Lazarus Group (intelligence ops)." },
],
},
financial: {
heading: "💰 Financial",
lines: [
{ label: "Goal", text: "Monetary gain through fraud, ransomware, cryptocurrency theft, or banking trojans." },
{ label: "Typical", text: "Opportunistic attacks, ransomware deployment, data exfiltration for sale." },
{ label: "Example", text: "FIN7, Carbanak, Cobalt Group, Wizard Spider." },
],
},
destruction: {
heading: "💥 Destruction",
lines: [
{ label: "Goal", text: "Disrupt or destroy critical infrastructure, systems, or data — often geopolitically motivated." },
{ label: "Typical", text: "Wiper malware, ICS/SCADA attacks, denial of service campaigns." },
{ label: "Example", text: "Sandworm, APT37 (destructive ops), Cleaver." },
],
},
hacktivism: {
heading: "✊ Hacktivism",
lines: [
{ label: "Goal", text: "Promote political or ideological causes through defacement, leaks, or disruptive attacks." },
{ label: "Typical", text: "Website defacement, data leaks, DDoS campaigns, public statements." },
{ label: "Example", text: "Anonymous Sudan, various politically motivated collectives." },
],
},
};
interface MotivationBadgeProps {
motivation: string;
size?: "sm" | "md";
/** Show icon before label */
showIcon?: boolean;
className?: string;
}
export default function MotivationBadge({
motivation,
size = "md",
className = "",
}: MotivationBadgeProps) {
const key = motivation.toLowerCase();
const color = COLORS[key] ?? DEFAULT_COLOR;
const tip = TOOLTIPS[key];
const pill = size === "sm" ? "px-2 py-0.5 text-[10px]" : "px-2 py-0.5 text-[11px]";
return (
<span className={`relative group inline-flex ${className}`}>
{/* Badge */}
<span className={`inline-flex cursor-help items-center rounded-full border font-medium capitalize ${pill} ${color}`}>
{motivation}
</span>
{/* Tooltip — below badge */}
{tip && (
<span
className="
pointer-events-none absolute top-full left-0 z-50 mt-2
w-72
rounded-xl border border-gray-700 bg-gray-900 p-3 shadow-xl
opacity-0 transition-opacity duration-150
group-hover:opacity-100
"
>
{/* Arrow */}
<span className="absolute bottom-full left-4 border-4 border-transparent border-b-gray-700" />
<p className="mb-2 text-xs font-semibold text-white">{tip.heading}</p>
{tip.lines.map(({ label, text }) => (
<div key={label} className="mb-1.5 last:mb-0">
<span className="text-[10px] font-medium uppercase tracking-wide text-gray-500">
{label}
</span>
<p className="text-[11px] leading-snug text-gray-300">{text}</p>
</div>
))}
</span>
)}
</span>
);
}

View File

@@ -1,5 +1,6 @@
import { useParams, useNavigate } from "react-router-dom"; import { useParams, useNavigate } from "react-router-dom";
import MarkdownText from "../components/MarkdownText"; import MarkdownText from "../components/MarkdownText";
import MotivationBadge from "../components/MotivationBadge";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { import {
Loader2, Loader2,
@@ -218,10 +219,7 @@ export default function ThreatActorDetailPage() {
</span> </span>
)} )}
{actor.motivation && ( {actor.motivation && (
<span className="inline-flex items-center gap-1.5 rounded-full border border-purple-500/30 bg-purple-900/50 px-3 py-1 text-xs text-purple-400"> <MotivationBadge motivation={actor.motivation} />
<Target className="h-3.5 w-3.5" />
{actor.motivation}
</span>
)} )}
{actor.sophistication && ( {actor.sophistication && (
<span className="inline-flex items-center gap-1.5 rounded-full border border-cyan-500/30 bg-cyan-900/50 px-3 py-1 text-xs text-cyan-400"> <span className="inline-flex items-center gap-1.5 rounded-full border border-cyan-500/30 bg-cyan-900/50 px-3 py-1 text-xs text-cyan-400">

View File

@@ -35,20 +35,7 @@ function coverageBg(pct: number) {
} }
/** Motivation badge colour. */ /** Motivation badge colour. */
function motivationColor(m: string | null) { import MotivationBadge from "../components/MotivationBadge";
switch (m?.toLowerCase()) {
case "espionage":
return "border-purple-500/30 bg-purple-900/50 text-purple-400";
case "financial":
return "border-yellow-500/30 bg-yellow-900/50 text-yellow-400";
case "destruction":
return "border-red-500/30 bg-red-900/50 text-red-400";
case "hacktivism":
return "border-cyan-500/30 bg-cyan-900/50 text-cyan-400";
default:
return "border-gray-600/30 bg-gray-800/50 text-gray-400";
}
}
export default function ThreatActorsPage() { export default function ThreatActorsPage() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -161,9 +148,7 @@ export default function ThreatActorsPage() {
</span> </span>
)} )}
{actor.motivation && ( {actor.motivation && (
<span className={`inline-flex rounded-full border px-2 py-0.5 text-[11px] font-medium ${motivationColor(actor.motivation)}`}> <MotivationBadge motivation={actor.motivation} size="sm" />
{actor.motivation}
</span>
)} )}
</div> </div>