"""OSINT enrichment endpoints — view, review, and trigger enrichment of OSINT items linked to techniques.""" # Import UUID from uuid from uuid import UUID # Import APIRouter, Depends, HTTPException, Query, status from fastapi from fastapi import APIRouter, Depends, HTTPException, Query, status # 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_any_role from app.dependencies.auth from app.dependencies.auth import get_current_user, require_any_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.osint_enrichment_service 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, ) # Import from app.services.osint_enrichment_service from app.services.osint_enrichment_service import ( list_osint_items as service_list_osint_items, ) # Assign router = APIRouter(prefix="/osint", tags=["osint"]) router = APIRouter(prefix="/osint", tags=["osint"]) # ── Schemas ────────────────────────────────────────────────────────── class OsintItemOut(BaseModel): """Serialized OSINT item returned by the API.""" # id: str id: str # technique_id: str technique_id: str # source_type: str source_type: str # source_url: str source_url: str # title: str title: str # description: str | None description: str | None # severity: str | None severity: str | None # discovered_at: str | None discovered_at: str | None # reviewed: bool reviewed: bool # Assign metadata_ = None metadata_: dict | None = None # Define class Config class Config: """ORM mode configuration for SQLAlchemy model mapping.""" # Assign from_attributes = True from_attributes = True # ── Endpoints ──────────────────────────────────────────────────────── @router.get("/items") # Define function list_osint_items def list_osint_items( # Entry: technique_id technique_id: UUID | None = Query(None), # Entry: source_type source_type: str | None = Query(None), # Entry: reviewed reviewed: bool | None = Query(None), # Entry: offset offset: int = Query(0, ge=0), # Entry: limit limit: int = Query(50, ge=1, le=200), # Entry: db db: Session = Depends(get_db), # Entry: user user: User = Depends(get_current_user), ) -> dict: """List OSINT items with optional filters. Args: technique_id (UUID | None): Filter by the technique's UUID. source_type (str | None): Filter by source type (e.g. ``nvd_cve``, ``advisory``). reviewed (bool | None): Filter by review status; ``None`` returns all. offset (int): Number of records to skip for pagination. limit (int): Maximum number of records to return. db (Session): SQLAlchemy database session. user (User): Authenticated user making the request. Returns: list: Serialised list of OSINT item dicts matching the filters. """ # Return service_list_osint_items( return service_list_osint_items( db, # Keyword argument: technique_id technique_id=technique_id, # Keyword argument: source_type source_type=source_type, # Keyword argument: reviewed reviewed=reviewed, # Keyword argument: offset offset=offset, # Keyword argument: limit limit=limit, ) # Apply the @router.get decorator @router.get("/summary") # Define function osint_summary def osint_summary( # Entry: db db: Session = Depends(get_db), # Entry: user user: User = Depends(get_current_user), ) -> dict: """Return summary statistics for OSINT items. Args: db (Session): SQLAlchemy database session. user (User): Authenticated user making the request. Returns: dict: Counts of total, reviewed, and unreviewed items broken down by source type. """ # Return get_osint_summary(db) return get_osint_summary(db) # Apply the @router.post decorator @router.post("/items/{item_id}/review") # Define function review_osint_item def review_osint_item( # Entry: item_id item_id: UUID, # Entry: db db: Session = Depends(get_db), # Entry: user user: User = Depends(get_current_user), ) -> dict: """Mark an OSINT item as reviewed. Args: item_id (UUID): Primary key of the OSINT item to mark reviewed. db (Session): SQLAlchemy database session. user (User): Authenticated user performing the review. Returns: dict: Contains ``id`` (str) and ``reviewed`` (bool ``True``). """ # Open context manager with UnitOfWork(db) as uow: # Assign item = mark_osint_reviewed(db, str(item_id)) item = mark_osint_reviewed(db, str(item_id)) # Check: not item if not item: # Raise HTTPException raise HTTPException( # Keyword argument: status_code status_code=status.HTTP_404_NOT_FOUND, # Keyword argument: detail detail="OSINT item not found", ) # Call uow.commit() uow.commit() # Return {"id": str(item.id), "reviewed": True} return {"id": str(item.id), "reviewed": True} # Apply the @router.post decorator @router.post("/enrich/{technique_id}") # Define function trigger_technique_enrichment def trigger_technique_enrichment( # Entry: technique_id technique_id: UUID, # Entry: db db: Session = Depends(get_db), # Entry: user user: User = Depends(require_any_role("red_lead", "blue_lead")), ) -> dict: """Manually trigger OSINT enrichment for a single technique. Args: technique_id (UUID): Primary key of the technique to enrich. db (Session): SQLAlchemy database session. user (User): Authenticated red_lead or blue_lead requesting enrichment. Returns: dict: Contains ``technique_id`` (str), ``mitre_id`` (str), and ``new_items`` (int). """ # Assign technique = get_technique_or_raise(db, technique_id) technique = get_technique_or_raise(db, technique_id) # Assign count = enrich_technique_with_cves(db, technique) count = enrich_technique_with_cves(db, technique) # Return { return { # Literal argument value "technique_id": str(technique.id), # Literal argument value "mitre_id": technique.mitre_id, # Literal argument value "new_items": count, } # Apply the @router.get decorator @router.get("/technique/{technique_id}") # Define function get_technique_osint def get_technique_osint( # Entry: technique_id technique_id: UUID, # Entry: source_type source_type: str | None = Query(None), # Entry: reviewed reviewed: bool | None = Query(None), # Entry: db db: Session = Depends(get_db), # Entry: user user: User = Depends(get_current_user), ) -> list: """Get all OSINT items for a specific technique. Args: technique_id (UUID): Primary key of the technique. source_type (str | None): Filter by source type (e.g. ``nvd_cve``). reviewed (bool | None): Filter by review status; ``None`` returns all. db (Session): SQLAlchemy database session. user (User): Authenticated user making the request. Returns: list: Dicts with OSINT item fields including source URL, severity, and review status. """ # Assign items = get_osint_items_for_technique( items = get_osint_items_for_technique( db, str(technique_id), # Keyword argument: source_type source_type=source_type, # Keyword argument: reviewed reviewed=reviewed, ) # Return [ return [ { # Literal argument value "id": str(item.id), # Literal argument value "source_type": item.source_type, # Literal argument value "source_url": item.source_url, # Literal argument value "title": item.title, # Literal argument value "description": item.description, # Literal argument value "severity": item.severity, # Literal argument value "discovered_at": item.discovered_at.isoformat() if item.discovered_at else None, # Literal argument value "reviewed": item.reviewed, # Literal argument value "metadata": item.metadata_, } for item in items ]