Evidence API Reference

This document is the complete API reference for the StellaOps unified evidence model, implemented in the StellaOps.Evidence.Core library (src/__Libraries/StellaOps.Evidence.Core/).

Audience: module developers integrating content-addressed evidence (scan findings, VEX statements, reachability, provenance, exceptions) into the shared evidence store.

Scope note: StellaOps.Evidence.Core is a class library, not an HTTP service. The contracts below are .NET interfaces, records, and enums consumed in-process by other modules. There is no REST/HTTP surface owned by this library; module-specific HTTP endpoints (for example Scanner’s FindingsEvidenceController) expose their own DTOs and are documented in those modules’ dossiers.

Interfaces

IEvidence

Unified evidence contract for content-addressed proof records. All evidence types implement this interface to enable cross-module evidence linking, verification, and storage.

namespace StellaOps.Evidence.Core;

public interface IEvidence
{
    /// <summary>
    /// Content-addressed identifier for the subject this evidence applies to.
    /// Format: "sha256:{hex}" or algorithm-prefixed hash.
    /// </summary>
    string SubjectNodeId { get; }

    /// <summary>
    /// Type discriminator for the evidence payload.
    /// </summary>
    EvidenceType EvidenceType { get; }

    /// <summary>
    /// Content-addressed identifier for this evidence record.
    /// Computed from versioned canonicalized (SubjectNodeId, EvidenceType, Payload, Provenance).
    /// Format: "sha256:{hex}".
    /// </summary>
    string EvidenceId { get; }

    /// <summary>
    /// Type-specific evidence payload as canonical JSON bytes.
    /// The payload format is determined by <see cref="PayloadSchemaVersion"/>.
    /// </summary>
    ReadOnlyMemory<byte> Payload { get; }

    /// <summary>
    /// Cryptographic signatures attesting to this evidence. May be empty for unsigned evidence.
    /// </summary>
    IReadOnlyList<EvidenceSignature> Signatures { get; }

    /// <summary>
    /// Provenance information: who generated, when, how.
    /// </summary>
    EvidenceProvenance Provenance { get; }

    /// <summary>
    /// Optional CID (Content Identifier) for large payloads stored externally.
    /// When set, <see cref="Payload"/> may be empty or contain a summary.
    /// </summary>
    string? ExternalPayloadCid { get; }

    /// <summary>
    /// Schema version for the payload format.
    /// Format: "{type}/{version}" (e.g., "reachability/v1", "vex/v1").
    /// </summary>
    string PayloadSchemaVersion { get; }
}

IEvidenceStore

Storage and retrieval interface for evidence records. Implementations may be in-memory (testing), PostgreSQL (production), or external stores.

namespace StellaOps.Evidence.Core;

public interface IEvidenceStore
{
    /// <summary>
    /// Stores an evidence record. Idempotent for a given EvidenceId.
    /// Returns the evidence ID (for confirmation or chaining).
    /// </summary>
    Task<string> StoreAsync(IEvidence evidence, CancellationToken ct = default);

    /// <summary>
    /// Stores multiple evidence records in a single transaction.
    /// Returns the number of records stored (excluding duplicates).
    /// </summary>
    Task<int> StoreBatchAsync(IEnumerable<IEvidence> evidenceRecords, CancellationToken ct = default);

    /// <summary>
    /// Retrieves evidence by its content-addressed ID. Returns null if not found.
    /// </summary>
    Task<IEvidence?> GetByIdAsync(string evidenceId, CancellationToken ct = default);

    /// <summary>
    /// Retrieves all evidence for a subject node, optionally filtered by type.
    /// </summary>
    Task<IReadOnlyList<IEvidence>> GetBySubjectAsync(
        string subjectNodeId,
        EvidenceType? typeFilter = null,
        CancellationToken ct = default);

    /// <summary>
    /// Retrieves evidence by type across all subjects, up to <paramref name="limit"/> records.
    /// </summary>
    Task<IReadOnlyList<IEvidence>> GetByTypeAsync(
        EvidenceType evidenceType,
        int limit = 100,
        CancellationToken ct = default);

