{
  "product": "Verdikt",
  "generated_at": "2026-09-17T04:00:00Z",
  "commit": "a574942",
  "summary": {
    "verdict": "APPROVED",
    "controls_shipped": 27,
    "controls_pending": 1,
    "open_items": 3,
    "risk_level": "LOW",
    "license": "MIT",
    "last_audit": "2026-09-17"
  },
  "posture": [
    {
      "principle": "AI-in, deterministic-out",
      "why_it_matters": "No language model sits on the code path that generates the audited artifact. Same input \u2192 same package, forever. Enforced mechanically, not in comments.",
      "enforced_by": "tests/test_boundaries.py::test_core_never_imports_ai_layer"
    },
    {
      "principle": "One-way substrate dependency",
      "why_it_matters": "docgen never imports pap_pipeline. Reviewers can rip out the pipeline without breaking Quire.",
      "enforced_by": "tests/test_boundaries.py::test_docgen_never_imports_pap_pipeline"
    },
    {
      "principle": "Merge-only reply path",
      "why_it_matters": "The reply letter is filled by a template merge over structured pydantic Decision. No LLM between decision and outbound bytes.",
      "enforced_by": "test_pap_inbox_hook.py"
    },
    {
      "principle": "Tenant scope by default",
      "why_it_matters": "Every tenant-scoped model requires a tenant_id filter within 40 lines. Suppressions are documented at the call site.",
      "enforced_by": "scripts/tenant_scope_audit.py (CI gate)"
    }
  ],
  "controls": [
    {
      "owasp": "A01 Broken Access Control",
      "control": "Multi-tenant scope audit (CI gate)",
      "status": "shipped",
      "evidence": "scripts/tenant_scope_audit.py (6 file/line suppressions justified)"
    },
    {
      "owasp": "A01 Broken Access Control",
      "control": "RBAC role model + middleware enforcement",
      "status": "shipped",
      "evidence": "test_rbac.py"
    },
    {
      "owasp": "A01 Broken Access Control",
      "control": "IDOR-shaped tenant-hop returns 404 (not 403)",
      "status": "shipped",
      "evidence": "test_ssrf_and_idor.py"
    },
    {
      "owasp": "A02 Cryptographic Failures",
      "control": "API keys hashed at rest (SHA-256); return-once creation UX",
      "status": "shipped",
      "evidence": "src/docgen/auth.py :: hash_api_key + admin_create_key flow"
    },
    {
      "owasp": "A02 Cryptographic Failures",
      "control": "Password hashing (argon2id)",
      "status": "shipped",
      "evidence": "src/docgen/auth.py"
    },
    {
      "owasp": "A02 Cryptographic Failures",
      "control": "Session cookies HttpOnly + SameSite=Lax + Secure (proxy-aware)",
      "status": "shipped",
      "evidence": "secure_cookie_flag() computes Secure from scheme / X-Forwarded-Proto / env"
    },
    {
      "owasp": "A03 Injection",
      "control": "Caller-profile mapping engine (closed grammar, no eval)",
      "status": "shipped",
      "evidence": "Threat modelled 2026-09-03 (roadmap Security). Strict regex source-path parser; explicit-dispatch transforms."
    },
    {
      "owasp": "A03 Injection",
      "control": "DOCX magic-byte + macro sanitiser",
      "status": "shipped",
      "evidence": "src/docgen/core/docx_sanitize.py (rejects non-ZIP, strips vbaProject.bin)"
    },
    {
      "owasp": "A03 Injection",
      "control": "PDF preflight (text-less flat PDFs rejected at router boundary)",
      "status": "shipped",
      "evidence": "designtime/preflight_pdf.py + upload.py :: PdfPreflightRejected"
    },
    {
      "owasp": "A03 Injection",
      "control": "Triage-routing write allowlist (routes constrained to a server-side tuple)",
      "status": "shipped",
      "evidence": "src/docgen/ui/routes.py :: _TRIAGE_ROUTES + admin_save_tenant_triage_routing + program_triage_routing_save (allowlisted 2026-09-17)"
    },
    {
      "owasp": "A03 Injection",
      "control": "Triage LLM classifier response bounded to routing-table vocabulary",
      "status": "shipped",
      "evidence": "src/pap_pipeline/stages/triage.py :: _llm_classify (rejects case_type outside allowed_vocab; confidence/reason capped; falls through to deterministic rules on any parse failure)"
    },
    {
      "owasp": "A04 Insecure Design",
      "control": "Deterministic render boundary (no model at render time)",
      "status": "shipped",
      "evidence": "tests/test_boundaries.py (core never imports providers)"
    },
    {
      "owasp": "A05 Security Misconfiguration",
      "control": "Security headers: CSP + HSTS + X-Frame-Options DENY + Referrer-Policy + Permissions-Policy",
      "status": "shipped",
      "evidence": "SecurityHeadersMiddleware + test_security_headers.py"
    },
    {
      "owasp": "A05 Security Misconfiguration",
      "control": "CSP frame-ancestors 'none' + object-src 'none'",
      "status": "shipped",
      "evidence": "SecurityHeadersMiddleware (/docs relaxed only for Swagger CDN)"
    },
    {
      "owasp": "A05 Security Misconfiguration",
      "control": "CSRF middleware + per-session token",
      "status": "shipped",
      "evidence": "test_csrf.py"
    },
    {
      "owasp": "A06 Vulnerable Components",
      "control": "License scan (\u00a712) allowlist + explicit exceptions",
      "status": "shipped",
      "evidence": "scripts/license_check.py (MIT/BSD/Apache/PSF allowlist; LGPL deps in EXPLICIT_EXCEPTIONS with justification)"
    },
    {
      "owasp": "A06 Vulnerable Components",
      "control": "AGPL retirement (pymupdf swapped to pypdfium2 + pdfplumber)",
      "status": "shipped",
      "evidence": "ADR-0001 superseded 2026-09-09"
    },
    {
      "owasp": "A06 Vulnerable Components",
      "control": "Dependency CVE scan (pip-audit) in CI",
      "status": "shipped",
      "evidence": "pip-audit --strict --skip-editable in .github/workflows/ci.yml. Zero known vulns at HEAD; CVE-safe floors codified in pyproject.toml."
    },
    {
      "owasp": "A07 Auth Failures",
      "control": "Login rate limit (sliding-window Redis, 10/15min per (ip, username))",
      "status": "shipped",
      "evidence": "src/docgen/ratelimit.py + test_login_ratelimit.py (429 even on correct password)"
    },
    {
      "owasp": "A07 Auth Failures",
      "control": "Failed-login audit log",
      "status": "shipped",
      "evidence": "admin_login_failed event with username + ip + request_id"
    },
    {
      "owasp": "A08 Software Integrity",
      "control": "Frozen template versions (content-addressed hash immutable)",
      "status": "shipped",
      "evidence": "designtime/freeze.py (version_hash locks the source bytes + canonical placements)"
    },
    {
      "owasp": "A08 Software Integrity",
      "control": "Provenance audit log per render",
      "status": "shipped",
      "evidence": "provenance model (template version, caller profile, mapping engine version)"
    },
    {
      "owasp": "A08 Software Integrity",
      "control": "Doc-package assembly log (per-item version resolution)",
      "status": "shipped",
      "evidence": "alembic 0029 (doc_package_assemblies.resolved_items JSONB)"
    },
    {
      "owasp": "A08 Software Integrity",
      "control": "Idempotency-Key on /render + /render/mapped",
      "status": "shipped",
      "evidence": "src/docgen/core/idempotency.py (24h replay window, 409 on fingerprint mismatch)"
    },
    {
      "owasp": "A09 Logging Failures",
      "control": "OpenTelemetry end-to-end tracing",
      "status": "backlog",
      "evidence": "roadmap Observability (FastAPI \u2192 SQLAlchemy \u2192 boto3 \u2192 render span pending)"
    },
    {
      "owasp": "A09 Logging Failures",
      "control": "DLQ visibility + watchdog reaper",
      "status": "shipped",
      "evidence": "Dashboard 'Failing jobs (last 7d)' + /ui/renders?state=failed + jobs/watchdog.py"
    },
    {
      "owasp": "A10 SSRF",
      "control": "Outbound HTTP through URL allowlist",
      "status": "shipped",
      "evidence": "test_net_safety.py + test_ssrf_and_idor.py"
    },
    {
      "owasp": "CWE-352 CSRF",
      "control": "SameSite=Lax + CSRF middleware",
      "status": "shipped",
      "evidence": "SecurityHeadersMiddleware + test_csrf.py"
    }
  ],
  "audits": [
    {
      "name": "Triage stage + 3-level config chain review",
      "shipped": "2026-09-17",
      "outcome": "1 blocker fixed (route allowlist), 5 notes filed. No cross-tenant escalation surface introduced by the new admin write-paths; every new route is behind _require_super_admin or _enforce_scope. LLM prompt injection contained to same-tenant self-poisoning; classifier response is allowlisted before it reaches the pipeline.",
      "findings": [
        "FIXED: server-side route allowlist added on both admin_save_tenant_triage_routing and program_triage_routing_save (was: any string persisted verbatim to JSONB).",
        "FIXED: dead routing block in program_models_save that silently cleared program.triage_routing on every Models-tab save.",
        "FIXED: msg.subject now threaded to Triage classifier in production (was: LLM saw empty subject on every real inbound case).",
        "FIXED: bare assert in _llm_classify replaced with explicit guard so the LLM-unavailable fallback survives `python -O`.",
        "PRAISE: skip-Decide path preserves the full audit trail (event.status, review_history, pap_decision all written) so a routed refill is as traceable as a fully-decided case.",
        "PRAISE: Nebius API key sourced from settings only; no per-tenant credential persistence. Tenant/program columns hold model NAMES not secrets.",
        "NOTE (queued): cap user_payload size before serializing to the LLM to bound token-flood risk on very large attachment extractions.",
        "NOTE (queued): consider returning HTTP 400 on malformed routing_json instead of silently coercing to empty."
      ]
    },
    {
      "name": "Caller-profile mapping threat model",
      "shipped": "2026-09-03",
      "outcome": "No injection surface. Closed grammar via strict regex; explicit-dispatch transforms.",
      "findings": [
        "No eval, exec, dynamic dispatch",
        "DoS-adjacent bounds (mapping-entry count, source-path length, constant size) intentionally deferred (auth + rate limit bound volume first)"
      ]
    },
    {
      "name": "Multi-tenancy scope audit",
      "shipped": "2026-09-03",
      "outcome": "Passing on all in-scope models. 6 suppressions justified.",
      "findings": [
        "ui/routes.py (admin superuser portal)",
        "jobs/render.py + jobs/webhooks.py + jobs/watchdog.py (worker actors trust queue-side scoping)",
        "designtime/freeze.py + publish.py (PK-only row locks)",
        "_bump_last_used(api_key_id) (api-key row resolved by hash before tenant is known)"
      ]
    },
    {
      "name": "Docx sanitiser + magic-byte gate",
      "shipped": "2026-09-03",
      "outcome": "7 tests. Non-zip rejected, macros stripped in-place, unrelated parts untouched.",
      "findings": [
        "Strip chosen over reject so an operator uploading a mis-saved .docm\u2192.docx still gets a usable template",
        "Audit event records which parts were stripped"
      ]
    },
    {
      "name": "Print-to-PDF reject at router boundary",
      "shipped": "2026-09-09",
      "outcome": "Rejects at boundary before any DB / blob write; templates count unchanged after rejected upload.",
      "findings": [
        "Producer-aware hint (Word Save-As-PDF vs generic OCR)",
        "AcroForm flavor untouched (widget rects anchor without needing text)"
      ]
    }
  ],
  "open_items": [
    {
      "priority": "medium",
      "item": "OpenTelemetry end-to-end traces",
      "why_blocked": "Not scheduled. Waiting on deploy timing.",
      "plan": "FastAPI \u2192 SQLAlchemy \u2192 boto3 \u2192 render span. Enable against the self-hosted deploy so trace shape reflects prod topology."
    },
    {
      "priority": "medium",
      "item": "Per-tenant metrics + queue-depth alerts",
      "why_blocked": "Needs telemetry backbone (OTel above) first.",
      "plan": "renders/hr, preflight-failure rate, P99 latency. Alert on queue depth over threshold + DLQ growth."
    },
    {
      "priority": "low",
      "item": "Signature-field render",
      "why_blocked": "Product decision on payload format (base64 PNG? blob key? URL?). Missing-value UX also unresolved.",
      "plan": "Coerced to text today (safe fallback but no signature semantics)."
    }
  ],
  "threat_notes": [
    {
      "category": "Attack surface",
      "note": "Public surfaces: /ui/* (session-cookie auth), /api/* (API-key auth), MCP transport (API-key auth), IMAP inbox (allowlist + resolver). Everything else is internal."
    },
    {
      "category": "Blast radius",
      "note": "Compromise of one tenant's API key surfaces that tenant's templates + renders only. Cross-tenant reach requires either an admin session or a vulnerability in the tenant-scope audit."
    },
    {
      "category": "Data at rest",
      "note": "PDF / DOCX artifacts in MinIO/S3 (bucket-scoped IAM). Postgres holds hashes + metadata + audit rows. Session secret + webhook signing secret injected via env; never on disk."
    },
    {
      "category": "Data in transit",
      "note": "TLS terminated at proxy. HSTS + Secure cookies signal HTTPS-only downstream. Webhooks HMAC-signed with per-tenant secret."
    },
    {
      "category": "Compliance framing",
      "note": "EU AI Act categorises healthcare eligibility decisions as high-risk and requires the exact reproducibility guarantees Verdikt provides mechanically. Building audit-defensible in 2026 is ahead of the regulatory curve."
    }
  ]
}
