Files
git.stella-ops.org/docs/api/triage.contract.v1.md

6.5 KiB

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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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):

{ "reason": "Mistake; reachability now observed." }

Response 200:

{ "revokedAt": "2025-12-16T02:00:00Z", "signatureRef": "dsse:rekor:uuid" }

5. Snapshots & Smart-Diff

5.1 List snapshots

GET /cases/{caseId}/snapshots

Response 200:

{
  "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:

{
  "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:

{
  "exportId": "uuid",
  "status": "QUEUED"
}

6.2 Poll export

GET /exports/{exportId}

Response 200:

{
  "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