feat(scoring): composite recency decay and severity weights persisted in DB [FASE-5.1]
This commit is contained in:
@@ -9,7 +9,8 @@ number of SQL queries regardless of technique count.
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -43,6 +44,11 @@ def serialize_snapshot_summary(snap: CoverageSnapshot) -> dict:
|
||||
"not_covered_count": snap.not_covered_count,
|
||||
"in_progress_count": snap.in_progress_count,
|
||||
"not_evaluated_count": snap.not_evaluated_count,
|
||||
"coverage_percentage": getattr(snap, "coverage_percentage", 0.0),
|
||||
"by_tactic": getattr(snap, "by_tactic", None) or {},
|
||||
"by_status": getattr(snap, "by_status", None) or {},
|
||||
"stale_count": getattr(snap, "stale_count", 0),
|
||||
"never_tested_count": getattr(snap, "never_tested_count", 0),
|
||||
"created_by": str(snap.created_by) if snap.created_by else None,
|
||||
"created_at": snap.created_at.isoformat() if snap.created_at else None,
|
||||
}
|
||||
@@ -148,6 +154,13 @@ def create_snapshot(
|
||||
not_covered_count = 0
|
||||
in_progress_count = 0
|
||||
not_evaluated_count = 0
|
||||
stale_count = 0
|
||||
never_tested_count = 0
|
||||
|
||||
by_tactic: dict[str, dict] = defaultdict(
|
||||
lambda: {"total": 0, "validated": 0, "partial": 0, "score_sum": 0.0}
|
||||
)
|
||||
by_status: dict[str, int] = defaultdict(int)
|
||||
|
||||
technique_rows: list[dict] = []
|
||||
|
||||
@@ -170,15 +183,43 @@ def create_snapshot(
|
||||
not_evaluated_count += 1
|
||||
|
||||
entry = scores_map.get(tech.id, {})
|
||||
score = entry.get("total_score", 0)
|
||||
technique_rows.append({
|
||||
"technique_id": tech.id,
|
||||
"mitre_id": tech.mitre_id,
|
||||
"status": status_value,
|
||||
"score": entry.get("total_score", 0),
|
||||
"score": score,
|
||||
})
|
||||
|
||||
by_status[status_value] += 1
|
||||
tactic_key = tech.tactic or "unknown"
|
||||
bucket = by_tactic[tactic_key]
|
||||
bucket["total"] += 1
|
||||
bucket["score_sum"] += score
|
||||
if status_value == "validated":
|
||||
bucket["validated"] += 1
|
||||
elif status_value == "partial":
|
||||
bucket["partial"] += 1
|
||||
|
||||
if status_value == "not_evaluated":
|
||||
never_tested_count += 1
|
||||
if tech.review_required:
|
||||
stale_count += 1
|
||||
|
||||
org_data = calculate_organization_score(db)
|
||||
org_score = org_data.get("overall_score", 0)
|
||||
total_techniques = len(techniques) or 1
|
||||
coverage_pct = round((validated_count / total_techniques) * 100, 1)
|
||||
|
||||
by_tactic_out = {
|
||||
tactic: {
|
||||
"total": data["total"],
|
||||
"validated": data["validated"],
|
||||
"partial": data["partial"],
|
||||
"average_score": round(data["score_sum"] / data["total"], 1) if data["total"] else 0,
|
||||
}
|
||||
for tactic, data in by_tactic.items()
|
||||
}
|
||||
|
||||
snapshot = CoverageSnapshot(
|
||||
name=name,
|
||||
@@ -189,6 +230,11 @@ def create_snapshot(
|
||||
not_covered_count=not_covered_count,
|
||||
in_progress_count=in_progress_count,
|
||||
not_evaluated_count=not_evaluated_count,
|
||||
coverage_percentage=coverage_pct,
|
||||
by_tactic=by_tactic_out,
|
||||
by_status=dict(by_status),
|
||||
stale_count=stale_count,
|
||||
never_tested_count=never_tested_count,
|
||||
created_by=user_id,
|
||||
)
|
||||
db.add(snapshot)
|
||||
@@ -320,6 +366,37 @@ def compare_snapshots(
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Coverage evolution (trends)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def get_coverage_evolution(db: Session, *, months: int = 12) -> list[dict]:
|
||||
"""Return snapshot trend points for the last *months* months."""
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(days=months * 30)
|
||||
snapshots = (
|
||||
db.query(CoverageSnapshot)
|
||||
.filter(CoverageSnapshot.created_at >= cutoff)
|
||||
.order_by(CoverageSnapshot.created_at.asc())
|
||||
.all()
|
||||
)
|
||||
return [
|
||||
{
|
||||
"date": snap.created_at.isoformat() if snap.created_at else None,
|
||||
"name": snap.name,
|
||||
"org_score": snap.organization_score,
|
||||
"coverage_pct": getattr(snap, "coverage_percentage", 0.0),
|
||||
"by_tactic": getattr(snap, "by_tactic", None) or {},
|
||||
"by_status": getattr(snap, "by_status", None) or {},
|
||||
"stale_count": getattr(snap, "stale_count", 0),
|
||||
"never_tested_count": getattr(snap, "never_tested_count", 0),
|
||||
"validated_count": snap.validated_count,
|
||||
"total_techniques": snap.total_techniques,
|
||||
}
|
||||
for snap in snapshots
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cleanup
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user