"""Stale coverage detection — marks techniques whose last validated test is older than a configurable threshold. This is the simple version. The full Decay Engine (Fase 8) will replace this with a multi-factor, configurable decay model with confidence scores. """ # Import logging import logging # Import datetime, timedelta, timezone from datetime from datetime import datetime, timedelta, timezone # Import func from sqlalchemy from sqlalchemy import func # Import Session from sqlalchemy.orm from sqlalchemy.orm import Session # Import settings from app.config from app.config import settings # Import TechniqueStatus, TestState from app.models.enums from app.models.enums import TechniqueStatus, TestState # Import Technique from app.models.technique from app.models.technique import Technique # Import Test from app.models.test from app.models.test import Test # Assign logger = logging.getLogger(__name__) logger = logging.getLogger(__name__) # Assign STALE_THRESHOLD_DAYS = settings.STALE_THRESHOLD_DAYS STALE_THRESHOLD_DAYS = settings.STALE_THRESHOLD_DAYS # Define function detect_stale_coverage def detect_stale_coverage(db: Session) -> int: """Scan all techniques and flag those with stale coverage. A technique is considered stale when: - It has a status other than ``not_evaluated``, AND - Its most recent *validated* test is older than *STALE_THRESHOLD_DAYS*, OR - It has never had a validated test (but has been manually marked as covered/partial). Args: db (Session): Active SQLAlchemy database session. Returns: int: Number of techniques newly flagged as stale (``review_required`` set to ``True``) in this run. """ # Assign cutoff = datetime.now(timezone.utc) - timedelta(days=STALE_THRESHOLD_DAYS) cutoff = datetime.now(timezone.utc) - timedelta(days=STALE_THRESHOLD_DAYS) # Assign last_validated = func.coalesce( last_validated = func.coalesce( Test.blue_validated_at, Test.red_validated_at, Test.created_at, ) # Subquery: latest validated test date per technique latest_test = ( db.query( Test.technique_id, func.max(last_validated).label("last_tested"), ) # Chain .filter() call .filter(Test.state == TestState.validated) # Chain .group_by() call .group_by(Test.technique_id) # Chain .subquery() call .subquery() ) # Find techniques that are stale stale_techniques = ( db.query(Technique) # Chain .outerjoin() call .outerjoin(latest_test, Technique.id == latest_test.c.technique_id) # Chain .filter() call .filter( # Either tested before cutoff, or never tested at all (latest_test.c.last_tested < cutoff) | (latest_test.c.last_tested.is_(None)) ) # Chain .filter() call .filter( # Only flag techniques that have a real status (not never-evaluated ones) Technique.status_global != TechniqueStatus.not_evaluated ) # Chain .all() call .all() ) # Assign count = 0 count = 0 # Iterate over stale_techniques for tech in stale_techniques: # Check: not tech.review_required if not tech.review_required: # Assign tech.review_required = True tech.review_required = True # Assign count = 1 count += 1 # Log info: "Marked %s as stale coverage", tech.mitre_id logger.info("Marked %s as stale coverage", tech.mitre_id) # Check: count > 0 if count > 0: # Commit all pending changes to the database db.commit() # Log info: logger.info( # Literal argument value "Stale coverage detection complete — %d techniques flagged", count ) # Fallback: handle remaining cases else: # Log info: "Stale coverage detection complete — no new stale logger.info("Stale coverage detection complete — no new stale techniques") # Return count return count