Files
git.stella-ops.org/tests/integration/StellaOps.Integration.Determinism/VerdictIdContentAddressingTests.cs
StellaOps Bot b9f71fc7e9 sprints work
2025-12-24 21:46:08 +02:00

466 lines
17 KiB
C#

// -----------------------------------------------------------------------------
// 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;
/// <summary>
/// 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
/// </summary>
[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<DeltaVerdict>(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<string>();
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<DeltaDriver> ParseDrivers(JsonElement element)
{
var drivers = new List<DeltaDriver>();
foreach (var item in element.EnumerateArray())
{
var type = item.GetProperty("Type").GetString()!;
var severityStr = item.GetProperty("Severity").GetString()!;
var severity = Enum.Parse<DeltaDriverSeverity>(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<DeltaDriver> ParseDriversFromCamelCase(JsonElement element)
{
var drivers = new List<DeltaDriver>();
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<string> ParseExceptions(JsonElement element)
{
var exceptions = new List<string>();
foreach (var item in element.EnumerateArray())
{
exceptions.Add(item.GetString()!);
}
return exceptions;
}
#endregion
}