    /// <summary>
    /// Checks if matching evidence exists for a subject and type.
    /// </summary>
    Task<bool> ExistsAsync(string subjectNodeId, EvidenceType type, CancellationToken ct = default);

    /// <summary>
    /// Deletes evidence by ID (for expiration/cleanup). Returns true if deleted; false if not found.
    /// </summary>
    Task<bool> DeleteAsync(string evidenceId, CancellationToken ct = default);

    /// <summary>
    /// Gets the count of evidence records for a subject.
    /// </summary>
    Task<int> CountBySubjectAsync(string subjectNodeId, CancellationToken ct = default);
}

IEvidenceAdapter<TSource>

Adapter interface for converting module-specific evidence types to unified IEvidence records.

namespace StellaOps.Evidence.Core.Adapters;

public interface IEvidenceAdapter<TSource>
{
    /// <summary>
    /// Converts a module-specific evidence object to unified IEvidence record(s).
    /// A single source object may produce multiple evidence records.
    /// </summary>
    IReadOnlyList<IEvidence> Convert(TSource source, string subjectNodeId, EvidenceProvenance provenance);

    /// <summary>
    /// Checks if the adapter can handle the given source object.
    /// </summary>
    bool CanConvert(TSource source);
}

Records

EvidenceRecord

Concrete, immutable implementation of IEvidence. The EvidenceId is content-addressed: it is computed from the canonicalized contents of the record.

namespace StellaOps.Evidence.Core;

public sealed record EvidenceRecord : IEvidence
{
    public required string SubjectNodeId { get; init; }
    public required EvidenceType EvidenceType { get; init; }
    public required string EvidenceId { get; init; }
    public required ReadOnlyMemory<byte> Payload { get; init; }
    public IReadOnlyList<EvidenceSignature> Signatures { get; init; } = [];
    public required EvidenceProvenance Provenance { get; init; }
    public string? ExternalPayloadCid { get; init; }
    public required string PayloadSchemaVersion { get; init; }

    // Computes the content-addressed EvidenceId ("sha256:{hex}") from
    // (SubjectNodeId, EvidenceType, Payload, Provenance) via versioned canonicalization.
    public static string ComputeEvidenceId(
        string subjectNodeId,
        EvidenceType evidenceType,
        ReadOnlySpan<byte> payload,
        EvidenceProvenance provenance);

    // Creates an EvidenceRecord with auto-computed EvidenceId.
    public static EvidenceRecord Create(
        string subjectNodeId,
        EvidenceType evidenceType,
        ReadOnlyMemory<byte> payload,
        EvidenceProvenance provenance,
        string payloadSchemaVersion,
        IReadOnlyList<EvidenceSignature>? signatures = null,
        string? externalPayloadCid = null);

    // Verifies that EvidenceId matches the recomputed hash of the record contents.
    public bool VerifyIntegrity();
}

The hash input for ComputeEvidenceId is the internal EvidenceHashInput record (GeneratedAt, GeneratorId, GeneratorVersion, EvidenceType, PayloadBase64, SubjectNodeId), canonicalized via StellaOps.Canonical.Json (CanonJson.HashVersionedPrefixed).

EvidenceSignature

Cryptographic signature on evidence.

namespace StellaOps.Evidence.Core;

public sealed record EvidenceSignature
{
    /// <summary>Signer identity (key ID, certificate subject, or service account).</summary>
    public required string SignerId { get; init; }

    /// <summary>Signature algorithm (e.g., "ES256", "RS256", "EdDSA", "GOST3411-2012").</summary>
    public required string Algorithm { get; init; }

    /// <summary>Base64-encoded signature bytes.</summary>
    public required string SignatureBase64 { get; init; }

    /// <summary>Timestamp when signature was created (UTC).</summary>
    public required DateTimeOffset SignedAt { get; init; }

    /// <summary>Signer type for categorization and filtering. Defaults to SignerType.Internal.</summary>
    public SignerType SignerType { get; init; } = SignerType.Internal;

