Witness Schema v1 Contract

Version: stellaops.witness.v1
Status: Active (implemented)
Sprint: SPRINT_3700_0001_0001_witness_foundation


Overview

Audience: engineers producing, consuming, or verifying Stella Ops reachability witnesses (Scanner, Attestor, CLI, and Evidence Locker integrators).

A witness is a cryptographically-signed proof of a reachability path from an entrypoint to a vulnerable sink. Witnesses provide:

  1. Auditability - Proof that a path was found at scan time
  2. Offline verification - Verify claims without re-running analysis
  3. Provenance - Links to the source graph and analysis context
  4. Transparency - Can be published to transparency logs

Two representations

This contract describes two related-but-distinct on-disk shapes. Do not confuse them:

  1. Wire / DSSE payload schema (stellaops.witness.v1) — the canonical signed payload produced and consumed by the Scanner. This is the authoritative runtime shape. Modelled by StellaOps.Scanner.Reachability.Witnesses.PathWitness (snake_case JSON). The DSSE-signed payload, the CLI witness show/list/verify/export commands, and the GET /api/v1/witnesses API all return this shape.
  2. In-toto predicate JSON Schema (stellaops-path-witness.v1.schema.json) — a separate draft JSON Schema used by the Attestor for predicate validation. It uses a UUID witness_id, a separate witness_hash, a provenance block, a nested path.{entrypoint,sink,steps,hop_count} structure, and cas://-style evidence_uris. These fields are NOT present in the wire schema.

The same field-shape divergence is reflected in storage: the scanner.witnesses table (see Storage) persists predicate-style columns (witness_hash, witness_type, graph_hash, scan_id, run_id, entrypoint_fqn, sink_cve, created_at) in addition to the full wire payload in payload_json.

Sections below are labelled with which representation they describe.


Schema Definition — wire / DSSE payload (stellaops.witness.v1)

Source of truth: src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Witnesses/PathWitness.cs and WitnessSchema.cs. This is the shape that is DSSE-signed, stored in scanner.witnesses.payload_json, and returned by the CLI and API. JSON is serialized snake_case.

{
  "witness_schema": "stellaops.witness.v1",
  "witness_id": "wit:sha256:abcd1234...",
  "artifact": {
    "sbom_digest": "sha256:...",
    "component_purl": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1"
  },
  "vuln": {
    "id": "CVE-2021-44228",
    "source": "NVD",
    "affected_range": "<2.15.0"
  },
  "entrypoint": {
    "kind": "http",
    "name": "GET /api/users/{id}",
    "symbol_id": "com.example.MyController.handleRequest"
  },
  "path": [
    {
      "symbol": "MyController.handleRequest",
      "symbol_id": "com.example.MyController.handleRequest",
      "file": "src/main/java/com/example/MyController.java",
      "line": 45,
      "column": null
    },
    {
      "symbol": "LoggingService.logMessage",
      "symbol_id": "com.example.LoggingService.logMessage",
      "file": "src/main/java/com/example/LoggingService.java",
      "line": 23
    },
    {
      "symbol": "Logger.log",
      "symbol_id": "org.apache.log4j.Logger.log",
      "file": null,
      "line": null
    }
  ],
  "sink": {
    "symbol": "Logger.log",
    "symbol_id": "org.apache.log4j.Logger.log",
    "sink_type": "deserialization"
  },
  "gates": [
    {
      "type": "AuthRequired",
      "guard_symbol": "com.example.MyController.requireAuth",
      "confidence": 0.92,
      "detail": "Requires authenticated user"
    }
  ],
  "evidence": {
    "callgraph_digest": "blake3:...",
    "surface_digest": "sha256:...",
    "analysis_config_digest": "sha256:...",
    "build_id": "gnu-build-id:..."
  },
  "observed_at": "2025-12-18T12:00:00Z",
  "path_hash": "path:sha256:...",
  "node_hashes": ["sha256:...", "sha256:..."],
  "evidence_uris": ["evidence:callgraph:blake3:...", "evidence:sbom:sha256:..."],
  "predicate_type": "https://stella.ops/predicates/path-witness/v1",
  "observation_type": 0,
  "claim_id": null,
  "observations": null,
  "symbolization": null
}

