"""Rotas segurança wizard — Spec 021.""" from __future__ import annotations from typing import Any from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request from pydantic import BaseModel, Field from app import auth, security_store from app.permissions import can_read_audit_overview router = APIRouter(prefix="/api/v1/security", tags=["wizard-security"]) class SecurityWebhookBody(BaseModel): event: str = Field(..., min_length=3) domain: str | None = None session_id: str | None = None data: dict | None = None class SecurityAuditTestBody(BaseModel): field: str = "domain" value: str = Field(..., min_length=1) def _require_security_reader(user: auth.DeskUser = Depends(auth.get_current_user)) -> auth.DeskUser: if not can_read_audit_overview(user.role): raise HTTPException(403, "permissão insuficiente") return user def _client_ip(request: Request) -> str | None: forwarded = request.headers.get("x-forwarded-for") if forwarded: return forwarded.split(",")[0].strip() if request.client: return request.client.host return None def _module_enabled() -> bool: from app.modules import store as module_store return module_store.is_module_enabled("wizard-security") @router.post("/csp-report") async def csp_report(request: Request): if not _module_enabled(): return {"accepted": False, "reason": "module_disabled"} try: body = await request.json() except Exception: body = {} conn = auth.db() try: return security_store.ingest_csp_report(conn, body if isinstance(body, dict) else {}, _client_ip(request)) finally: conn.close() @router.post("/webhook") def security_webhook( body: SecurityWebhookBody, request: Request, x_webhook_secret: str | None = Header(default=None), ): from app.main import INTEGRATION_SECRETS, _verify_secret if not _module_enabled(): return {"accepted": False, "reason": "module_disabled"} _verify_secret("onboard", x_webhook_secret) if not security_store.is_security_event(body.event): raise HTTPException(400, "event must start with security.") conn = auth.db() try: return security_store.ingest_event( conn, event=body.event, session_id=body.session_id, domain=body.domain, data=body.data, client_ip=_client_ip(request), ) finally: conn.close() @router.get("/summary") def security_summary( window_hours: int = Query(24, ge=1, le=168), user: auth.DeskUser = Depends(_require_security_reader), ): if not _module_enabled(): return {"enabled": False, "window_hours": window_hours, "total": 0} conn = auth.db() try: return security_store.build_summary(conn, window_hours=window_hours) finally: conn.close() @router.get("/events") def security_events( limit: int = Query(100, ge=1, le=500), offset: int = Query(0, ge=0), window_hours: int = Query(168, ge=1, le=720), session_id: str = "", user: auth.DeskUser = Depends(_require_security_reader), ): if not _module_enabled(): return {"events": [], "total": 0, "enabled": False} conn = auth.db() try: return security_store.list_events( conn, limit=limit, offset=offset, window_hours=window_hours, session_id=session_id.strip() or None, ) finally: conn.close() @router.post("/audit-test") def security_audit_test( body: SecurityAuditTestBody, user: auth.DeskUser = Depends(_require_security_reader), ): """Teste interno — simula heurística de input (sem gravar).""" if user.role not in ("super_admin", "ops_lead"): raise HTTPException(403, "apenas admin") return security_store.audit_field_value(body.value, field=body.field)