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,
|
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)},
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user