feat(exec-dashboard): vertical bars for Coverage by Tactic in MITRE order
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

- Convert horizontal bar chart to vertical bars (columns)
- Sort all 14 MITRE ATT&CK tactics in official order:
  Reconnaissance → Resource Development → Initial Access → Execution →
  Persistence → Privilege Escalation → Defense Evasion → Credential Access →
  Discovery → Lateral Movement → Collection → C2 → Exfiltration → Impact
- Show ALL tactics (not a subset)
- Labels rotated -45° to fit all names
- Bars have rounded top corners; horizontal gridlines only
This commit is contained in:
kitos
2026-06-03 10:13:09 +02:00
parent e03a222ab0
commit 688e843e03

View File

@@ -232,14 +232,39 @@ export default function ExecutiveDashboardPage() {
}) })
.slice(0, 10); .slice(0, 10);
// Coverage by tactic for bar chart // Official MITRE ATT&CK tactic order (slug → display label)
const tacticData = (tacticCoverage || []).map((tc) => ({ const MITRE_TACTIC_ORDER: Record<string, string> = {
name: tc.tactic "reconnaissance": "Reconnaissance",
.split("-") "resource-development": "Resource Development",
.map((w: string) => w.charAt(0).toUpperCase() + w.slice(1)) "initial-access": "Initial Access",
.join(" "), "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, 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) => { const getBarColor = (coverage: number) => {
if (coverage < 30) return "#ef4444"; if (coverage < 30) return "#ef4444";
@@ -512,24 +537,25 @@ export default function ExecutiveDashboardPage() {
Coverage by Tactic 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." /> <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> </h2>
<ResponsiveContainer width="100%" height={300}> <ResponsiveContainer width="100%" height={260}>
<BarChart <BarChart
data={tacticData} data={tacticData}
layout="vertical" margin={{ top: 8, right: 8, bottom: 64, left: 8 }}
margin={{ left: 120 }} barCategoryGap="20%"
> >
<CartesianGrid strokeDasharray="3 3" stroke="#1f2937" /> <CartesianGrid strokeDasharray="3 3" stroke="#1f2937" vertical={false} />
<XAxis <XAxis
type="number" dataKey="name"
tick={{ fill: "#9ca3af", fontSize: 10 }}
angle={-45}
textAnchor="end"
interval={0}
/>
<YAxis
domain={[0, 100]} domain={[0, 100]}
tick={{ fill: "#6b7280", fontSize: 10 }} tick={{ fill: "#6b7280", fontSize: 10 }}
tickFormatter={(v) => `${v}%`} tickFormatter={(v) => `${v}%`}
/> width={36}
<YAxis
type="category"
dataKey="name"
width={120}
tick={{ fill: "#9ca3af", fontSize: 10 }}
/> />
<Tooltip <Tooltip
contentStyle={{ contentStyle={{
@@ -540,7 +566,7 @@ export default function ExecutiveDashboardPage() {
}} }}
formatter={(value: number) => [`${value}%`, "Coverage"]} formatter={(value: number) => [`${value}%`, "Coverage"]}
/> />
<Bar dataKey="coverage" radius={[0, 4, 4, 0]}> <Bar dataKey="coverage" radius={[4, 4, 0, 0]}>
{tacticData.map((entry, index) => ( {tacticData.map((entry, index) => (
<Cell <Cell
key={`cell-${index}`} key={`cell-${index}`}