# Unified Evidence Model > **Module:** `StellaOps.Evidence.Core` > **Status:** Production > **Owner:** Platform Guild ## Overview The Unified Evidence Model provides a standardized interface (`IEvidence`) and implementation (`EvidenceRecord`) for representing evidence across all StellaOps modules. This enables: - **Cross-module evidence linking**: Evidence from Scanner, Attestor, Excititor, and Policy modules share a common contract. - **Content-addressed verification**: Evidence records are immutable and verifiable via deterministic hashing. - **Unified storage**: A single `IEvidenceStore` interface abstracts persistence across modules. - **Cryptographic attestation**: Multiple signatures from different signers (internal, vendor, CI, operator) can vouch for evidence. ## Core Types ### IEvidence Interface ```csharp public interface IEvidence { string SubjectNodeId { get; } // Content-addressed subject EvidenceType EvidenceType { get; } // Type discriminator string EvidenceId { get; } // Computed hash identifier ReadOnlyMemory Payload { get; } // Canonical JSON payload IReadOnlyList Signatures { get; } EvidenceProvenance Provenance { get; } string? ExternalPayloadCid { get; } // For large payloads string PayloadSchemaVersion { get; } } ``` ### EvidenceType Enum The platform supports these evidence types: | Type | Value | Description | Example Payload | |------|-------|-------------|-----------------| | `Reachability` | 1 | Call graph analysis | Paths, confidence, graph digest | | `Scan` | 2 | Vulnerability finding | CVE, severity, affected package | | `Policy` | 3 | Policy evaluation | Rule ID, verdict, inputs | | `Artifact` | 4 | SBOM entry metadata | PURL, digest, build info | | `Vex` | 5 | VEX statement | Status, justification, impact | | `Epss` | 6 | EPSS score | Score, percentile, model date | | `Runtime` | 7 | Runtime observation | eBPF/ETW traces, call frames | | `Provenance` | 8 | Build provenance | SLSA attestation, builder info | | `Exception` | 9 | Applied exception | Exception ID, reason, expiry | | `Guard` | 10 | Guard/gate analysis | Gate type, condition, bypass | | `Kev` | 11 | KEV status | In-KEV flag, added date | | `License` | 12 | License analysis | SPDX ID, compliance status | | `Dependency` | 13 | Dependency metadata | Graph edge, version range | | `Custom` | 100 | User-defined | Schema-versioned custom payload | ### EvidenceRecord The concrete implementation with deterministic identity: ```csharp public sealed record EvidenceRecord : IEvidence { public static EvidenceRecord Create( string subjectNodeId, EvidenceType evidenceType, ReadOnlyMemory payload, EvidenceProvenance provenance, string payloadSchemaVersion, IReadOnlyList? signatures = null, string? externalPayloadCid = null); public bool VerifyIntegrity(); } ``` **EvidenceId Computation:** The `EvidenceId` is a SHA-256 hash of the canonicalized fields using versioned prefixing: ``` EvidenceId = "evidence:" + CanonJson.HashVersionedPrefixed("IEvidence", "v1", { SubjectNodeId, EvidenceType, PayloadHash, Provenance.GeneratorId, Provenance.GeneratorVersion, Provenance.GeneratedAt (ISO 8601) }) ``` ### EvidenceSignature Cryptographic attestation by a signer: ```csharp public sealed record EvidenceSignature { public required string SignerId { get; init; } public required string Algorithm { get; init; } // ES256, RS256, EdDSA public required string SignatureBase64 { get; init; } public required DateTimeOffset SignedAt { get; init; } public SignerType SignerType { get; init; } public IReadOnlyList? CertificateChain { get; init; } } ``` **SignerType Values:** - `Internal` (0): StellaOps service - `Vendor` (1): External vendor/supplier - `CI` (2): CI/CD pipeline - `Operator` (3): Human operator - `TransparencyLog` (4): Rekor/transparency log - `Scanner` (5): Security scanner - `PolicyEngine` (6): Policy engine - `Unknown` (255): Unclassified ### EvidenceProvenance Generation context: ```csharp public sealed record EvidenceProvenance { public required string GeneratorId { get; init; } public required string GeneratorVersion { get; init; } public required DateTimeOffset GeneratedAt { get; init; } public string? CorrelationId { get; init; } public Guid? TenantId { get; init; } // ... additional fields } ``` ## Adapters Adapters convert module-specific evidence types to the unified `IEvidence` interface: ### Available Adapters | Adapter | Source Module | Source Type | Target Evidence Types | |---------|---------------|-------------|----------------------| | `EvidenceBundleAdapter` | Scanner | `EvidenceBundle` | Reachability, Vex, Provenance, Scan | | `EvidenceStatementAdapter` | Attestor | `EvidenceStatement` (in-toto) | Scan | | `ProofSegmentAdapter` | Scanner | `ProofSegment` | Varies by segment type | | `VexObservationAdapter` | Excititor | `VexObservation` | Vex, Provenance | | `ExceptionApplicationAdapter` | Policy | `ExceptionApplication` | Exception | ### Adapter Interface ```csharp public interface IEvidenceAdapter { IReadOnlyList Convert( TSource source, string subjectNodeId, EvidenceProvenance provenance); bool CanConvert(TSource source); } ``` ### Using Adapters Adapters use **input DTOs** to avoid circular dependencies: ```csharp // Using VexObservationAdapter var adapter = new VexObservationAdapter(); var input = new VexObservationInput { ObservationId = "obs-001", ProviderId = "nvd", StreamId = "cve-feed", // ... other fields from VexObservation }; var provenance = new EvidenceProvenance { GeneratorId = "excititor-ingestor", GeneratorVersion = "1.0.0", GeneratedAt = DateTimeOffset.UtcNow }; if (adapter.CanConvert(input)) { IReadOnlyList records = adapter.Convert( input, subjectNodeId: "sha256:abc123", provenance); } ``` ## Evidence Store ### IEvidenceStore Interface ```csharp public interface IEvidenceStore { Task StoreAsync( EvidenceRecord record, CancellationToken ct = default); Task> StoreBatchAsync( IEnumerable records, CancellationToken ct = default); Task GetByIdAsync( string evidenceId, CancellationToken ct = default); Task> GetBySubjectAsync( string subjectNodeId, EvidenceType? evidenceType = null, CancellationToken ct = default); Task> GetByTypeAsync( EvidenceType evidenceType, int limit = 100, CancellationToken ct = default); Task ExistsAsync( string evidenceId, CancellationToken ct = default); Task DeleteAsync( string evidenceId, CancellationToken ct = default); } ``` ### Implementations - **`InMemoryEvidenceStore`**: Thread-safe in-memory store for testing and development. - **`PostgresEvidenceStore`** (planned): Production store with tenant isolation and indexing. ## Usage Examples ### Creating Evidence ```csharp var provenance = new EvidenceProvenance { GeneratorId = "scanner-service", GeneratorVersion = "2.1.0", GeneratedAt = DateTimeOffset.UtcNow, TenantId = tenantId }; // Serialize payload to canonical JSON var payloadBytes = CanonJson.Canonicalize(new { cveId = "CVE-2024-1234", severity = "HIGH", affectedPackage = "pkg:npm/lodash@4.17.20" }); var evidence = EvidenceRecord.Create( subjectNodeId: "sha256:abc123def456...", evidenceType: EvidenceType.Scan, payload: payloadBytes, provenance: provenance, payloadSchemaVersion: "scan/v1"); ``` ### Storing and Retrieving ```csharp var store = new InMemoryEvidenceStore(); // Store await store.StoreAsync(evidence); // Retrieve by ID var retrieved = await store.GetByIdAsync(evidence.EvidenceId); // Retrieve all evidence for a subject var allForSubject = await store.GetBySubjectAsync( "sha256:abc123def456...", evidenceType: EvidenceType.Scan); // Verify integrity bool isValid = retrieved!.VerifyIntegrity(); ``` ### Cross-Module Evidence Linking ```csharp // Scanner produces evidence bundle var bundle = scanner.ProduceEvidenceBundle(target); // Convert to unified evidence var adapter = new EvidenceBundleAdapter(); var evidenceRecords = adapter.Convert(bundle, subjectNodeId, provenance); // Store all records await store.StoreBatchAsync(evidenceRecords); // Later, any module can query by subject var allEvidence = await store.GetBySubjectAsync(subjectNodeId); // Filter by type var reachabilityEvidence = allEvidence .Where(e => e.EvidenceType == EvidenceType.Reachability); var vexEvidence = allEvidence .Where(e => e.EvidenceType == EvidenceType.Vex); ``` ## Schema Versioning Each evidence type payload has a schema version (`PayloadSchemaVersion`) for forward compatibility: - `scan/v1`: Initial scan evidence schema - `reachability/v1`: Reachability evidence schema - `vex-statement/v1`: VEX statement evidence schema - `proof-segment/v1`: Proof segment evidence schema - `exception-application/v1`: Exception application schema Consumers should check `PayloadSchemaVersion` before deserializing payloads to handle schema evolution. ## Integration Patterns ### Module Integration Each module that produces evidence should: 1. Create an adapter if converting from module-specific types 2. Use `EvidenceRecord.Create()` for new evidence 3. Store evidence via `IEvidenceStore` 4. Include provenance with generator identification ### Verification Flow ``` 1. Retrieve evidence by SubjectNodeId 2. Call VerifyIntegrity() to check EvidenceId 3. Verify signatures against known trust roots 4. Deserialize and validate payload against schema ``` ## Testing The `StellaOps.Evidence.Core.Tests` project includes: - **111 unit tests** covering: - EvidenceRecord creation and hash computation - InMemoryEvidenceStore CRUD operations - All adapter conversions (VexObservation, ExceptionApplication, ProofSegment) - Edge cases and error handling Run tests: ```bash dotnet test src/__Libraries/StellaOps.Evidence.Core.Tests/ ``` ## Related Documentation - [Proof Chain Architecture](../attestor/proof-chain.md) - [Evidence Bundle Design](../scanner/evidence-bundle.md) - [VEX Observation Model](../excititor/vex-observation.md) - [Policy Exceptions](../policy/exceptions.md)