Files
Aegis/frontend/src/pages/ExecutiveDashboardPage.tsx
T

766 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import {
Loader2,
AlertCircle,
TrendingUp,
TrendingDown,
Minus,
ArrowRight,
RefreshCw,
} from "lucide-react";
import {
LineChart,
Line,
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Cell,
} from "recharts";
import { getOrganizationScore, getScoreHistory } from "../api/scores";
import MetricTooltip from "../components/MetricTooltip";
import {
getOperationalMetrics,
getMetricsByTeam,
} from "../api/operational-metrics";
import { getCoverageByTactic } from "../api/metrics";
import { getThreatActors } from "../api/threat-actors";
import { getTechniques, type TechniqueSummary } from "../api/techniques";
import { getRiskProfiles, computeRiskScores, type RiskProfile } from "../api/risk";
// ── Score Gauge Component ────────────────────────────────────────────
function ScoreGauge({ score, label }: { score: number; label: string }) {
const getColor = (s: number) => {
if (s < 30) return "#ef4444";
if (s < 50) return "#f97316";
if (s < 70) return "#eab308";
return "#22c55e";
};
const color = getColor(score);
const circumference = 2 * Math.PI * 54;
const strokeDasharray = `${(score / 100) * circumference} ${circumference}`;
return (
<div className="flex flex-col items-center">
<div className="relative h-32 w-32">
<svg className="h-32 w-32 -rotate-90" viewBox="0 0 120 120">
<circle
cx="60"
cy="60"
r="54"
fill="none"
stroke="#1f2937"
strokeWidth="8"
/>
<circle
cx="60"
cy="60"
r="54"
fill="none"
stroke={color}
strokeWidth="8"
strokeLinecap="round"
strokeDasharray={strokeDasharray}
className="transition-all duration-1000"
/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="text-3xl font-bold text-white">{Math.round(score)}</span>
<span className="text-[10px] text-gray-500">/ 100</span>
</div>
</div>
<div className="mt-2 flex items-center gap-1">
<span className="text-xs font-medium text-gray-400">{label}</span>
<MetricTooltip
title="Overall Security Score"
description="A composite score (0100) representing the organisation's overall security posture. Calculated from 4 dimensions: Coverage, Detection maturity, Critical technique coverage, and Response readiness."
context="< 30 = Critical gaps | 3050 = High risk | 5070 = Improving | > 70 = Good posture"
position="below"
/>
</div>
</div>
);
}
// ── KPI Card Component ──────────────────────────────────────────────
function KPICard({
label,
value,
unit,
trend,
tooltip,
}: {
label: string;
value: string | number;
unit?: string;
trend?: "improving" | "declining" | "stable" | null;
tooltip?: { description: string; context?: string };
}) {
const TrendIcon =
trend === "improving"
? TrendingUp
: trend === "declining"
? TrendingDown
: Minus;
const trendColor =
trend === "improving"
? "text-green-400"
: trend === "declining"
? "text-red-400"
: "text-gray-500";
return (
<div className="rounded-xl border border-gray-800 bg-gray-900 p-4">
<p className="text-xs text-gray-500 uppercase tracking-wider flex items-center gap-0.5">
{label}
{tooltip && <MetricTooltip title={label} description={tooltip.description} context={tooltip.context} />}
</p>
<div className="mt-2 flex items-end justify-between">
<div>
<span className="text-2xl font-bold text-white">
{value === null || value === undefined ? "N/A" : value}
</span>
{unit && <span className="ml-1 text-sm text-gray-500">{unit}</span>}
</div>
{trend && (
<TrendIcon className={`h-5 w-5 ${trendColor}`} />
)}
</div>
</div>
);
}
// ── Main Component ──────────────────────────────────────────────────
export default function ExecutiveDashboardPage() {
const navigate = useNavigate();
const { data: orgScore, isLoading: loadingScore } = useQuery({
queryKey: ["org-score"],
queryFn: getOrganizationScore,
});
const { data: scoreHistory } = useQuery({
queryKey: ["score-history", "90d"],
queryFn: () => getScoreHistory("90d"),
});
const { data: opMetrics, isLoading: loadingMetrics } = useQuery({
queryKey: ["operational-metrics"],
queryFn: getOperationalMetrics,
});
const { data: teamMetrics } = useQuery({
queryKey: ["team-metrics"],
queryFn: getMetricsByTeam,
});
const { data: tacticCoverage } = useQuery({
queryKey: ["tactic-coverage"],
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: 100 }),
});
const { data: allTechniques } = useQuery({
queryKey: ["techniques-exec"],
queryFn: () => getTechniques(),
});
const queryClient = useQueryClient();
const { data: riskProfiles, isLoading: loadingRisk } = useQuery({
queryKey: ["risk-profiles-exec"],
queryFn: () => getRiskProfiles({ limit: 500 }),
});
const computeMutation = useMutation({
mutationFn: computeRiskScores,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["risk-profiles-exec"] });
},
});
// Auto-compute on first load if no profiles exist
useEffect(() => {
if (!loadingRisk && riskProfiles !== undefined && riskProfiles.length === 0) {
computeMutation.mutate();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loadingRisk, riskProfiles?.length]);
const isLoading = loadingScore || loadingMetrics;
if (isLoading) {
return (
<div className="flex h-64 items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-cyan-400" />
</div>
);
}
// Build a lookup: techniqueId → risk profile (already sorted by risk_score DESC server-side)
const riskByTechniqueId = new Map<string, RiskProfile>(
(riskProfiles || []).map((p) => [p.technique_id, p])
);
// Critical gaps: not_covered or not_evaluated, sorted by risk_score DESC
// Techniques with a risk profile rank higher; ties broken by status (not_covered first)
const criticalGaps: TechniqueSummary[] = (allTechniques || [])
.filter((t) => t.status_global === "not_covered" || t.status_global === "not_evaluated")
.sort((a, b) => {
const scoreA = riskByTechniqueId.get(a.id)?.risk_score ?? -1;
const scoreB = riskByTechniqueId.get(b.id)?.risk_score ?? -1;
if (scoreB !== scoreA) return scoreB - scoreA;
// same score: not_covered before not_evaluated
if (a.status_global === "not_covered" && b.status_global !== "not_covered") return -1;
if (b.status_global === "not_covered" && a.status_global !== "not_covered") return 1;
return 0;
})
.slice(0, 10);
// Official MITRE ATT&CK tactic order (slug → display label)
const MITRE_TACTIC_ORDER: Record<string, string> = {
"reconnaissance": "Reconnaissance",
"resource-development": "Resource Development",
"initial-access": "Initial Access",
"execution": "Execution",
"persistence": "Persistence",
"privilege-escalation": "Privilege Escalation",
"defense-evasion": "Defense Evasion",
"credential-access": "Credential Access",
"discovery": "Discovery",
"lateral-movement": "Lateral Movement",
"collection": "Collection",
"command-and-control": "C2",
"exfiltration": "Exfiltration",
"impact": "Impact",
};
const tacticDataRaw = (tacticCoverage || []).map((tc) => {
const slug = tc.tactic.toLowerCase();
const label = MITRE_TACTIC_ORDER[slug] ||
tc.tactic.split("-").map((w: string) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
const order = Object.keys(MITRE_TACTIC_ORDER).indexOf(slug);
return {
name: label,
slug,
order: order === -1 ? 99 : order,
coverage: tc.total > 0 ? Math.round(((tc.validated + tc.partial) / tc.total) * 100) : 0,
};
});
// Sort by official MITRE order; include any unknown tactics at end
const tacticData = tacticDataRaw.sort((a, b) => a.order - b.order);
const getBarColor = (coverage: number) => {
if (coverage < 30) return "#ef4444";
if (coverage < 50) return "#f97316";
if (coverage < 70) return "#eab308";
return "#22c55e";
};
return (
<div className="space-y-6">
{/* Header */}
<div>
<h1 className="text-2xl font-bold text-white">Executive Dashboard</h1>
<p className="mt-1 text-sm text-gray-400">
Organization security posture overview
</p>
</div>
{/* Section 1: Score Card + Sub-scores */}
<div className="grid grid-cols-1 gap-6 lg:grid-cols-4">
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6 lg:col-span-1 flex flex-col items-center justify-center">
<ScoreGauge
score={orgScore?.overall_score ?? 0}
label="Overall Score"
/>
<div className="mt-4 grid w-full grid-cols-2 gap-2">
<div className="rounded-lg bg-gray-800 px-3 py-2 text-center">
<p className="text-lg font-bold text-white">
{orgScore?.total_coverage ?? 0}
</p>
<p className="text-[10px] text-gray-500 flex items-center justify-center gap-0.5">Coverage<MetricTooltip title="Coverage Score" description="Breadth of attack technique coverage — how many of the known MITRE ATT&CK techniques have been tested and detected. Scale 0100." position="above" /></p>
</div>
<div className="rounded-lg bg-gray-800 px-3 py-2 text-center">
<p className="text-lg font-bold text-white">
{orgScore?.detection_maturity ?? 0}
</p>
<p className="text-[10px] text-gray-500 flex items-center justify-center gap-0.5">Detection<MetricTooltip title="Detection Score" description="Quality and maturity of detection capabilities — based on detection rules, validated tests and Blue Team confirmation rate. Scale 0100." position="above" /></p>
</div>
<div className="rounded-lg bg-gray-800 px-3 py-2 text-center">
<p className="text-lg font-bold text-white">
{orgScore?.critical_coverage ?? 0}
</p>
<p className="text-[10px] text-gray-500 flex items-center justify-center gap-0.5">Critical<MetricTooltip title="Critical Coverage Score" description="Coverage specifically for high-risk and critical techniques used by known threat actors targeting your sector. Scale 0100." position="above" /></p>
</div>
<div className="rounded-lg bg-gray-800 px-3 py-2 text-center">
<p className="text-lg font-bold text-white">
{orgScore?.response_readiness ?? 0}
</p>
<p className="text-[10px] text-gray-500 flex items-center justify-center gap-0.5">Response<MetricTooltip title="Response Readiness Score" description="How quickly and efficiently tests flow through the Red/Blue validation process. High score = fast, smooth validation workflow. Scale 0100." position="above" /></p>
</div>
</div>
</div>
{/* Section 2: Trend Chart */}
<div className="rounded-xl border border-gray-800 bg-gray-900 p-4 lg:col-span-3">
<h2 className="mb-3 text-sm font-semibold text-gray-300 flex items-center gap-1">
Score Trend (90 days)
<MetricTooltip title="Score Trend" description="How the overall security posture score has evolved over the past 90 days. An upward trend indicates improving security coverage and maturity." context="Aim for a steady upward trend. Sudden drops may indicate new uncovered threats discovered." />
</h2>
<ResponsiveContainer width="100%" height={220}>
<LineChart data={scoreHistory || []}>
<CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
<XAxis
dataKey="date"
tick={{ fill: "#6b7280", fontSize: 10 }}
tickFormatter={(val) => {
const d = new Date(val);
return `${d.getMonth() + 1}/${d.getDate()}`;
}}
/>
<YAxis
domain={[0, 100]}
tick={{ fill: "#6b7280", fontSize: 10 }}
/>
<Tooltip
contentStyle={{
backgroundColor: "#111827",
border: "1px solid #374151",
borderRadius: "8px",
fontSize: "12px",
}}
labelStyle={{ color: "#9ca3af" }}
/>
<Line
type="monotone"
dataKey="score"
stroke="#06b6d4"
strokeWidth={2}
dot={false}
name="Score"
/>
</LineChart>
</ResponsiveContainer>
</div>
</div>
{/* Section 3: Team Performance */}
{teamMetrics && (
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
{/* Red Team */}
<div className="rounded-xl border border-gray-800 bg-gray-900 p-4">
<h2 className="mb-3 flex items-center gap-2 text-sm font-semibold text-red-400">
<div className="h-2 w-2 rounded-full bg-red-500" />
Red Team
<MetricTooltip title="Red Team Performance" description="Metrics for the offensive security team that simulates attacks. Tests Done = completed executions; Avg Time = mean execution duration; Rejection = % of results rejected by leads." context="Lower rejection rate = higher quality test documentation." />
</h2>
<div className="grid grid-cols-3 gap-3">
<div className="rounded-lg bg-gray-800 px-3 py-2 text-center">
<p className="text-lg font-bold text-white">
{teamMetrics.red_team.tests_completed}
</p>
<p className="text-[10px] text-gray-500 flex items-center justify-center gap-0.5">Tests Done<MetricTooltip title="Tests Done (Red)" description="Total number of attack simulations completed by Red Team and submitted to Blue Team for evaluation." /></p>
</div>
<div className="rounded-lg bg-gray-800 px-3 py-2 text-center">
<p className="text-lg font-bold text-white">
{teamMetrics.red_team.avg_completion_hours != null
? `${teamMetrics.red_team.avg_completion_hours}${(teamMetrics.red_team as any).avg_unit ?? "h"}`
: "N/A"}
</p>
<p className="text-[10px] text-gray-500 flex items-center justify-center gap-0.5">Avg Time<MetricTooltip title="Avg Execution Time (Red)" description="Average hours Red Team spends executing and documenting each attack simulation." /></p>
</div>
<div className="rounded-lg bg-gray-800 px-3 py-2 text-center">
<p className="text-lg font-bold text-white">
{teamMetrics.red_team.rejection_rate}%
</p>
<p className="text-[10px] text-gray-500 flex items-center justify-center gap-0.5">Rejection<MetricTooltip title="Rejection Rate (Red)" description="Percentage of Red Team submissions rejected by the Red Lead during validation review. High rejection = quality issues in test documentation." /></p>
</div>
</div>
</div>
{/* Blue Team */}
<div className="rounded-xl border border-gray-800 bg-gray-900 p-4">
<h2 className="mb-3 flex items-center gap-2 text-sm font-semibold text-blue-400">
<div className="h-2 w-2 rounded-full bg-blue-500" />
Blue Team
<MetricTooltip title="Blue Team Performance" description="Metrics for the defensive security team that analyses attacks and validates detections. Tests Done = evaluations completed; Avg Time = mean evaluation duration; Rejection = % rejected by Blue Lead." context="Lower rejection rate = consistent, high-quality detection analysis." />
</h2>
<div className="grid grid-cols-3 gap-3">
<div className="rounded-lg bg-gray-800 px-3 py-2 text-center">
<p className="text-lg font-bold text-white">
{teamMetrics.blue_team.tests_completed}
</p>
<p className="text-[10px] text-gray-500 flex items-center justify-center gap-0.5">Tests Done<MetricTooltip title="Tests Done (Blue)" description="Total attack simulations evaluated by Blue Team — verified whether security controls detected or missed the attack." /></p>
</div>
<div className="rounded-lg bg-gray-800 px-3 py-2 text-center">
<p className="text-lg font-bold text-white">
{teamMetrics.blue_team.avg_completion_hours != null
? `${teamMetrics.blue_team.avg_completion_hours}${(teamMetrics.blue_team as any).avg_unit ?? "h"}`
: "N/A"}
</p>
<p className="text-[10px] text-gray-500 flex items-center justify-center gap-0.5">Avg Time<MetricTooltip title="Avg Evaluation Time (Blue)" description="Average time Blue Team spends actively evaluating a test — from 'Start Evaluation' click to validation approved. Paused time is excluded." context="⚠ Does NOT include queue wait time (time the test sat unattended before Blue picked it up). Only measures active work time when available." /></p>
</div>
<div className="rounded-lg bg-gray-800 px-3 py-2 text-center">
<p className="text-lg font-bold text-white">
{teamMetrics.blue_team.rejection_rate}%
</p>
<p className="text-[10px] text-gray-500 flex items-center justify-center gap-0.5">Rejection<MetricTooltip title="Rejection Rate (Blue)" description="Percentage of Blue Team evaluations rejected by the Blue Lead. May indicate unclear detection documentation or disagreement on results." /></p>
</div>
</div>
</div>
</div>
)}
{/* Section 4: 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 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);
// 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
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}`)}
>
<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-xs font-medium text-white truncate">
{actor.name}
</p>
<p className="text-[10px] text-gray-500 truncate">
{side === "risk"
? `${uncovered} uncovered / ${actor.technique_count} techniques`
: `${actor.technique_count} techniques covered`}
</p>
</div>
<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={{
width: `${actor.coverage_pct}%`,
backgroundColor:
actor.coverage_pct > 70
? "#22c55e"
: actor.coverage_pct > 40
? "#eab308"
: "#ef4444",
}}
/>
</div>
<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
label="MTTD"
value={opMetrics?.mttd?.mean_hours != null ? opMetrics.mttd.mean_hours : "N/A"}
unit={opMetrics?.mttd?.mean_hours != null ? (opMetrics.mttd.mean_hours < 1 ? "min" : "hrs") : undefined}
tooltip={{ description: "Mean Time To Detect — average time from attack execution start to Blue Team receiving the test. Shows Red Team execution efficiency.", context: "Lower is better." }}
/>
<KPICard
label="MTTR"
value={opMetrics?.mttr?.mean_hours != null ? opMetrics.mttr.mean_hours : "N/A"}
unit={opMetrics?.mttr?.mean_hours != null ? (opMetrics.mttr.mean_hours < 1 ? "min" : "hrs") : undefined}
tooltip={{ description: "Mean Time To Respond — average time from Red Team attack start to full validation complete. Measures the total security test cycle time end-to-end.", context: "Lower is better. Long MTTR may indicate bottlenecks in the review pipeline." }}
/>
<KPICard
label="Detection Efficacy"
value={opMetrics?.detection_efficacy?.percentage ?? 0}
unit="%"
tooltip={{ description: "Percentage of executed attacks that Blue Team successfully detected. 100% means every simulated attack triggered an alert.", context: "Higher is better. Below 60% indicates significant detection gaps." }}
/>
<KPICard
label="Pipeline Conversion"
value={opMetrics?.validation_throughput?.conversion_rate ?? opMetrics?.validation_throughput?.tests_per_week ?? 0}
unit="%"
trend={opMetrics?.validation_throughput?.trend}
tooltip={{ description: "Percentage of tests that have entered the validation phase and been successfully approved. Formula: Validated ÷ (Validated + Rejected + In Review) × 100.", context: "100% = all reviewed tests approved. < 60% = quality or process issues. High backlog (many In Review) lowers this score." }}
/>
</div>
{/* Section 5: Coverage by Tactic */}
<div className="rounded-xl border border-gray-800 bg-gray-900 p-4">
<h2 className="mb-3 text-sm font-semibold text-gray-300 flex items-center gap-1">
Coverage by Tactic
<MetricTooltip title="Coverage by Tactic" description="How well each MITRE ATT&CK tactic (attack phase) is covered by validated tests. Tactics represent different stages of an attack, from Initial Access to Impact." context="Red bars (low coverage) are highest priority. Focus testing on those tactics first." />
</h2>
<ResponsiveContainer width="100%" height={260}>
<BarChart
data={tacticData}
margin={{ top: 8, right: 8, bottom: 64, left: 8 }}
barCategoryGap="20%"
>
<CartesianGrid strokeDasharray="3 3" stroke="#1f2937" vertical={false} />
<XAxis
dataKey="name"
tick={{ fill: "#9ca3af", fontSize: 10 }}
angle={-45}
textAnchor="end"
interval={0}
/>
<YAxis
domain={[0, 100]}
tick={{ fill: "#6b7280", fontSize: 10 }}
tickFormatter={(v) => `${v}%`}
width={36}
/>
<Tooltip
contentStyle={{
backgroundColor: "#111827",
border: "1px solid #374151",
borderRadius: "8px",
fontSize: "12px",
}}
formatter={(value: number) => [`${value}%`, "Coverage"]}
/>
<Bar dataKey="coverage" radius={[4, 4, 0, 0]}>
{tacticData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={getBarColor(entry.coverage)}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
{/* Section 6: Critical Gaps */}
<div className="rounded-xl border border-gray-800 bg-gray-900 p-4">
<div className="mb-3 flex items-center justify-between">
<h2 className="text-sm font-semibold text-gray-300 flex items-center gap-1">
Critical Gaps Top 10 by Risk Priority
<MetricTooltip title="Critical Gaps" description="The 10 most dangerous attack techniques with no detection coverage, ranked by a composite risk score based on: how many threat actors use this technique, recent threat intelligence, and test history." context="These are your highest-priority gaps — real adversaries actively use these techniques and you cannot currently detect them." />
</h2>
<button
onClick={() => computeMutation.mutate()}
disabled={computeMutation.isPending}
className="flex items-center gap-1.5 rounded-lg border border-gray-700 bg-gray-800 px-2.5 py-1 text-xs text-gray-400 hover:text-cyan-400 hover:border-cyan-500/30 disabled:opacity-50 transition-colors"
title="Recompute risk scores"
>
{computeMutation.isPending ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<RefreshCw className="h-3 w-3" />
)}
{computeMutation.isPending ? "Computing…" : "Refresh scores"}
</button>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-800 text-left text-xs text-gray-500">
<th className="pb-2 pr-3">#</th>
<th className="pb-2 pr-4">Risk</th>
<th className="pb-2 pr-4">MITRE ID</th>
<th className="pb-2 pr-4">Name</th>
<th className="pb-2 pr-4">Tactic</th>
<th className="pb-2 pr-4">Status</th>
</tr>
</thead>
<tbody>
{criticalGaps.map((tech, idx) => {
const profile = riskByTechniqueId.get(tech.id);
const riskScore = profile?.risk_score;
const riskLevel = profile?.risk_level;
const riskLevelColors: Record<string, string> = {
critical: "bg-red-500/20 text-red-400 border border-red-500/30",
high: "bg-orange-500/20 text-orange-400 border border-orange-500/30",
medium: "bg-yellow-500/20 text-yellow-400 border border-yellow-500/30",
low: "bg-blue-500/20 text-blue-400 border border-blue-500/30",
info: "bg-gray-500/20 text-gray-400 border border-gray-500/30",
};
const scoreColor =
riskScore === undefined ? "text-gray-600"
: riskScore >= 75 ? "text-red-400"
: riskScore >= 50 ? "text-orange-400"
: riskScore >= 25 ? "text-yellow-400"
: "text-blue-400";
return (
<tr
key={tech.mitre_id}
className="border-b border-gray-800/50 cursor-pointer hover:bg-gray-800/30"
onClick={() => navigate(`/techniques/${tech.mitre_id}`)}
>
<td className="py-2 pr-3 text-xs text-gray-600 font-mono">{idx + 1}</td>
<td className="py-2 pr-4">
<div className="flex items-center gap-2">
<span className={`text-sm font-bold tabular-nums ${scoreColor}`}>
{riskScore !== undefined ? Math.round(riskScore) : "—"}
</span>
{riskLevel && (
<span
className={`hidden sm:inline-block rounded-full px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide ${
riskLevelColors[riskLevel] || riskLevelColors.info
}`}
>
{riskLevel}
</span>
)}
</div>
</td>
<td className="py-2 pr-4 font-mono text-xs text-cyan-400">
{tech.mitre_id}
</td>
<td className="py-2 pr-4 text-gray-300 truncate max-w-[180px]">
{tech.name}
</td>
<td className="py-2 pr-4 text-gray-500 text-xs">
{tech.tactic
?.split(",")[0]
.trim()
.split("-")
.map((w: string) => w.charAt(0).toUpperCase() + w.slice(1))
.join(" ")}
</td>
<td className="py-2 pr-4">
<span
className={`inline-block rounded-full px-2 py-0.5 text-[10px] font-medium ${
tech.status_global === "not_covered"
? "bg-red-500/10 text-red-400"
: "bg-gray-500/10 text-gray-400"
}`}
>
{tech.status_global?.replace(/_/g, " ")}
</span>
</td>
</tr>
);
})}
{criticalGaps.length === 0 && (
<tr>
<td colSpan={6} className="py-4 text-center text-gray-500">
No critical gaps found
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
);
}