fix(heatmap): detection rules layer uses absolute rule count, not relative max
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

Before: score = (rules/max_rules)*50 + (evaluated/rules)*50
  -> everything red because relative to the 1 technique with most rules

After: score = min(rules/4 * 100, 100)  — absolute thresholds
  0 rules  = gray  (not covered)
  1 rule   = red   (25 — minimal)
  2 rules  = orange (50 — some)
  3 rules  = yellow (75 — good)
  4+ rules = green  (100 — well covered)

Also update HeatmapLegend labels to show actual rule counts instead of
meaningless percentage ranges.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-05-28 16:11:29 +02:00
parent 8024f32954
commit 2371318e9e
2 changed files with 25 additions and 15 deletions

View File

@@ -300,10 +300,19 @@ def build_detection_rules_layer(
tactics: str | None = None, tactics: str | None = None,
min_score: int = 0, min_score: int = 0,
) -> dict: ) -> dict:
"""Detection rules layer -- score based on rule availability and evaluation ratio.""" """Detection rules layer -- score based on absolute rule count per technique.
Scoring uses fixed thresholds so the colour reflects real coverage regardless
of what other techniques have:
0 rules → gray (score 0)
1 rule → red (score 25)
2 rules → orange (score 50)
3 rules → yellow (score 75)
4+ rules → green (score 100)
"""
layer = _build_layer_skeleton( layer = _build_layer_skeleton(
"Detection Rules Coverage", "Detection Rules Coverage",
"Coverage of detection rules per technique", "Number of active detection rules per technique",
) )
query = _apply_filters( query = _apply_filters(
@@ -318,7 +327,6 @@ def build_detection_rules_layer(
.group_by(DetectionRule.mitre_technique_id) .group_by(DetectionRule.mitre_technique_id)
.all() .all()
) )
max_rules = max(rule_counts.values()) if rule_counts else 1
evaluated_counts = dict( evaluated_counts = dict(
db.query(DetectionRule.mitre_technique_id, func.count(TestDetectionResult.id)) db.query(DetectionRule.mitre_technique_id, func.count(TestDetectionResult.id))
@@ -328,26 +336,28 @@ def build_detection_rules_layer(
.all() .all()
) )
# 4 rules = full coverage (100). Each rule adds 25 points.
RULES_FOR_FULL_COVERAGE = 4
for tech in techniques: for tech in techniques:
total_rules = rule_counts.get(tech.mitre_id, 0) total_rules = rule_counts.get(tech.mitre_id, 0)
evaluated_rules = evaluated_counts.get(tech.mitre_id, 0) evaluated_rules = evaluated_counts.get(tech.mitre_id, 0)
if total_rules > 0: score = min(int((total_rules / RULES_FOR_FULL_COVERAGE) * 100), 100)
availability_score = min((total_rules / max_rules) * 50, 50)
evaluation_score = (evaluated_rules / total_rules) * 50
score = int(min(availability_score + evaluation_score, 100))
else:
score = 0
if score < min_score: if score < min_score:
continue continue
rule_word = "rule" if total_rules == 1 else "rules"
eval_note = f", {evaluated_rules} evaluated" if evaluated_rules > 0 else ""
comment = f"{total_rules} active {rule_word}{eval_note}"
layer["techniques"].append({ layer["techniques"].append({
"techniqueID": tech.mitre_id, "techniqueID": tech.mitre_id,
"tactic": _format_tactic(tech.tactic), "tactic": _format_tactic(tech.tactic),
"color": _score_to_color(score), "color": _score_to_color(score),
"score": score, "score": score,
"comment": f"{total_rules} rules available, {evaluated_rules} evaluated", "comment": comment,
"enabled": True, "enabled": True,
"metadata": [ "metadata": [
{"name": "total_rules", "value": str(total_rules)}, {"name": "total_rules", "value": str(total_rules)},

View File

@@ -29,11 +29,11 @@ const LEGENDS: Record<
"detection-rules": { "detection-rules": {
label: "Detection Rules Coverage", label: "Detection Rules Coverage",
colors: [ colors: [
{ color: "#d3d3d3", label: "No Rules (0)" }, { color: "#d3d3d3", label: "0 rules" },
{ color: "#ff6666", label: "Few Rules (<25)" }, { color: "#ff6666", label: "1 rule" },
{ color: "#ff9933", label: "Some Rules (25-50)" }, { color: "#ff9933", label: "2 rules" },
{ color: "#ffff66", label: "Good Coverage (50-75)" }, { color: "#ffff66", label: "3 rules" },
{ color: "#66ff66", label: "Full Coverage (75-100)" }, { color: "#66ff66", label: "4+ rules" },
], ],
}, },
campaign: { campaign: {