Score Proofs & Reachability API Reference

Version: 1.0.0
Sprint: 3500.0004.0004
Status: Reconciled against implementation 2026-05-29 (was “Complete”)

This document is the API reference for the Score Proofs, Reachability Analysis, and Unknowns management features, spanning the Scanner WebService and the Attestor proof-chain surface. It is written for backend integrators and CI pipelines wiring these endpoints into release gates.

Reconciliation note (2026-05-29): Routes and scopes were corrected against src/Scanner/StellaOps.Scanner.WebService/ and src/Attestor/.../StellaOps.Attestor.WebService/. Two classes of error were fixed: (1) Scanner endpoints are mounted under base path /api/v1(not /api/v1/scanner), and (2) several documented endpoints had no backing controller or are 501 stubs. See the inline correction blocks and §6 Proof Chain API.


Table of Contents

  1. Overview
  2. Authentication
  3. Score Proofs API
  4. Reachability API
  5. Unknowns API
  6. Proof Chain API
  7. Data Models
  8. Error Handling
  9. Rate Limiting
  10. Examples

1. Overview

Design Principles

Base URLs

ServiceBase URLDescription
Scanner/api/v1 (default; scanner:Api:BasePath)Scans, manifests, score replay, reachability, unknowns
Attestor proof chain/api/v1/proofs (ProofChainController, implemented)Proof-by-subject query + verify
Attestor proof spine/proofs, /anchors, /verify (stubs, return 501)Spine/receipt/anchor/verify scaffolding

