# Evidence Reconciliation This document describes the evidence reconciliation algorithm implemented in the `StellaOps.AirGap.Importer` module. The algorithm provides deterministic, lattice-based reconciliation of security evidence from air-gapped bundles. ## Overview Evidence reconciliation is a 5-step pipeline that transforms raw evidence artifacts (SBOMs, attestations, VEX documents) into a unified, content-addressed evidence graph suitable for policy evaluation and audit trails. ## Architecture ``` ┌─────────────────────────────────────────────────────────────────┐ │ Evidence Reconciliation Pipeline │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ Step 1: Artifact Indexing │ │ ├── EvidenceDirectoryDiscovery │ │ ├── ArtifactIndex (digest-keyed) │ │ └── Digest normalization (sha256:...) │ │ │ │ Step 2: Evidence Collection │ │ ├── SbomCollector (CycloneDX, SPDX) │ │ ├── AttestationCollector (DSSE) │ │ └── Integration with DsseVerifier │ │ │ │ Step 3: Normalization │ │ ├── JsonNormalizer (stable sorting) │ │ ├── Timestamp stripping │ │ └── URI lowercase normalization │ │ │ │ Step 4: Lattice Rules │ │ ├── SourcePrecedenceLattice │ │ ├── VEX merge with precedence │ │ └── Conflict resolution │ │ │ │ Step 5: Graph Emission │ │ ├── EvidenceGraph construction │ │ ├── Deterministic serialization │ │ └── SHA-256 manifest generation │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ## Components ### Step 1: Artifact Indexing **`ArtifactIndex`** - A digest-keyed index of all artifacts in the evidence bundle. ```csharp // Key types public readonly record struct DigestKey(string Algorithm, string Value); // Normalization DigestKey.Parse("sha256:abc123...") → DigestKey("sha256", "abc123...") ``` **`EvidenceDirectoryDiscovery`** - Discovers evidence files from a directory structure. Expected structure: ``` evidence/ ├── sboms/ │ ├── component-a.cdx.json │ └── component-b.spdx.json ├── attestations/ │ └── artifact.dsse.json └── vex/ └── vendor-vex.json ``` ### Step 2: Evidence Collection **Parsers:** - `CycloneDxParser` - Parses CycloneDX 1.4/1.5/1.6 format - `SpdxParser` - Parses SPDX 2.3 format - `DsseAttestationParser` - Parses DSSE envelopes **Collectors:** - `SbomCollector` - Orchestrates SBOM parsing and indexing - `AttestationCollector` - Orchestrates attestation parsing and verification ### Step 3: Normalization **`SbomNormalizer`** applies format-specific normalization: | Rule | Description | |------|-------------| | Stable JSON sorting | Keys sorted alphabetically (ordinal) | | Timestamp stripping | Removes `created`, `modified`, `timestamp` fields | | URI normalization | Lowercases scheme, host, normalizes paths | | Whitespace normalization | Consistent formatting | ### Step 4: Lattice Rules **`SourcePrecedenceLattice`** implements a bounded lattice for VEX source authority: ``` Vendor (top) ↑ Maintainer ↑ ThirdParty ↑ Unknown (bottom) ``` **Lattice Properties (verified by property-based tests):** - **Commutativity**: `Join(a, b) = Join(b, a)` - **Associativity**: `Join(Join(a, b), c) = Join(a, Join(b, c))` - **Idempotence**: `Join(a, a) = a` - **Absorption**: `Join(a, Meet(a, b)) = a` **Conflict Resolution Order:** 1. Higher precedence source wins 2. More recent timestamp wins (when same precedence) 3. Status priority: NotAffected > Fixed > UnderInvestigation > Affected > Unknown ### Step 5: Graph Emission **`EvidenceGraph`** - A content-addressed graph of reconciled evidence: ```csharp public sealed record EvidenceGraph { public required string Version { get; init; } public required string DigestAlgorithm { get; init; } public required string RootDigest { get; init; } public required IReadOnlyList Nodes { get; init; } public required IReadOnlyList Edges { get; init; } public required DateTimeOffset GeneratedAt { get; init; } } ``` **Determinism guarantees:** - Nodes sorted by digest (ordinal) - Edges sorted by (source, target, type) - SHA-256 manifest includes content hash - Reproducible across runs with same inputs ## Integration ### CLI Usage ```bash # Verify offline evidence bundle stellaops verify offline \ --evidence-dir /evidence \ --artifact sha256:def456... \ --policy verify-policy.yaml ``` ### API ```csharp // Reconcile evidence var reconciler = new EvidenceReconciler(options); var graph = await reconciler.ReconcileAsync(evidenceDir, cancellationToken); // Verify determinism var hash1 = graph.ComputeHash(); var graph2 = await reconciler.ReconcileAsync(evidenceDir, cancellationToken); var hash2 = graph2.ComputeHash(); Debug.Assert(hash1 == hash2); // Always true ``` ## Testing ### Golden-File Tests Test fixtures in `tests/AirGap/StellaOps.AirGap.Importer.Tests/Reconciliation/Fixtures/`: - `cyclonedx-sample.json` - CycloneDX 1.5 sample - `spdx-sample.json` - SPDX 2.3 sample - `dsse-attestation-sample.json` - DSSE envelope sample ### Property-Based Tests `SourcePrecedenceLatticePropertyTests` verifies: - Lattice algebraic properties (commutativity, associativity, idempotence, absorption) - Ordering properties (antisymmetry, transitivity, reflexivity) - Bound properties (join is LUB, meet is GLB) - Merge determinism ## Related Documents - [Air-Gap Module Architecture](./architecture.md) *(pending)* - [DSSE Verification](../../adr/dsse-verification.md) *(if exists)* - [Offline Kit Import Flow](./exporter-cli-coordination.md)