Skip to content

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 (default 0), set via PATCH /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, same residency_basis rule).

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-format metric,value CSV as the live report, rebuilt from the frozen metrics (including the frozen residency_basis and generated_at — the snapshot's frozen generated_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 is approximate). 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.