sprints work

This commit is contained in:
StellaOps Bot
2025-12-24 21:46:08 +02:00
parent 43e2af88f6
commit b9f71fc7e9
161 changed files with 29566 additions and 527 deletions

View File

@@ -124,6 +124,9 @@ public enum DeltaGateLevel
/// </summary>
public sealed class DeltaVerdictBuilder
{
private static readonly IVerdictIdGenerator DefaultIdGenerator = new VerdictIdGenerator();
private readonly IVerdictIdGenerator _idGenerator;
private DeltaVerdictStatus _status = DeltaVerdictStatus.Pass;
private DeltaGateLevel _gate = DeltaGateLevel.G1;
private int _riskPoints;
@@ -133,6 +136,22 @@ public sealed class DeltaVerdictBuilder
private readonly List<string> _recommendations = [];
private string? _explanation;
/// <summary>
/// Creates a new <see cref="DeltaVerdictBuilder"/> with the default ID generator.
/// </summary>
public DeltaVerdictBuilder() : this(DefaultIdGenerator)
{
}
/// <summary>
/// Creates a new <see cref="DeltaVerdictBuilder"/> with a custom ID generator.
/// </summary>
/// <param name="idGenerator">Custom verdict ID generator for testing or specialized scenarios.</param>
public DeltaVerdictBuilder(IVerdictIdGenerator idGenerator)
{
_idGenerator = idGenerator ?? throw new ArgumentNullException(nameof(idGenerator));
}
public DeltaVerdictBuilder WithStatus(DeltaVerdictStatus status)
{
_status = status;
@@ -206,17 +225,29 @@ public sealed class DeltaVerdictBuilder
_status = DeltaVerdictStatus.PassWithExceptions;
}
var blockingDrivers = _blockingDrivers.ToList();
var warningDrivers = _warningDrivers.ToList();
var appliedExceptions = _exceptions.ToList();
// Compute content-addressed VerdictId from inputs
var verdictId = _idGenerator.ComputeVerdictId(
deltaId,
blockingDrivers,
warningDrivers,
appliedExceptions,
_gate);
return new DeltaVerdict
{
VerdictId = $"dv:{Guid.NewGuid():N}",
VerdictId = verdictId,
DeltaId = deltaId,
EvaluatedAt = DateTimeOffset.UtcNow,
Status = _status,
RecommendedGate = _gate,
RiskPoints = _riskPoints,
BlockingDrivers = _blockingDrivers.ToList(),
WarningDrivers = _warningDrivers.ToList(),
AppliedExceptions = _exceptions.ToList(),
BlockingDrivers = blockingDrivers,
WarningDrivers = warningDrivers,
AppliedExceptions = appliedExceptions,
Explanation = _explanation ?? GenerateExplanation(),
Recommendations = _recommendations.ToList()
};

View File

@@ -0,0 +1,35 @@
namespace StellaOps.Policy.Deltas;
/// <summary>
/// Service for generating content-addressed IDs for delta verdicts.
/// </summary>
public interface IVerdictIdGenerator
{
/// <summary>
/// Computes a content-addressed verdict ID from individual components.
/// </summary>
/// <param name="deltaId">The delta ID being evaluated.</param>
/// <param name="blockingDrivers">Drivers that caused blocking status.</param>
/// <param name="warningDrivers">Drivers that raised warnings.</param>
/// <param name="appliedExceptions">Exception IDs that were applied.</param>
/// <param name="gateLevel">The recommended gate level.</param>
/// <returns>A content-addressed verdict ID in format "verdict:sha256:&lt;hex&gt;".</returns>
string ComputeVerdictId(
string deltaId,
IReadOnlyList<DeltaDriver> blockingDrivers,
IReadOnlyList<DeltaDriver> warningDrivers,
IReadOnlyList<string> appliedExceptions,
DeltaGateLevel gateLevel);
/// <summary>
/// Computes a content-addressed verdict ID from an existing verdict.
/// </summary>
/// <param name="verdict">The verdict to compute an ID for.</param>
/// <returns>A content-addressed verdict ID in format "verdict:sha256:&lt;hex&gt;".</returns>
/// <remarks>
/// This method is useful for recomputing the expected ID of a verdict
/// during verification. The computed ID should match the verdict's
/// <see cref="DeltaVerdict.VerdictId"/> if it was generated correctly.
/// </remarks>
string ComputeVerdictId(DeltaVerdict verdict);
}

View File

@@ -0,0 +1,135 @@
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Serialization;
using StellaOps.Canonical.Json;
namespace StellaOps.Policy.Deltas;
/// <summary>
/// Generates content-addressed IDs for delta verdicts.
/// </summary>
/// <remarks>
/// VerdictId Formula:
/// <code>
/// verdict:sha256:&lt;hex&gt; = SHA256(CanonicalJson(
/// DeltaId,
/// Sort(BlockingDrivers by Type, CveId, Purl, Severity),
/// Sort(WarningDrivers by Type, CveId, Purl, Severity),
/// Sort(AppliedExceptions),
/// GateLevel
/// ))
/// </code>
///
/// The canonical JSON uses RFC 8785 (JCS) format to ensure deterministic output
/// regardless of property order or whitespace.
/// </remarks>
public sealed class VerdictIdGenerator : IVerdictIdGenerator
{
private static readonly JsonSerializerOptions SerializerOptions = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
/// <summary>
/// Creates a new <see cref="VerdictIdGenerator"/>.
/// </summary>
public VerdictIdGenerator()
{
}
/// <inheritdoc />
public string ComputeVerdictId(
string deltaId,
IReadOnlyList<DeltaDriver> blockingDrivers,
IReadOnlyList<DeltaDriver> warningDrivers,
IReadOnlyList<string> appliedExceptions,
DeltaGateLevel gateLevel)
{
ArgumentException.ThrowIfNullOrWhiteSpace(deltaId);
ArgumentNullException.ThrowIfNull(blockingDrivers);
ArgumentNullException.ThrowIfNull(warningDrivers);
ArgumentNullException.ThrowIfNull(appliedExceptions);
var payload = new VerdictIdPayload
{
CanonVersion = CanonVersion.Current,
DeltaId = deltaId,
BlockingDrivers = SortDrivers(blockingDrivers),
WarningDrivers = SortDrivers(warningDrivers),
AppliedExceptions = SortExceptions(appliedExceptions),
GateLevel = gateLevel.ToString()
};
// Canonicalize the payload with deterministic key ordering
var canonical = CanonJson.Canonicalize(payload, SerializerOptions);
var hash = SHA256.HashData(canonical);
return $"verdict:sha256:{Convert.ToHexStringLower(hash)}";
}
/// <inheritdoc />
public string ComputeVerdictId(DeltaVerdict verdict)
{
ArgumentNullException.ThrowIfNull(verdict);
return ComputeVerdictId(
verdict.DeltaId,
verdict.BlockingDrivers,
verdict.WarningDrivers,
verdict.AppliedExceptions,
verdict.RecommendedGate);
}
private static List<DriverPayload> SortDrivers(IReadOnlyList<DeltaDriver> drivers)
{
return drivers
.OrderBy(d => d.Type, StringComparer.Ordinal)
.ThenBy(d => d.CveId ?? string.Empty, StringComparer.Ordinal)
.ThenBy(d => d.Purl ?? string.Empty, StringComparer.Ordinal)
.ThenBy(d => d.Severity.ToString(), StringComparer.Ordinal)
.Select(d => new DriverPayload
{
Type = d.Type,
Severity = d.Severity.ToString(),
Description = d.Description,
CveId = d.CveId,
Purl = d.Purl
})
.ToList();
}
private static List<string> SortExceptions(IReadOnlyList<string> exceptions)
{
return exceptions
.OrderBy(e => e, StringComparer.Ordinal)
.ToList();
}
/// <summary>
/// Payload structure for verdict ID computation.
/// </summary>
private sealed record VerdictIdPayload
{
[JsonPropertyName("_canonVersion")]
public required string CanonVersion { get; init; }
public required string DeltaId { get; init; }
public required List<DriverPayload> BlockingDrivers { get; init; }
public required List<DriverPayload> WarningDrivers { get; init; }
public required List<string> AppliedExceptions { get; init; }
public required string GateLevel { get; init; }
}
/// <summary>
/// Serializable driver payload for deterministic ordering.
/// </summary>
private sealed record DriverPayload
{
public required string Type { get; init; }
public required string Severity { get; init; }
public required string Description { get; init; }
public string? CveId { get; init; }
public string? Purl { get; init; }
}
}

View File

@@ -141,12 +141,105 @@ public sealed class DeltaVerdictTests
}
[Fact]
public void Build_GeneratesUniqueVerdictId()
public void Build_GeneratesDeterministicVerdictId_ForIdenticalInputs()
{
var verdict1 = new DeltaVerdictBuilder().Build("delta:sha256:test");
var verdict2 = new DeltaVerdictBuilder().Build("delta:sha256:test");
verdict1.VerdictId.Should().StartWith("dv:");
verdict1.VerdictId.Should().NotBe(verdict2.VerdictId);
// Content-addressed IDs are deterministic
verdict1.VerdictId.Should().StartWith("verdict:sha256:");
verdict1.VerdictId.Should().Be(verdict2.VerdictId, "identical inputs must produce identical VerdictId");
}
[Fact]
public void Build_GeneratesDifferentVerdictId_ForDifferentInputs()
{
var verdict1 = new DeltaVerdictBuilder().Build("delta:sha256:test1");
var verdict2 = new DeltaVerdictBuilder().Build("delta:sha256:test2");
verdict1.VerdictId.Should().StartWith("verdict:sha256:");
verdict2.VerdictId.Should().StartWith("verdict:sha256:");
verdict1.VerdictId.Should().NotBe(verdict2.VerdictId, "different inputs must produce different VerdictId");
}
[Theory]
[InlineData(10)]
public void Build_IsIdempotent_AcrossMultipleIterations(int iterations)
{
var driver = new DeltaDriver
{
Type = "new-reachable-cve",
Severity = DeltaDriverSeverity.High,
Description = "High severity CVE",
CveId = "CVE-2024-999"
};
var expected = new DeltaVerdictBuilder()
.AddBlockingDriver(driver)
.Build("delta:sha256:determinism-test")
.VerdictId;
for (int i = 0; i < iterations; i++)
{
var verdict = new DeltaVerdictBuilder()
.AddBlockingDriver(driver)
.Build("delta:sha256:determinism-test");
verdict.VerdictId.Should().Be(expected, $"iteration {i}: VerdictId must be stable");
}
}
[Fact]
public void Build_VerdictIdIsDeterministic_RegardlessOfDriverAddOrder()
{
var driver1 = new DeltaDriver
{
Type = "aaa-first",
Severity = DeltaDriverSeverity.Medium,
Description = "First driver"
};
var driver2 = new DeltaDriver
{
Type = "zzz-last",
Severity = DeltaDriverSeverity.Low,
Description = "Second driver"
};
// Add in one order
var verdict1 = new DeltaVerdictBuilder()
.AddWarningDriver(driver1)
.AddWarningDriver(driver2)
.Build("delta:sha256:order-test");
// Add in reverse order
var verdict2 = new DeltaVerdictBuilder()
.AddWarningDriver(driver2)
.AddWarningDriver(driver1)
.Build("delta:sha256:order-test");
// Content-addressed IDs should be same because drivers are sorted by Type
verdict1.VerdictId.Should().Be(verdict2.VerdictId, "drivers are sorted by Type before hashing");
}
[Fact]
public void VerdictIdGenerator_ComputeFromVerdict_MatchesOriginal()
{
var driver = new DeltaDriver
{
Type = "recompute-test",
Severity = DeltaDriverSeverity.Critical,
Description = "Test driver"
};
var verdict = new DeltaVerdictBuilder()
.AddBlockingDriver(driver)
.AddException("EXCEPTION-001")
.Build("delta:sha256:recompute-test");
var generator = new VerdictIdGenerator();
var recomputed = generator.ComputeVerdictId(verdict);
recomputed.Should().Be(verdict.VerdictId, "recomputed VerdictId must match original");
}
}