Correction (2026-05-29): Scanner is /api/v1, not /api/v1/scanner. Unknowns are a Scanner route group at /api/v1/unknowns. The Attestor exposes a real proof-chain query controller at /api/v1/proofs/{subjectDigest} and a separate, not-yet-implemented spine/anchor/verify surface at root-relative /proofs/*, /anchors/*, /verify/* — see §6.

Supported Content Types

TypeDescription
application/jsonStandard JSON (responses are canonical)
application/x-ndjsonStreaming NDJSON for large call graphs
application/zipProof bundle archives

2. Authentication

All endpoints require OAuth 2.0 Bearer token authentication.

Token Request

POST /connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&
client_id=ci-bot&
client_secret=REDACTED&
scope=scanner.scans.read scanner.scans.write scanner.callgraph.ingest

Response

{
  "access_token": "eyJraWQi...",
  "token_type": "Bearer",
  "expires_in": 3600
}

Required Scopes

Correction (2026-05-29): The scopes scanner.scans, scanner.proofs, scanner.unknowns, scanner.unknowns:write, proofs.read, proofs.write, proofs.verify, and anchors.manage do not exist anywhere in src/. The real Scanner scope policies (Security/ScannerAuthorityScopes.cs, wired in Program.cs) and the real Attestor authorization policies (AttestorWebServiceComposition.cs) are below.

Scanner (read paths accept the canonical scanner:read; write paths accept scanner:write):

ScopeGrants
scanner.scans.read (or scanner:read)Read scans, manifests, proof bundles, reachability, unknowns
scanner.scans.write (or scanner:write)Score replay/verify, compute reachability
scanner.scans.enqueue (or scanner:scan/scanner:write)Submit scans
scanner.callgraph.ingestUpload call graphs

Attestor proof chain (policy → required JWT scope; see §6):

PolicyAccepts scope(s)
attestor:readattestor.read, attestor.verify, attestor.write
attestor:verifyattestor.verify, attestor.write
attestor:writeattestor.write

3. Score Proofs API

3.1 Submit Scan

POST /api/v1/scansEndpoints/ScanEndpoints.cs (scanner.scans.submit)

Correction (2026-05-29): This submits a scan for an OCI artifact (image reference and/or digest) and returns 202 Accepted; it does not create a pre-built manifest. The 201 Created manifest-creation contract with feed/VEX/policy snapshot hashes was not implemented. The manifest is read back via §3.2.

Request (ScanSubmitRequest)

{
  "image": { "reference": "registry.example.com/myapp:1.0.0", "digest": "sha256:abc123..." },
  "metadata": { "env": "stage" }
}

Response (202 Accepted, ScanSubmitResponse)

{
  "scanId": "550e8400-e29b-41d4-a716-446655440000",
  "status": "Pending",
  "location": "/api/v1/scans/550e8400-e29b-41d4-a716-446655440000",
  "created": true
}

Errors

CodeDescription
400Neither image reference nor digest supplied, or invalid digest prefix
409Duplicate submission

3.2 Get Scan Manifest

GET /api/v1/scans/{scanId}/manifestEndpoints/ManifestEndpoints.cs (scanner.scans.manifest)

Returns the manifest with canonical input hashes. Send Accept: application/dsse+json for the DSSE-signed envelope. Rate-limited (ManifestPolicy).

Correction (2026-05-29): The wire shape is ScanManifestResponse with sbomHash, rulesHash, feedHash, policyHash (plus versions/timestamps and an RFC 9530 contentDigest). The concelierSnapshotHash / excititorSnapshotHash / latticePolicyHash / seed / knobs fields and the nested manifest+dsseEnvelope JSON envelope are only present in the DSSE negotiated form (SignedScanManifestResponse), not the default application/json body.

Response (200 OK, application/json)

{
  "manifestId": "manifest-001",
  "scanId": "550e8400-e29b-41d4-a716-446655440000",
  "manifestHash": "sha256:manifest123...",
  "sbomHash": "sha256:sbom...",
  "rulesHash": "sha256:rules...",
  "feedHash": "sha256:feed...",
  "policyHash": "sha256:policy...",
  "scannerVersion": "1.0.0",
  "createdAt": "2025-12-17T12:00:00Z",
  "contentDigest": "sha-256=:<base64>:"
}

3.3 Replay Score Computation

POST /api/v1/scans/{scanId}/score/replayEndpoints/ScoreReplayEndpoints.cs (scanner.scans.score.replay). Registered only when scanner:ScoreReplay:Enabled is true.

Correction (2026-05-29): The request is ScoreReplayRequest (manifestHash, freezeTimestamp — both optional); there is no overrides/snapshot-substitution body. The response is a flat ScoreReplayResponse with a factorised score, not a nested scoreProof.nodes[] ledger.

Request (ScoreReplayRequest)

{ "manifestHash": "sha256:manifest123...", "freezeTimestamp": "2025-12-17T13:00:00Z" }

Response (200 OK, ScoreReplayResponse)

{
  "score": 0.50,
  "rootHash": "sha256:proof123...",
  "bundleUri": "/api/v1/scans/.../score/bundle?rootHash=sha256:proof123...",
  "manifestHash": "sha256:manifest123...",
  "canonicalInputHash": "sha256:cinput...",
  "factors": [ { "name": "cvss_base", "weight": 0.5, "raw": 9.1, "weighted": 0.50, "source": "cvss" } ],
  "verificationStatus": "verified",
  "replayedAt": "2025-12-17T13:00:00Z",
  "deterministic": true
}

Sibling routes (same group): GET /score/bundle?rootHash=..., POST /score/verify, GET /score/history.


3.4 Fetch Proof Bundle Metadata

GET /api/v1/scans/{scanId}/proofs/{rootHash}Endpoints/ManifestEndpoints.cs (scanner.scans.proofs.get)

Correction (2026-05-29): This returns a JSON ProofBundleResponse(hashes + signature metadata), not an application/zip archive. There are no Content-Disposition / X-Proof-Root-Hash / X-Manifest-Hash headers and no bundle-contents file manifest. A sibling GET /api/v1/scans/{scanId}/proofs lists bundles for a scan.

Response (200 OK, application/json, ProofBundleResponse)

{
  "scanId": "550e8400-e29b-41d4-a716-446655440000",
  "rootHash": "sha256:proof123...",
  "bundleType": "score-proof",
  "bundleHash": "sha256:bundle...",
  "manifestHash": "sha256:manifest...",
  "signatureKeyId": "ecdsa-p256-key-001",
  "signatureAlgorithm": "ES256",
  "createdAt": "2025-12-17T12:00:00Z",
  "signatureValid": true,
  "contentDigest": "sha-256=:<base64>:"
}

4. Reachability API

4.1 Upload Call Graph

POST /api/v1/scans/{scanId}/callgraphsEndpoints/CallGraphEndpoints.cs (scanner.scans.callgraphs.submit). Scope: scanner.callgraph.ingest. Content-Digest header required.

Correction (2026-05-29): A top-level scanKey field is required, schema must be exactly stella.callgraph.v1, and the response is CallGraphAcceptedResponseDto (callgraphId, nodeCount, edgeCount, digest) — no scanId/entrypointsCount/status. A duplicate Content-Digest returns 409 Conflict.

Request (CallGraphV1Dto)

{
  "schema": "stella.callgraph.v1",
  "scanKey": "550e8400-e29b-41d4-a716-446655440000",
  "language": "dotnet",
  "artifacts": [
    {
      "artifactKey": "MyApp.WebApi.dll",
      "kind": "assembly",
      "sha256": "sha256:artifact123..."
    }
  ],
  "nodes": [
    {
      "nodeId": "sha256:node1...",
      "artifactKey": "MyApp.WebApi.dll",
      "symbolKey": "MyApp.Controllers.OrdersController::Get(System.Guid)",
      "visibility": "public",
      "isEntrypointCandidate": true
    }
  ],
  "edges": [
    {
      "from": "sha256:node1...",
      "to": "sha256:node2...",
      "kind": "static",
      "reason": "direct_call",
      "weight": 1.0
    }
  ],
  "entrypoints": [
    {
      "nodeId": "sha256:node1...",
      "kind": "http",
      "route": "/api/orders/{id}",
      "framework": "aspnetcore"
    }
  ]
}

Response (202 Accepted, CallGraphAcceptedResponseDto)

{ "callgraphId": "cg-001", "nodeCount": 1234, "edgeCount": 5678, "digest": "sha256:cg123..." }

4.2 Compute Reachability

POST /api/v1/scans/{scanId}/compute-reachabilityEndpoints/ReachabilityEndpoints.cs (scanner.scans.compute-reachability). Scope: scanner.scans.write.

Correction (2026-05-29): The route is /scans/{scanId}/compute-reachability, not /scans/{scanId}/reachability/compute. Response is ComputeReachabilityResponseDto (jobId, status, estimatedDuration); no scanId/_links. The default wiring registers a NullReachabilityComputeService, so without a reachability runtime the endpoint returns 501 Not Implemented. There is no documented Scanner /jobs/{jobId} polling route.

Response (202 Accepted, ComputeReachabilityResponseDto)

{ "jobId": "reachability-job-001", "status": "queued", "estimatedDuration": "30s" }

4.3 Get Reachability Findings

GET /api/v1/scans/{scanId}/reachability/findingsEndpoints/ReachabilityEndpoints.cs (scanner.scans.reachability.findings)

Correction (2026-05-29): Query params are cve and status (not cveId). Response is ReachabilityFindingListDto with a flat items[] and total; there is no scanId, computedAt, per-finding path/evidence, nor a summary aggregate. A sibling GET /reachability/components returns ComponentReachabilityListDto.

Query Parameters

ParameterTypeDescription
cvestringFilter by CVE ID
statusstringFilter by reachability status

Response (200 OK, ReachabilityFindingListDto)

{
  "items": [
    {
      "cveId": "CVE-2024-1234",
      "purl": "pkg:npm/lodash@4.17.20",
      "status": "reachable",
      "confidence": 0.70,
      "latticeState": "REACHABLE_STATIC",
      "severity": "high",
      "affectedVersions": ["<4.17.21"]
    }
  ],
  "total": 1
}

4.4 Explain Reachability

GET /api/v1/scans/{scanId}/reachability/explainEndpoints/ReachabilityEndpoints.cs (scanner.scans.reachability.explain)

Correction (2026-05-29): Response is ReachabilityExplanationDto. The path is a pathWitness string list, reasons are a why[] list of {code, description, impact}, and evidence is grouped under evidence (staticAnalysis, runtimeEvidence, policyEvaluation) plus an optional spineId. There is no explanation.shortestPath[], confidenceFactors, or alternativePaths. Missing cve/purl returns 400.

Query Parameters

ParameterRequiredDescription
cveYesCVE ID
purlYesPackage URL

Response (200 OK, ReachabilityExplanationDto)

{
  "cveId": "CVE-2024-1234",
  "purl": "pkg:npm/lodash@4.17.20",
  "status": "reachable",
  "confidence": 0.70,
  "latticeState": "REACHABLE_STATIC",
  "pathWitness": ["OrdersController::Get -> OrderService::Process -> Lodash.merge"],
  "why": [ { "code": "static_path", "description": "Static call path exists", "impact": "positive" } ],
  "evidence": {
    "staticAnalysis": { "callgraphDigest": "sha256:cg123...", "pathLength": 3, "edgeTypes": ["static"] },
    "runtimeEvidence": null,
    "policyEvaluation": { "policyDigest": "sha256:policy...", "verdict": "blocked", "verdictReason": "reachable_critical" }
  },
  "spineId": "spine-001"
}

5. Unknowns API

Correction (2026-05-29): The Unknowns API is a Scanner route group (Endpoints/UnknownsEndpoints.cs, all scanner.scans.read). It is read-only: there is no escalate, resolve, proof, or summary endpoint. The implemented routes are: GET /unknowns, /unknowns/stats, /unknowns/bands, /unknowns/{id}, /unknowns/{id}/evidence, /unknowns/{id}/history. The rich blast-radius / containment / exploit-pressure / score-breakdown / reason-details fields below were not implemented.

5.1 List Unknowns

GET /api/v1/unknownsscanner.unknowns.list

Query Parameters

ParameterTypeDefaultDescription
bandstringFilter: HOT, WARM, COLD
artifactDigeststringFilter by artifact digest
vulnIdstringFilter by vulnerability ID
sortBystringscorescore, created, updated
sortOrderstringdescasc, desc
limitint50Clamped 1–500
offsetint0Pagination offset

Response (200 OK, UnknownsListResponse)

{
  "items": [
    {
      "id": "unk-3f1c...",
      "artifactDigest": "sha256:abc123...",
      "vulnerabilityId": "CVE-2024-1234",
      "packagePurl": "pkg:npm/lodash@4.17.20",
      "score": 0.62,
      "band": "WARM",
      "createdAtUtc": "2025-01-15T10:30:00Z",
      "updatedAtUtc": "2025-01-15T10:30:00Z"
    }
  ],
  "totalCount": 142,
  "limit": 50,
  "offset": 0
}

5.2 Get Unknown by ID

GET /api/v1/unknowns/{id}scanner.unknowns.get

Returns UnknownDetailResponse (id, artifactDigest, vulnerabilityId, packagePurl, score, band, proofRef, createdAtUtc, updatedAtUtc). The id accepts either a GUID or the external unk-<32hex> form.

{
  "id": "unk-3f1c...",
  "artifactDigest": "sha256:abc123...",
  "vulnerabilityId": "CVE-2024-1234",
  "packagePurl": "pkg:npm/lodash@4.17.20",
  "score": 0.62,
  "band": "WARM",
  "proofRef": "proofs/unknowns/unk-3f1c/tree.json",
  "createdAtUtc": "2025-01-15T10:30:00Z",
  "updatedAtUtc": "2025-01-15T10:30:00Z"
}

5.3 Get Unknown Evidence

GET /api/v1/unknowns/{id}/evidencescanner.unknowns.evidence

Replaces the previously documented /{id}/proof (which does not exist). Returns UnknownEvidenceResponse.

{ "id": "unk-3f1c...", "proofRef": "proofs/unknowns/unk-3f1c/tree.json", "lastUpdatedAtUtc": "2025-01-15T10:30:00Z" }

5.4 Get Unknown History

GET /api/v1/unknowns/{id}/historyscanner.unknowns.history

Returns UnknownHistoryResponse (id, history[] of {capturedAtUtc, score, band}).


5.5 Unknowns Stats

GET /api/v1/unknowns/statsscanner.unknowns.stats

Replaces the previously documented /unknowns/summary (which does not exist). Returns UnknownsStatsResponse.

{ "total": 142, "hot": 12, "warm": 48, "cold": 82 }

GET /api/v1/unknowns/bands (scanner.unknowns.bands) returns a band → count distribution map.


5.6 Escalate / Resolve — NOT IMPLEMENTED

Removed (2026-05-29): POST /unknowns/{id}/escalate and POST /unknowns/{id}/resolve have no backing route in UnknownsEndpoints.cs (the group exposes only the GET routes above). File a sprint to add write routes + audit actions before re-documenting these.


6. Proof Chain API

Major correction (2026-05-29): The Attestor (src/Attestor/.../StellaOps.Attestor.WebService/) hosts two distinct proof surfaces. The {entry}/spine, {entry}/receipt, {entry}/vex, trust-anchor, and verify endpoints documented here are part of the spine/anchor/verify scaffolding (ProofsController at route proofs, AnchorsController at anchors, VerifyController at verify — all root-relative, no /api/v1 prefix). Every handler in that scaffolding returns 501 Not Implemented, gated behind feature flags (AnchorsEnabled/ProofsEnabled/VerifyEnabled, all default false) — and even when the flag is on, the body still returns 501. They are forward-looking stubs, not live endpoints. The authorization policies are attestor:write/attestor:read/attestor:verify (requiring scopes attestor.write/attestor.read/attestor.verify), not proofs.*/anchors.manage.

