Compliance report API
The compliance report API exports a per-tenant governance metric set as a CSV file. It aggregates data the gateway already stores — request log, control-plane audit log, and per-request billing/residency legs — into a fixed set of mechanical counts over a time window. It is the reporting surface for the KI-Governance metric layer.
The figures are neutral mechanical counts. This endpoint makes no legal or certification claim; the metric labels describe what was counted, not a compliance verdict.
Base URL: https://<your-gateway-host>/admin/v1
GET /compliance/report
Returns the metric set for one tenant as a CSV attachment (Content-Type: text/csv; charset=utf-8, Content-Disposition: attachment; filename="compliance-report.csv").
Required role: admin, tenant_admin, or ki_manager.
The report is a pure tenant aggregate — it contains no per-user or natural-person identifiers — so a ki_manager reads their own tenant's aggregate (unlike the per-user analytics endpoints, no group-scoping applies).
curl 'https://<your-gateway-host>/admin/v1/compliance/report?from=1767225600&to=1798761600' \
-o compliance-report.csv
Tenant scope
Tenant scope is decided server-side; the client value never widens it.
| Caller | Scope |
|---|---|
admin |
Must target one tenant with ?tenant_id=<id>. |
tenant_admin / ki_manager |
Always their own tenant. A ?tenant_id naming another tenant is ignored (pinned to the caller's own). |
This endpoint is inherently per-tenant: there is no global/all-tenant variant. An admin who omits ?tenant_id is rejected 400 { "error": "tenant_id required" } (the query never runs). A non-admin whose account carries no tenant is refused 403 { "error": "forbidden" }.
Query parameters
| Parameter | Accepted | Rejected |
|---|---|---|
tenant_id |
A single tenant id string (admin only; ignored for other roles). |
A repeated (?tenant_id=a&tenant_id=b) or valueless (?tenant_id) parameter → 400 { "error": "tenant_id must be a single value" }. |
from, to |
A pair of finite unix-second integers with from < to. Both must be present to take effect. |
A present non-numeric or non-finite bound, or from >= to → 400. A lone from or to (not both) is ignored and the default window applies. |
Default window: the last 30 days. Maximum window: the span is clamped to 366 days (a wider request keeps its to and moves from forward to the cap) so a single synchronous request cannot scan an unbounded range.
Bad input is rejected at the boundary before any database work; a storage failure returns 500 { "error": "..." } (never a partial CSV).
CSV shape
A long-format metric,value table: a header row, five self-describing context rows, then one row per metric.
metric,value
tenant_id,acme
period_from,2026-06-18T00:00:00Z
period_to,2026-07-18T00:00:00Z
generated_at,2026-07-18T09:41:07Z
residency_basis,approximate
requests_total,48213
requests_redacted,192
requests_detector_hit,204
requests_blocked,57
blocked_rate_limit,4
blocked_budget_quota,2
blocked_pii_gate,9
blocked_ip_allowlist,1
blocked_plan_model,3
blocked_guardrail,38
auth_failures,11
residency_eu,47010
residency_non_eu,20
residency_unknown,6
residency_no_dispatch,120
period_from / period_to are the ISO-8601 UTC window bounds; generated_at is the report instant. CSV cells are RFC-4180 framed (CRLF) and formula-injection-armored.
residency_basis qualifies the four residency_* rows below:
| Value | Meaning |
|---|---|
authoritative |
Every request in the window carried a resolved dispatch-time residency zone (residency_unknown is 0) and the whole period is at/after dispatch-time residency recording went live. The residency breakdown is complete dispatch evidence. |
approximate |
Some requests carry an unresolved zone, or the period starts before dispatch-time residency recording was complete (legs recorded earlier have no zone and fold to residency_no_dispatch, not unknown, so a clean-looking unknown=0 alone does not prove coverage). The residency breakdown is a best-effort approximation, not authoritative evidence. |
The go-live instant is an operator setting configured by Myra for your deployment. Until it is declared, every report is approximate — the gateway never presents residency figures as authoritative that it cannot vouch for.
Metric definitions
All counts are scoped to the resolved tenant and the [period_from, period_to) window.
Request activity (from the request log):
| Metric | Definition |
|---|---|
requests_total |
All logged requests. |
requests_redacted |
Requests where PII redaction was applied (scrub_applied). This is an independent signal, not disjoint from requests_blocked (a request can be redacted and then blocked), and it differs from the dashboard's exclusive "scrubbed" bucket (which excludes blocked requests). |
requests_detector_hit |
Requests where at least one guardrail detector fired (non-empty detector list). |
requests_blocked |
Requests blocked before completion. |
Blocked-reason breakdown — these six buckets partition requests_blocked (they sum to it exactly):
| Metric | Definition |
|---|---|
blocked_rate_limit |
Blocked by a rate limit. |
blocked_budget_quota |
Blocked by a budget / quota / subscription limit. |
blocked_pii_gate |
Blocked by the mandatory PII-residency gate (protection required / unmaskable media). A PII detector firing a block verdict is counted under blocked_guardrail, not here. |
blocked_ip_allowlist |
Blocked by the IP allowlist. |
blocked_plan_model |
Blocked because the model is outside the plan's allowlist. |
blocked_guardrail |
Every other block — guardrail/detector verdicts, including PII detectors. Computed as the residual so the six buckets always sum to requests_blocked. |
OWASP-LLM / MITRE-ATLAS crosswalk
The report also carries a static crosswalk mapping each blocked-reason category to the
OWASP Top 10 for LLM Applications
and MITRE ATLAS technique IDs. It is a reference/taxonomy layer — no
new detection — for governance and SIEM correlation. In the CSV it appears as
crosswalk.<category>.owasp / crosswalk.<category>.atlas rows; in the Markdown/PDF export as an
"OWASP-LLM / MITRE-ATLAS crosswalk" table. The exported cells carry the ids only (e.g.
LLM01, LLM05 / AML.T0048, AML.T0051); the table below is the legend that names them.
| Blocked reason | OWASP-LLM Top 10 | MITRE ATLAS |
|---|---|---|
| Guardrail / detector verdict | LLM01 (Prompt Injection), LLM05 (Improper Output Handling) | AML.T0048, AML.T0051, AML.T0054 |
| PII gate | LLM02 (Sensitive Information Disclosure) | AML.T0057 |
| Rate limit | LLM10 (Unbounded Consumption) | AML.T0034 |
| Budget / quota | LLM10 (Unbounded Consumption) | AML.T0034 |
| IP allowlist | — (operational access control, no LLM threat class) | — |
| Plan model allowlist | — (operational entitlement, no LLM threat class) | — |
Per-request tagging. Beyond the aggregate report, each request that trips a guardrail verdict
carries the same taxonomy on its request-log entry (meta.threat_taxonomy = { owasp: […],
atlas: […] }) and on the SIEM CEF record (extension fields cs9=owasp_llm, cs10=atlas; the
JSON SIEM formats carry it under meta.threat_taxonomy). The per-request map is derived from the
reliable verdict signals (PII-gate reasons, resource limits, egress-exfil kind, the content-
safety S-code list, and indirect prompt-injection detected over tool/RAG/MCP results → LLM01 /
AML.T0051) plus a best-effort match on default-named detectors; a renamed detector is
left untagged rather than mis-tagged. Categories with no LLM threat class (IP allowlist, plan
allowlist, control-unavailable) are intentionally not tagged.
Security events (from the control-plane audit log):
| Metric | Definition |
|---|---|
auth_failures |
Tenant-attributable authentication / authorization / SSO failures (status ≥ 400): failed or blocked OTP attempts, denied access (403), and failed/replayed SSO/SAML logins. Successful logins are excluded. Failures that cannot be attributed to a tenant (e.g. an OTP attempt for an unknown account) are not counted. |
Data residency (from the per-request dispatch legs, folded worst-case per request):
Each request is classified by the worst residency zone across its dispatch legs: any non-EU leg ⇒ non_eu; otherwise any unprovable leg ⇒ unknown; otherwise any EU-vouched leg ⇒ eu; a request whose legs carry no zone ⇒ no_dispatch.
| Metric | Definition |
|---|---|
residency_eu |
Requests whose model dispatch was positively EU-vouched at dispatch time. A mechanical dispatch fact — not a legal EU-residency certification. |
residency_non_eu |
Requests with at least one positively non-EU dispatch leg. |
residency_unknown |
Requests with at least one leg whose residency was unprovable at dispatch (e.g. variable-routing providers). |
residency_no_dispatch |
Requests whose legs carry no residency zone: sidecar/guardrail-only legs, or legs recorded before residency logging was enabled. |
The residency counts are over dispatched requests (those with at least one leg), which is generally fewer than requests_total (a request blocked before dispatch, or served from cache, has no dispatch leg).
Scheduled monthly snapshots
GET /compliance/report recomputes its figures live on every call. Two of its inputs decay over time: the control-plane audit log is periodically pruned (so auth_failures for an old window shrinks as rows age out), and a later change to a metric definition would silently rewrite historical numbers. To keep durable, stable governance evidence, the gateway also freezes the metric set once per closed calendar month.
A background job on the maintenance worker snapshots, for each active tenant, the complete metric map for each recently closed UTC calendar month, storing it immutably. Properties:
- Cadence: the month containing "now" is still open, so the job snapshots the prior closed month (and backfills a bounded window of earlier closed months after a first deploy or downtime). A tenant + month pair is snapshotted once — a re-run is a no-op that preserves the original row.
- Every month is recorded, including quiet ones: a month with no activity is snapshotted all-zero (a positive "no incidents" record), so the tenant's compliance history has no gaps. A month that fully predates the tenant's creation is not snapshotted.
- Scope: only active (non-deleted) tenants that have opted in are enumerated. Each snapshot is tenant-isolated.
- Off by default, enabled per tenant in the database: the job ships dark. Enablement is the per-tenant flag
compliance_report_enabled(default0), set viaPATCH /admin/v1/tenants/{id}— platform-admin-only (the snapshot content is DPO-gated governance evidence, not tenant self-service). Enablement is per-tenant only — there is no deployment-wide switch. Snapshot content mirrors the live report exactly (same metric definitions, sameresidency_basisrule).
GET /compliance/reports
Lists the calling tenant's stored monthly snapshots, newest period first, as JSON. Metadata only — the frozen metric bytes are fetched per report by the download endpoint below.
Required role and tenant scope: identical to GET /compliance/report (admin must pass ?tenant_id; tenant_admin / ki_manager are pinned to their own tenant; an admin without ?tenant_id is rejected 400). A tenant with no snapshots returns { "reports": [] } (always a JSON array).
{
"reports": [
{ "id": "1f9c…", "period_from_ms": 1748736000000, "period_to_ms": 1751328000000, "generated_at_ms": 1751330000000 }
]
}
GET /compliance/reports/:id
Downloads one stored snapshot. The :id is resolved scoped to the caller's tenant, so an unknown id and another tenant's id both return 404 { "error": "report not found" } (a foreign id's existence is never confirmed).
| Query parameter | Accepted | Rejected |
|---|---|---|
format |
csv (default) or pdf. |
Any other value → 400 { "error": "format must be csv or pdf" }. |
csv— the same long-formatmetric,valueCSV as the live report, rebuilt from the frozen metrics (including the frozenresidency_basisandgenerated_at— the snapshot's frozengenerated_at_ms, not the download time).pdf— a rendered PDF report (headed tables for requests, blocked-by-reason, security events, and data residency, carrying the residency caveat when the basis isapproximate). The PDF is produced by the shared document-render pipeline and is branded with the report's tenant branding.
Failure modes: a corrupt stored snapshot → 500 { "error": "stored report is unreadable" }; the render pool saturated → 503 with a Retry-After header (retry shortly); any other render failure → 500.