Files
git.stella-ops.org/docs/modules/airgap/evidence-reconciliation.md
2025-12-25 12:16:13 +02:00

189 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.41.7 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<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
```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)