feat(heatmap): expose technique name + real status on every layer

The Coverage Matrix heatmap only ever showed the bare MITRE ID on each
cell (name only via hover tooltip), and colored cells from a continuous
score -> hex gradient with no discrete status concept — unlike the
Techniques page, which showed ID+name inline with a fixed 5-color status
palette. Unifying the two pages' visual style requires both pieces of
data to exist server-side.

- Adds "name" to all 4 layer builders (coverage/threat-actor/detection-
  rules/campaign) — harmless extra field for Navigator exports, lets the
  frontend show it inline.
- Adds "status" (the real TechniqueStatus value) to the coverage and
  threat-actor layers specifically — the only two backed by an actual
  TechniqueStatus concept; detection-rules/campaign scores don't
  correspond to one and don't get this field.
This commit is contained in:
kitos
2026-07-09 14:12:58 +02:00
parent 4e31359fca
commit 0de9f4a6c0
2 changed files with 103 additions and 0 deletions
+18
View File
@@ -353,6 +353,17 @@ def build_coverage_layer(
layer["techniques"].append({
# Literal argument value
"techniqueID": tech.mitre_id,
# Technique display name — not part of the official ATT&CK
# Navigator schema, but harmless extra data for Navigator
# imports and lets Aegis's own heatmap UI show names inline
# instead of only ID + hover tooltip.
"name": tech.name,
# Only the coverage layer has a real TechniqueStatus concept —
# exposed so the frontend can color cells by the exact same
# discrete status palette the Techniques page uses, instead of
# a continuous score-derived hex. The other 3 layer types don't
# set this (their score doesn't correspond to a TechniqueStatus).
"status": tech.status_global.value,
# Literal argument value
"tactic": _format_tactic(tech.tactic),
# Literal argument value
@@ -491,6 +502,11 @@ def build_threat_actor_layer(
)
layer["techniques"].append({
"techniqueID": tech.mitre_id,
"name": tech.name,
# Every appended row here is an actor-used technique (non-actor
# ones are skipped above), so status_global is meaningful here
# too — see the coverage layer's "status" field for why.
"status": tech.status_global.value,
"tactic": _format_tactic(tech.tactic),
"color": _score_to_color(score),
"score": score,
@@ -586,6 +602,7 @@ def build_detection_rules_layer(
layer["techniques"].append({
# Literal argument value
"techniqueID": tech.mitre_id,
"name": tech.name,
# Literal argument value
"tactic": _format_tactic(tech.tactic),
# Literal argument value
@@ -754,6 +771,7 @@ def build_campaign_layer(
layer["techniques"].append({
# Literal argument value
"techniqueID": mitre_id,
"name": tech.name,
# Literal argument value
"tactic": _format_tactic(tech.tactic),
# Literal argument value