c99cc4946a
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.
289 lines
8.9 KiB
Python
289 lines
8.9 KiB
Python
"""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),
|
|
) -> list:
|
|
"""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
|
|
]
|