From ba75baeb7d675cb7c08ace6fb3e4517bd9c0ef23 Mon Sep 17 00:00:00 2001 From: kitos Date: Tue, 2 Jun 2026 10:19:57 +0200 Subject: [PATCH] fix(exec-dashboard): sort Top Threat Actors by uncovered techniques MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- frontend/src/pages/ExecutiveDashboardPage.tsx | 40 ++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/frontend/src/pages/ExecutiveDashboardPage.tsx b/frontend/src/pages/ExecutiveDashboardPage.tsx index 5f67d34..6504eb1 100644 --- a/frontend/src/pages/ExecutiveDashboardPage.tsx +++ b/frontend/src/pages/ExecutiveDashboardPage.tsx @@ -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 */}
-

+

Top Threat Actors

+

+ Ranked by uncovered techniques (most exposure first) +

- {(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) => (
navigate(`/threat-actors/${actor.id}`)} > -
- {actor.country?.slice(0, 2).toUpperCase() || "??"} + {/* Rank badge */} +
+ {idx + 1}

{actor.name}

- {actor.target_sectors?.slice(0, 3).join(", ")} + {Math.round(actor.technique_count * (1 - actor.coverage_pct / 100))} uncovered + {" / "} + {actor.technique_count} techniques

-
+
- - {actor.coverage_pct}% + + {actor.coverage_pct.toFixed(0)}%