feat(threat-actors): hover tooltip on motivation badges
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
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:
103
frontend/src/components/MotivationBadge.tsx
Normal file
103
frontend/src/components/MotivationBadge.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import MarkdownText from "../components/MarkdownText";
|
||||
import MotivationBadge from "../components/MotivationBadge";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Loader2,
|
||||
@@ -218,10 +219,7 @@ export default function ThreatActorDetailPage() {
|
||||
</span>
|
||||
)}
|
||||
{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">
|
||||
<Target className="h-3.5 w-3.5" />
|
||||
{actor.motivation}
|
||||
</span>
|
||||
<MotivationBadge motivation={actor.motivation} />
|
||||
)}
|
||||
{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">
|
||||
|
||||
@@ -35,20 +35,7 @@ function coverageBg(pct: number) {
|
||||
}
|
||||
|
||||
/** Motivation badge colour. */
|
||||
function motivationColor(m: string | null) {
|
||||
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";
|
||||
}
|
||||
}
|
||||
import MotivationBadge from "../components/MotivationBadge";
|
||||
|
||||
export default function ThreatActorsPage() {
|
||||
const navigate = useNavigate();
|
||||
@@ -161,9 +148,7 @@ export default function ThreatActorsPage() {
|
||||
</span>
|
||||
)}
|
||||
{actor.motivation && (
|
||||
<span className={`inline-flex rounded-full border px-2 py-0.5 text-[11px] font-medium ${motivationColor(actor.motivation)}`}>
|
||||
{actor.motivation}
|
||||
</span>
|
||||
<MotivationBadge motivation={actor.motivation} size="sm" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user