Files
Aegis/backend/app/routers/osint.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

144 lines
4.2 KiB
Python

"""OSINT enrichment endpoints — view, review, and trigger enrichment of
OSINT items (CVEs, advisories, etc.) linked to techniques.
"""
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel
from sqlalchemy.orm import Session
from app.database import get_db
from app.dependencies.auth import get_current_user, require_any_role
from app.domain.unit_of_work import UnitOfWork
from app.models.user import User
from app.services.osint_enrichment_service import (
enrich_technique_with_cves,
get_osint_items_for_technique,
get_osint_summary,
get_technique_or_raise,
mark_osint_reviewed,
)
from app.services.osint_enrichment_service import (
list_osint_items as service_list_osint_items,
)
router = APIRouter(prefix="/osint", tags=["osint"])
# ── Schemas ──────────────────────────────────────────────────────────
class OsintItemOut(BaseModel):
id: str
technique_id: str
source_type: str
source_url: str
title: str
description: str | None
severity: str | None
discovered_at: str | None
reviewed: bool
metadata_: dict | None = None
class Config:
from_attributes = True
# ── Endpoints ────────────────────────────────────────────────────────
@router.get("/items")
def list_osint_items(
technique_id: UUID | None = Query(None),
source_type: str | None = Query(None),
reviewed: bool | None = Query(None),
offset: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=200),
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
) -> list:
"""List OSINT items with optional filters."""
return service_list_osint_items(
db,
technique_id=technique_id,
source_type=source_type,
reviewed=reviewed,
offset=offset,
limit=limit,
)
@router.get("/summary")
def osint_summary(
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
) -> dict:
"""Summary statistics for OSINT items."""
return get_osint_summary(db)
@router.post("/items/{item_id}/review")
def review_osint_item(
item_id: UUID,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
) -> dict:
"""Mark an OSINT item as reviewed."""
with UnitOfWork(db) as uow:
item = mark_osint_reviewed(db, str(item_id))
if not item:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="OSINT item not found",
)
uow.commit()
return {"id": str(item.id), "reviewed": True}
@router.post("/enrich/{technique_id}")
def trigger_technique_enrichment(
technique_id: UUID,
db: Session = Depends(get_db),
user: User = Depends(require_any_role("red_lead", "blue_lead")),
) -> dict:
"""Manually trigger OSINT enrichment for a single technique."""
technique = get_technique_or_raise(db, technique_id)
count = enrich_technique_with_cves(db, technique)
return {
"technique_id": str(technique.id),
"mitre_id": technique.mitre_id,
"new_items": count,
}
@router.get("/technique/{technique_id}")
def get_technique_osint(
technique_id: UUID,
source_type: str | None = Query(None),
reviewed: bool | None = Query(None),
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
) -> list:
"""Get all OSINT items for a specific technique."""
items = get_osint_items_for_technique(
db,
str(technique_id),
source_type=source_type,
reviewed=reviewed,
)
return [
{
"id": str(item.id),
"source_type": item.source_type,
"source_url": item.source_url,
"title": item.title,
"description": item.description,
"severity": item.severity,
"discovered_at": item.discovered_at.isoformat() if item.discovered_at else None,
"reviewed": item.reviewed,
"metadata": item.metadata_,
}
for item in items
]