fix(heatmap): detection rules layer uses absolute rule count, not relative max
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
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:
@@ -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)},
|
||||
|
||||
Reference in New Issue
Block a user