Files
Aegis/backend/tests/test_heatmap_technique_name.py
T
kitos 0de9f4a6c0 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.
2026-07-09 14:12:58 +02:00

86 lines
3.0 KiB
Python

"""Every heatmap layer must include the technique's display name.
The frontend heatmap previously showed only the MITRE ID on each cell
(name only available via hover tooltip), unlike the Techniques page which
always showed both. Unifying the two pages' cell style requires the name
to be available on every layer's technique entries, not just derived
client-side (HeatmapTechnique has no name field to derive it from).
"""
from app.models.campaign import Campaign, CampaignTest
from app.models.detection_rule import DetectionRule
from app.models.enums import TestState
from app.models.technique import Technique
from app.models.test import Test
from app.models.threat_actor import ThreatActor, ThreatActorTechnique
from app.services.heatmap_service import (
build_campaign_layer,
build_coverage_layer,
build_detection_rules_layer,
build_threat_actor_layer,
)
def _seed_technique(db, mitre_id="T1059", name="Command and Scripting Interpreter"):
tech = Technique(mitre_id=mitre_id, name=name, tactic="execution", platforms=["windows"])
db.add(tech)
db.flush()
return tech
def test_coverage_layer_includes_technique_name_and_status(db):
tech = _seed_technique(db)
db.commit()
layer = build_coverage_layer(db)
entry = next(t for t in layer["techniques"] if t["techniqueID"] == tech.mitre_id)
assert entry["name"] == tech.name
assert entry["status"] == "not_evaluated"
def test_threat_actor_layer_includes_technique_name_and_status(db):
tech = _seed_technique(db)
actor = ThreatActor(name="APT-Test")
db.add(actor)
db.flush()
db.add(ThreatActorTechnique(threat_actor_id=actor.id, technique_id=tech.id))
db.commit()
layer = build_threat_actor_layer(db, actor.id)
entry = next(t for t in layer["techniques"] if t["techniqueID"] == tech.mitre_id)
assert entry["name"] == tech.name
assert entry["status"] == "not_evaluated"
def test_detection_rules_layer_includes_technique_name(db):
tech = _seed_technique(db)
db.add(DetectionRule(
mitre_technique_id=tech.mitre_id, title="Rule 1", source="sigma",
rule_content="title: Rule 1", rule_format="sigma",
))
db.commit()
layer = build_detection_rules_layer(db)
entry = next(t for t in layer["techniques"] if t["techniqueID"] == tech.mitre_id)
assert entry["name"] == tech.name
def test_campaign_layer_includes_technique_name(db, red_lead_user):
tech = _seed_technique(db)
campaign = Campaign(name="Test Campaign", type="custom", status="active", created_by=red_lead_user.id)
db.add(campaign)
db.flush()
test = Test(technique_id=tech.id, name="A test", state=TestState.validated, created_by=red_lead_user.id)
db.add(test)
db.flush()
db.add(CampaignTest(campaign_id=campaign.id, test_id=test.id, order_index=0))
db.commit()
layer = build_campaign_layer(db, campaign.id)
entry = next(t for t in layer["techniques"] if t["techniqueID"] == tech.mitre_id)
assert entry["name"] == tech.name