The implemented proof-chain surface is ProofChainController at /api/v1/proofs, which is keyed by artifact subject digest rather than an SBOM-entry composite — see §6.5.

6.1 Create Proof Spine — NOT IMPLEMENTED (501 stub)

POST /proofs/{entry}/spineControllers/ProofsController.cs (scope attestor:write). Returns 501. {entry} format: sha256:<hash>:pkg:<purl>. Documented request (evidenceIds, reasoningId, vexVerdictId, policyVersion) and 201 response (proofBundleId, receiptUrl) describe the intended contract once implemented.

6.2 Get Proof Spine — NOT IMPLEMENTED (501 stub)

GET /proofs/{entry}/spineProofsController (scope attestor:read). Returns 501.

6.3 Get Verification Receipt — NOT IMPLEMENTED (501 stub)

GET /proofs/{entry}/receiptProofsController (scope attestor:read). Returns 501. A GET /proofs/{entry}/vex stub also exists (scope attestor:read, 501).

6.4 Verify Proof Bundle — NOT IMPLEMENTED (501 stub)

POST /verify/{proofBundleId}Controllers/VerifyController.cs (scope attestor:verify). Returns 501.

Correction: the bundle id is a path parameter (/verify/{proofBundleId}), not a body field, and there is no POST /verify (or POST /proofs/verify) collection route and no POST /verify/batch. Additional stubs: GET /verify/envelope/{envelopeHash} and GET /verify/rekor/{envelopeHash} (both scope attestor:read, 501). Trust-anchor stubs live on Controllers/AnchorsController.cs: GET /anchors, GET /anchors/{anchorId}, POST /anchors, PATCH /anchors/{anchorId:guid}, POST /anchors/{anchorId:guid}/revoke-key, DELETE /anchors/{anchorId:guid} — all 501. (Note revocation is POST .../revoke-key, not a plain DELETE.)