    /// <summary>
    /// Optional key certificate chain for verification (PEM or Base64 DER).
    /// First element is the signing certificate, followed by intermediates.
    /// </summary>
    public IReadOnlyList<string>? CertificateChain { get; init; }

    /// <summary>Optional transparency log entry ID (e.g., Rekor log index).</summary>
    public string? TransparencyLogEntryId { get; init; }

    /// <summary>Optional timestamp authority response (RFC 3161 TST, Base64).</summary>
    public string? TimestampToken { get; init; }
}

EvidenceProvenance

Provenance information for evidence generation: who generated the evidence, when, and with what inputs.

namespace StellaOps.Evidence.Core;

public sealed record EvidenceProvenance
{
    /// <summary>
    /// Tool or service that generated this evidence.
    /// Format: "stellaops/{module}/{component}" or vendor identifier
    /// (e.g., "stellaops/scanner/trivy", "stellaops/policy/opa", "vendor/snyk").
    /// </summary>
    public required string GeneratorId { get; init; }

    /// <summary>Version of the generator tool.</summary>
    public required string GeneratorVersion { get; init; }

    /// <summary>When the evidence was generated (UTC).</summary>
    public required DateTimeOffset GeneratedAt { get; init; }

    /// <summary>
    /// Content-addressed hash of inputs used to generate this evidence (enables replay verification).
    /// Format: "sha256:{hex}" or similar.
    /// </summary>
    public string? InputsDigest { get; init; }

    /// <summary>Environment/region where evidence was generated (e.g., "production", "eu-west-1").</summary>
    public string? Environment { get; init; }

    /// <summary>Scan run or evaluation ID for correlation across multiple evidence records.</summary>
    public string? CorrelationId { get; init; }

    /// <summary>Optional tenant identifier for multi-tenant deployments.</summary>
    public Guid? TenantId { get; init; }

    /// <summary>Additional metadata for organization-specific tracking.</summary>
    public IReadOnlyDictionary<string, string>? Metadata { get; init; }

    // Creates a minimal provenance record for testing or internal use
    // (GeneratedAt = DateTimeOffset.UtcNow).
    public static EvidenceProvenance CreateMinimal(string generatorId, string generatorVersion);
}

Enumerations

EvidenceType

Categorizes the kind of proof or observation attached to a subject node. The numeric values are non-contiguous; Custom is 255.

namespace StellaOps.Evidence.Core;

public enum EvidenceType
{
    Reachability = 1,  // Call graph reachability analysis result.
    Scan = 2,          // Vulnerability scan finding.
    Policy = 3,        // Policy evaluation result.
    Artifact = 4,      // Artifact metadata (SBOM entry, layer info, provenance).
    Vex = 5,           // VEX statement (vendor exploitability assessment).
    Epss = 6,          // EPSS score snapshot.
    Runtime = 7,       // Runtime observation (eBPF, dyld, ETW).
    Provenance = 8,    // Build provenance (SLSA, reproducibility).
    Exception = 9,     // Exception/waiver applied.
    Guard = 10,        // Guard/gate analysis (feature flags, auth gates).
    Kev = 11,          // KEV (Known Exploited Vulnerabilities) status.
    License = 12,      // License compliance evidence.
    Dependency = 13,   // Dependency relationship evidence.
    Custom = 255       // Unknown or custom evidence type.
}

SignerType

Categorizes the entity that produced a signature. The numeric values are non-contiguous; Unknown is 255.

namespace StellaOps.Evidence.Core;

public enum SignerType
{
    Internal = 0,         // Internal StellaOps service.
    Vendor = 1,           // External vendor/supplier.
    CI = 2,               // CI/CD pipeline.
    Operator = 3,         // Human operator.
    TransparencyLog = 4,  // Third-party attestation service (e.g., Rekor).
    Scanner = 5,          // Automated security scanner.
    PolicyEngine = 6,     // Policy engine or decision service.
    Unknown = 255         // Unknown or unclassified signer.
}

