Files
Aegis/frontend/src/pages/ExecutiveDashboardPage.tsx
kitos 757d99d22a
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
feat(dashboards): hover tooltips on all metric cards
New MetricTooltip component — a small ⓘ icon showing an executive-
friendly explanation panel on hover (CSS, no JS, instant).

DashboardPage: tooltips on all 6 coverage summary cards (Total
Techniques, Validated, Partial, In Progress, Not Covered, Not
Evaluated), Coverage Evolution chart, Test Pipeline funnel,
Team Activity and Validation Rate section headers.

ExecutiveDashboardPage: tooltips on all 4 sub-scores (Coverage,
Detection, Critical, Response), Score Trend, Top Threat Actors,
4 KPIs (MTTD, MTTR, Detection Efficacy, Validation Throughput),
Coverage by Tactic, Critical Gaps table, and all 6 team metrics
(Red/Blue Tests Done, Avg Time, Rejection).

Each tooltip explains what the metric measures, what a good/bad
value looks like, and what action to take — written for non-
technical executives.
2026-06-03 09:49:58 +02:00

659 lines
30 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>
<span className="mt-2 text-xs font-medium text-gray-400">{label}</span>
</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);
// Coverage by tactic for bar chart
const tacticData = (tacticCoverage || []).map((tc) => ({
name: tc.tactic
.split("-")
.map((w: string) => w.charAt(0).toUpperCase() + w.slice(1))
.join(" "),
coverage: tc.total > 0 ? Math.round(((tc.validated + tc.partial) / tc.total) * 100) : 0,
}));
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: 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
.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}`)}
>
{/* 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">
{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-20 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-10 text-right text-xs font-medium text-gray-300 tabular-nums">
{actor.coverage_pct.toFixed(0)}%
</span>
</div>
</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 ?? "N/A"}
unit={opMetrics?.mttd ? "hrs" : undefined}
tooltip={{ description: "Mean Time To Detect — average hours from attack execution to Blue Team raising an alert or detecting the intrusion.", context: "Lower is better. Industry benchmark: < 24h." }}
/>
<KPICard
label="MTTR"
value={opMetrics?.mttr?.mean_hours ?? "N/A"}
unit={opMetrics?.mttr ? "hrs" : undefined}
tooltip={{ description: "Mean Time To Respond — average hours for a test to go from execution through the full Red/Blue validation and reach a final result.", context: "Lower is better. Reflects process efficiency and team responsiveness." }}
/>
<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="Validation Throughput"
value={opMetrics?.validation_throughput?.tests_per_week ?? 0}
unit="/week"
trend={opMetrics?.validation_throughput?.trend}
tooltip={{ description: "Number of tests fully validated (approved by Red + Blue leads) per week. Shows how productive the security testing programme is.", context: "Increasing trend = programme is accelerating. Decreasing may indicate process bottlenecks." }}
/>
</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={300}>
<BarChart
data={tacticData}
layout="vertical"
margin={{ left: 120 }}
>
<CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
<XAxis
type="number"
domain={[0, 100]}
tick={{ fill: "#6b7280", fontSize: 10 }}
tickFormatter={(v) => `${v}%`}
/>
<YAxis
type="category"
dataKey="name"
width={120}
tick={{ fill: "#9ca3af", fontSize: 10 }}
/>
<Tooltip
contentStyle={{
backgroundColor: "#111827",
border: "1px solid #374151",
borderRadius: "8px",
fontSize: "12px",
}}
formatter={(value: number) => [`${value}%`, "Coverage"]}
/>
<Bar dataKey="coverage" radius={[0, 4, 4, 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>
{/* Section 7: 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
? `${teamMetrics.red_team.avg_completion_hours}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
? `${teamMetrics.blue_team.avg_completion_hours}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 hours Blue Team takes to evaluate an attack simulation and document the detection result." /></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>
)}
</div>
);
}