Findings Ledger Proxy Contract (Web V)
This contract defines how the Stella Ops Web gateway forwards vulnerability-finding workflow actions (open, ack, close, reopen, export) to the Findings Ledger service. It covers the required headers, idempotency and correlation semantics, retry and offline-kit behavior, and the deterministic error envelope clients can rely on.
Status
- Final v1.0 (2025-12-01); validated with Findings Ledger Guild for Sprint 0216.
- Design draft for the gateway proxy surface — partially implemented. The
POST /ledger/findings/{findingId}/actionsgateway path below is a forward Web V contract; the Findings Ledger service today exposes its routes under/api/v1/findings/*(e.g./api/v1/findings/ledger,/api/v1/findings/{findingId}/...,/api/v1/scoring) — seeStellaOps.Findings.Ledger.WebService/Endpoints/. The scopes have been reconciled against the canonical catalog (StellaOps.Auth.Abstractions/StellaOpsScopes.cs): the service usesfindings:read/findings:write. There is noledger:read/ledger:writescope, and theaud=stellaops-ledgeraudience is illustrative.
Purpose
- Gateway → Findings Ledger forwarding for vulnerability workflow actions (
open,ack,close,reopen,export). - Idempotency and correlation headers, with retry/backoff defaults for offline and offline-kit-safe behavior.
Required Headers
| Name | Requirement | Notes |
|---|---|---|
Authorization: Bearer <jwt> | Required | RS256/ES256 service token. The Findings Ledger service enforces findings:read (reads) and findings:write (writes/appends); see StellaOps.Findings.Ledger.WebService/Program.cs. (There is no ledger:read/ledger:write scope; the aud=stellaops-ledger audience shown elsewhere is illustrative.) |
X-StellaOps-TenantId | Required | Tenant slug/UUID (must align with tenant-auth contract). |
X-StellaOps-Project | Conditional | Required for project-scoped findings. (Legacy alias X-Stella-Project is accepted only when the gateway is started with EnableLegacyHeaders=true.) |
X-Idempotency-Key | Required on POST/PUT | Deterministic BLAKE3-256(base64url(tenant + route + canonical_body)); 44 chars. TTL: 24h. |
X-Correlation-Id | Required | UUID/ULID stable across gateway → ledger → notifier; echoed by responses. |
Content-Type | Required | application/json. |
If-Match | Optional | When present, ledger enforces optimistic concurrency using the last ETag value. |
Behavior
- Delivery semantics: gateway is at-least-once; Findings Ledger guarantees exactly-once per
X-Idempotency-Keywithin 24h TTL. - Retry/backoff (gateway): base delay 500 ms, factor 2, jitter ±20%, max 3 attempts, cap total wait 10 s. Offline kits persist NDJSON (headers+body) and replay on next sync window.
- Timeout: 5 s per attempt; timeout →
ERR_LEDGER_TIMEOUT. - Concurrency: ledger returns
ETagfor each workflow record; gateway includesIf-Matchon retries when available. Mismatch → 409 +ERR_LEDGER_CONFLICT. - Error mapping (deterministic envelope with
trace_id+ echoedX-Correlation-Id):- 400 →
ERR_LEDGER_BAD_REQUEST(propagatedetails). - 404 →
ERR_LEDGER_NOT_FOUND. - 409 →
ERR_LEDGER_CONFLICT. - 429/503 →
ERR_LEDGER_RETRY. - 500+ →
ERR_LEDGER_UPSTREAM.
- 400 →
Payload Contract
{
"action": "ack", // open|ack|close|reopen|export
"finding_id": "f-7e12d9",
"reason_code": "triage_accept",
"comment": "Owner acknowledged risk and started fix",
"attachments": [ { "name": "triage.pdf", "digest": "sha256-..." } ],
"actor": { "subject": "svc-console", "type": "service" },
"metadata": { "policy_version": "2025.11.0", "vex_statement_id": "vex-123" }
}
- Body must be canonical JSON (sorted keys) before hashing for
X-Idempotency-Key. - Maximum size: 64 KiB; larger bodies rejected with 413.
Example Request
curl -X POST https://gateway.stellaops.local/ledger/findings/f-7e12d9/actions \
-H "Authorization: Bearer $LEDGER_TOKEN" \
-H "X-StellaOps-TenantId: acme-tenant" \
-H "X-Correlation-Id: 01HXYZABCD1234567890" \
-H "X-Idempotency-Key: 3cV1..." \
-H "Content-Type: application/json" \
--data '{"action":"ack","finding_id":"f-7e12d9","reason_code":"triage_accept","actor":{"subject":"svc-console","type":"service"}}'
Example Response
{
"status": "accepted",
"ledger_event_id": "ledg-01HF7T4X6E4S7A6PK8",
"etag": "W/\"01-2a9c\"",
"trace_id": "01HXYZABCD1234567890",
"correlation_id": "01HXYZABCD1234567890"
}
Open Questions
- Confirm ledger idempotency TTL (proposed 24h) and whether ETag is returned for optimistic concurrency.
- Confirm expected payload schemas for each workflow action (open/ack/close/export).
- Confirm whether ledger enforces ordering per
tenant_id.
