ec26183e2e
- ruff.toml: select E/W/F/I/N rules, line-length=120, drop legacy ignores - Auto-fix: sort 82 import blocks (isort), remove 29 unused imports, strip 6 trailing-whitespace blank lines in docstrings - main.py: move setup_logging and settings imports to top (E402) - errors.py: noqa N818 on DDD exception names (96 call sites, safe) - intel_service.py: noqa N817 for universal ET alias - atomic/elastic/sigma import services: move _MAX_UNCOMPRESSED_SIZE and _MAX_ENTRIES to module level (N806) - compliance_import_service.py: move SAMPLE_CONTROLS / CIS_CONTROLS to module level; wrap long description strings (N806 + E501) - snapshot_service.py: move STATUS_ORDER dict to module level (N806) - sigma_import_service.py: remove dead dedup_key expression (F841) - threat_actor_import_service.py: remove dead stix_to_actor expression (F841) - data_source.py, seed_demo.py, campaign_scheduler_service.py, lolbas_import_service.py: wrap lines exceeding 120 chars (E501) - d3fend_import_service.py: per-file E501 ignore (data file with long strings) All 439 unit tests pass. ruff check app/ → All checks passed! Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
144 lines
4.2 KiB
Python
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 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),
|
|
):
|
|
"""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),
|
|
):
|
|
"""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")),
|
|
):
|
|
"""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),
|
|
):
|
|
"""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
|
|
]
|