7.1 KiB
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:
CycloneDxParser- Parses CycloneDX 1.4–1.7 formatSpdxParser- Parses SPDX 2.3 formatDsseAttestationParser- Parses DSSE envelopes
Collectors:
SbomCollector- Orchestrates SBOM parsing and indexingAttestationCollector- 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:
- Higher precedence source wins
- More recent timestamp wins (when same precedence)
- 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:
- 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
# 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/:
cyclonedx-sample.json- CycloneDX 1.5 samplespdx-sample.json- SPDX 2.3 sampledsse-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 (pending)
- DSSE Verification (if exists)
- Offline Kit Import Flow