Field Definitions — wire schema

Root Fields

FieldTypeRequiredDescription
witness_schemastringYesMust be stellaops.witness.v1
witness_idstringYesContent-addressed ID: wit:sha256:<hex> (see Witness ID)
artifactobjectYesSBOM/component context
vulnobjectYesVulnerability the witness concerns
entrypointobjectYesEntry point that starts the path
patharrayYesFlat ordered list of PathStep (caller -> callee)
sinkobjectYesVulnerable sink at end of path
gatesarrayNoDetected guards/controls along the path
evidenceobjectYesEvidence digests for reproducibility
observed_atISO8601YesWhen the witness was generated (UTC)
path_hashstringNoPath fingerprint (path:sha256:<hex>; see Path Hash Recipe)
node_hashesarrayNoTop-K node hashes (sha256:<hex>), deterministically ordered
evidence_urisarrayNoevidence:<kind>:<digest> traceability URIs
predicate_typestringNoDefault https://stella.ops/predicates/path-witness/v1
observation_typeenum (int)No0=Static (default), 1=Runtime, 2=Confirmed (serialized as integer)
claim_idstringNoLinks runtime witnesses to a static claim: claim:<artifact-digest>:<path-hash>
observationsarrayNoRuntime observations (empty for purely static witnesses)
symbolizationobjectNoDeterministic symbolization tuple; required for runtime/confirmed witnesses (see Runtime Symbolization Tuple)

There is no witness_hash, witness_type, provenance, created_at, or hop_count field in the wire schema. Those fields belong to the in-toto predicate / storage row only (see below).

artifact

FieldTypeRequiredDescription
sbom_digeststringYesSHA-256 digest of the SBOM
component_purlstringYesPURL of the vulnerable component

vuln

FieldTypeRequiredDescription
idstringYesVulnerability identifier (e.g., CVE-2024-12345)
sourcestringYesSource (e.g., NVD, OSV, GHSA)
affected_rangestringYesAffected version range expression

entrypoint

FieldTypeRequiredDescription
kindstringYesEntrypoint kind (e.g., http, grpc, cli, job, event)
namestringYesHuman-readable name (e.g., GET /api/users/{id})
symbol_idstringYesCanonical symbol ID for the entrypoint

Path Step (path[])

FieldTypeRequiredDescription
symbolstringYesHuman-readable symbol name
symbol_idstringYesCanonical symbol ID
filestringNoSource file path (null for external/binary symbols)
lineintegerNoLine number (1-based)
columnintegerNoColumn number (1-based)

The wire PathStep has no index, call_site, or edge_type field — those exist only in the in-toto predicate’s pathStep definition.

sink

FieldTypeRequiredDescription
symbolstringYesHuman-readable symbol name
symbol_idstringYesCanonical symbol ID
sink_typestringYesSink taxonomy (e.g., deserialization, sql_injection, path_traversal)

Gates (gates[])

Optional array of detected guards/mitigating controls along the path.

FieldTypeRequiredDescription
typestringYesGate type. Emitted as the GateType enum’s .ToString() value (e.g., AuthRequired, InputValidation, RateLimited), not a snake_case enum
guard_symbolstringYesSymbol that implements the gate
confidencenumberYesConfidence level (0.0 - 1.0)
detailstringNoHuman-readable detail about the gate

evidence

FieldTypeRequiredDescription
callgraph_digeststringYesBLAKE3 digest of the call graph used
surface_digeststringNoSHA-256 digest of the attack surface manifest
analysis_config_digeststringNoSHA-256 digest of the analysis configuration
build_idstringNoBuild identifier for the analyzed artifact

In-toto predicate JSON Schema (separate artifact)

