sprints work
This commit is contained in:
@@ -0,0 +1,465 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user