Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
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.
659 lines
30 KiB
TypeScript
659 lines
30 KiB
TypeScript
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 0–100." 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 0–100." 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 0–100." 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 0–100." 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>
|
||
);
|
||
}
|