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.

// 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:

Collectors:

Step 3: Normalization

SbomNormalizerapplies format-specific normalization:

RuleDescription
Stable JSON sortingKeys sorted alphabetically (ordinal)
Timestamp strippingRemoves created, modified, timestamp fields
URI normalizationLowercases scheme, host, normalizes paths
Whitespace normalizationConsistent formatting

Step 4: Lattice Rules

SourcePrecedenceLatticeimplements a bounded lattice for VEX source authority:

    Vendor (top)
       ↑
   Maintainer
       ↑
   ThirdParty
       ↑
    Unknown (bottom)

Lattice Properties (verified by property-based tests):

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:

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<EvidenceNode> Nodes { get; init; }
    public required IReadOnlyList<EvidenceEdge> Edges { get; init; }
    public required DateTimeOffset GeneratedAt { get; init; }
}

Determinism guarantees:

Integration

CLI Usage

# Verify offline evidence bundle
stellaops verify offline \
  --evidence-dir /evidence \
  --artifact sha256:def456... \
  --policy verify-policy.yaml

API

// 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/:

Property-Based Tests

SourcePrecedenceLatticePropertyTests verifies: