Stella Ops Triage API Contract v1

This is the v1 HTTP contract for the Stella Ops triage surface: the findings table, per-case narrative and evidence, signed triage decisions, snapshots and Smart-Diff, evidence-bundle export, and gating explainability. It is written for UI, CLI, and integration developers building on top of Stella Ops triage. For the evidence-export endpoints in depth, see the Triage Evidence Export API Reference.

Base path: /api/v1/triage

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:

0. Conventions

0.1 Identifiers

0.2 Caching

0.3 Errors

Standard error envelope:

{
  "error": {
    "code": "string",
    "message": "string",
    "details": { "any": "json" },
    "traceId": "string"
  }
}

Common codes:

1. Findings Table

1.1 List findings

GET /findings

Query params:

Response 200:

{
  "page": 1,
  "pageSize": 50,
  "total": 12345,
  "actionableCount": 847,
  "mutedCounts": { "reach": 1904, "vex": 513, "compensated": 18 },
  "gatedBuckets": {
    "unreachableCount": 1904,
    "policyDismissedCount": 234,
    "backportedCount": 456,
    "vexNotAffectedCount": 513,
    "supersededCount": 12,
    "userMutedCount": 18,
    "totalHiddenCount": 3137
  },
  "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",
      "gatingReason": null,
      "isHiddenByDefault": false
    }
  ]
}

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:

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"
    }
  ]
}

type is a closed wire vocabulary and is emitted exactly as one of: SBOM_SLICE, VEX_DOC, PROVENANCE, CALLSTACK_SLICE, REACHABILITY_PROOF, REPLAY_MANIFEST, POLICY, SCAN_LOG, or OTHER. CLR/PascalCase enum names are not part of the API contract.

3.2 Get raw evidence object

GET /evidence/{evidenceId}/raw

Returns:

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:

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:

7. Events (Notify.WebService integration)

These are emitted by notify.webservice when scanner outputs change.

Event payload includes:

8. Gating Explainability

8.1 GatingReason enum

Reason why a finding is hidden by default in quiet-by-design triage:

ValueDescription
noneNot gated - visible in default view
unreachableNot reachable from any application entrypoint
policy_dismissedWaived or tolerated by policy rules
backportedPatched via distro backport
vex_not_affectedVEX statement declares not affected with sufficient trust
supersededSuperseded by newer advisory
user_mutedExplicitly muted by user

8.2 GatedBucketsSummary

{
  "unreachableCount": 1904,
  "policyDismissedCount": 234,
  "backportedCount": 456,
  "vexNotAffectedCount": 513,
  "supersededCount": 12,
  "userMutedCount": 18,
  "totalHiddenCount": 3137
}

8.3 VEX Trust Scoring

VEX statements include trust scoring for policy-driven acceptance:

{
  "status": "not_affected",
  "justification": "vulnerable_code_not_in_execute_path",
  "issuedBy": "vendor.example",
  "issuedAt": "2025-12-15T10:00:00Z",
  "trustScore": 0.85,
  "policyTrustThreshold": 0.80,
  "meetsPolicyThreshold": true,
  "trustBreakdown": {
    "authority": 0.90,
    "accuracy": 0.85,
    "timeliness": 0.80,
    "verification": 0.85
  }
}

Trust score components:

8.4 Finding gating fields

Extended fields on finding response:

FieldTypeDescription
gatingReasonstringWhy this finding is hidden (null if visible)
isHiddenByDefaultbooleanTrue if gated by default view
subgraphIdstringLink to reachability subgraph
deltasIdstringLink to delta comparison
gatingExplanationstringHuman-readable explanation

9. Unified Evidence

9.1 Get unified evidence

GET /findings/{findingId}/evidence

Returns all available evidence tabs in one call for the tenant-scoped Triage finding identified by the bare UUID. The response is still 200 OK when the finding exists but one or more child evidence producers have not produced a row. In that case, the absent tab is null and availability.missing[] identifies the evidence type, stable absence code, owning producer, and recovery action.

Response 200:

{
  "findingId": "uuid",
  "cveId": "CVE-2024-1234",
  "componentPurl": "pkg:npm/lodash@4.17.20",
  "scan": {
    "scanId": "uuid",
    "status": "completed",
    "imageReference": "registry.example/app@sha256:...",
    "imageDigest": "sha256:...",
    "startedAt": "2026-07-11T10:00:00Z",
    "completedAt": "2026-07-11T10:01:00Z"
  },
  "findingTimestamps": {
    "firstSeenAt": "2026-07-11T10:01:00Z",
    "lastSeenAt": "2026-07-11T10:01:00Z",
    "updatedAt": "2026-07-11T10:01:00Z"
  },
  "sbom": { ... },
  "reachability": null,
  "vexClaims": [ ... ],
  "attestations": [ ... ],
  "deltas": { ... },
  "policy": { ... },
  "manifests": {
    "artifactDigest": "sha256:...",
    "feedDigest": "sha256:...",
    "policyDigest": "sha256:...",
    "manifestDigest": "sha256:..."
  },
  "availability": {
    "status": "partial",
    "missing": [
      {
        "type": "reachability",
        "code": "scanner.triage.reachability_not_produced",
        "producer": "scanner.reachability",
        "recovery": "Run reachability analysis for the finding's scan and retry."
      }
    ]
  },
  "verification": {
    "status": "unknown",
    "hashesVerified": false,
    "attestationsVerified": false,
    "evidenceComplete": false,
    "verifiedAt": null
  },
  "replayCommand": null,
  "generatedAt": "2025-12-16T01:02:03Z"
}

Scanner publishes only persisted values. It does not synthesize missing hashes, SBOM versions, policy versions, verification success, or replay inputs. Unknown values remain null or unknown. When equal-timestamp child rows exist, the latest-row selectors use UUID ordering as the deterministic tie-break.

Response 404 uses Problem Details with code scanner.triage.finding_not_found, producer scanner.triage, and recovery guidance to query the inbox. It deliberately does not echo the requested UUID, so tenant boundaries do not disclose record existence.

9.2 Export evidence bundle

GET /findings/{findingId}/evidence/export?format=zip

Downloads complete evidence package as archive (ZIP or TAR.GZ).

Response headers:

9.3 Get replay command

GET /findings/{findingId}/replay-command

Returns copy-ready CLI command for deterministic reproduction.

Response 200:

{
  "fullCommand": "stella scan replay --artifact sha256:abc... --manifest sha256:def...",
  "shortCommand": "stella replay snapshot --verdict V-12345",
  "bundleDownloadUrl": "/api/v1/triage/findings/uuid/evidence/export",
  "inputHashes": {
    "artifactDigest": "sha256:...",
    "manifestHash": "sha256:...",
    "feedSnapshotHash": "sha256:...",
    "policyHash": "sha256:..."
  }
}

See Also


Document version: 1.1 (revision of the v1 contract)  ·  Updated: 2025-12-24  ·  Target platform: .NET 10, PostgreSQL ≥ 16