Adapters

All adapters derive from EvidenceAdapterBase and implement IEvidenceAdapter<TSource>. The base class provides CreateEvidence<T>(...) (canonicalizes a payload object via StellaOps.Canonical.Json and builds an EvidenceRecord) and CreateProvenance(...) helpers. Each adapter accepts a decoupled input DTO (no direct cross-module dependency) plus a subjectNodeId and an EvidenceProvenance.

EvidenceStatementAdapter

Converts Attestor in-toto evidence statements. Schema version: evidence-statement/v1.

Input: EvidenceStatementInput

public sealed record EvidenceStatementInput
{
    public required string SubjectDigest { get; init; }
    public required string Source { get; init; }
    public required string SourceVersion { get; init; }
    public required DateTimeOffset CollectionTime { get; init; }
    public required string SbomEntryId { get; init; }
    public string? VulnerabilityId { get; init; }
    public string? RawFindingHash { get; init; }
    public string? EvidenceId { get; init; }
}

Output: a single IEvidence record with EvidenceType = Scan. A FromStatement(...) static helper builds the input from raw statement fields.


ProofSegmentAdapter

Converts Scanner ProofSegment chain links. Schema version: proof-segment/v1.

Input: ProofSegmentInput

public sealed record ProofSegmentInput
{
    public required string SegmentId { get; init; }
    public required string SegmentType { get; init; }
    public required int Index { get; init; }
    public required string InputHash { get; init; }
    public required string ResultHash { get; init; }
    public string? PrevSegmentHash { get; init; }
    public required string ToolId { get; init; }
    public required string ToolVersion { get; init; }
    public required string Status { get; init; }
    public string? SpineId { get; init; }
}

Output: a single IEvidence record. The EvidenceType is derived from SegmentType (case-insensitive):

SegmentTypeEvidenceType
SbomSliceArtifact
MatchScan
ReachabilityReachability
GuardAnalysisGuard
RuntimeObservationRuntime
PolicyEvalPolicy
(anything else)Custom

VexObservationAdapter

Converts Excititor VexObservation documents. Schema version: 1.0.0.

Input: VexObservationInput

public sealed record VexObservationInput
{
    public required string ObservationId { get; init; }
    public required string Tenant { get; init; }
    public required string ProviderId { get; init; }
    public required string StreamId { get; init; }
    public required VexObservationUpstreamInput Upstream { get; init; }
    public required ImmutableArray<VexObservationStatementInput> Statements { get; init; }
    public required VexObservationContentInput Content { get; init; }
    public required DateTimeOffset CreatedAt { get; init; }
    public ImmutableArray<string> Supersedes { get; init; } = [];
    public ImmutableDictionary<string, string> Attributes { get; init; } =
        ImmutableDictionary<string, string>.Empty;
}

public sealed record VexObservationUpstreamInput
{
    public required string UpstreamId { get; init; }
    public string? DocumentVersion { get; init; }
    public required DateTimeOffset FetchedAt { get; init; }
    public required DateTimeOffset ReceivedAt { get; init; }
    public required string ContentHash { get; init; }
    public required VexObservationSignatureInput Signature { get; init; }
    public ImmutableDictionary<string, string> Metadata { get; init; } =
        ImmutableDictionary<string, string>.Empty;
}

public sealed record VexObservationStatementInput
{
    public required string VulnerabilityId { get; init; }
    public required string ProductKey { get; init; }
    public required string Status { get; init; }
    public DateTimeOffset? LastObserved { get; init; }
    public string? Locator { get; init; }
    public string? Justification { get; init; }
    public string? IntroducedVersion { get; init; }
    public string? FixedVersion { get; init; }
    public string? Purl { get; init; }
    public string? Cpe { get; init; }
    public ImmutableArray<JsonNode> Evidence { get; init; } = [];
    public ImmutableDictionary<string, string> Metadata { get; init; } =
        ImmutableDictionary<string, string>.Empty;
}

