"OBS compliance views: scoped JSON API + Ed25519 proof"
TASK-OBS-008: OBS compliance views — tenant-scoped JSON API + Ed25519 proof
Summary
Ship a read-only obs-compliance-view service over the memory audit chain. As-built surface is a flat Rust crate: views.rs (four regulation kind-filters), auth.rs (auditor JWT + tenant scope), query.rs / summary.rs / window.rs / pii_scan.rs, proof.rs (Ed25519 over canonical JSON — not chain_proof.rs), and main.rs (axum JSON HTTP API on GET /:view + GET /healthz). Responses are { payload, proof } JSON envelopes.
Problem
The prior engineering-spec claimed a per-regime views/{eu_ai_act,pdpl,soc2,iso27001}.rs tree, chain_proof.rs, PDF/JSON exporters, Grafana compliance.json, and standalone integration tests. The live crate consolidates views into views.rs, names the signer proof.rs, serves JSON only from main.rs, and keeps unit tests inline in each module. FM-004 blocked re-entry (task@1 frontmatter + ## §N body).
Proposed Solution
Adopt the as-built layout:
views.rs—Viewenum, slug parse, fixedkinds()per regulation (DEC-175)auth.rs— RS256 JWKS or HS256 dev verifier;external_auditorrole;enforce_tenant_scopequery.rs— tenant + kind + window read onl1_audit_logwith RLS GUC (read-only, DEC-177)window.rs— reject inverted or >365-day windowssummary.rs—total_rows,by_kind(BTreeMap), seq spanproof.rs— Ed25519sign/verifyover canonical payload bytes (DEC-176)pii_scan.rs— email / VN CCCD / VN phone regex defencemain.rs— authenticate → scope tenant → validate window → fetch → summarize → PII scan → sign → best-effortobs.compliance_view_accessedemit
Alternatives Considered
- Resume the engineering-spec paths as-is. Rejected: phantom modules (
chain_proof.rs,views/eu_ai_act.rs) do not exist; FM-004 blocks. - PDF export in slice 3. Rejected: JSON API + proof ship first; PDF deferred.
- Per-view export integration tests against live Postgres. Rejected: slice ships inline unit tests; dedicated integration suite deferred.
Success Metrics
- Primary: four view slugs parse and filter kinds; cross-tenant
?tenant_id=refused; Ed25519 proof round-trips; 365-day window enforced. - Guardrail:
cargo test -p cyberos-obs-compliance-viewgreen on the inline module tests cited below.
Scope
In scope (as-built):
services/obs-compliance-view/src/{views,auth,proof,query,summary,window,pii_scan,main,lib}.rs- JSON HTTP API (
GET /eu-ai-act|pdpl|soc2|iso27001,GET /healthz) - Inline
#[cfg(test)]modules in the files above
Out of scope / Non-Goals
- Per-regime file tree (
views/eu_ai_act.rs,views/mod.rs, etc.) - PDF export (
export/pdf.rs, wkhtmltopdf) - Grafana dashboard (
deploy/obs/grafana/dashboards/compliance.json) - Postgres integration tests (
tests/eu_ai_act_test.rs,tests/cross_tenant_test.rs, etc.) - Chain-of-custody manifest signing (TASK-OBS-009)
Dependencies
depends_on: [TASK-OBS-002] (tenant-aware query proxy pattern). Soft: TASK-AUTH-004 JWT/JWKS; cyberos-audit-chain for access-audit emit; TASK-OBS-009 manifest builds on this crate.
1. Description (normative)
- 1.1
views.rsMUST define exactly fourViewvariants (EU AI Act, PDPL, SOC 2, ISO 27001) with stable URL slugs and non-emptykinds()filters per DEC-175. - 1.2
main.rsMUST expose a JSON HTTP API:GET /:viewfor the four slugs andGET /healthz; successful responses MUST be a JSON envelope{ payload, proof }(not PDF). - 1.3
auth.rsMUST verify auditor JWTs and MUST require theexternal_auditorrole before serving any view (DEC-178). - 1.4
enforce_tenant_scopeMUST refuse a cross-tenant?tenant_id=query parameter when it differs from the JWT tenant (403). - 1.5
query.rsMUST fetch rows read-only froml1_audit_logfiltered by tenant, view kinds, and time window; it MUST NOT mutate the chain (DEC-177). - 1.6
window.rsMUST reject inverted windows and windows wider than 365 days. - 1.7
proof.rsMUST Ed25519-sign the canonical serialized payload bytes and expose offlineverify; the module MUST NOT be namedchain_proof.rsin the as-built tree. - 1.8
pii_scan.rsMUST scan the serialized response for raw PII patterns before serve; matches MUST fail closed (HTTP 500 path inmain.rs). - 1.9
summary.rsMUST produce a summary block with stableby_kindordering (BTreeMap) included in the signed payload. - 1.10
main.rsSHOULD best-effort emitobs.compliance_view_accessedviacyberos_audit_chain::emit_genesiswithout failing the auditor read on emit error.
Acceptance criteria
- [ ] AC 1 (traces_to: #1.1) - view slug parse round-trips all four regulations - test:
services/obs-compliance-view/src/views.rs::parse_roundtrips_slug_for_every_view - [ ] AC 2 (traces_to: #1.1) - every view selects a non-empty kind set - test:
services/obs-compliance-view/src/views.rs::every_view_selects_a_non_empty_kind_set - [ ] AC 3 (traces_to: #1.1) - headline kinds present per regulation - test:
services/obs-compliance-view/src/views.rs::views_select_their_headline_kinds - [ ] AC 4 (traces_to: #1.3) - external_auditor role authorized - test:
services/obs-compliance-view/src/auth.rs::auditor_role_is_authorized - [ ] AC 5 (traces_to: #1.3) - missing auditor role refused - test:
services/obs-compliance-view/src/auth.rs::missing_auditor_role_is_refused - [ ] AC 6 (traces_to: #1.4) - cross-tenant tenant_id param refused - test:
services/obs-compliance-view/src/auth.rs::cross_tenant_param_is_refused - [ ] AC 7 (traces_to: #1.6) - window over 365 days rejected - test:
services/obs-compliance-view/src/window.rs::a_window_over_the_limit_is_rejected - [ ] AC 8 (traces_to: #1.6) - window at 365-day limit allowed - test:
services/obs-compliance-view/src/window.rs::a_window_at_the_limit_is_allowed - [ ] AC 9 (traces_to: #1.7) - Ed25519 sign then verify round-trips - test:
services/obs-compliance-view/src/proof.rs::sign_then_verify_roundtrips - [ ] AC 10 (traces_to: #1.7) - tampered canonical bytes fail verify - test:
services/obs-compliance-view/src/proof.rs::a_tampered_response_fails_verification - [ ] AC 11 (traces_to: #1.8) - raw email caught by PII scan - test:
services/obs-compliance-view/src/pii_scan.rs::raw_email_is_caught - [ ] AC 12 (traces_to: #1.9) - summary counts group by kind - test:
services/obs-compliance-view/src/summary.rs::counts_group_by_kind_and_track_seq_span - [ ] AC 13 (traces_to: #1.5) - read-only tenant-scoped l1_audit_log query - verify:
services/obs-compliance-view/src/query.rsfetch_rows - [ ] AC 14 (traces_to: #1.10) - best-effort obs.compliance_view_accessed emit - verify:
services/obs-compliance-view/src/main.rsemit_genesisblock - [ ] AC 15 (traces_to: #1.2) - JSON HTTP routes live in main.rs (not per-view modules) - verify:
services/obs-compliance-view/src/main.rsrouter +View::parse - [ ] AC 16 (traces_to: #1.2,#1.7) - Out of scope lists phantom paths; new_files cite flat as-built tree only - verify: this spec Scope / new_files
Verification
cd services && cargo test -p cyberos-obs-compliance-view proof::
cd services && cargo test -p cyberos-obs-compliance-view views:: auth:: window:: pii_scan:: summary::
| Path | Covers |
|---|---|
src/views.rs tests | DEC-175 kind table + slug routing |
src/auth.rs tests | DEC-178 auditor role + tenant scope |
src/window.rs tests | 365-day cap |
src/proof.rs tests | DEC-176 Ed25519 proof |
src/pii_scan.rs tests | Response PII defence |
src/summary.rs tests | Summary block shape |
src/main.rs | JSON HTTP shell + handler pipeline |
AI Authorship Disclosure
- Tools used: Cursor agent (Composer) on branch
batch/9b-obs. - Scope: Re-spec/adopt against as-built
cyberos-obs-compliance-view; removed phantom per-regime tree,chain_proof.rs, PDF, Grafana dashboard, and Postgres integration tests from claimed surface. - Human review: Required at the two HITL gates (
entered_via: rework,routed_back_count: 1).
batch/9b-obs adopt — TASK-OBS-008 re-spec against as-built obs-compliance-view.