fix(exec-dashboard): sort Top Threat Actors by uncovered techniques
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Previously: alphabetical order (first 5 actors from list_actors query). Now: ranked by uncovered technique count = technique_count × (1 - coverage_pct/100). Tiebreak: higher technique_count first (broader attack surface). Fetches 100 actors, sorts client-side, shows top 5 with: - Rank badge (1-5) colored red/orange/yellow/gray - 'N uncovered / M techniques' subtitle instead of target sectors - Coverage bar + percentage This ensures the actors with the largest coverage gap appear first.
This commit is contained in:
@@ -155,9 +155,10 @@ export default function ExecutiveDashboardPage() {
|
||||
queryFn: getCoverageByTactic,
|
||||
});
|
||||
|
||||
// Fetch enough actors to rank properly — sort client-side by uncovered techniques
|
||||
const { data: threatActors } = useQuery({
|
||||
queryKey: ["threat-actors-top"],
|
||||
queryFn: () => getThreatActors({ limit: 5 }),
|
||||
queryFn: () => getThreatActors({ limit: 100 }),
|
||||
});
|
||||
|
||||
const { data: allTechniques } = useQuery({
|
||||
@@ -322,29 +323,50 @@ export default function ExecutiveDashboardPage() {
|
||||
|
||||
{/* Section 3: Top Threat Actors */}
|
||||
<div className="rounded-xl border border-gray-800 bg-gray-900 p-4">
|
||||
<h2 className="mb-3 text-sm font-semibold text-gray-300">
|
||||
<h2 className="mb-1 text-sm font-semibold text-gray-300">
|
||||
Top Threat Actors
|
||||
</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 || []).map((actor) => (
|
||||
{[...(threatActors?.items || [])]
|
||||
// Sort by uncovered technique count DESC (= technique_count × gap%)
|
||||
// Tiebreak: higher technique_count = broader attack surface
|
||||
.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;
|
||||
})
|
||||
.slice(0, 5)
|
||||
.map((actor, idx) => (
|
||||
<div
|
||||
key={actor.id}
|
||||
className="flex items-center gap-3 rounded-lg bg-gray-800/50 p-3 cursor-pointer hover:bg-gray-800"
|
||||
onClick={() => navigate(`/threat-actors/${actor.id}`)}
|
||||
>
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-700 text-xs font-bold text-gray-300">
|
||||
{actor.country?.slice(0, 2).toUpperCase() || "??"}
|
||||
{/* 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"
|
||||
}`}>
|
||||
{idx + 1}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-white truncate">
|
||||
{actor.name}
|
||||
</p>
|
||||
<p className="text-[10px] text-gray-500 truncate">
|
||||
{actor.target_sectors?.slice(0, 3).join(", ")}
|
||||
{Math.round(actor.technique_count * (1 - actor.coverage_pct / 100))} uncovered
|
||||
{" / "}
|
||||
{actor.technique_count} techniques
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-24 h-2 rounded-full bg-gray-700 overflow-hidden">
|
||||
<div className="w-20 h-1.5 rounded-full bg-gray-700 overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all"
|
||||
style={{
|
||||
@@ -358,8 +380,8 @@ export default function ExecutiveDashboardPage() {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="w-10 text-right text-xs font-medium text-gray-300">
|
||||
{actor.coverage_pct}%
|
||||
<span className="w-10 text-right text-xs font-medium text-gray-300 tabular-nums">
|
||||
{actor.coverage_pct.toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user