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,
min_score: int = 0,
) -> 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(
"Detection Rules Coverage",
"Coverage of detection rules per technique",
"Number of active detection rules per technique",
)
query = _apply_filters(
@@ -318,7 +327,6 @@ def build_detection_rules_layer(
.group_by(DetectionRule.mitre_technique_id)
.all()
)
max_rules = max(rule_counts.values()) if rule_counts else 1
evaluated_counts = dict(
db.query(DetectionRule.mitre_technique_id, func.count(TestDetectionResult.id))
@@ -328,26 +336,28 @@ def build_detection_rules_layer(
.all()
)
# 4 rules = full coverage (100). Each rule adds 25 points.
RULES_FOR_FULL_COVERAGE = 4
for tech in techniques:
total_rules = rule_counts.get(tech.mitre_id, 0)
evaluated_rules = evaluated_counts.get(tech.mitre_id, 0)
if total_rules > 0:
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
score = min(int((total_rules / RULES_FOR_FULL_COVERAGE) * 100), 100)
if score < min_score:
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({
"techniqueID": tech.mitre_id,
"tactic": _format_tactic(tech.tactic),
"color": _score_to_color(score),
"score": score,
"comment": f"{total_rules} rules available, {evaluated_rules} evaluated",
"comment": comment,
"enabled": True,
"metadata": [
{"name": "total_rules", "value": str(total_rules)},

View File

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