using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; using StellaOps.Policy.TrustLattice; namespace StellaOps.AdvisoryAI.PolicyStudio; /// /// Interface for compiling AI-generated rules into versioned, signed policy bundles. /// Sprint: SPRINT_20251226_017_AI_policy_copilot /// Task: POLICY-13 /// public interface IPolicyBundleCompiler { /// /// Compiles lattice rules into a policy bundle. /// Task CompileAsync( PolicyCompilationRequest request, CancellationToken cancellationToken = default); /// /// Validates a compiled policy bundle. /// Task ValidateAsync( PolicyBundle bundle, CancellationToken cancellationToken = default); /// /// Signs a compiled policy bundle. /// Task SignAsync( PolicyBundle bundle, PolicySigningOptions options, CancellationToken cancellationToken = default); } /// /// Request to compile rules into a policy bundle. /// public sealed record PolicyCompilationRequest { /// /// Rules to compile. /// public required IReadOnlyList Rules { get; init; } /// /// Test cases to include. /// public IReadOnlyList? TestCases { get; init; } /// /// Policy bundle name. /// public required string Name { get; init; } /// /// Policy version. /// public string Version { get; init; } = "1.0.0"; /// /// Target policy pack ID (if extending existing). /// public string? TargetPolicyPack { get; init; } /// /// Trust roots to include. /// public IReadOnlyList? TrustRoots { get; init; } /// /// Trust requirements. /// public TrustRequirements? TrustRequirements { get; init; } /// /// Whether to validate before compilation. /// public bool ValidateBeforeCompile { get; init; } = true; /// /// Whether to run test cases. /// public bool RunTests { get; init; } = true; } /// /// Result of policy compilation. /// public sealed record PolicyCompilationResult { /// /// Whether compilation was successful. /// public required bool Success { get; init; } /// /// Compiled policy bundle. /// public PolicyBundle? Bundle { get; init; } /// /// Compilation errors. /// public IReadOnlyList Errors { get; init; } = []; /// /// Compilation warnings. /// public IReadOnlyList Warnings { get; init; } = []; /// /// Validation report. /// public PolicyValidationReport? ValidationReport { get; init; } /// /// Test run report. /// public PolicyTestReport? TestReport { get; init; } /// /// Compilation timestamp (UTC ISO-8601). /// public required string CompiledAt { get; init; } /// /// Bundle digest. /// public string? BundleDigest { get; init; } } /// /// Validation report for a policy bundle. /// public sealed record PolicyValidationReport { /// /// Whether validation passed. /// public required bool Valid { get; init; } /// /// Syntax valid. /// public bool SyntaxValid { get; init; } /// /// Semantics valid. /// public bool SemanticsValid { get; init; } /// /// Syntax errors. /// public IReadOnlyList SyntaxErrors { get; init; } = []; /// /// Semantic warnings. /// public IReadOnlyList SemanticWarnings { get; init; } = []; /// /// Rule conflicts detected. /// public IReadOnlyList Conflicts { get; init; } = []; /// /// Coverage estimate (0.0 - 1.0). /// public double Coverage { get; init; } } /// /// Test report for a policy bundle. /// public sealed record PolicyTestReport { /// /// Total tests run. /// public int TotalTests { get; init; } /// /// Tests passed. /// public int Passed { get; init; } /// /// Tests failed. /// public int Failed { get; init; } /// /// Pass rate (0.0 - 1.0). /// public double PassRate => TotalTests > 0 ? (double)Passed / TotalTests : 0; /// /// Failure details. /// public IReadOnlyList Failures { get; init; } = []; } /// /// Test failure detail. /// public sealed record TestFailure { public required string TestId { get; init; } public required string RuleId { get; init; } public required string Description { get; init; } public required string Expected { get; init; } public required string Actual { get; init; } } /// /// Options for signing a policy bundle. /// public sealed record PolicySigningOptions { /// /// Key ID to use for signing. /// public string? KeyId { get; init; } /// /// Crypto scheme (eidas, fips, gost, sm). /// public string? CryptoScheme { get; init; } /// /// Signer identity. /// public string? SignerIdentity { get; init; } /// /// Include timestamp. /// public bool IncludeTimestamp { get; init; } = true; /// /// Timestamping authority URL. /// public string? TimestampAuthority { get; init; } } /// /// Signed policy bundle. /// public sealed record SignedPolicyBundle { /// /// The policy bundle. /// public required PolicyBundle Bundle { get; init; } /// /// Bundle content hash. /// public required string ContentDigest { get; init; } /// /// Signature bytes (base64). /// public required string Signature { get; init; } /// /// Signing algorithm used. /// public required string Algorithm { get; init; } /// /// Key ID used for signing. /// public string? KeyId { get; init; } /// /// Signer identity. /// public string? SignerIdentity { get; init; } /// /// Signature timestamp (UTC ISO-8601). /// public string? SignedAt { get; init; } /// /// Timestamp token (if requested). /// public string? TimestampToken { get; init; } /// /// Certificate chain (PEM). /// public string? CertificateChain { get; init; } } /// /// Compiles AI-generated rules into versioned, signed policy bundles. /// Sprint: SPRINT_20251226_017_AI_policy_copilot /// Task: POLICY-13 /// public sealed class PolicyBundleCompiler : IPolicyBundleCompiler { private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web) { WriteIndented = false, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; private readonly IPolicyRuleGenerator _ruleGenerator; private readonly IPolicyBundleSigner? _signer; private readonly ILogger _logger; private readonly TimeProvider _timeProvider; public PolicyBundleCompiler( IPolicyRuleGenerator ruleGenerator, IPolicyBundleSigner? signer, ILogger logger, TimeProvider? timeProvider = null) { _ruleGenerator = ruleGenerator ?? throw new ArgumentNullException(nameof(ruleGenerator)); _signer = signer; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _timeProvider = timeProvider ?? TimeProvider.System; } public async Task CompileAsync( PolicyCompilationRequest request, CancellationToken cancellationToken = default) { _logger.LogInformation("Compiling policy bundle '{Name}' with {RuleCount} rules", request.Name, request.Rules.Count); var errors = new List(); var warnings = new List(); PolicyValidationReport? validationReport = null; PolicyTestReport? testReport = null; // Step 1: Validate rules if requested if (request.ValidateBeforeCompile) { var validationResult = await _ruleGenerator.ValidateAsync( request.Rules, null, cancellationToken); validationReport = new PolicyValidationReport { Valid = validationResult.Valid, SyntaxValid = validationResult.Valid, SemanticsValid = validationResult.Conflicts.Count == 0, Conflicts = validationResult.Conflicts, SemanticWarnings = validationResult.UnreachableConditions.Concat(validationResult.PotentialLoops).ToList(), Coverage = validationResult.Coverage }; if (!validationResult.Valid) { errors.AddRange(validationResult.Conflicts.Select(c => $"Rule conflict: {c.Description}")); errors.AddRange(validationResult.UnreachableConditions); errors.AddRange(validationResult.PotentialLoops); } warnings.AddRange(validationResult.UnreachableConditions); } // Step 2: Run tests if requested if (request.RunTests && request.TestCases?.Count > 0) { testReport = RunTests(request.Rules, request.TestCases); if (testReport.Failed > 0) { warnings.Add($"{testReport.Failed} of {testReport.TotalTests} tests failed"); } } // Check for blocking errors if (errors.Count > 0) { return new PolicyCompilationResult { Success = false, Errors = errors, Warnings = warnings, ValidationReport = validationReport, TestReport = testReport, CompiledAt = _timeProvider.GetUtcNow().ToString("O") }; } // Step 3: Build the policy bundle var bundle = BuildBundle(request); // Step 4: Compute bundle digest var bundleDigest = ComputeBundleDigest(bundle); _logger.LogInformation("Compiled policy bundle '{Name}' v{Version} with digest {Digest}", bundle.Name, bundle.Version, bundleDigest); return new PolicyCompilationResult { Success = true, Bundle = bundle, Errors = errors, Warnings = warnings, ValidationReport = validationReport, TestReport = testReport, CompiledAt = DateTime.UtcNow.ToString("O"), BundleDigest = bundleDigest }; } public Task ValidateAsync( PolicyBundle bundle, CancellationToken cancellationToken = default) { var syntaxErrors = new List(); var semanticWarnings = new List(); var conflicts = new List(); // Validate trust roots foreach (var root in bundle.TrustRoots) { if (root.ExpiresAt.HasValue && root.ExpiresAt.Value < _timeProvider.GetUtcNow()) { semanticWarnings.Add($"Trust root '{root.Principal.Id}' has expired"); } } // Validate custom rules foreach (var rule in bundle.CustomRules) { if (string.IsNullOrEmpty(rule.Name)) { syntaxErrors.Add($"Rule is missing a name"); } } // Check for rule conflicts var rules = bundle.CustomRules.ToList(); for (int i = 0; i < rules.Count; i++) { for (int j = i + 1; j < rules.Count; j++) { // Simple overlap check based on atom patterns if (HasOverlappingAtoms(rules[i], rules[j])) { conflicts.Add(new RuleConflict { RuleId1 = rules[i].Name, RuleId2 = rules[j].Name, Description = "Rules may have overlapping conditions", SuggestedResolution = "Review rule priorities", Severity = "warning" }); } } } return Task.FromResult(new PolicyValidationReport { Valid = syntaxErrors.Count == 0, SyntaxValid = syntaxErrors.Count == 0, SemanticsValid = conflicts.Count == 0, SyntaxErrors = syntaxErrors, SemanticWarnings = semanticWarnings, Conflicts = conflicts, Coverage = EstimateCoverage(bundle) }); } public async Task SignAsync( PolicyBundle bundle, PolicySigningOptions options, CancellationToken cancellationToken = default) { var contentDigest = ComputeBundleDigest(bundle); if (_signer is null) { _logger.LogWarning("No signer configured, returning unsigned bundle"); return new SignedPolicyBundle { Bundle = bundle, ContentDigest = contentDigest, Signature = string.Empty, Algorithm = "none", SignedAt = _timeProvider.GetUtcNow().ToString("O") }; } var signature = await _signer.SignAsync(contentDigest, options, cancellationToken); _logger.LogInformation("Signed policy bundle '{Name}' with key {KeyId}", bundle.Name, options.KeyId); return new SignedPolicyBundle { Bundle = bundle, ContentDigest = contentDigest, Signature = signature.SignatureBase64, Algorithm = signature.Algorithm, KeyId = options.KeyId, SignerIdentity = options.SignerIdentity, SignedAt = _timeProvider.GetUtcNow().ToString("O"), CertificateChain = signature.CertificateChain }; } private PolicyBundle BuildBundle(PolicyCompilationRequest request) { // Convert LatticeRules to SelectionRules var customRules = request.Rules.Select(ConvertToSelectionRule).ToList(); return new PolicyBundle { Id = $"bundle:{ComputeHash(request.Name)[..12]}", Name = request.Name, Version = request.Version, TrustRoots = request.TrustRoots ?? [], TrustRequirements = request.TrustRequirements ?? new TrustRequirements(), CustomRules = customRules, ConflictResolution = ConflictResolution.ReportConflict, AssumeReachableWhenUnknown = true }; } private static SelectionRule ConvertToSelectionRule(LatticeRule rule) { // Map disposition string to Disposition enum var disposition = rule.Disposition.ToLowerInvariant() switch { "block" or "exploitable" => Disposition.Exploitable, "allow" or "resolved" => Disposition.Resolved, "resolved_with_pedigree" => Disposition.ResolvedWithPedigree, "not_affected" => Disposition.NotAffected, "false_positive" => Disposition.FalsePositive, "warn" or "in_triage" or _ => Disposition.InTriage }; // Build condition function from lattice expression var condition = BuildConditionFromExpression(rule.LatticeExpression); return new SelectionRule { Name = rule.Name, Priority = rule.Priority, Disposition = disposition, ConditionDescription = rule.LatticeExpression, Condition = condition, ExplanationTemplate = rule.Description }; } private static Func, bool> BuildConditionFromExpression(string latticeExpression) { // Parse lattice expression and build condition function // This is a simplified parser - production would use proper expression parsing var expr = latticeExpression.ToUpperInvariant(); return atoms => { // Check for negated atoms first if (expr.Contains("¬REACHABLE") || expr.Contains("NOT REACHABLE") || expr.Contains("!REACHABLE")) { if (atoms.TryGetValue(SecurityAtom.Reachable, out var r) && r != K4Value.False) return false; } else if (expr.Contains("REACHABLE")) { if (atoms.TryGetValue(SecurityAtom.Reachable, out var r) && r != K4Value.True) return false; } if (expr.Contains("¬PRESENT") || expr.Contains("NOT PRESENT") || expr.Contains("!PRESENT")) { if (atoms.TryGetValue(SecurityAtom.Present, out var p) && p != K4Value.False) return false; } else if (expr.Contains("PRESENT")) { if (atoms.TryGetValue(SecurityAtom.Present, out var p) && p != K4Value.True) return false; } if (expr.Contains("¬APPLIES") || expr.Contains("NOT APPLIES") || expr.Contains("!APPLIES")) { if (atoms.TryGetValue(SecurityAtom.Applies, out var a) && a != K4Value.False) return false; } else if (expr.Contains("APPLIES")) { if (atoms.TryGetValue(SecurityAtom.Applies, out var a) && a != K4Value.True) return false; } if (expr.Contains("MITIGATED")) { if (atoms.TryGetValue(SecurityAtom.Mitigated, out var m) && m != K4Value.True) return false; } if (expr.Contains("FIXED")) { if (atoms.TryGetValue(SecurityAtom.Fixed, out var f) && f != K4Value.True) return false; } if (expr.Contains("MISATTRIBUTED")) { if (atoms.TryGetValue(SecurityAtom.Misattributed, out var m) && m != K4Value.True) return false; } return true; }; } /// /// Extract referenced atoms from a lattice expression for overlap detection. /// private static HashSet ExtractAtomsFromExpression(string expression) { var atoms = new HashSet(); var expr = expression.ToUpperInvariant(); if (expr.Contains("REACHABLE")) atoms.Add(SecurityAtom.Reachable); if (expr.Contains("PRESENT")) atoms.Add(SecurityAtom.Present); if (expr.Contains("APPLIES")) atoms.Add(SecurityAtom.Applies); if (expr.Contains("MITIGATED")) atoms.Add(SecurityAtom.Mitigated); if (expr.Contains("FIXED")) atoms.Add(SecurityAtom.Fixed); if (expr.Contains("MISATTRIBUTED")) atoms.Add(SecurityAtom.Misattributed); return atoms; } private PolicyTestReport RunTests( IReadOnlyList rules, IReadOnlyList testCases) { var failures = new List(); var passed = 0; foreach (var test in testCases) { // Find all target rules for this test var targetRules = rules.Where(r => test.TargetRuleIds.Contains(r.RuleId)).ToList(); if (targetRules.Count == 0) { failures.Add(new TestFailure { TestId = test.TestCaseId, RuleId = string.Join(",", test.TargetRuleIds), Description = "Target rules not found", Expected = test.ExpectedDisposition, Actual = "not_found" }); continue; } // Evaluate the test against the rules var result = EvaluateTest(targetRules, test); if (result == test.ExpectedDisposition) { passed++; } else { failures.Add(new TestFailure { TestId = test.TestCaseId, RuleId = string.Join(",", test.TargetRuleIds), Description = test.Description, Expected = test.ExpectedDisposition, Actual = result }); } } return new PolicyTestReport { TotalTests = testCases.Count, Passed = passed, Failed = failures.Count, Failures = failures }; } private static string EvaluateTest(IReadOnlyList rules, PolicyTestCase test) { // Simplified test evaluation - find highest priority matching rule // In production, use proper lattice engine with full atom evaluation var bestMatch = rules.OrderBy(r => r.Priority).FirstOrDefault(); return bestMatch?.Disposition ?? "unknown"; } private static bool HasOverlappingAtoms(SelectionRule rule1, SelectionRule rule2) { // Extract atoms from condition descriptions (which contain the lattice expressions) var atoms1 = ExtractAtomsFromExpression(rule1.ConditionDescription); var atoms2 = ExtractAtomsFromExpression(rule2.ConditionDescription); return atoms1.Overlaps(atoms2); } private static double EstimateCoverage(PolicyBundle bundle) { // Count distinct atoms referenced across all rules var atomsCovered = bundle.CustomRules .SelectMany(r => ExtractAtomsFromExpression(r.ConditionDescription)) .Distinct() .Count(); // 6 possible security atoms, estimate coverage as percentage return Math.Min(1.0, (double)atomsCovered / 6.0); } private static string ComputeBundleDigest(PolicyBundle bundle) { var json = JsonSerializer.Serialize(bundle, SerializerOptions); var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(json)); return $"sha256:{Convert.ToHexStringLower(bytes)}"; } private static string ComputeHash(string content) { var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(content)); return Convert.ToHexStringLower(bytes); } } /// /// Interface for signing policy bundles. /// public interface IPolicyBundleSigner { /// /// Signs content and returns signature. /// Task SignAsync( string contentDigest, PolicySigningOptions options, CancellationToken cancellationToken = default); } /// /// Policy signature result. /// public sealed record PolicySignature { /// /// Signature bytes (base64). /// public required string SignatureBase64 { get; init; } /// /// Signing algorithm. /// public required string Algorithm { get; init; } /// /// Certificate chain (PEM). /// public string? CertificateChain { get; init; } }