268 lines
7.5 KiB
C#
268 lines
7.5 KiB
C#
namespace StellaOps.Policy.Deltas;
|
|
|
|
/// <summary>
|
|
/// Verdict for a security state delta.
|
|
/// Determines whether a change should be allowed to proceed.
|
|
/// </summary>
|
|
public sealed record DeltaVerdict
|
|
{
|
|
/// <summary>
|
|
/// Unique identifier for this verdict.
|
|
/// </summary>
|
|
public required string VerdictId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Reference to the delta being evaluated.
|
|
/// </summary>
|
|
public required string DeltaId { get; init; }
|
|
|
|
/// <summary>
|
|
/// When this verdict was rendered.
|
|
/// </summary>
|
|
public required DateTimeOffset EvaluatedAt { get; init; }
|
|
|
|
/// <summary>
|
|
/// The verdict outcome.
|
|
/// </summary>
|
|
public required DeltaVerdictStatus Status { get; init; }
|
|
|
|
/// <summary>
|
|
/// Recommended gate level based on delta risk.
|
|
/// </summary>
|
|
public DeltaGateLevel RecommendedGate { get; init; }
|
|
|
|
/// <summary>
|
|
/// Risk points consumed by this change.
|
|
/// </summary>
|
|
public int RiskPoints { get; init; }
|
|
|
|
/// <summary>
|
|
/// Drivers that contributed to the verdict.
|
|
/// </summary>
|
|
public IReadOnlyList<DeltaDriver> BlockingDrivers { get; init; } = [];
|
|
|
|
/// <summary>
|
|
/// Drivers that raised warnings but didn't block.
|
|
/// </summary>
|
|
public IReadOnlyList<DeltaDriver> WarningDrivers { get; init; } = [];
|
|
|
|
/// <summary>
|
|
/// Applied exceptions that allowed blocking drivers.
|
|
/// </summary>
|
|
public IReadOnlyList<string> AppliedExceptions { get; init; } = [];
|
|
|
|
/// <summary>
|
|
/// Human-readable explanation.
|
|
/// </summary>
|
|
public string? Explanation { get; init; }
|
|
|
|
/// <summary>
|
|
/// Recommendations for addressing issues.
|
|
/// </summary>
|
|
public IReadOnlyList<string> Recommendations { get; init; } = [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Possible verdict outcomes for a delta.
|
|
/// </summary>
|
|
public enum DeltaVerdictStatus
|
|
{
|
|
/// <summary>
|
|
/// Delta is safe to proceed.
|
|
/// </summary>
|
|
Pass,
|
|
|
|
/// <summary>
|
|
/// Delta has warnings but can proceed.
|
|
/// </summary>
|
|
Warn,
|
|
|
|
/// <summary>
|
|
/// Delta should not proceed without remediation.
|
|
/// </summary>
|
|
Fail,
|
|
|
|
/// <summary>
|
|
/// Delta is blocked but covered by exceptions.
|
|
/// </summary>
|
|
PassWithExceptions
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gate levels aligned with diff-aware release gates.
|
|
/// </summary>
|
|
public enum DeltaGateLevel
|
|
{
|
|
/// <summary>
|
|
/// G0: No-risk (docs, comments only).
|
|
/// </summary>
|
|
G0,
|
|
|
|
/// <summary>
|
|
/// G1: Low risk (unit tests, 1 review).
|
|
/// </summary>
|
|
G1,
|
|
|
|
/// <summary>
|
|
/// G2: Moderate risk (integration tests, code owner, canary).
|
|
/// </summary>
|
|
G2,
|
|
|
|
/// <summary>
|
|
/// G3: High risk (security scan, migration plan, release captain).
|
|
/// </summary>
|
|
G3,
|
|
|
|
/// <summary>
|
|
/// G4: Very high risk (formal review, extended canary, comms plan).
|
|
/// </summary>
|
|
G4
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builder for delta verdicts.
|
|
/// </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;
|
|
private readonly List<DeltaDriver> _blockingDrivers = [];
|
|
private readonly List<DeltaDriver> _warningDrivers = [];
|
|
private readonly List<string> _exceptions = [];
|
|
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;
|
|
return this;
|
|
}
|
|
|
|
public DeltaVerdictBuilder WithGate(DeltaGateLevel gate)
|
|
{
|
|
_gate = gate;
|
|
return this;
|
|
}
|
|
|
|
public DeltaVerdictBuilder WithRiskPoints(int points)
|
|
{
|
|
_riskPoints = points;
|
|
return this;
|
|
}
|
|
|
|
public DeltaVerdictBuilder AddBlockingDriver(DeltaDriver driver)
|
|
{
|
|
_blockingDrivers.Add(driver);
|
|
_status = DeltaVerdictStatus.Fail;
|
|
|
|
// Escalate gate based on severity
|
|
if (driver.Severity == DeltaDriverSeverity.Critical && _gate < DeltaGateLevel.G4)
|
|
_gate = DeltaGateLevel.G4;
|
|
else if (driver.Severity == DeltaDriverSeverity.High && _gate < DeltaGateLevel.G3)
|
|
_gate = DeltaGateLevel.G3;
|
|
|
|
return this;
|
|
}
|
|
|
|
public DeltaVerdictBuilder AddWarningDriver(DeltaDriver driver)
|
|
{
|
|
_warningDrivers.Add(driver);
|
|
if (_status == DeltaVerdictStatus.Pass)
|
|
_status = DeltaVerdictStatus.Warn;
|
|
|
|
// Escalate gate for medium severity warnings
|
|
if (driver.Severity >= DeltaDriverSeverity.Medium && _gate < DeltaGateLevel.G2)
|
|
_gate = DeltaGateLevel.G2;
|
|
|
|
return this;
|
|
}
|
|
|
|
public DeltaVerdictBuilder AddException(string exceptionId)
|
|
{
|
|
_exceptions.Add(exceptionId);
|
|
return this;
|
|
}
|
|
|
|
public DeltaVerdictBuilder AddRecommendation(string recommendation)
|
|
{
|
|
_recommendations.Add(recommendation);
|
|
return this;
|
|
}
|
|
|
|
public DeltaVerdictBuilder WithExplanation(string explanation)
|
|
{
|
|
_explanation = explanation;
|
|
return this;
|
|
}
|
|
|
|
public DeltaVerdict Build(string deltaId)
|
|
{
|
|
// If all blocking drivers are excepted, change to PassWithExceptions
|
|
if (_status == DeltaVerdictStatus.Fail &&
|
|
_blockingDrivers.Count > 0 &&
|
|
_exceptions.Count >= _blockingDrivers.Count)
|
|
{
|
|
_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 = verdictId,
|
|
DeltaId = deltaId,
|
|
EvaluatedAt = DateTimeOffset.UtcNow,
|
|
Status = _status,
|
|
RecommendedGate = _gate,
|
|
RiskPoints = _riskPoints,
|
|
BlockingDrivers = blockingDrivers,
|
|
WarningDrivers = warningDrivers,
|
|
AppliedExceptions = appliedExceptions,
|
|
Explanation = _explanation ?? GenerateExplanation(),
|
|
Recommendations = _recommendations.ToList()
|
|
};
|
|
}
|
|
|
|
private string GenerateExplanation()
|
|
{
|
|
return _status switch
|
|
{
|
|
DeltaVerdictStatus.Pass => "No blocking changes detected",
|
|
DeltaVerdictStatus.Warn => $"{_warningDrivers.Count} warning(s) detected",
|
|
DeltaVerdictStatus.Fail => $"{_blockingDrivers.Count} blocking issue(s) detected",
|
|
DeltaVerdictStatus.PassWithExceptions => $"Blocked by {_blockingDrivers.Count} issue(s), covered by exceptions",
|
|
_ => "Unknown status"
|
|
};
|
|
}
|
|
}
|