# Stella Ops Triage API Contract v1 Base path: `/api/triage/v1` This contract is served by `scanner.webservice` (or a dedicated triage facade that reads scanner-owned tables). All risk/lattice outputs originate from `scanner.webservice`. Key requirements: - Deterministic outputs (policyId + policyVersion + inputsHash). - Proof-linking (chips reference evidenceIds). - `concelier` and `excititor` preserve prune source: API surfaces source chains via `sourceRefs`. ## 0. Conventions ### 0.1 Identifiers - `caseId` == `findingId` (UUID). A case is a finding scoped to an asset/environment. - Hashes are hex strings. ### 0.2 Caching - GET endpoints SHOULD return `ETag`. - Clients SHOULD send `If-None-Match`. ### 0.3 Errors Standard error envelope: ```json { "error": { "code": "string", "message": "string", "details": { "any": "json" }, "traceId": "string" } } ``` Common codes: * `not_found` * `validation_error` * `conflict` * `unauthorized` * `forbidden` * `rate_limited` ## 1. Findings Table ### 1.1 List findings `GET /findings` Query params: * `showMuted` (bool, default false) * `lane` (optional, enum) * `search` (optional string; searches asset, purl, cveId) * `page` (int, default 1) * `pageSize` (int, default 50; max 200) * `sort` (optional: `updatedAt`, `score`, `lane`) * `order` (optional: `asc|desc`) Response 200: ```json { "page": 1, "pageSize": 50, "total": 12345, "mutedCounts": { "reach": 1904, "vex": 513, "compensated": 18 }, "rows": [ { "id": "uuid", "lane": "BLOCKED", "verdict": "BLOCK", "score": 87, "reachable": "YES", "vex": "affected", "exploit": "YES", "asset": "prod/api-gateway:1.2.3", "updatedAt": "2025-12-16T01:02:03Z" } ] } ``` ## 2. Case Narrative ### 2.1 Get case header `GET /cases/{caseId}` Response 200: ```json { "id": "uuid", "verdict": "BLOCK", "lane": "BLOCKED", "score": 87, "policyId": "prod-strict", "policyVersion": "2025.12.14", "inputsHash": "hex", "why": "Reachable path observed; exploit signal present; prod-strict blocks.", "chips": [ { "key": "reachability", "label": "Reachability", "value": "Reachable (92%)", "evidenceIds": ["uuid"] }, { "key": "vex", "label": "VEX", "value": "affected", "evidenceIds": ["uuid"] }, { "key": "gate", "label": "Gate", "value": "BLOCKED by prod-strict", "evidenceIds": ["uuid"] } ], "sourceRefs": [ { "domain": "concelier", "kind": "cve_record", "ref": "concelier:osv:...", "pruned": false }, { "domain": "excititor", "kind": "effective_vex", "ref": "excititor:openvex:...", "pruned": false } ], "updatedAt": "2025-12-16T01:02:03Z" } ``` Notes: * `sourceRefs` provides preserved provenance chains (including pruned markers when applicable). ## 3. Evidence ### 3.1 List evidence for case `GET /cases/{caseId}/evidence` Response 200: ```json { "caseId": "uuid", "items": [ { "id": "uuid", "type": "VEX_DOC", "title": "Vendor OpenVEX assertion", "issuer": "vendor.example", "signed": true, "signedBy": "CN=Vendor VEX Signer", "contentHash": "hex", "createdAt": "2025-12-15T22:10:00Z", "previewUrl": "/api/triage/v1/evidence/uuid/preview", "rawUrl": "/api/triage/v1/evidence/uuid/raw" } ] } ``` ### 3.2 Get raw evidence object `GET /evidence/{evidenceId}/raw` Returns: * `application/json` for JSON evidence * `application/octet-stream` for binary * MUST include `Content-SHA256` header (hex) when possible. ### 3.3 Preview evidence object `GET /evidence/{evidenceId}/preview` Returns a compact representation safe for UI preview. ## 4. Decisions ### 4.1 Create decision `POST /decisions` Request body: ```json { "caseId": "uuid", "kind": "MUTE_REACH", "reasonCode": "NON_REACHABLE", "note": "No entry path in this env; reviewed runtime traces.", "ttl": "2026-01-16T00:00:00Z" } ``` Response 201: ```json { "decision": { "id": "uuid", "kind": "MUTE_REACH", "reasonCode": "NON_REACHABLE", "note": "No entry path in this env; reviewed runtime traces.", "ttl": "2026-01-16T00:00:00Z", "actor": { "subject": "user:abc", "display": "Vlad" }, "createdAt": "2025-12-16T01:10:00Z", "signatureRef": "dsse:rekor:uuid" } } ``` Rules: * Server signs decisions (DSSE) and persists signature reference. * Creating a decision MUST create a `Snapshot` with trigger `DECISION`. ### 4.2 Revoke decision `POST /decisions/{decisionId}/revoke` Body (optional): ```json { "reason": "Mistake; reachability now observed." } ``` Response 200: ```json { "revokedAt": "2025-12-16T02:00:00Z", "signatureRef": "dsse:rekor:uuid" } ``` ## 5. Snapshots & Smart-Diff ### 5.1 List snapshots `GET /cases/{caseId}/snapshots` Response 200: ```json { "caseId": "uuid", "items": [ { "id": "uuid", "trigger": "POLICY_UPDATE", "changedAt": "2025-12-16T00:00:00Z", "fromInputsHash": "hex", "toInputsHash": "hex", "summary": "Policy version changed; gate threshold crossed." } ] } ``` ### 5.2 Smart-Diff between two snapshots `GET /cases/{caseId}/smart-diff?from={inputsHashA}&to={inputsHashB}` Response 200: ```json { "fromInputsHash": "hex", "toInputsHash": "hex", "inputsChanged": [ { "key": "policyVersion", "before": "2025.12.14", "after": "2025.12.16", "evidenceIds": ["uuid"] } ], "outputsChanged": [ { "key": "verdict", "before": "SHIP", "after": "BLOCK", "evidenceIds": ["uuid"] } ] } ``` ## 6. Export Evidence Bundle ### 6.1 Start export `POST /cases/{caseId}/export` Response 202: ```json { "exportId": "uuid", "status": "QUEUED" } ``` ### 6.2 Poll export `GET /exports/{exportId}` Response 200: ```json { "exportId": "uuid", "status": "READY", "downloadUrl": "/api/triage/v1/exports/uuid/download" } ``` ### 6.3 Download bundle `GET /exports/{exportId}/download` Returns: * `application/zip` * DSSE envelope embedded (or alongside in zip) * bundle contains replay manifest, artifacts, risk result, snapshots ## 7. Events (Notify.WebService integration) These are emitted by `notify.webservice` when scanner outputs change. * `first_signal` * fired on first actionable detection for an asset/environment * `risk_changed` * fired when verdict/lane changes or thresholds crossed * `gate_blocked` * fired when CI gate blocks Event payload includes: * caseId * old/new verdict/lane/score (for changed events) * inputsHash * links to `/cases/{caseId}` --- **Document Version**: 1.0 **Target Platform**: .NET 10, PostgreSQL >= 16