"""Scoring endpoints — technique, tactic, threat actor, and organization scores. Provides granular scoring with breakdowns and configurable weights. """ # Import Optional from typing from typing import Optional # Import APIRouter, Depends, Query from fastapi from fastapi import APIRouter, Depends, Query # Import BaseModel from pydantic from pydantic import BaseModel # Import Session from sqlalchemy.orm from sqlalchemy.orm import Session # Import get_db from app.database from app.database import get_db # Import get_current_user, require_role from app.dependencies.auth from app.dependencies.auth import get_current_user, require_role # Import UnitOfWork from app.domain.unit_of_work from app.domain.unit_of_work import UnitOfWork # Import User from app.models.user from app.models.user import User # Import from app.services.scoring_config_service from app.services.scoring_config_service import ( get_weights_dict, update_scoring_weights, ) # Import from app.services.scoring_service from app.services.scoring_service import ( calculate_tactic_score, get_score_history, score_actor_by_id, score_technique_by_mitre_id, ) # Assign router = APIRouter(prefix="/scores", tags=["scores"]) router = APIRouter(prefix="/scores", tags=["scores"]) # ── GET /scores/technique/{mitre_id} ───────────────────────────────── @router.get("/technique/{mitre_id}") # Define function score_technique def score_technique( # Entry: mitre_id mitre_id: str, # Entry: db db: Session = Depends(get_db), # Entry: current_user current_user: User = Depends(get_current_user), ) -> dict: """Get detailed score with breakdown for a specific technique. Args: mitre_id (str): MITRE ATT&CK technique ID (e.g. ``T1059``). db (Session): SQLAlchemy database session. current_user (User): Authenticated user making the request. Returns: dict: Score value and component breakdown (tests, detection rules, recency, etc.). """ # Return score_technique_by_mitre_id(db, mitre_id) return score_technique_by_mitre_id(db, mitre_id) # ── GET /scores/tactic/{tactic} ────────────────────────────────────── @router.get("/tactic/{tactic}") # Define function score_tactic def score_tactic( # Entry: tactic tactic: str, # Entry: db db: Session = Depends(get_db), # Entry: current_user current_user: User = Depends(get_current_user), ) -> dict: """Get average score for a tactic. Args: tactic (str): MITRE ATT&CK tactic slug (e.g. ``initial-access``). db (Session): SQLAlchemy database session. current_user (User): Authenticated user making the request. Returns: dict: Average score and per-technique breakdown for the tactic. """ # Return calculate_tactic_score(tactic, db) return calculate_tactic_score(tactic, db) # ── GET /scores/threat-actor/{id} ──────────────────────────────────── @router.get("/threat-actor/{actor_id}") # Define function score_threat_actor def score_threat_actor( # Entry: actor_id actor_id: str, # Entry: db db: Session = Depends(get_db), # Entry: current_user current_user: User = Depends(get_current_user), ) -> dict: """Get coverage score against a specific threat actor. Args: actor_id (str): UUID string of the threat actor to score against. db (Session): SQLAlchemy database session. current_user (User): Authenticated user making the request. Returns: dict: Coverage score and per-technique breakdown for the threat actor. """ # Return score_actor_by_id(db, actor_id) return score_actor_by_id(db, actor_id) # ── GET /scores/organization ───────────────────────────────────────── @router.get("/organization") # Define function score_organization def score_organization( # Entry: db db: Session = Depends(get_db), # Entry: current_user current_user: User = Depends(get_current_user), ) -> dict: """Get the overall organization security score (cached for 5 min). Args: db (Session): SQLAlchemy database session. current_user (User): Authenticated user making the request. Returns: dict: Aggregate organization score with tactic-level breakdowns. """ # Import get_organization_score_cached from app.services.score_cache from app.services.score_cache import get_organization_score_cached # Return get_organization_score_cached(db) return get_organization_score_cached(db) # ── GET /scores/history ────────────────────────────────────────────── @router.get("/history") # Define function score_history def score_history( # Entry: period period: str = Query("90d", pattern="^(30d|90d|1y)$"), # Entry: db db: Session = Depends(get_db), # Entry: current_user current_user: User = Depends(get_current_user), ) -> dict: """Get historical score data points (weekly). Args: period (str): Time window for history — one of ``30d``, ``90d``, or ``1y``. db (Session): SQLAlchemy database session. current_user (User): Authenticated user making the request. Returns: dict: Weekly score data points for the requested period. """ # Return get_score_history(db, period) return get_score_history(db, period) # ── GET /scores/config ─────────────────────────────────────────────── @router.get("/config") # Define function get_scoring_config def get_scoring_config( # Entry: db db: Session = Depends(get_db), # Entry: current_user current_user: User = Depends(require_role("admin")), ) -> dict: """Get current scoring weights (admin only). Args: db (Session): SQLAlchemy database session. current_user (User): Authenticated admin user. Returns: dict: Current weight values for each scoring component. """ # Return get_weights_dict(db) return get_weights_dict(db) # ── PATCH /scores/config ───────────────────────────────────────────── class ScoringConfigUpdate(BaseModel): """Partial update payload for the scoring weight configuration.""" # Assign tests = None tests: Optional[float] = None # Assign detection_rules = None detection_rules: Optional[float] = None # Assign d3fend = None d3fend: Optional[float] = None # Assign recency = None recency: Optional[float] = None # Assign severity = None severity: Optional[float] = None # Assign freshness = None freshness: Optional[float] = None # Assign platform_diversity = None platform_diversity: Optional[float] = None # Apply the @router.patch decorator @router.patch("/config") # Define function update_scoring_config def update_scoring_config( # Entry: payload payload: ScoringConfigUpdate, # Entry: db db: Session = Depends(get_db), # Entry: current_user current_user: User = Depends(require_role("admin")), ) -> dict: """Update scoring weights (admin only). Weights are persisted in the database and survive restarts. Validation enforces that all weights are non-negative and sum to 100. Args: payload (ScoringConfigUpdate): Partial weight update; only set fields are changed. db (Session): SQLAlchemy database session. current_user (User): Authenticated admin user. Returns: dict: Confirmation message plus the full updated weight configuration. """ # Open context manager with UnitOfWork(db) as uow: # Assign result = update_scoring_weights( result = update_scoring_weights( db, # Keyword argument: tests tests=payload.tests, # Keyword argument: detection_rules detection_rules=payload.detection_rules, # Keyword argument: d3fend d3fend=payload.d3fend, # Keyword argument: recency recency=payload.recency, # Keyword argument: severity severity=payload.severity, # Keyword argument: freshness freshness=payload.freshness, # Keyword argument: platform_diversity platform_diversity=payload.platform_diversity, # Keyword argument: updated_by updated_by=current_user.id, ) # Call uow.commit() uow.commit() # Import invalidate from app.services.score_cache from app.services.score_cache import invalidate # Call invalidate() invalidate() # Return {"message": "Scoring config updated", **result} return {"message": "Scoring config updated", **result}