feat(exec-dashboard): split threat actors into exposure vs detection strength
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

Replace single list with two-column layout:
- LEFT '⚠ Highest Exposure': top 5 actors by uncovered technique count,
  red border, text explaining 'these attacks would go unnoticed today'
- RIGHT ' Strongest Detection': top 5 actors by coverage %, green border,
  text explaining 'Blue Team would likely detect an intrusion from these'

Shows both the risks (where to focus testing) and the strengths
(what's already well protected) to give executives a balanced view.
This commit is contained in:
kitos
2026-06-03 10:01:22 +02:00
parent b33562a34e
commit 9e36b683fa

View File

@@ -336,53 +336,73 @@ export default function ExecutiveDashboardPage() {
</div>
</div>
{/* Section 3: Top Threat Actors */}
<div className="rounded-xl border border-gray-800 bg-gray-900 p-4">
<h2 className="mb-1 text-sm font-semibold text-gray-300 flex items-center gap-1">
Top Threat Actors
<MetricTooltip title="Top Threat Actors" description="The 5 most dangerous known adversary groups ranked by how many of their attack techniques we cannot currently detect. Higher uncovered count = greater exposure to that group." context="These are real APT groups (e.g. APT28, FIN7) with documented attack playbooks. Focus testing on their techniques." />
</h2>
<p className="mb-3 text-[10px] text-gray-500">
Ranked by uncovered techniques (most exposure first)
</p>
<div className="space-y-2">
{[...(threatActors?.items || [])]
// Sort by uncovered technique count DESC (= technique_count × gap%)
// Tiebreak: higher technique_count = broader attack surface
{/* Section 3: Threat Actor Exposure vs Detection Strength */}
{(() => {
const allActors = [...(threatActors?.items || [])].filter(
(a) => a.technique_count > 0,
);
// Most vulnerable: highest uncovered technique count
const mostVulnerable = [...allActors]
.sort((a, b) => {
const uncoveredA = a.technique_count * (1 - a.coverage_pct / 100);
const uncoveredB = b.technique_count * (1 - b.coverage_pct / 100);
if (uncoveredB !== uncoveredA) return uncoveredB - uncoveredA;
return b.technique_count - a.technique_count;
const ua = a.technique_count * (1 - a.coverage_pct / 100);
const ub = b.technique_count * (1 - b.coverage_pct / 100);
return ub !== ua ? ub - ua : b.technique_count - a.technique_count;
})
.slice(0, 5)
.map((actor, idx) => (
.slice(0, 5);
// Best covered: highest coverage_pct with ≥1 technique
const bestCovered = [...allActors]
.filter((a) => a.coverage_pct > 0)
.sort((a, b) =>
b.coverage_pct !== a.coverage_pct
? b.coverage_pct - a.coverage_pct
: b.technique_count - a.technique_count,
)
.slice(0, 5);
const ActorRow = ({
actor,
idx,
side,
}: {
actor: (typeof allActors)[0];
idx: number;
side: "risk" | "strength";
}) => {
const uncovered = Math.round(
actor.technique_count * (1 - actor.coverage_pct / 100),
);
return (
<div
key={actor.id}
className="flex items-center gap-3 rounded-lg bg-gray-800/50 p-3 cursor-pointer hover:bg-gray-800"
className="flex items-center gap-3 rounded-lg bg-gray-800/50 p-2.5 cursor-pointer hover:bg-gray-800 transition-colors"
onClick={() => navigate(`/threat-actors/${actor.id}`)}
>
{/* Rank badge */}
<div className={`flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-[10px] font-bold ${
idx === 0 ? "bg-red-500/20 text-red-400"
: idx === 1 ? "bg-orange-500/20 text-orange-400"
: idx === 2 ? "bg-yellow-500/20 text-yellow-400"
: "bg-gray-700 text-gray-400"
}`}>
<div
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-[10px] font-bold ${
side === "risk"
? idx === 0
? "bg-red-500/20 text-red-400"
: idx === 1
? "bg-orange-500/20 text-orange-400"
: "bg-yellow-500/20 text-yellow-400"
: "bg-green-500/20 text-green-400"
}`}
>
{idx + 1}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-white truncate">
<p className="text-xs font-medium text-white truncate">
{actor.name}
</p>
<p className="text-[10px] text-gray-500 truncate">
{Math.round(actor.technique_count * (1 - actor.coverage_pct / 100))} uncovered
{" / "}
{actor.technique_count} techniques
{side === "risk"
? `${uncovered} uncovered / ${actor.technique_count} techniques`
: `${actor.technique_count} techniques covered`}
</p>
</div>
<div className="flex items-center gap-2">
<div className="w-20 h-1.5 rounded-full bg-gray-700 overflow-hidden">
<div className="flex items-center gap-1.5 shrink-0">
<div className="w-16 h-1.5 rounded-full bg-gray-700 overflow-hidden">
<div
className="h-full rounded-full transition-all"
style={{
@@ -396,15 +416,67 @@ export default function ExecutiveDashboardPage() {
}}
/>
</div>
<span className="w-10 text-right text-xs font-medium text-gray-300 tabular-nums">
<span className="w-8 text-right text-[10px] font-medium text-gray-300 tabular-nums">
{actor.coverage_pct.toFixed(0)}%
</span>
</div>
</div>
);
};
return (
<div className="grid gap-4 lg:grid-cols-2">
{/* Left: highest exposure */}
<div className="rounded-xl border border-red-900/30 bg-gray-900 p-4">
<h2 className="mb-1 text-sm font-semibold text-red-400 flex items-center gap-1">
Highest Exposure
<MetricTooltip
title="Highest Exposure Threat Actors"
description="Adversary groups whose attack techniques we are LEAST prepared to detect. We have the most uncovered techniques for these groups."
context="We are most vulnerable to these groups. Prioritise testing their TTPs urgently."
/>
</h2>
<p className="mb-3 text-[10px] text-gray-500 leading-snug">
These threat actors use techniques we <span className="text-red-400 font-medium">cannot currently detect</span>.
If they targeted us today, most of their attacks would go unnoticed.
</p>
<div className="space-y-1.5">
{mostVulnerable.map((actor, idx) => (
<ActorRow key={actor.id} actor={actor} idx={idx} side="risk" />
))}
{mostVulnerable.length === 0 && (
<p className="py-4 text-center text-xs text-gray-500">No data available</p>
)}
</div>
</div>
{/* Right: best detection capability */}
<div className="rounded-xl border border-green-900/30 bg-gray-900 p-4">
<h2 className="mb-1 text-sm font-semibold text-green-400 flex items-center gap-1">
Strongest Detection
<MetricTooltip
title="Strongest Detection Capability"
description="Adversary groups whose attack techniques we are BEST prepared to detect. High coverage means our Blue Team can identify most of their TTPs."
context="These are our strengths. If these groups attacked us, we would likely detect them."
/>
</h2>
<p className="mb-3 text-[10px] text-gray-500 leading-snug">
For these threat actors we have <span className="text-green-400 font-medium">strong detection coverage</span>.
Their techniques are well tested and our Blue Team would likely identify an intrusion.
</p>
<div className="space-y-1.5">
{bestCovered.map((actor, idx) => (
<ActorRow key={actor.id} actor={actor} idx={idx} side="strength" />
))}
{bestCovered.length === 0 && (
<p className="py-4 text-center text-xs text-gray-500">No coverage data yet run tests to populate</p>
)}
</div>
</div>
</div>
);
})()}
{/* Section 4: Operational KPIs */}
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
<KPICard