The documented verify request/response shape (checkRekor, anchorIds, checks.{signatureValid, idRecomputed, merklePathValid, rekorInclusionValid}) and the four-step verification flow remain the target contract for these stubs.


6.5 Implemented Proof-Chain Query Controller

Source: Controllers/ProofChainController.cs, route /api/v1/proofs, scope attestor:read (verify route additionally attestor:verify). Backed by IProofChainQueryService + IProofVerificationService (real implementations).

MethodRouteDescription
GET/api/v1/proofs/{subjectDigest}List proofs for an artifact (ProofListResponse)
GET/api/v1/proofs/{subjectDigest}/chain?maxDepth=Evidence-chain graph (ProofChainResponse, depth 1–10, default 5)
GET/api/v1/proofs/id/{proofId}Proof detail (ProofDetail)
GET/api/v1/proofs/id/{proofId}/verifyVerify a proof (ProofVerificationResult; scope attestor:verify)

A related implemented controller, Controllers/ChainController.cs at /api/v1/chains, exposes attestation-chain traversal (/{attestationId}, /{attestationId}/upstream, /{attestationId}/downstream, /{attestationId}/graph?format=mermaid|dot|json, /artifact/{artifactDigest}?chain=).


7. Data Models

ScanManifestResponse (wire shape)

