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);
|
.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}`}
|
||||||
|
|||||||
Reference in New Issue
Block a user