// ----------------------------------------------------------------------------- // VerdictIdContentAddressingTests.cs // Sprint: SPRINT_8200_0001_0001 - Verdict ID Content-Addressing Fix // Task: VERDICT-8200-010 - Integration test: VerdictId in attestation matches recomputed ID // Description: Verifies that VerdictId is content-addressed and deterministic across // attestation creation and verification workflows. // ----------------------------------------------------------------------------- using System.Text.Json; using FluentAssertions; using StellaOps.Canonical.Json; using StellaOps.Policy.Deltas; using Xunit; namespace StellaOps.Integration.Determinism; /// /// Integration tests for VerdictId content-addressing. /// Validates that: /// 1. VerdictId in generated verdicts matches recomputed ID from components /// 2. VerdictId is deterministic across multiple generations /// 3. VerdictId in serialized/deserialized verdicts remains stable /// 4. Different verdict contents produce different VerdictIds /// [Trait("Category", "Integration")] [Trait("Sprint", "8200.0001.0001")] [Trait("Feature", "VerdictId-ContentAddressing")] public sealed class VerdictIdContentAddressingTests { #region Attestation Match Tests [Fact(DisplayName = "VerdictId in built verdict matches recomputed ID")] public void VerdictId_InBuiltVerdict_MatchesRecomputedId() { // Arrange - Create a verdict using the builder var deltaId = "delta:sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; var blockingDriver1 = new DeltaDriver { Type = "new-finding", Severity = DeltaDriverSeverity.Critical, Description = "New CVE-2024-0001", CveId = "CVE-2024-0001", Purl = "pkg:npm/lodash@4.17.20" }; var blockingDriver2 = new DeltaDriver { Type = "severity-increase", Severity = DeltaDriverSeverity.High, Description = "Severity increase", CveId = "CVE-2024-0002", Purl = "pkg:npm/axios@0.21.0" }; var warningDriver = new DeltaDriver { Type = "severity-decrease", Severity = DeltaDriverSeverity.Low, Description = "Severity decrease", CveId = "CVE-2024-0003", Purl = "pkg:npm/moment@2.29.0" }; // Act - Build verdict using DeltaVerdictBuilder var verdict = new DeltaVerdictBuilder() .WithGate(DeltaGateLevel.G4) .AddBlockingDriver(blockingDriver1) .AddBlockingDriver(blockingDriver2) .AddWarningDriver(warningDriver) .AddException("exception-123") .AddException("exception-456") .Build(deltaId); // Act - Recompute VerdictId from the verdict's components var generator = new VerdictIdGenerator(); var recomputedId = generator.ComputeVerdictId( verdict.DeltaId, verdict.BlockingDrivers, verdict.WarningDrivers, verdict.AppliedExceptions, verdict.RecommendedGate); // Assert - VerdictId should match recomputed value verdict.VerdictId.Should().Be(recomputedId); verdict.VerdictId.Should().StartWith("verdict:sha256:"); verdict.VerdictId.Should().MatchRegex("^verdict:sha256:[0-9a-f]{64}$"); } [Fact(DisplayName = "VerdictId matches after serialization round-trip")] public void VerdictId_AfterSerializationRoundTrip_MatchesRecomputedId() { // Arrange - Create a verdict var verdict = CreateSampleVerdict(); var originalVerdictId = verdict.VerdictId; // Act - Serialize to JSON var json = JsonSerializer.Serialize(verdict, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = false }); // Act - Deserialize back var deserialized = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); // Act - Recompute VerdictId from deserialized verdict var generator = new VerdictIdGenerator(); var recomputedId = generator.ComputeVerdictId( deserialized!.DeltaId, deserialized.BlockingDrivers, deserialized.WarningDrivers, deserialized.AppliedExceptions, deserialized.RecommendedGate); // Assert deserialized!.VerdictId.Should().Be(originalVerdictId); recomputedId.Should().Be(originalVerdictId); } [Fact(DisplayName = "VerdictId matches after canonical JSON round-trip")] public void VerdictId_AfterCanonicalJsonRoundTrip_MatchesRecomputedId() { // Arrange - Create a verdict var verdict = CreateSampleVerdict(); var originalVerdictId = verdict.VerdictId; // Act - Serialize to canonical JSON (uses camelCase property names) var canonicalJson = CanonJson.Serialize(verdict); // Act - Parse canonical JSON to extract components and verify hash using var doc = JsonDocument.Parse(canonicalJson); var root = doc.RootElement; var deltaId = root.GetProperty("deltaId").GetString()!; // RecommendedGate is serialized as a number (enum value) var gateLevelValue = root.GetProperty("recommendedGate").GetInt32(); var gateLevel = (DeltaGateLevel)gateLevelValue; var blockingDrivers = ParseDriversFromCamelCase(root.GetProperty("blockingDrivers")); var warningDrivers = ParseDriversFromCamelCase(root.GetProperty("warningDrivers")); var appliedExceptions = ParseExceptions(root.GetProperty("appliedExceptions")); // Act - Recompute VerdictId from parsed components var generator = new VerdictIdGenerator(); var recomputedId = generator.ComputeVerdictId( deltaId, blockingDrivers, warningDrivers, appliedExceptions, gateLevel); // Assert recomputedId.Should().Be(originalVerdictId); } [Fact(DisplayName = "VerdictId is deterministic across 100 iterations")] public void VerdictId_IsDeterministic_Across100Iterations() { // Arrange var deltaId = "delta:sha256:stable_delta_id_for_testing_determinism_0000000000000"; var blockingDriver = new DeltaDriver { Type = "new-finding", Severity = DeltaDriverSeverity.Critical, Description = "Test finding", CveId = "CVE-2024-9999", Purl = "pkg:npm/test@1.0.0" }; // Act - Generate verdict 100 times var verdictIds = new HashSet(); for (int i = 0; i < 100; i++) { var verdict = new DeltaVerdictBuilder() .WithGate(DeltaGateLevel.G4) .AddBlockingDriver(blockingDriver) .Build(deltaId); verdictIds.Add(verdict.VerdictId); } // Assert - All iterations should produce the same VerdictId verdictIds.Should().HaveCount(1, "100 identical inputs should produce exactly 1 unique VerdictId"); } [Fact(DisplayName = "Different verdicts produce different VerdictIds")] public void DifferentVerdicts_ProduceDifferentVerdictIds() { // Arrange - Create base driver with Low severity (to avoid gate escalation) var deltaId = "delta:sha256:test_delta_00000000000000000000000000000000000000000000"; var baseDriver = new DeltaDriver { Type = "new-finding", Severity = DeltaDriverSeverity.Low, // Low to avoid gate escalation Description = "Test", CveId = "CVE-2024-0001", Purl = "pkg:npm/a@1.0.0" }; // Act - Create verdicts with variations // Note: Using warning drivers instead of blocking to avoid status changes var verdict1 = new DeltaVerdictBuilder() .WithGate(DeltaGateLevel.G1) .AddWarningDriver(baseDriver) .Build(deltaId); // Different severity var modifiedDriver = new DeltaDriver { Type = "new-finding", Severity = DeltaDriverSeverity.Medium, // Different severity Description = "Test", CveId = "CVE-2024-0001", Purl = "pkg:npm/a@1.0.0" }; var verdict2 = new DeltaVerdictBuilder() .WithGate(DeltaGateLevel.G1) .AddWarningDriver(modifiedDriver) .Build(deltaId); // Different deltaId var verdict3 = new DeltaVerdictBuilder() .WithGate(DeltaGateLevel.G1) .AddWarningDriver(baseDriver) .Build("delta:sha256:different_delta_id_000000000000000000000000000000000000"); // Assert - All should have different VerdictIds verdict1.VerdictId.Should().NotBe(verdict2.VerdictId, "Different severity should produce different VerdictId"); verdict1.VerdictId.Should().NotBe(verdict3.VerdictId, "Different deltaId should produce different VerdictId"); verdict2.VerdictId.Should().NotBe(verdict3.VerdictId); } [Fact(DisplayName = "VerdictId is independent of driver order")] public void VerdictId_IsIndependent_OfDriverOrder() { // Arrange - Same drivers in different orders var deltaId = "delta:sha256:order_test_0000000000000000000000000000000000000000000000"; var driver1 = new DeltaDriver { Type = "new-finding", Severity = DeltaDriverSeverity.Critical, Description = "A", CveId = "CVE-2024-0001", Purl = "pkg:npm/a@1.0.0" }; var driver2 = new DeltaDriver { Type = "new-finding", Severity = DeltaDriverSeverity.High, Description = "B", CveId = "CVE-2024-0002", Purl = "pkg:npm/b@1.0.0" }; var driver3 = new DeltaDriver { Type = "severity-increase", Severity = DeltaDriverSeverity.Medium, Description = "C", CveId = "CVE-2024-0003", Purl = "pkg:npm/c@1.0.0" }; // Act - Create verdicts with drivers in different orders var verdict1 = new DeltaVerdictBuilder() .WithGate(DeltaGateLevel.G4) .AddBlockingDriver(driver1) .AddBlockingDriver(driver2) .AddBlockingDriver(driver3) .Build(deltaId); var verdict2 = new DeltaVerdictBuilder() .WithGate(DeltaGateLevel.G4) .AddBlockingDriver(driver3) .AddBlockingDriver(driver1) .AddBlockingDriver(driver2) .Build(deltaId); var verdict3 = new DeltaVerdictBuilder() .WithGate(DeltaGateLevel.G4) .AddBlockingDriver(driver2) .AddBlockingDriver(driver3) .AddBlockingDriver(driver1) .Build(deltaId); // Assert - All should produce the same VerdictId (canonical ordering is applied) verdict1.VerdictId.Should().Be(verdict2.VerdictId); verdict2.VerdictId.Should().Be(verdict3.VerdictId); } #endregion #region Verification Workflow Tests [Fact(DisplayName = "VerdictId can be verified against attestation payload")] public void VerdictId_CanBeVerified_AgainstAttestationPayload() { // Arrange - Simulate an attestation workflow var verdict = CreateSampleVerdict(); // Simulate creating an attestation with the verdict var attestationPayload = new { verdict.DeltaId, verdict.VerdictId, verdict.BlockingDrivers, verdict.WarningDrivers, verdict.AppliedExceptions, verdict.RecommendedGate, attestedAt = DateTimeOffset.UtcNow.ToString("O"), predicateType = "delta-verdict.stella/v1" }; // Act - Extract VerdictId from "attestation" and verify it var attestedVerdictId = attestationPayload.VerdictId; // Recompute from attestation components var generator = new VerdictIdGenerator(); var recomputedId = generator.ComputeVerdictId( attestationPayload.DeltaId, attestationPayload.BlockingDrivers, attestationPayload.WarningDrivers, attestationPayload.AppliedExceptions, attestationPayload.RecommendedGate); // Assert - The attested VerdictId should match recomputed value attestedVerdictId.Should().Be(recomputedId); } [Fact(DisplayName = "Tampered verdict fails VerdictId verification")] public void TamperedVerdict_FailsVerdictIdVerification() { // Arrange - Create an original verdict var originalVerdict = CreateSampleVerdict(); var originalVerdictId = originalVerdict.VerdictId; // Act - Simulate tampering by modifying severity var tamperedDrivers = originalVerdict.BlockingDrivers .Select(d => new DeltaDriver { Type = d.Type, Severity = DeltaDriverSeverity.Low, // Tampered! Description = d.Description, CveId = d.CveId, Purl = d.Purl }) .ToList(); // Recompute VerdictId with tampered data var generator = new VerdictIdGenerator(); var tamperedId = generator.ComputeVerdictId( originalVerdict.DeltaId, tamperedDrivers, originalVerdict.WarningDrivers, originalVerdict.AppliedExceptions, originalVerdict.RecommendedGate); // Assert - Tampered content should produce different VerdictId tamperedId.Should().NotBe(originalVerdictId, "Tampered content should fail VerdictId verification"); } #endregion #region Helper Methods private static DeltaVerdict CreateSampleVerdict() { var deltaId = "delta:sha256:sample_delta_for_testing_123456789abcdef0123456789abcdef"; var blockingDriver1 = new DeltaDriver { Type = "new-finding", Severity = DeltaDriverSeverity.Critical, Description = "Critical finding", CveId = "CVE-2024-1111", Purl = "pkg:npm/vulnerable@1.0.0" }; var blockingDriver2 = new DeltaDriver { Type = "severity-increase", Severity = DeltaDriverSeverity.High, Description = "Severity increase", CveId = "CVE-2024-2222", Purl = "pkg:npm/risky@2.0.0" }; var warningDriver = new DeltaDriver { Type = "new-finding", Severity = DeltaDriverSeverity.Medium, Description = "Medium finding", CveId = "CVE-2024-3333", Purl = "pkg:npm/warning@3.0.0" }; return new DeltaVerdictBuilder() .WithGate(DeltaGateLevel.G4) .AddBlockingDriver(blockingDriver1) .AddBlockingDriver(blockingDriver2) .AddWarningDriver(warningDriver) .AddException("exc-001") .AddException("exc-002") .Build(deltaId); } private static List ParseDrivers(JsonElement element) { var drivers = new List(); foreach (var item in element.EnumerateArray()) { var type = item.GetProperty("Type").GetString()!; var severityStr = item.GetProperty("Severity").GetString()!; var severity = Enum.Parse(severityStr, true); var description = item.GetProperty("Description").GetString()!; var cveId = item.TryGetProperty("CveId", out var cve) ? cve.GetString() : null; var purl = item.TryGetProperty("Purl", out var p) ? p.GetString() : null; drivers.Add(new DeltaDriver { Type = type, Severity = severity, Description = description, CveId = cveId, Purl = purl }); } return drivers; } private static List ParseDriversFromCamelCase(JsonElement element) { var drivers = new List(); foreach (var item in element.EnumerateArray()) { var type = item.GetProperty("type").GetString()!; // Severity is serialized as a number (enum value) var severityValue = item.GetProperty("severity").GetInt32(); var severity = (DeltaDriverSeverity)severityValue; var description = item.GetProperty("description").GetString()!; var cveId = item.TryGetProperty("cveId", out var cve) ? cve.GetString() : null; var purl = item.TryGetProperty("purl", out var p) ? p.GetString() : null; drivers.Add(new DeltaDriver { Type = type, Severity = severity, Description = description, CveId = cveId, Purl = purl }); } return drivers; } private static List ParseExceptions(JsonElement element) { var exceptions = new List(); foreach (var item in element.EnumerateArray()) { exceptions.Add(item.GetString()!); } return exceptions; } #endregion }