using System.Text.Json.Serialization; namespace StellaOps.Scanner.Reachability.Slices; /// /// Policy binding mode for slices. /// public enum PolicyBindingMode { /// /// Slice is invalid if policy changes at all. /// Strict, /// /// Slice is valid with newer policy versions only. /// Forward, /// /// Slice is valid with any policy version. /// Any } /// /// Policy binding information for a reachability slice. /// public sealed record PolicyBinding { /// /// Content-addressed hash of the policy DSL. /// [JsonPropertyName("policyDigest")] public required string PolicyDigest { get; init; } /// /// Semantic version of the policy. /// [JsonPropertyName("policyVersion")] public required string PolicyVersion { get; init; } /// /// When the policy was bound to this slice. /// [JsonPropertyName("boundAt")] public required DateTimeOffset BoundAt { get; init; } /// /// Binding mode for validation. /// [JsonPropertyName("mode")] [JsonConverter(typeof(JsonStringEnumConverter))] public required PolicyBindingMode Mode { get; init; } /// /// Optional policy name/identifier. /// [JsonPropertyName("policyName")] public string? PolicyName { get; init; } /// /// Optional policy source (e.g., git commit hash). /// [JsonPropertyName("policySource")] public string? PolicySource { get; init; } } /// /// Result of policy binding validation. /// 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; } } /// /// Validator for policy bindings. /// public sealed class PolicyBindingValidator { /// /// Validate a policy binding against current policy. /// 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 }; } }