// ----------------------------------------------------------------------------- // PolicyLockGenerator.cs // Sprint: SPRINT_20251229_001_001_BE_cgs_infrastructure (CGS-006) // Task: Implement PolicyLock generator // ----------------------------------------------------------------------------- using System.Security.Cryptography; using System.Text; using System.Text.Json; using Microsoft.Extensions.Logging; namespace StellaOps.Verdict; /// /// Implementation of policy lock generator. /// Freezes policy rules for deterministic verdict computation. /// public sealed class PolicyLockGenerator : IPolicyLockGenerator { private readonly ILogger _logger; private readonly TimeProvider _timeProvider; private const string SchemaVersion = "1.0"; private const string EngineVersion = "1.0.0"; // TODO: Inject actual policy repository when available // private readonly IPolicyRepository _policyRepository; public PolicyLockGenerator(ILogger logger, TimeProvider? timeProvider = null) { _logger = logger; _timeProvider = timeProvider ?? TimeProvider.System; } public async ValueTask GenerateAsync( string policyId, CancellationToken ct = default) { _logger.LogInformation("Generating policy lock for policy {PolicyId}", policyId); // TODO: Query current policy configuration from PolicyRepository // For now, generate a placeholder lock var ruleHashes = await GenerateCurrentRuleHashesAsync(policyId, ct); var policyLock = new PolicyLock( SchemaVersion: SchemaVersion, PolicyVersion: $"{policyId}-{_timeProvider.GetUtcNow():yyyyMMddHHmmss}", RuleHashes: ruleHashes, EngineVersion: EngineVersion, GeneratedAt: _timeProvider.GetUtcNow() ); _logger.LogInformation( "Generated policy lock {Version} with {RuleCount} rules", policyLock.PolicyVersion, policyLock.RuleHashes.Count); return policyLock; } public async ValueTask GenerateForVersionAsync( string policyId, string version, CancellationToken ct = default) { _logger.LogInformation( "Generating policy lock for policy {PolicyId} version {Version}", policyId, version); // TODO: Query specific policy version from PolicyRepository // For now, generate a placeholder lock var ruleHashes = await GenerateRuleHashesForVersionAsync(policyId, version, ct); var policyLock = new PolicyLock( SchemaVersion: SchemaVersion, PolicyVersion: version, RuleHashes: ruleHashes, EngineVersion: EngineVersion, GeneratedAt: _timeProvider.GetUtcNow() ); return policyLock; } public ValueTask ValidateAsync( PolicyLock policyLock, CancellationToken ct = default) { _logger.LogDebug("Validating policy lock {Version}", policyLock.PolicyVersion); // Basic validation var errors = new List(); if (string.IsNullOrWhiteSpace(policyLock.SchemaVersion)) errors.Add("SchemaVersion is required"); if (string.IsNullOrWhiteSpace(policyLock.PolicyVersion)) errors.Add("PolicyVersion is required"); if (string.IsNullOrWhiteSpace(policyLock.EngineVersion)) errors.Add("EngineVersion is required"); if (policyLock.RuleHashes.Count == 0) errors.Add("At least one rule hash is required"); if (policyLock.GeneratedAt > _timeProvider.GetUtcNow().AddMinutes(5)) errors.Add("GeneratedAt timestamp is in the future"); // TODO: Validate rule hashes against stored policy configurations // For now, just basic validation var mismatched = new List(); foreach (var (ruleId, hash) in policyLock.RuleHashes) { if (!IsValidHash(hash)) { mismatched.Add(ruleId); errors.Add($"Invalid hash format for rule {ruleId}"); } } var isValid = errors.Count == 0; var errorMessage = errors.Count > 0 ? string.Join("; ", errors) : null; var result = new PolicyLockValidation( IsValid: isValid, ErrorMessage: errorMessage, MismatchedRules: mismatched ); if (isValid) _logger.LogDebug("Policy lock {Version} is valid", policyLock.PolicyVersion); else _logger.LogWarning("Policy lock {Version} validation failed: {Error}", policyLock.PolicyVersion, errorMessage); return ValueTask.FromResult(result); } /// /// Generate rule hashes for current policy configuration. /// private async ValueTask> GenerateCurrentRuleHashesAsync( string policyId, CancellationToken ct) { await Task.CompletedTask; // Placeholder for async policy fetch // TODO: Fetch actual rules from PolicyRepository // For now, generate placeholder rules var rules = new Dictionary { ["rule.cve.severity.high"] = ComputeRuleHash("high-severity-rule", "v1.0"), ["rule.cve.severity.critical"] = ComputeRuleHash("critical-severity-rule", "v1.0"), ["rule.vex.consensus.threshold"] = ComputeRuleHash("vex-consensus-0.8", "v1.0"), ["rule.reachability.direct"] = ComputeRuleHash("direct-reach-rule", "v1.0"), ["rule.reachability.transitive"] = ComputeRuleHash("transitive-reach-rule", "v1.0") }; return rules; } /// /// Generate rule hashes for a specific policy version. /// private async ValueTask> GenerateRuleHashesForVersionAsync( string policyId, string version, CancellationToken ct) { await Task.CompletedTask; // Placeholder for async policy fetch // TODO: Fetch specific version rules from PolicyRepository // For now, return the same as current (placeholder) return await GenerateCurrentRuleHashesAsync(policyId, ct); } /// /// Compute deterministic hash for a rule. /// Uses SHA256 of canonical JSON representation. /// private static string ComputeRuleHash(string ruleDefinition, string ruleVersion) { var canonical = JsonSerializer.Serialize(new { definition = ruleDefinition, version = ruleVersion }, new JsonSerializerOptions { WriteIndented = false, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); var bytes = Encoding.UTF8.GetBytes(canonical); var hash = SHA256.HashData(bytes); return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}"; } /// /// Validate hash format (sha256:hex). /// private static bool IsValidHash(string hash) { if (string.IsNullOrWhiteSpace(hash)) return false; if (!hash.StartsWith("sha256:", StringComparison.Ordinal)) return false; var hex = hash["sha256:".Length..]; return hex.Length == 64 && hex.All(c => char.IsAsciiHexDigit(c)); } }