From 15eda30b753d6ed91856a61718cacc2fa3c3c4de Mon Sep 17 00:00:00 2001 From: kitos Date: Thu, 4 Jun 2026 17:23:28 +0200 Subject: [PATCH] fix(heatmap): hide empty tactics in threat-actor layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build_threat_actor_layer was adding ALL techniques to the layer — actor techniques with their real score and non-actor techniques with score=0/enabled=False. This caused every tactic column to appear in the matrix even when the actor has no techniques for that tactic. Now only actor techniques are included. The frontend already filters visible tactics to those with data, so empty tactic columns disappear automatically. Co-Authored-By: Claude Sonnet 4.6 --- backend/app/services/heatmap_service.py | 54 +++++++++++-------------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/backend/app/services/heatmap_service.py b/backend/app/services/heatmap_service.py index a6672d5..d39c657 100644 --- a/backend/app/services/heatmap_service.py +++ b/backend/app/services/heatmap_service.py @@ -259,36 +259,30 @@ def build_threat_actor_layer( if is_actor_technique and score < min_score: continue - if is_actor_technique: - tc = test_counts.get(tech.id, 0) - rc = rule_counts.get(tech.mitre_id, 0) - metadata = [ - {"name": "tests_count", "value": str(tc)}, - {"name": "detection_rules", "value": str(rc)}, - ] - if tech.last_review_date: - metadata.append( - {"name": "last_validated", "value": tech.last_review_date.strftime("%Y-%m-%d")} - ) - layer["techniques"].append({ - "techniqueID": tech.mitre_id, - "tactic": _format_tactic(tech.tactic), - "color": _score_to_color(score), - "score": score, - "comment": f"Used by {actor.name} - Coverage: {tech.status_global.value}", - "enabled": True, - "metadata": metadata, - }) - else: - layer["techniques"].append({ - "techniqueID": tech.mitre_id, - "tactic": _format_tactic(tech.tactic), - "color": "", - "score": 0, - "comment": "", - "enabled": False, - "metadata": [], - }) + # Only include techniques actually used by this actor — skip the rest + # so that tactics with no actor techniques are hidden in the matrix. + if not is_actor_technique: + continue + + tc = test_counts.get(tech.id, 0) + rc = rule_counts.get(tech.mitre_id, 0) + metadata = [ + {"name": "tests_count", "value": str(tc)}, + {"name": "detection_rules", "value": str(rc)}, + ] + if tech.last_review_date: + metadata.append( + {"name": "last_validated", "value": tech.last_review_date.strftime("%Y-%m-%d")} + ) + layer["techniques"].append({ + "techniqueID": tech.mitre_id, + "tactic": _format_tactic(tech.tactic), + "color": _score_to_color(score), + "score": score, + "comment": f"Used by {actor.name} - Coverage: {tech.status_global.value}", + "enabled": True, + "metadata": metadata, + }) return layer