/** * ProofBundle - Content-addressable audit trail for disposition decisions. * Sprint: SPRINT_3600_0001_0001 (Trust Algebra and Lattice Engine) * Task: TRUST-015 * * The proof bundle captures all inputs, normalization, atom evaluation, * and decision trace for deterministic replay and audit. */ using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; namespace StellaOps.Policy.TrustLattice; /// /// Input evidence that was ingested. /// public sealed record ProofInput { /// /// The content-addressable digest of the input. /// public required string Digest { get; init; } /// /// The type of input (e.g., "sbom", "vex", "scan", "attestation"). /// public required string Type { get; init; } /// /// The format of the input (e.g., "CycloneDX", "SPDX", "OpenVEX"). /// public string? Format { get; init; } /// /// URI/path to the original input. /// public string? Source { get; init; } /// /// Timestamp when the input was ingested. /// public DateTimeOffset IngestedAt { get; init; } = DateTimeOffset.UtcNow; } /// /// Normalization trace for a VEX statement. /// public sealed record NormalizationTrace { /// /// The original statement ID. /// public string? OriginalId { get; init; } /// /// The VEX format. /// public required string SourceFormat { get; init; } /// /// The original status/state value. /// public string? OriginalStatus { get; init; } /// /// The original justification value. /// public string? OriginalJustification { get; init; } /// /// The generated claim ID. /// public required string ClaimId { get; init; } /// /// The atoms that were asserted. /// public required IReadOnlyList GeneratedAssertions { get; init; } } /// /// The atom table showing final values for a subject. /// public sealed record AtomTable { /// /// The subject digest. /// public required string SubjectDigest { get; init; } /// /// The subject details. /// public required Subject Subject { get; init; } /// /// Atom values with support sets. /// public required IReadOnlyDictionary Atoms { get; init; } } /// /// The decision result for a subject. /// public sealed record DecisionRecord { /// /// The subject digest. /// public required string SubjectDigest { get; init; } /// /// The selected disposition. /// public required Disposition Disposition { get; init; } /// /// The rule that matched. /// public required string MatchedRule { get; init; } /// /// Human-readable explanation. /// public required string Explanation { get; init; } /// /// Full decision trace. /// public required IReadOnlyList Trace { get; init; } /// /// Detected conflicts. /// public IReadOnlyList Conflicts { get; init; } = []; /// /// Detected unknowns. /// public IReadOnlyList Unknowns { get; init; } = []; } /// /// Content-addressable proof bundle for audit and replay. /// public sealed record ProofBundle { /// /// The proof bundle ID (content-addressable). /// public string? Id { get; init; } /// /// Proof bundle version for schema evolution. /// public string Version { get; init; } = "1.0.0"; /// /// Timestamp when the proof bundle was created. /// public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow; /// /// The policy bundle used for evaluation. /// public required string PolicyBundleId { get; init; } /// /// Policy bundle version. /// public string? PolicyBundleVersion { get; init; } /// /// All inputs that were ingested. /// public required IReadOnlyList Inputs { get; init; } /// /// Normalization traces for VEX statements. /// public IReadOnlyList Normalization { get; init; } = []; /// /// Claims that were generated/ingested. /// public required IReadOnlyList Claims { get; init; } /// /// Atom tables for all subjects. /// public required IReadOnlyList AtomTables { get; init; } /// /// Decision records for all subjects. /// public required IReadOnlyList Decisions { get; init; } /// /// Summary statistics. /// public ProofBundleStats? Stats { get; init; } /// /// Computes a content-addressable ID for the proof bundle. /// public string ComputeId() { // Canonicalize and hash var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, WriteIndented = false, }; // Create a canonical form without the Id field var canonical = new { version = Version, created_at = CreatedAt.ToUnixTimeSeconds(), policy_bundle_id = PolicyBundleId, policy_bundle_version = PolicyBundleVersion, input_digests = Inputs.Select(i => i.Digest).Order().ToList(), claim_ids = Claims.Select(c => c.Id ?? c.ComputeId()).Order().ToList(), subject_digests = AtomTables.Select(a => a.SubjectDigest).Order().ToList(), }; var json = JsonSerializer.Serialize(canonical, options); var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json)); return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}"; } /// /// Creates a proof bundle with computed ID. /// public ProofBundle WithComputedId() => this with { Id = ComputeId() }; } /// /// Summary statistics for a proof bundle. /// public sealed record ProofBundleStats { /// /// Total number of inputs. /// public int InputCount { get; init; } /// /// Total number of claims. /// public int ClaimCount { get; init; } /// /// Total number of subjects. /// public int SubjectCount { get; init; } /// /// Number of subjects with conflicts. /// public int ConflictCount { get; init; } /// /// Number of subjects with incomplete data. /// public int IncompleteCount { get; init; } /// /// Disposition counts. /// public IReadOnlyDictionary DispositionCounts { get; init; } = new Dictionary(); } /// /// Builder for creating proof bundles. /// public sealed class ProofBundleBuilder { private readonly List _inputs = []; private readonly List _normalization = []; private readonly List _claims = []; private readonly List _atomTables = []; private readonly List _decisions = []; private string _policyBundleId = "unknown"; private string? _policyBundleVersion; /// /// Sets the policy bundle. /// public ProofBundleBuilder WithPolicyBundle(PolicyBundle policy) { _policyBundleId = policy.Id ?? "unknown"; _policyBundleVersion = policy.Version; return this; } /// /// Adds an input. /// public ProofBundleBuilder AddInput(ProofInput input) { _inputs.Add(input); return this; } /// /// Adds a normalization trace. /// public ProofBundleBuilder AddNormalization(NormalizationTrace trace) { _normalization.Add(trace); return this; } /// /// Adds a claim. /// public ProofBundleBuilder AddClaim(Claim claim) { _claims.Add(claim); return this; } /// /// Adds an atom table from subject state. /// public ProofBundleBuilder AddAtomTable(SubjectState state) { _atomTables.Add(new AtomTable { SubjectDigest = state.SubjectDigest, Subject = state.Subject, Atoms = state.ToSnapshot(), }); return this; } /// /// Adds a decision record. /// public ProofBundleBuilder AddDecision(string subjectDigest, DispositionResult result) { _decisions.Add(new DecisionRecord { SubjectDigest = subjectDigest, Disposition = result.Disposition, MatchedRule = result.MatchedRule, Explanation = result.Explanation, Trace = result.Trace, Conflicts = result.Conflicts, Unknowns = result.Unknowns, }); return this; } /// /// Builds the proof bundle. /// public ProofBundle Build() { var dispositionCounts = _decisions .GroupBy(d => d.Disposition) .ToDictionary(g => g.Key, g => g.Count()); var stats = new ProofBundleStats { InputCount = _inputs.Count, ClaimCount = _claims.Count, SubjectCount = _atomTables.Count, ConflictCount = _decisions.Count(d => d.Conflicts.Count > 0), IncompleteCount = _decisions.Count(d => d.Unknowns.Count > 0), DispositionCounts = dispositionCounts, }; var bundle = new ProofBundle { PolicyBundleId = _policyBundleId, PolicyBundleVersion = _policyBundleVersion, Inputs = _inputs, Normalization = _normalization, Claims = _claims, AtomTables = _atomTables, Decisions = _decisions, Stats = stats, }; return bundle.WithComputedId(); } }