public sealed record VexObservationContentInput
{
    public required string Format { get; init; }
    public string? SpecVersion { get; init; }
    public JsonNode? Raw { get; init; }
    public ImmutableDictionary<string, string> Metadata { get; init; } =
        ImmutableDictionary<string, string>.Empty;
}

public sealed record VexObservationSignatureInput
{
    public bool Present { get; init; }
    public string? Format { get; init; }
    public string? KeyId { get; init; }
    public string? Signature { get; init; }
}

Output: multiple IEvidence records:

When Upstream.Signature.Present is true and a signature is supplied, each emitted record carries an EvidenceSignature with SignerType = Vendor.


ExceptionApplicationAdapter

Converts Policy ExceptionApplication records. Schema version: 1.0.0.

Input: ExceptionApplicationInput

public sealed record ExceptionApplicationInput
{
    public required Guid Id { get; init; }
    public required Guid TenantId { get; init; }
    public required string ExceptionId { get; init; }
    public required string FindingId { get; init; }
    public string? VulnerabilityId { get; init; }
    public required string OriginalStatus { get; init; }
    public required string AppliedStatus { get; init; }
    public required string EffectName { get; init; }
    public required string EffectType { get; init; }
    public Guid? EvaluationRunId { get; init; }
    public string? PolicyBundleDigest { get; init; }
    public required DateTimeOffset AppliedAt { get; init; }
    public ImmutableDictionary<string, string> Metadata { get; init; } =
        ImmutableDictionary<string, string>.Empty;
}

Output: a single IEvidence record with EvidenceType = Exception.


EvidenceBundleAdapter

Converts Scanner’s EvidenceBundle (StellaOps.Evidence.Bundle). A bundle may contain several sub-evidence sections; each section that has Status = EvidenceStatus.Available is converted to a separate IEvidence record.

Bundle sectionEvidenceTypeSchema version
ReachabilityReachabilityreachability/v1
VexStatusVexvex/v1
ProvenanceProvenanceprovenance/v1
CallStackRuntimecallstack/v1
DiffArtifactdiff/v1
GraphRevisionDependencygraph-revision/v1
BinaryDiffArtifactbinary-diff/v1

Output: zero or more IEvidence records (one per available section).


Implementations

InMemoryEvidenceStore

Thread-safe in-memory implementation of IEvidenceStore, intended for testing, development, and ephemeral processing. Backed by ConcurrentDictionary indices (_byId, _bySubject).

var store = new InMemoryEvidenceStore();

// Store evidence (idempotent; returns the EvidenceId)
string id = await store.StoreAsync(record);

// Store a batch (returns number of newly stored records, excluding duplicates)
int stored = await store.StoreBatchAsync(records);

// Retrieve by ID
IEvidence? evidence = await store.GetByIdAsync("sha256:abc123...");

// Query by subject (optionally filter by type)
var subjectEvidence = await store.GetBySubjectAsync("sha256:def456...");
var subjectVex = await store.GetBySubjectAsync("sha256:def456...", EvidenceType.Vex);

// Query by type (default limit 100)
var vexRecords = await store.GetByTypeAsync(EvidenceType.Vex);

// Existence and counts
bool exists = await store.ExistsAsync("sha256:def456...", EvidenceType.Vex);
int count = await store.CountBySubjectAsync("sha256:def456...");

// Delete (for expiration/cleanup)
bool deleted = await store.DeleteAsync("sha256:abc123...");

Additional members not on the interface: Clear() (test-only reset) and Count (total record count). Note: DeleteAsync removes from the _byId index but leaves the _bySubject index entry (a ConcurrentBag does not support removal); GetBySubjectAsync filters out stale/dangling IDs.

Thread Safety: all operations use ConcurrentDictionary.


Payloads

Evidence payloads are opaque canonical-JSON byte buffers (IEvidence.Payload), not a key/value property bag. The concrete payload shape is determined by PayloadSchemaVersion and the originating adapter. For example:

To inspect a payload, deserialize Payload according to the record’s PayloadSchemaVersion.