""" Voll-Audit Findings Router — unified view across all 4 finding sources. Endpoint: GET /api/compliance/agent/findings/{check_id} ?source=mc|pflichtangabe|vendor|redundanz|all &severity=CRITICAL|HIGH|MEDIUM|LOW|INFO|all &doc_type=impressum|dse|cookie|...|all &status=failed|passed|skipped|na|info|all &q= &limit= Liefert summary + filtered findings list. Frontend rendert daraus den Voll-Audit-Tab unter /sdk/agent/audit/. """ from __future__ import annotations import logging from urllib.parse import urlparse from fastapi import APIRouter, HTTPException, Query from compliance.services.unified_findings_store import ( findings_summary, list_findings, ) from compliance.services.compliance_audit_log import get_check_run logger = logging.getLogger(__name__) router = APIRouter(prefix="/compliance/agent", tags=["agent"]) def _normalize_domain(d: str) -> str: if not d: return "" if "://" not in d: d = "https://" + d host = urlparse(d).netloc.lower() return host[4:] if host.startswith("www.") else host @router.get("/findings/{check_id}") def get_findings( check_id: str, source: str | None = Query(None, description="mc|pflichtangabe|vendor|redundanz|all"), severity: str | None = Query(None, description="CRITICAL|HIGH|MEDIUM|LOW|INFO|all"), doc_type: str | None = Query(None), status: str | None = Query(None, description="failed|passed|skipped|na|info|all"), q: str | None = Query(None, description="freitext-suche label/vendor"), limit: int = Query(1000, ge=1, le=5000), expected_domain: str | None = Query( None, description="Hard-Assertion: Run muss zu dieser Domain gehoeren (Cross-Tenant-Schutz)", ), ) -> dict: """Return aggregated findings + summary counters for a check run.""" # P7-Restpunkt: optionale Domain-Assertion. Verhindert dass ein Frontend # einen check_id einer fremden Tenant-Domain anfragen kann. if expected_domain: run = get_check_run(check_id) actual = _normalize_domain((run or {}).get("base_domain") or "") if not run or actual != _normalize_domain(expected_domain): raise HTTPException( status_code=403, detail=f"Cross-tenant access blocked: check_id {check_id} " f"gehoert zu Domain '{actual or '?'}', angefragt: " f"'{_normalize_domain(expected_domain)}'", ) try: summary = findings_summary(check_id) findings = list_findings( check_id=check_id, source_type=source, severity=severity, doc_type=doc_type, status=status, q=q, limit=limit, ) return { "found": summary.get("total", 0) > 0, "check_id": check_id, "summary": summary, "filter": { "source": source or "all", "severity": severity or "all", "doc_type": doc_type or "all", "status": status or "all", "q": q or "", "limit": limit, }, "count": len(findings), "findings": findings, } except Exception as e: logger.exception("get_findings failed for %s", check_id) return { "found": False, "check_id": check_id, "error": str(e)[:200], "summary": {}, "count": 0, "findings": [], }