Files
Aegis/backend/app/routers/heatmap.py
T
kitos 9ff0f04ba3 refactor(types): add comprehensive type annotations across backend Python codebase
Enable ANN rules in ruff.toml (flake8-annotations) and resolve all 221 violations:

ANN201/ANN202 — return types on 168 public/private functions:
- All 28 FastAPI routers: endpoints annotated with dict/list/specific schema/
  StreamingResponse/FileResponse/JSONResponse as appropriate
- main.py: lifespan→AsyncGenerator[None,None], exception handlers→JSONResponse
- database.py: get_db→Generator[Session,None,None], proxy methods→correct types
- middleware/request_context.py: dispatch→Response with Callable call_next type

ANN001/ANN002/ANN003 — 32 missing argument types:
- seed_demo.py: all db parameters typed as Session
- domain/unit_of_work.py: __aexit__ exc_type/exc_val/exc_tb typed with TracebackType
- services: audit_service user_id→UUID|None, heatmap_service query/model/builder,
  notification_service test→Test, tempo_service test→Test/user→User,
  test_workflow_service test_id→UUID, campaign_crud **fields→object,
  test_crud **fields→object (4 sites)

ANN401 — 16 Any usages resolved:
- Domain entities (campaign/technique/threat_actor/test_entity): replaced Any with
  actual ORM types via TYPE_CHECKING guards to avoid circular imports
- detection_rule_service: test_id/detection_rule_id/evaluator_id→UUID
- score_cache: kept Any with # noqa: ANN401 (genuinely generic cache)
- jira_service/tempo_service: kept Any with # noqa: ANN401 (lazy optional deps)
- d3fend_import_service: _to_str(v: Any) kept with # noqa: ANN401

ANN204/ANN205/ANN206 — special/static/class methods:
- database.py proxy __call__/__getattr__: *args: object/**kwargs: object
- schemas/test.py model_validate: obj→object, **kwargs→object
- sa_technique_repository._int_type→type

All 439 unit tests pass. ruff check app/ → All checks passed!
2026-06-11 11:06:54 +02:00

106 lines
3.7 KiB
Python

"""Heatmap endpoints — ATT&CK Navigator-compatible layer generation.
Thin router that delegates entirely to :mod:`app.services.heatmap_service`.
No business logic lives here — only request validation and response
formatting.
"""
import io
import json
from typing import Optional
from fastapi import APIRouter, Depends, Query
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from app.database import get_db
from app.dependencies.auth import get_current_user
from app.models.user import User
from app.services import heatmap_service
router = APIRouter(prefix="/heatmap", tags=["heatmap"])
@router.get("/coverage")
def heatmap_coverage(
platforms: Optional[str] = Query(None, description="Comma-separated platforms"),
tactics: Optional[str] = Query(None, description="Comma-separated tactics"),
min_score: int = Query(0, ge=0, le=100),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> dict:
"""Coverage layer — score based on status_global of each technique."""
return heatmap_service.build_coverage_layer(
db, platforms=platforms, tactics=tactics, min_score=min_score,
)
@router.get("/threat-actor/{actor_id}")
def heatmap_threat_actor(
actor_id: str,
platforms: Optional[str] = Query(None),
tactics: Optional[str] = Query(None),
min_score: int = Query(0, ge=0, le=100),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> dict:
"""Threat actor layer — techniques used by an actor with coverage color."""
return heatmap_service.build_threat_actor_layer(
db, actor_id, platforms=platforms, tactics=tactics, min_score=min_score,
)
@router.get("/detection-rules")
def heatmap_detection_rules(
platforms: Optional[str] = Query(None),
tactics: Optional[str] = Query(None),
min_score: int = Query(0, ge=0, le=100),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> dict:
"""Detection rules layer — score based on ratio of rules available vs total."""
return heatmap_service.build_detection_rules_layer(
db, platforms=platforms, tactics=tactics, min_score=min_score,
)
@router.get("/campaign/{campaign_id}")
def heatmap_campaign(
campaign_id: str,
platforms: Optional[str] = Query(None),
tactics: Optional[str] = Query(None),
min_score: int = Query(0, ge=0, le=100),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> dict:
"""Campaign layer — only techniques in the campaign, colored by test state."""
return heatmap_service.build_campaign_layer(
db, campaign_id, platforms=platforms, tactics=tactics, min_score=min_score,
)
@router.get("/export-navigator")
def export_navigator(
layer: str = Query(..., description="Layer type: coverage, threat-actor, detection-rules, campaign"),
layer_id: Optional[str] = Query(None, description="Actor ID or Campaign ID (if applicable)"),
platforms: Optional[str] = Query(None),
tactics: Optional[str] = Query(None),
min_score: int = Query(0, ge=0, le=100),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> StreamingResponse:
"""Export a heatmap layer as a downloadable JSON file for ATT&CK Navigator."""
data = heatmap_service.build_navigator_export(
db, layer, layer_id=layer_id,
platforms=platforms, tactics=tactics, min_score=min_score,
)
json_content = json.dumps(data, indent=2, default=str)
buffer = io.BytesIO(json_content.encode("utf-8"))
return StreamingResponse(
buffer,
media_type="application/json",
headers={"Content-Disposition": f"attachment; filename=aegis_{layer}_layer.json"},
)