refactor(scoring): persist weights in DB table, replace mutable Settings with scoring_config_service

This commit is contained in:
2026-02-19 17:46:02 +01:00
parent 93fde55389
commit 4e3787d091
6 changed files with 210 additions and 83 deletions

View File

@@ -1,7 +1,8 @@
"""Scoring service — granular 0-100 scoring for techniques, tactics, actors, and org.
Uses configurable weights from Settings to compute coverage scores with
detailed breakdowns.
Reads configurable weights from the ``scoring_config`` table (falling
back to env-var defaults) to compute coverage scores with detailed
breakdowns.
Bulk helpers (``bulk_technique_scores``) pre-fetch all scoring data in a
fixed number of aggregated queries so that organisation-wide calculations
@@ -14,7 +15,6 @@ from typing import Optional
from sqlalchemy import case, func
from sqlalchemy.orm import Session
from app.config import settings
from app.models.technique import Technique
from app.models.test import Test
from app.models.detection_rule import DetectionRule
@@ -22,20 +22,12 @@ from app.models.test_detection_result import TestDetectionResult
from app.models.defensive_technique import DefensiveTechniqueMapping
from app.models.threat_actor import ThreatActor, ThreatActorTechnique
from app.models.enums import TestState, TestResult
from app.services.scoring_config_service import get_scoring_weights
# ── Bulk scoring helpers (5 queries for ALL techniques) ───────────────
def _build_empty_stats():
return {
"validated": 0,
"detected": 0,
"platforms": set(),
"latest_validated_at": None,
}
def bulk_technique_scores(db: Session) -> dict:
"""Pre-fetch all scoring data and compute per-technique scores in memory.
@@ -48,11 +40,12 @@ def bulk_technique_scores(db: Session) -> dict:
Returns ``{technique_id: {"total_score": float, "breakdown": dict}}``.
"""
w_tests = settings.SCORING_WEIGHT_TESTS
w_detection = settings.SCORING_WEIGHT_DETECTION_RULES
w_d3fend = settings.SCORING_WEIGHT_D3FEND
w_freshness = settings.SCORING_WEIGHT_FRESHNESS
w_diversity = settings.SCORING_WEIGHT_PLATFORM_DIVERSITY
w = get_scoring_weights(db)
w_tests = w.tests
w_detection = w.detection_rules
w_d3fend = w.d3fend
w_freshness = w.freshness
w_diversity = w.platform_diversity
# Q1: test stats grouped by technique_id
test_rows = (
@@ -242,18 +235,14 @@ def bulk_technique_scores(db: Session) -> dict:
def calculate_technique_score(technique: Technique, db: Session) -> dict:
"""Calculate a 0-100 score for a technique with detailed breakdown.
Weights (configurable via settings):
- tests_validated: weight from SCORING_WEIGHT_TESTS
- detection_rules: weight from SCORING_WEIGHT_DETECTION_RULES
- d3fend_coverage: weight from SCORING_WEIGHT_D3FEND
- freshness: weight from SCORING_WEIGHT_FRESHNESS
- platform_diversity: weight from SCORING_WEIGHT_PLATFORM_DIVERSITY
Weights are read from the ``scoring_config`` table (or env defaults).
"""
w_tests = settings.SCORING_WEIGHT_TESTS
w_detection = settings.SCORING_WEIGHT_DETECTION_RULES
w_d3fend = settings.SCORING_WEIGHT_D3FEND
w_freshness = settings.SCORING_WEIGHT_FRESHNESS
w_diversity = settings.SCORING_WEIGHT_PLATFORM_DIVERSITY
w = get_scoring_weights(db)
w_tests = w.tests
w_detection = w.detection_rules
w_d3fend = w.d3fend
w_freshness = w.freshness
w_diversity = w.platform_diversity
breakdown = {}