Correction (2026-05-29): This is the actual application/json shape returned by GET /scans/{scanId}/manifest (Contracts/ManifestContracts.cs). The internal StellaOps.Scanner.Core/ScanManifest.cs model has additional fields (incl. latticePolicyHash, seed, knobs) but they are not on this response DTO.

interface ScanManifestResponse {
  manifestId: string;
  scanId: string;
  manifestHash: string;           // sha256:...
  sbomHash: string;
  rulesHash: string;
  feedHash: string;
  policyHash: string;
  scanStartedAt: string | null;   // ISO 8601
  scanCompletedAt: string | null;
  scannerVersion: string;
  createdAt: string;
  contentDigest: string;          // RFC 9530, sha-256=:...:
}

ProofNode

interface ProofNode {
  id: string;
  kind: "Input" | "Transform" | "Delta" | "Score";
  ruleId: string;
  parentIds: string[];
  evidenceRefs: string[];
  delta: number;
  total: number;
  actor: string;
  tsUtc: string;
  seed: string;
  nodeHash: string;
}

DsseEnvelope

interface DsseEnvelope {
  payloadType: string;
  payload: string;                // base64 canonical JSON
  signatures: DsseSignature[];
}

interface DsseSignature {
  keyid: string;
  sig: string;                    // base64
}

