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 { 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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user