Source of truth: src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-path-witness.v1.schema.json ($id https://stella.ops/schemas/predicates/path-witness/v1).

This draft-2020-12 JSON Schema is used by the Attestor for predicate validation and has a different shape than the wire payload. It is the origin of the historical example previously shown in this document. Its required fields are witness_id, witness_hash, provenance, path.

{
  "witness_id": "uuid",
  "witness_hash": "sha256:abcd1234...  (pattern ^(blake3|sha256):[a-f0-9]{64}$)",
  "witness_type": "reachability_path  (enum: reachability_path | gate_proof; default reachability_path)",
  "provenance": {
    "graph_hash": "sha256:efgh5678...  (required)",
    "scan_id": "uuid",
    "run_id": "uuid",
    "analyzer_version": "1.0.0  (required)",
    "analysis_timestamp": "2025-12-18T11:59:00Z  (required)"
  },
  "path": {
    "entrypoint": { "fqn": "...", "kind": "http_handler", "location": {"file": "...", "line": 42}, "node_hash": "sha256:..." },
    "sink": { "fqn": "...", "cve": "CVE-2021-44228", "package": "pkg:maven/...", "node_hash": "sha256:..." },
    "steps": [
      { "index": 0, "fqn": "...", "call_site": "...:45", "edge_type": "call", "node_hash": "sha256:..." }
    ],
    "hop_count": 3,
    "path_hash": "sha256:...",
    "node_hashes": ["sha256:..."]
  },
  "gates": [
    { "type": "auth_required", "location": "MyController.java:40", "description": "Requires authenticated user" }
  ],
  "evidence": {
    "graph_fragment_hash": "sha256:ijkl9012...",
    "path_hash": "sha256:mnop3456..."
  },
  "evidence_uris": {
    "graph": "cas://sha256:...",
    "sbom": "cas://sha256:...",
    "attestation": "cas://sha256:...",
    "rekor": "https://rekor.sigstore.dev/..."
  }
}

Predicate enum domains (from the JSON Schema):

FieldAllowed values
witness_typereachability_path, gate_proof
path.entrypoint.kindhttp_handler, grpc_handler, cli_main, scheduler, message_handler, other
path.steps[].edge_typecall, virtual, static, sink, interface, delegate
gates[].typeauth_required, feature_flag, admin_only, non_default_config, rate_limited, other

Hash patterns in the predicate schema accept either blake3: or sha256: prefixes (^(blake3\|sha256):[a-f0-9]{64}$), except path.path_hash, node_hashes, and *.node_hash which are ^sha256:[a-f0-9]{64}$ only.


Witness ID (content addressing)

The wire witness_id is a content address, not a UUID. It is computed by PathWitnessBuilder.ComputeWitnessId over a fixed subset of fields:

  1. Build a canonical projection containing exactly: witness_schema, artifact, vuln, entrypoint, path, sink, evidence (snake_case, no indentation, Web defaults). Note this projection excludes gates, observed_at, path_hash, node_hashes, evidence_uris, predicate_type, and the runtime fields.
  2. Hash the UTF-8 bytes via ICryptoHash.ComputePrefixedHashForPurpose(json, HashPurpose.Content). The default compliance profile maps HashPurpose.Content to SHA-256 (prefix sha256:); the FIPS profile is also SHA-256.
  3. Prefix the result with wit: (WitnessSchema.WitnessIdPrefix).

Result format: wit:sha256:<64-hex>.

var canonical = new {
    witness.WitnessSchema, witness.Artifact, witness.Vuln,
    witness.Entrypoint, witness.Path, witness.Sink, witness.Evidence
};
var json = JsonSerializer.SerializeToUtf8Bytes(canonical, jsonOptions);
var hash = _cryptoHash.ComputePrefixedHashForPurpose(json, HashPurpose.Content); // "sha256:<hex>"
var witnessId = $"{WitnessSchema.WitnessIdPrefix}{hash}";                         // "wit:sha256:<hex>"

The separate witness_hash value (predicate / storage column) follows the predicate schema pattern ^(blake3|sha256):[a-f0-9]{64}$ and is a hash of the canonical witness payload. It is distinct from witness_id. There is no BLAKE3-over-the-whole-witness-excluding-created_at recipe in the implementation.


DSSE Constants

Sprint: SPRINT_3700_0001_0001 (WIT-007C)

The following constants are used for DSSE envelope creation and verification:

ConstantValueLocation
Payload Typeapplication/vnd.stellaops.witness.v1+jsonWitnessSchema.DssePayloadType
Schema Versionstellaops.witness.v1WitnessSchema.Version
Witness ID prefixwit:WitnessSchema.WitnessIdPrefix
JSON Schema URIhttps://stellaops.org/schemas/witness-v1.jsonWitnessSchema.JsonSchemaUri
Legacy predicate constantstella.ops/pathWitness@v1WitnessSchema.PredicateType, PredicateTypes.StellaOpsPathWitness
Canonical predicate (default)https://stella.ops/predicates/path-witness/v1WitnessPredicateTypes.PathWitnessCanonical

Note: WitnessSchema.PredicateType still equals the legacy short form stella.ops/pathWitness@v1, but PathWitness.predicate_type defaults to the canonical https://stella.ops/predicates/path-witness/v1 (see Canonical Predicate Type and Aliases). The DSSE payloadType written by WitnessDsseSigner is WitnessSchema.DssePayloadType (application/vnd.stellaops.witness.v1+json), not an in-toto predicate type. WitnessSchema.JsonSchemaUri (https://stellaops.org/schemas/witness-v1.json) is the wire-schema URI and is distinct from the in-toto predicate schema $id https://stella.ops/schemas/predicates/path-witness/v1.

Witness Types

Defined as WitnessSchema.WitnessTypeReachabilityPath / WitnessTypeGateProof. These values populate the storage witness_type column and the in-toto predicate witness_type enum. The wire PathWitness record itself has no witness_type field — it always represents a reachability path.

ValueDescription
reachability_pathPath witness from entrypoint to vulnerable sink
gate_proofEvidence of mitigating control (gate) along path

Canonical Predicate Type and Aliases

Sprint: SPRINT_20260112_004_SCANNER_path_witness_nodehash
Sprint: SPRINT_20260112_008_DOCS_path_witness_contracts (PW-DOC-001)

The canonical predicate type for path witnesses is:

https://stella.ops/predicates/path-witness/v1

The following aliases are recognized for backward compatibility:

AliasStatus
stella.ops/pathWitness@v1Active (legacy short form)
https://stella.ops/pathWitness/v1Active (URL variant)

Consumers must accept all aliases when verifying; producers should emit the canonical form.


Node Hash Recipe

Canonical node hash recipe for deterministic static/runtime evidence joining.

Recipe

NodeHash = SHA256(normalize(PURL) + ":" + normalize(SYMBOL_FQN))

Output format: sha256:<64-hex-chars>

PURL Normalization Rules

  1. Lowercase scheme (pkg:)
  2. Lowercase type (e.g., NPM -> npm)
  3. Preserve namespace/name case (some ecosystems are case-sensitive)
  4. Sort qualifiers alphabetically by key
  5. Remove trailing slashes
  6. Normalize empty version to unversioned

Symbol FQN Normalization Rules

  1. Trim whitespace
  2. Normalize multiple dots (..) to single dot
  3. Normalize signature whitespace: (type,type) -> (type, type)
  4. Empty signatures become ()
  5. Collapse the module-level placeholder ._. to .

Caveats (matching the current NodeHashRecipe code, not just its XML comments): rules 5/6 above are partial. NormalizePurl lowercases the scheme and type and sorts qualifiers, but does not substitute an empty version with unversioned (despite the doc-comment). NormalizeSymbolFqn collapses the literal ._. placeholder rather than stripping bare _ type placeholders.

Example

Input:
  PURL: pkg:npm/lodash@4.17.21
  Symbol: lodash.merge(object, object)

Normalized Input:
  "pkg:npm/lodash@4.17.21:lodash.merge(object, object)"

Output:
  sha256:a1b2c3d4e5f6... (64 hex chars)

Implementation

See src/__Libraries/StellaOps.Reachability.Core/NodeHashRecipe.cs (NodeHashRecipe.ComputeHash / NormalizePurl / NormalizeSymbolFqn). The normalization rules above describe this canonical recipe.

Divergence warning: PathWitnessBuilder does not call NodeHashRecipe. Its private ComputeNodeHash uses a simplified normalization — it lowercases the entire PURL (purl.Trim().ToLowerInvariant()) and does not sort qualifiers or expand empty versions — then hashes SHA256(normalizedPurl + ":" + symbolFqn.Trim()). The two recipes can therefore produce different node hashes for the same input. The canonical NodeHashRecipe is the intended long-term recipe; the builder’s inline variant is the value actually emitted in PathWitness.node_hashes today.


Path Hash Recipe

Canonical path hash recipe for deterministic path fingerprinting.

Recipe

PathHash = SHA256(nodeHash1 + ">" + nodeHash2 + ">" + ... + nodeHashN)

The > separator represents directed edges in the path.

Top-K Selection

For efficiency, witnesses include a top-K subset of node hashes:

  1. Take first K/2 nodes (entry points)
  2. Take last K/2 nodes (exit/vulnerable points)
  3. Deduplicate while preserving order
  4. Default K = 10

PathFingerprint Fields

The PathFingerprint record (PathHashRecipe.CreateFingerprint) carries:

Field (C# property)TypeDescription
PathHashstringsha256:<hex> of full path
NodeCountintegerTotal nodes in path
TopKNodeHashesarrayTop-K node hashes for lookup
SourceNodeHashstringHash of entry (first) node
SinkNodeHashstringHash of sink (last) node

PathFingerprint is an in-memory record only; it is not embedded as a path_fingerprint object in the witness JSON. The witness instead carries the flat path_hash and node_hashes fields.

Implementation

See src/__Libraries/StellaOps.Reachability.Core/PathHashRecipe.cs (PathHashRecipe.ComputeHash / ComputeWithTopK / CreateFingerprint).

Divergence warning: the >-separated recipe above is the canonical PathHashRecipe, but the path_hash actually emitted on a PathWitness is produced by PathWitnessBuilder’s private ComputeCombinedPathHash, which uses a different recipe: "path:sha256:" + SHA256(hexPart1 + ":" + hexPart2 + ... ) — i.e. a path:sha256: prefix and a : separator over the hex parts of each node hash (not the > directed-edge separator, and not the bare sha256: prefix). The builder also computes node hashes over all path steps for the path hash, while exposing only the deduplicated top-K (sorted, K=10) in node_hashes.


Evidence URI Fields

There are two distinct evidence-URI conventions; do not mix them.

Wire schema — evidence_uris (flat string array)

In the wire payload, evidence_uris is an array of strings built by PathWitnessBuilder.BuildEvidenceUris. Each entry uses the evidence:<kind>:<value> scheme and is emitted only when the corresponding request field is present:

EntryEmitted whenExample
evidence:callgraph:<callgraphDigest>CallgraphDigest setevidence:callgraph:blake3:...
evidence:sbom:<sbomDigest>SbomDigest setevidence:sbom:sha256:...
evidence:surface:<surfaceDigest>SurfaceDigest setevidence:surface:sha256:...
evidence:build:<buildId>BuildId setevidence:build:gnu-build-id:...
{
  "evidence_uris": [
    "evidence:callgraph:blake3:abc123...",
    "evidence:sbom:sha256:def456..."
  ]
}

In-toto predicate schema — evidence_uris (object with cas://)

The predicate JSON Schema (stellaops-path-witness.v1.schema.json) instead defines evidence_uris as an object whose graph/sbom/attestation values match ^cas://sha256:[a-f0-9]{64}$ and whose rekor value is a free-form URI:

{
  "evidence_uris": {
    "graph": "cas://sha256:abc123...",
    "sbom": "cas://sha256:def456...",
    "attestation": "cas://sha256:ghi789...",
    "rekor": "https://rekor.sigstore.dev/api/v1/log/entries/abc123def456"
  }
}

There are no graph_uri / sbom_uri / attestation_uri / rekor_uri scalar fields anywhere in the implementation.

stella:// evidence URIs (separate builder)

StellaOps.Reachability.Core.EvidenceUriBuilder produces a third, unrelated URI scheme used for ReachGraph/runtime-fact references — stella://reachgraph/<digest>, stella://signals/runtime/<tenant>/<digest>, stella://cvemap/<cve>, stella://attestation/<digest>, stella://hybrid/<digest> — not cas:// and not the witness evidence: scheme.


Runtime Symbolization Tuple

Runtime witnesses — those whose observation_type is Runtime (1) or Confirmed (2), i.e. anything other than Static (0), or that carry a non-empty observations array — must include a deterministic symbolization tuple. This is enforced by PathWitness.ValidateDeterministicSymbolization, which WitnessDsseSigner calls before signing and during verification (a runtime witness with a missing or invalid symbolization tuple fails both signing and verification):

{
  "symbolization": {
    "build_id": "gnu-build-id:...",
    "debug_artifact_uri": "cas://symbols/by-build-id/.../artifact.debug",
    "symbol_table_uri": "cas://symbols/by-build-id/.../symtab.json",
    "symbolizer": {
      "name": "llvm-symbolizer",
      "version": "18.1.7",
      "digest": "sha256:..."
    },
    "libc_variant": "glibc",
    "sysroot_digest": "sha256:..."
  }
}

Validation rules:

  1. build_id, symbolizer.name, symbolizer.version, symbolizer.digest, libc_variant, and sysroot_digest are required.
  2. At least one of debug_artifact_uri or symbol_table_uri must be present.
  3. Missing runtime symbolization inputs must fail witness signing/verification validation.
  4. Runtime observation arrays must be canonicalized before witness hashing/signing (stable sort by timestamp and deterministic tiebreakers) so equivalent inputs produce byte-identical DSSE payloads.

Runtime Witness Artifact Triplet (MWD-004)

Source of truth: src/EvidenceLocker/__Libraries/StellaOps.EvidenceLocker.Export/Models/BundleManifest.cs (ArtifactEntry, RuntimeWitnessIndexKey, RuntimeWitnessArtifactRoles, BundlePaths, BundleMediaTypes).

Runtime witnesses exported through Evidence Locker use a deterministic three-file profile, stored under the bundle’s runtime-witnesses/ directory (BundlePaths.RuntimeWitnessesDirectory) and indexed via the manifest’s runtimeWitnesses array:

  1. trace.json - canonical witness payload (media type application/vnd.stellaops.witness.v1+json)
  2. trace.dsse.json - DSSE envelope over trace.json (media type application/vnd.dsse.envelope+json)
  3. trace.sigstore.json - Sigstore bundle for offline replay (media type application/vnd.dev.sigstore.bundle.v0.3+json)

Each manifest ArtifactEntry links to:

(Field names above are the JSON property names; C# uses PascalCase, e.g. WitnessRole.)

Offline verification must use only bundle-contained artifacts; no network lookups are required for triplet integrity checks.

Cross-Distro Replay Matrix Verification (MWD-005)

Deterministic replay verification must include a minimum matrix of:

  1. Three kernel releases.
  2. Both glibc and musl libc variants.
  3. Fixed witness artifacts replayed across matrix rows with byte-identical replay-frame output.

QA evidence for MWD-005 was intended to be captured at:

Status — evidence pending. Neither file exists in the repository yet, and the ebpf-micro-witness-determinism/run-001/ directory is absent. The feature is documented at docs/modules/signals/contracts/ebpf-micro-witness-determinism-profile.md and docs/product/ebpf-micro-witness-determinism.md, but the cited replay-matrix run artifacts have not yet been produced. Treat the matrix requirement as a roadmap target until the evidence lands.


DSSE Signing

Witnesses are signed by WitnessDsseSigner.SignWitness using DSSE (Dead Simple Signing Envelope). The payload is the witness serialized via CanonJson.Canonicalize with snake_case naming, no indentation, and null-omission (DefaultIgnoreCondition.WhenWritingNull). The signature is produced over the DSSE PAE by the shared EnvelopeSignatureService and stored base64 (standard, via Convert.ToBase64String):

{
  "payloadType": "application/vnd.stellaops.witness.v1+json",
  "payload": "<base64-encoded canonical witness JSON>",
  "signatures": [
    {
      "keyid": "<signing key id>",
      "sig": "<base64-encoded signature>"
    }
  ]
}

Verification

WitnessDsseSigner.VerifyWitness performs, in order:

  1. Check payloadType equals application/vnd.stellaops.witness.v1+json (else reject).
  2. Deserialize the payload into PathWitness.
  3. Check witness_schema equals stellaops.witness.v1 (else reject as unsupported schema).
  4. Run ValidateDeterministicSymbolization() — runtime/confirmed witnesses must carry a valid symbolization tuple.
  5. Locate the signature whose keyid matches the supplied public key, then verify it via EnvelopeSignatureService.VerifyDsse.

Verification does not recompute witness_hash or witness_id; it validates payload type, schema version, symbolization, and the signature. Transparency-log inclusion is out of scope of the signer/verifier and is not performed here.


Storage

Source of truth: src/Scanner/__Libraries/StellaOps.Scanner.Storage/Postgres/Migrations/013_witness_storage.sql. The migration is idempotent (CREATE TABLE IF NOT EXISTS, CREATE INDEX IF NOT EXISTS).

Witnesses are stored in the scanner.witnesses table. Note this row uses the predicate-style fields (witness_hash, witness_type, graph_hash, created_at) alongside the full wire payload:

ColumnTypeDescription
witness_idUUIDPrimary key (gen_random_uuid() default). This is the DB row ID — not the wire wit:sha256: content address
witness_hashTEXTWitness payload hash; UNIQUE (uk_witness_hash)
schema_versionTEXTDefault stellaops.witness.v1
witness_typeTEXTreachability_path, gate_proof, etc.
graph_hashTEXTSource rich-graph reference (NOT NULL)
scan_idUUIDNullable
run_idUUIDNullable
payload_jsonJSONBFull PathWitness wire JSON
dsse_envelopeJSONBSigned envelope (nullable until signed)
created_atTIMESTAMPTZDefault now()
signed_atTIMESTAMPTZNullable
signer_key_idTEXTNullable
entrypoint_fqnTEXTLookup-by-entrypoint (nullable)
sink_cveTEXTLookup-by-CVE (nullable)

Indexes: ix_witnesses_graph_hash, ix_witnesses_scan_id (partial), ix_witnesses_sink_cve (partial), ix_witnesses_entrypoint (partial), ix_witnesses_created_at (DESC), and a GIN index ix_witnesses_payload_gin on payload_json.

A companion audit table scanner.witness_verifications records each verification attempt (verification_id, witness_id FK, verified_at, verified_by, verification_status, verification_error, verifier_key_id).


API Endpoints

Source of truth: src/Scanner/StellaOps.Scanner.WebService/Endpoints/WitnessEndpoints.cs, mounted under the API base path /api/v1 (ScannerWebServiceOptions.Api.BasePath). All endpoints require the scanner.scans.read authorization policy (ScannerPolicies.ScansRead).

MethodPathDescription
GET/api/v1/witnesses/{witnessId:guid}Get witness by row ID (UUID)
GET/api/v1/witnesses/by-hash/{witnessHash}Get witness by witness_hash
GET/api/v1/witnesses?scanId={scanId}List witnesses for a scan
GET/api/v1/witnesses?cve={cve}List witnesses for a CVE
GET/api/v1/witnesses?graphHash={graphHash}List witnesses for a graph hash
POST/api/v1/witnesses/{witnessId:guid}/verifyVerify witness signature (audited)

The list endpoint returns an empty list when none of scanId / cve / graphHash is supplied (it deliberately avoids a full-table scan). The path parameter is the DB-row UUID witnessId, and the list query parameter is scanId (not scan). There is no POST /api/v1/witnesses create endpoint — witnesses are produced by the Scanner pipeline, not created over the API.

Authorization

The witness endpoints reuse the Scanner read policy scanner.scans.read. There are no witness-specific scopes in the canonical scope catalog (src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs), which defines Scanner scopes such as scanner:read, scanner:scan, scanner:export, and scanner:write. The scanner.scans.read policy/scope name is a Scanner-local constant (ScannerPolicies.ScansRead / ScannerAuthorityScopes.ScansRead) and is not present in the canonical StellaOpsScopes catalog.

CLI

stella witness <show|list|verify|export> (src/Cli/StellaOps.Cli/Commands/CommandHandlers.Witness.cs) wrap the API. witness list accepts --scan, --vuln, --tier, --reachable-only, --probe-type, --limit; witness export supports json, dsse, and sarif formats.