T-109: Rewrite tests router with full Red/Blue workflow endpoints - list with filters, create from template, Red/Blue team updates with state guards, start-execution, submit-red, submit-blue, validate-red, validate-blue, reopen, and timeline. All using workflow service from Phase 11. T-110: Rewrite evidence router with Red/Blue separation - upload with team field, list with team filter, delete with state-based permissions. Red Team edits in draft/red_executing, Blue Team in blue_evaluating, admin bypasses all. T-111: Create test_templates router with full CRUD - paginated list with source/platform/severity/search filters, by-technique lookup, admin-only create/update, and soft delete. Registered in main.py. T-112: Add POST /system/import-atomic-tests endpoint to system router - admin-only trigger for Atomic Red Team import with error handling and statistics response. Includes validation tests for all four tasks (35 checks total).
106 lines
4.0 KiB
Python
106 lines
4.0 KiB
Python
import logging
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI, Request, status
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import JSONResponse
|
|
from fastapi.exceptions import RequestValidationError
|
|
from sqlalchemy.exc import SQLAlchemyError
|
|
|
|
from app.routers import auth as auth_router
|
|
from app.routers import techniques as techniques_router
|
|
from app.routers import tests as tests_router
|
|
from app.routers import evidence as evidence_router
|
|
from app.routers import test_templates as test_templates_router
|
|
from app.routers import system as system_router
|
|
from app.routers import metrics as metrics_router
|
|
from app.routers import users as users_router
|
|
from app.routers import audit as audit_router
|
|
from app.storage import ensure_bucket_exists
|
|
from app.jobs.mitre_sync_job import start_scheduler, scheduler
|
|
|
|
# ── Logging ───────────────────────────────────────────────────────────────
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s %(levelname)-8s %(name)s — %(message)s",
|
|
)
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
"""Startup / shutdown logic."""
|
|
ensure_bucket_exists()
|
|
start_scheduler()
|
|
yield
|
|
# Graceful shutdown of the background scheduler
|
|
scheduler.shutdown(wait=False)
|
|
|
|
|
|
app = FastAPI(title="Attack Coverage Platform", lifespan=lifespan)
|
|
|
|
# ── CORS ──────────────────────────────────────────────────────────────────
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["http://localhost:3000", "http://localhost:5173"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# ── Routers ──────────────────────────────────────────────────────────────
|
|
app.include_router(auth_router.router, prefix="/api/v1")
|
|
app.include_router(techniques_router.router, prefix="/api/v1")
|
|
app.include_router(tests_router.router, prefix="/api/v1")
|
|
app.include_router(evidence_router.router, prefix="/api/v1")
|
|
app.include_router(test_templates_router.router, prefix="/api/v1")
|
|
app.include_router(system_router.router, prefix="/api/v1")
|
|
app.include_router(metrics_router.router, prefix="/api/v1")
|
|
app.include_router(users_router.router, prefix="/api/v1")
|
|
app.include_router(audit_router.router, prefix="/api/v1")
|
|
|
|
|
|
@app.get("/health")
|
|
def health():
|
|
return {"status": "ok"}
|
|
|
|
|
|
# ── Exception Handlers ────────────────────────────────────────────────────
|
|
|
|
|
|
@app.exception_handler(RequestValidationError)
|
|
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
|
"""Handle validation errors with consistent format."""
|
|
return JSONResponse(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
content={
|
|
"detail": "Validation error",
|
|
"code": "VALIDATION_ERROR",
|
|
"errors": exc.errors(),
|
|
},
|
|
)
|
|
|
|
|
|
@app.exception_handler(SQLAlchemyError)
|
|
async def sqlalchemy_exception_handler(request: Request, exc: SQLAlchemyError):
|
|
"""Handle database errors."""
|
|
logging.error(f"Database error: {exc}")
|
|
return JSONResponse(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
content={
|
|
"detail": "Database error occurred",
|
|
"code": "DATABASE_ERROR",
|
|
},
|
|
)
|
|
|
|
|
|
@app.exception_handler(Exception)
|
|
async def general_exception_handler(request: Request, exc: Exception):
|
|
"""Handle all unhandled exceptions."""
|
|
logging.error(f"Unhandled exception: {exc}")
|
|
return JSONResponse(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
content={
|
|
"detail": "An internal server error occurred",
|
|
"code": "INTERNAL_ERROR",
|
|
},
|
|
)
|