174 lines
5.2 KiB
C#
174 lines
5.2 KiB
C#
using System.Text.Json.Serialization;
|
|
|
|
namespace StellaOps.Scanner.Reachability.Slices;
|
|
|
|
/// <summary>
|
|
/// Policy binding mode for slices.
|
|
/// </summary>
|
|
public enum PolicyBindingMode
|
|
{
|
|
/// <summary>
|
|
/// Slice is invalid if policy changes at all.
|
|
/// </summary>
|
|
Strict,
|
|
|
|
/// <summary>
|
|
/// Slice is valid with newer policy versions only.
|
|
/// </summary>
|
|
Forward,
|
|
|
|
/// <summary>
|
|
/// Slice is valid with any policy version.
|
|
/// </summary>
|
|
Any
|
|
}
|
|
|
|
/// <summary>
|
|
/// Policy binding information for a reachability slice.
|
|
/// </summary>
|
|
public sealed record PolicyBinding
|
|
{
|
|
/// <summary>
|
|
/// Content-addressed hash of the policy DSL.
|
|
/// </summary>
|
|
[JsonPropertyName("policyDigest")]
|
|
public required string PolicyDigest { get; init; }
|
|
|
|
/// <summary>
|
|
/// Semantic version of the policy.
|
|
/// </summary>
|
|
[JsonPropertyName("policyVersion")]
|
|
public required string PolicyVersion { get; init; }
|
|
|
|
/// <summary>
|
|
/// When the policy was bound to this slice.
|
|
/// </summary>
|
|
[JsonPropertyName("boundAt")]
|
|
public required DateTimeOffset BoundAt { get; init; }
|
|
|
|
/// <summary>
|
|
/// Binding mode for validation.
|
|
/// </summary>
|
|
[JsonPropertyName("mode")]
|
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
|
public required PolicyBindingMode Mode { get; init; }
|
|
|
|
/// <summary>
|
|
/// Optional policy name/identifier.
|
|
/// </summary>
|
|
[JsonPropertyName("policyName")]
|
|
public string? PolicyName { get; init; }
|
|
|
|
/// <summary>
|
|
/// Optional policy source (e.g., git commit hash).
|
|
/// </summary>
|
|
[JsonPropertyName("policySource")]
|
|
public string? PolicySource { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result of policy binding validation.
|
|
/// </summary>
|
|
public sealed record PolicyBindingValidationResult
|
|
{
|
|
public required bool Valid { get; init; }
|
|
public string? FailureReason { get; init; }
|
|
public required PolicyBinding SlicePolicy { get; init; }
|
|
public required PolicyBinding CurrentPolicy { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validator for policy bindings.
|
|
/// </summary>
|
|
public sealed class PolicyBindingValidator
|
|
{
|
|
/// <summary>
|
|
/// Validate a policy binding against current policy.
|
|
/// </summary>
|
|
public PolicyBindingValidationResult Validate(
|
|
PolicyBinding sliceBinding,
|
|
PolicyBinding currentPolicy)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(sliceBinding);
|
|
ArgumentNullException.ThrowIfNull(currentPolicy);
|
|
|
|
var result = sliceBinding.Mode switch
|
|
{
|
|
PolicyBindingMode.Strict => ValidateStrict(sliceBinding, currentPolicy),
|
|
PolicyBindingMode.Forward => ValidateForward(sliceBinding, currentPolicy),
|
|
PolicyBindingMode.Any => ValidateAny(sliceBinding, currentPolicy),
|
|
_ => throw new ArgumentException($"Unknown policy binding mode: {sliceBinding.Mode}")
|
|
};
|
|
|
|
return result with
|
|
{
|
|
SlicePolicy = sliceBinding,
|
|
CurrentPolicy = currentPolicy
|
|
};
|
|
}
|
|
|
|
private static PolicyBindingValidationResult ValidateStrict(
|
|
PolicyBinding sliceBinding,
|
|
PolicyBinding currentPolicy)
|
|
{
|
|
var digestMatch = string.Equals(
|
|
sliceBinding.PolicyDigest,
|
|
currentPolicy.PolicyDigest,
|
|
StringComparison.Ordinal);
|
|
|
|
return new PolicyBindingValidationResult
|
|
{
|
|
Valid = digestMatch,
|
|
FailureReason = digestMatch
|
|
? null
|
|
: $"Policy digest mismatch. Slice bound to {sliceBinding.PolicyDigest}, current is {currentPolicy.PolicyDigest}.",
|
|
SlicePolicy = sliceBinding,
|
|
CurrentPolicy = currentPolicy
|
|
};
|
|
}
|
|
|
|
private static PolicyBindingValidationResult ValidateForward(
|
|
PolicyBinding sliceBinding,
|
|
PolicyBinding currentPolicy)
|
|
{
|
|
// Check if current policy version is newer or equal
|
|
if (!Version.TryParse(sliceBinding.PolicyVersion, out var sliceVersion) ||
|
|
!Version.TryParse(currentPolicy.PolicyVersion, out var currentVersion))
|
|
{
|
|
return new PolicyBindingValidationResult
|
|
{
|
|
Valid = false,
|
|
FailureReason = "Invalid version format for forward compatibility check.",
|
|
SlicePolicy = sliceBinding,
|
|
CurrentPolicy = currentPolicy
|
|
};
|
|
}
|
|
|
|
var isForwardCompatible = currentVersion >= sliceVersion;
|
|
|
|
return new PolicyBindingValidationResult
|
|
{
|
|
Valid = isForwardCompatible,
|
|
FailureReason = isForwardCompatible
|
|
? null
|
|
: $"Policy version downgrade detected. Slice bound to {sliceVersion}, current is {currentVersion}.",
|
|
SlicePolicy = sliceBinding,
|
|
CurrentPolicy = currentPolicy
|
|
};
|
|
}
|
|
|
|
private static PolicyBindingValidationResult ValidateAny(
|
|
PolicyBinding sliceBinding,
|
|
PolicyBinding currentPolicy)
|
|
{
|
|
// Always valid in 'any' mode
|
|
return new PolicyBindingValidationResult
|
|
{
|
|
Valid = true,
|
|
FailureReason = null,
|
|
SlicePolicy = sliceBinding,
|
|
CurrentPolicy = currentPolicy
|
|
};
|
|
}
|
|
}
|