feat(scoring): composite recency decay and severity weights persisted in DB [FASE-5.1]
This commit is contained in:
@@ -18,7 +18,10 @@ from app.models.defensive_technique import DefensiveTechnique, DefensiveTechniqu
|
||||
from app.models.compliance import ComplianceFramework, ComplianceControl, ComplianceControlMapping
|
||||
from app.models.audit import AuditLog
|
||||
from app.models.enums import TestState, TestResult, TechniqueStatus
|
||||
from app.models.scoring_config import ScoringConfig
|
||||
from app.services.scoring_config_service import get_scoring_weights, update_scoring_weights
|
||||
from app.services.scoring_service import (
|
||||
_recency_factor,
|
||||
calculate_technique_score,
|
||||
calculate_tactic_score,
|
||||
calculate_organization_score,
|
||||
@@ -175,9 +178,11 @@ class TestScoring:
|
||||
assert result["breakdown"]["tests_validated"]["score"] > 0
|
||||
|
||||
def test_technique_score_no_tests(self, db, sample_technique_no_tests):
|
||||
"""Técnica sin tests → score 0."""
|
||||
"""Técnica sin tests → solo puede puntuar severidad por defecto."""
|
||||
result = calculate_technique_score(sample_technique_no_tests, db)
|
||||
assert result["total_score"] == 0
|
||||
assert result["breakdown"]["tests_validated"]["score"] == 0
|
||||
assert result["breakdown"]["recency"]["score"] == 0
|
||||
assert result["total_score"] == result["breakdown"]["severity"]["score"]
|
||||
|
||||
def test_technique_score_partial_detection(self, db, sample_technique, validated_tests):
|
||||
"""Técnica con detección parcial → score intermedio."""
|
||||
@@ -187,8 +192,8 @@ class TestScoring:
|
||||
breakdown = result["breakdown"]
|
||||
assert "2/3" in breakdown["tests_validated"]["detail"]
|
||||
|
||||
def test_technique_score_freshness_penalty(self, db, sample_technique, admin_user):
|
||||
"""Tests > 180 días → penalización en freshness."""
|
||||
def test_technique_score_recency_penalty(self, db, sample_technique, admin_user):
|
||||
"""Tests > 180 días → factor de recencia reducido."""
|
||||
old_date = datetime.utcnow() - timedelta(days=200)
|
||||
test = Test(
|
||||
technique_id=sample_technique.id,
|
||||
@@ -198,14 +203,53 @@ class TestScoring:
|
||||
created_by=admin_user.id,
|
||||
platform="windows",
|
||||
red_validated_at=old_date,
|
||||
blue_validated_at=old_date,
|
||||
)
|
||||
db.add(test)
|
||||
db.commit()
|
||||
|
||||
result = calculate_technique_score(sample_technique, db)
|
||||
# Freshness should be 0 for tests > 180 days old
|
||||
assert result["breakdown"]["freshness"]["score"] == 0
|
||||
assert "200" in result["breakdown"]["freshness"]["detail"]
|
||||
assert result["breakdown"]["recency"]["score"] == 5.0 # 0.5 * 10 (181–365 días)
|
||||
assert "200" in result["breakdown"]["recency"]["detail"]
|
||||
|
||||
def test_recency_recent_scores_higher_than_old(self, db, sample_technique, admin_user):
|
||||
"""Mismo resultado de detección: test reciente puntúa más que uno de hace 1 año."""
|
||||
now = datetime.utcnow()
|
||||
for days_ago in (1, 400):
|
||||
db.add(
|
||||
Test(
|
||||
technique_id=sample_technique.id,
|
||||
name=f"Recency {days_ago}",
|
||||
state=TestState.validated,
|
||||
detection_result=TestResult.detected,
|
||||
created_by=admin_user.id,
|
||||
platform="windows",
|
||||
red_validated_at=now - timedelta(days=days_ago),
|
||||
blue_validated_at=now - timedelta(days=days_ago),
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
result = calculate_technique_score(sample_technique, db)
|
||||
assert _recency_factor(now - timedelta(days=1)) == 1.0
|
||||
assert _recency_factor(now - timedelta(days=400)) == 0.2
|
||||
assert result["breakdown"]["recency"]["score"] == 10.0
|
||||
|
||||
def test_scoring_weights_persist_in_database(self, db, sample_technique, validated_tests):
|
||||
"""Cambiar pesos en BD se refleja en el breakdown."""
|
||||
update_scoring_weights(db, tests=50, detection_rules=20, d3fend=10, recency=10, severity=10)
|
||||
db.commit()
|
||||
|
||||
score = calculate_technique_score(sample_technique, db)
|
||||
assert score["breakdown"]["tests_validated"]["max"] == 50
|
||||
|
||||
row = db.query(ScoringConfig).first()
|
||||
assert row is not None
|
||||
assert row.weight_tests == 50
|
||||
|
||||
update_scoring_weights(db, tests=40, detection_rules=25, d3fend=15, recency=10, severity=10)
|
||||
db.commit()
|
||||
assert get_scoring_weights(db).tests == 40
|
||||
|
||||
def test_scoring_weights_configurable(self, db, sample_technique, validated_tests):
|
||||
"""Scoring weights are reflected in the breakdown max values."""
|
||||
|
||||
Reference in New Issue
Block a user