Files
git.stella-ops.org/docs/api/triage.contract.v1.md
2025-12-25 23:10:09 +02:00

10 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)
  • showHidden (bool, default false) — include gated findings in results
  • gatingReason (optional, enum) — filter by specific gating reason
  • 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,
  "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:

  • 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}

8. Gating Explainability

8.1 GatingReason enum

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

Value Description
none Not gated - visible in default view
unreachable Not reachable from any application entrypoint
policy_dismissed Waived or tolerated by policy rules
backported Patched via distro backport
vex_not_affected VEX statement declares not affected with sufficient trust
superseded Superseded by newer advisory
user_muted Explicitly 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:

  • Authority (0-1): Issuer reputation and category
  • Accuracy (0-1): Historical correctness
  • Timeliness (0-1): Response speed
  • Verification (0-1): Signature validity

8.4 Finding gating fields

Extended fields on finding response:

Field Type Description
gatingReason string Why this finding is hidden (null if visible)
isHiddenByDefault boolean True if gated by default view
subgraphId string Link to reachability subgraph
deltasId string Link to delta comparison
gatingExplanation string Human-readable explanation

9. Unified Evidence

9.1 Get unified evidence

GET /findings/{findingId}/evidence

Returns all evidence tabs in one call.

Response 200:

{
  "findingId": "uuid",
  "cveId": "CVE-2024-1234",
  "componentPurl": "pkg:npm/lodash@4.17.20",
  "sbom": { ... },
  "reachability": { ... },
  "vexClaims": [ ... ],
  "attestations": [ ... ],
  "deltas": { ... },
  "policy": { ... },
  "manifests": {
    "artifactDigest": "sha256:...",
    "feedDigest": "sha256:...",
    "policyDigest": "sha256:...",
    "manifestDigest": "sha256:..."
  },
  "verification": {
    "status": "verified",
    "hashesMatch": true,
    "signaturesValid": true,
    "isFresh": true
  },
  "replayCommand": "stella scan replay --artifact sha256:abc...",
  "generatedAt": "2025-12-16T01:02:03Z"
}

9.2 Export evidence bundle

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

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

Response headers:

  • Content-Type: application/zip or application/gzip
  • Content-Disposition: attachment; filename="evidence-{findingId}.zip"
  • X-Archive-Digest: sha256:{digest}

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": "/v1/triage/findings/uuid/evidence/export",
  "inputHashes": {
    "artifactDigest": "sha256:...",
    "manifestHash": "sha256:...",
    "feedSnapshotHash": "sha256:...",
    "policyHash": "sha256:..."
  }
}

Document Version: 1.1 Updated: 2025-12-24 (Sprint 9200 - Gating Explainability) Target Platform: .NET 10, PostgreSQL >= 16