ReachabilityStatus

enum ReachabilityStatus {
  UNREACHABLE = "UNREACHABLE",
  POSSIBLY_REACHABLE = "POSSIBLY_REACHABLE",
  REACHABLE_STATIC = "REACHABLE_STATIC",
  REACHABLE_PROVEN = "REACHABLE_PROVEN",
  UNKNOWN = "UNKNOWN"
}

UnknownReasonCode

CodeDescription
missing_vexNo VEX statement for vulnerability
ambiguous_indirect_callIndirect call target unresolved
incomplete_sbomSBOM missing component data
unknown_platformPlatform not recognized
missing_advisoryNo advisory data for CVE
conflicting_evidenceMultiple conflicting data sources
stale_dataData exceeds freshness threshold

8. Error Handling

All errors follow RFC 7807 Problem Details format.

Error Response

{
  "type": "https://stella-ops.org/errors/scan-not-found",
  "title": "Scan Not Found",
  "status": 404,
  "detail": "Scan ID '550e8400...' does not exist.",
  "instance": "/api/v1/scans/550e8400...",
  "traceId": "trace-001"
}

Error Types

Note (2026-05-29): Problem responses are built from localization keys, not a fixed slug registry. The invalid-manifest, duplicate-scan, snapshot-not-found, callgraph-not-uploaded, unknown-not-found, and escalation-conflict rows belong to the removed manifest-creation / escalate contracts and do not map to live endpoints.

