feat(exec-dashboard): vertical bars for Coverage by Tactic in MITRE order
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
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:
@@ -232,14 +232,39 @@ export default function ExecutiveDashboardPage() {
|
||||
})
|
||||
.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,
|
||||
}));
|
||||
// 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";
|
||||
@@ -512,24 +537,25 @@ export default function ExecutiveDashboardPage() {
|
||||
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}>
|
||||
<ResponsiveContainer width="100%" height={260}>
|
||||
<BarChart
|
||||
data={tacticData}
|
||||
layout="vertical"
|
||||
margin={{ left: 120 }}
|
||||
margin={{ top: 8, right: 8, bottom: 64, left: 8 }}
|
||||
barCategoryGap="20%"
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#1f2937" vertical={false} />
|
||||
<XAxis
|
||||
type="number"
|
||||
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}%`}
|
||||
/>
|
||||
<YAxis
|
||||
type="category"
|
||||
dataKey="name"
|
||||
width={120}
|
||||
tick={{ fill: "#9ca3af", fontSize: 10 }}
|
||||
width={36}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
@@ -540,7 +566,7 @@ export default function ExecutiveDashboardPage() {
|
||||
}}
|
||||
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) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
|
||||
Reference in New Issue
Block a user