Files
Aegis/backend/app/routers/scores.py
T
kitos c99cc4946a refactor(docs+comments): add Google-style docstrings and inline comments across backend
Task D — Google-style docstrings (Args/Returns) on every public function,
method, and class across all 158 Python files in the backend. Zero ruff D
violations (pydocstyle Google convention).

Task E — Explanatory one-line comment before every code line (~11600 new
comments). ruff check passes clean after isort re-sort.
2026-06-10 13:25:14 +02:00

285 lines
9.0 KiB
Python

"""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}