Type (illustrative)StatusDescription
scan-not-found404Scan ID not found
invalid-callgraph400Call-graph schema validation failed
callgraph-duplicate409Duplicate call-graph (Content-Digest)
payload-too-large413Request body exceeds size limit
proof-not-found404Proof root hash not found
reachability-in-progress409Reachability already running
reachability-unavailable501Reachability runtime not configured

9. Rate Limiting

Correction (2026-05-29): On the Scanner side, only GET /scans/{scanId}/manifest is rate-limited in code (RequireRateLimiting(ManifestPolicy)). The per-endpoint quotas previously listed for submit/replay/callgraphs/reachability/unknowns are not enforced and were aspirational. The Attestor proof-chain controllers use named rate-limiter policies (attestor-reads, attestor-submissions, attestor-verifications).

Response Headers

HeaderDescription
X-RateLimit-LimitMaximum requests per window
X-RateLimit-RemainingRemaining requests
X-RateLimit-ResetUnix timestamp when limit resets

Rate Limit Error (429)

{
  "type": "https://stella-ops.org/errors/rate-limit-exceeded",
  "title": "Rate Limit Exceeded",
  "status": 429,
  "detail": "Exceeded 100 requests/hour for POST /scans.",
  "retryAfter": 1234567890
}

10. Examples

Example 1: Complete Scan Workflow

# 1. Submit scan (image reference and/or digest)
SCAN_RESP=$(curl -X POST https://scanner.example.com/api/v1/scans \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "image": { "reference": "registry.example.com/myapp:1.0.0", "digest": "sha256:abc123..." } }')

SCAN_ID=$(echo $SCAN_RESP | jq -r '.scanId')

# 2. Upload call graph (Content-Digest header is required)
curl -X POST "https://scanner.example.com/api/v1/scans/$SCAN_ID/callgraphs" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Content-Digest: sha256=abc123..." \
  -d @callgraph.json

# 3. Compute reachability (501 if no reachability runtime configured)
curl -X POST "https://scanner.example.com/api/v1/scans/$SCAN_ID/compute-reachability" \
  -H "Authorization: Bearer $TOKEN"

# 4. Get findings
curl "https://scanner.example.com/api/v1/scans/$SCAN_ID/reachability/findings" \
  -H "Authorization: Bearer $TOKEN"

Example 2: Score Replay

# Replay deterministically against a frozen manifest (requires scanner:ScoreReplay:Enabled)
curl -X POST "https://scanner.example.com/api/v1/scans/$SCAN_ID/score/replay" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "manifestHash": "sha256:manifest123..." }'

Example 3: Unknowns (read-only)

# List HOT-band unknowns, score-sorted descending
curl "https://scanner.example.com/api/v1/unknowns?band=HOT&sortBy=score&sortOrder=desc" \
  -H "Authorization: Bearer $TOKEN"

# Aggregate counts
curl "https://scanner.example.com/api/v1/unknowns/stats" -H "Authorization: Bearer $TOKEN"

Example 4: Proof Bundle Metadata + Proof-Chain Query

# Fetch proof bundle metadata (JSON, not a zip)
curl "https://scanner.example.com/api/v1/scans/$SCAN_ID/proofs/sha256:proof123..." \
  -H "Authorization: Bearer $TOKEN"

# Query the Attestor implemented proof-chain controller by artifact subject digest
curl "https://attestor.example.com/api/v1/proofs/sha256:abc123..." \
  -H "Authorization: Bearer $TOKEN"   # scope: attestor.read

# NOTE: the /verify, /proofs/{entry}/spine, and /anchors endpoints are 501 stubs (see §6).


Last Updated: 2026-05-29 (doc↔code reconciliation)
API Version: 1.0.0
Sprint: 3500.0004.0004