Files
git.stella-ops.org/src/AdvisoryAI/StellaOps.AdvisoryAI/PolicyStudio/PolicyBundleCompiler.cs
2026-01-06 19:07:48 +02:00

773 lines
24 KiB
C#

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;
/// <summary>
/// Interface for compiling AI-generated rules into versioned, signed policy bundles.
/// Sprint: SPRINT_20251226_017_AI_policy_copilot
/// Task: POLICY-13
/// </summary>
public interface IPolicyBundleCompiler
{
/// <summary>
/// Compiles lattice rules into a policy bundle.
/// </summary>
Task<PolicyCompilationResult> CompileAsync(
PolicyCompilationRequest request,
CancellationToken cancellationToken = default);
/// <summary>
/// Validates a compiled policy bundle.
/// </summary>
Task<PolicyValidationReport> ValidateAsync(
PolicyBundle bundle,
CancellationToken cancellationToken = default);
/// <summary>
/// Signs a compiled policy bundle.
/// </summary>
Task<SignedPolicyBundle> SignAsync(
PolicyBundle bundle,
PolicySigningOptions options,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Request to compile rules into a policy bundle.
/// </summary>
public sealed record PolicyCompilationRequest
{
/// <summary>
/// Rules to compile.
/// </summary>
public required IReadOnlyList<LatticeRule> Rules { get; init; }
/// <summary>
/// Test cases to include.
/// </summary>
public IReadOnlyList<PolicyTestCase>? TestCases { get; init; }
/// <summary>
/// Policy bundle name.
/// </summary>
public required string Name { get; init; }
/// <summary>
/// Policy version.
/// </summary>
public string Version { get; init; } = "1.0.0";
/// <summary>
/// Target policy pack ID (if extending existing).
/// </summary>
public string? TargetPolicyPack { get; init; }
/// <summary>
/// Trust roots to include.
/// </summary>
public IReadOnlyList<TrustRoot>? TrustRoots { get; init; }
/// <summary>
/// Trust requirements.
/// </summary>
public TrustRequirements? TrustRequirements { get; init; }
/// <summary>
/// Whether to validate before compilation.
/// </summary>
public bool ValidateBeforeCompile { get; init; } = true;
/// <summary>
/// Whether to run test cases.
/// </summary>
public bool RunTests { get; init; } = true;
}
/// <summary>
/// Result of policy compilation.
/// </summary>
public sealed record PolicyCompilationResult
{
/// <summary>
/// Whether compilation was successful.
/// </summary>
public required bool Success { get; init; }
/// <summary>
/// Compiled policy bundle.
/// </summary>
public PolicyBundle? Bundle { get; init; }
/// <summary>
/// Compilation errors.
/// </summary>
public IReadOnlyList<string> Errors { get; init; } = [];
/// <summary>
/// Compilation warnings.
/// </summary>
public IReadOnlyList<string> Warnings { get; init; } = [];
/// <summary>
/// Validation report.
/// </summary>
public PolicyValidationReport? ValidationReport { get; init; }
/// <summary>
/// Test run report.
/// </summary>
public PolicyTestReport? TestReport { get; init; }
/// <summary>
/// Compilation timestamp (UTC ISO-8601).
/// </summary>
public required string CompiledAt { get; init; }
/// <summary>
/// Bundle digest.
/// </summary>
public string? BundleDigest { get; init; }
}
/// <summary>
/// Validation report for a policy bundle.
/// </summary>
public sealed record PolicyValidationReport
{
/// <summary>
/// Whether validation passed.
/// </summary>
public required bool Valid { get; init; }
/// <summary>
/// Syntax valid.
/// </summary>
public bool SyntaxValid { get; init; }
/// <summary>
/// Semantics valid.
/// </summary>
public bool SemanticsValid { get; init; }
/// <summary>
/// Syntax errors.
/// </summary>
public IReadOnlyList<string> SyntaxErrors { get; init; } = [];
/// <summary>
/// Semantic warnings.
/// </summary>
public IReadOnlyList<string> SemanticWarnings { get; init; } = [];
/// <summary>
/// Rule conflicts detected.
/// </summary>
public IReadOnlyList<RuleConflict> Conflicts { get; init; } = [];
/// <summary>
/// Coverage estimate (0.0 - 1.0).
/// </summary>
public double Coverage { get; init; }
}
/// <summary>
/// Test report for a policy bundle.
/// </summary>
public sealed record PolicyTestReport
{
/// <summary>
/// Total tests run.
/// </summary>
public int TotalTests { get; init; }
/// <summary>
/// Tests passed.
/// </summary>
public int Passed { get; init; }
/// <summary>
/// Tests failed.
/// </summary>
public int Failed { get; init; }
/// <summary>
/// Pass rate (0.0 - 1.0).
/// </summary>
public double PassRate => TotalTests > 0 ? (double)Passed / TotalTests : 0;
/// <summary>
/// Failure details.
/// </summary>
public IReadOnlyList<TestFailure> Failures { get; init; } = [];
}
/// <summary>
/// Test failure detail.
/// </summary>
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; }
}
/// <summary>
/// Options for signing a policy bundle.
/// </summary>
public sealed record PolicySigningOptions
{
/// <summary>
/// Key ID to use for signing.
/// </summary>
public string? KeyId { get; init; }
/// <summary>
/// Crypto scheme (eidas, fips, gost, sm).
/// </summary>
public string? CryptoScheme { get; init; }
/// <summary>
/// Signer identity.
/// </summary>
public string? SignerIdentity { get; init; }
/// <summary>
/// Include timestamp.
/// </summary>
public bool IncludeTimestamp { get; init; } = true;
/// <summary>
/// Timestamping authority URL.
/// </summary>
public string? TimestampAuthority { get; init; }
}
/// <summary>
/// Signed policy bundle.
/// </summary>
public sealed record SignedPolicyBundle
{
/// <summary>
/// The policy bundle.
/// </summary>
public required PolicyBundle Bundle { get; init; }
/// <summary>
/// Bundle content hash.
/// </summary>
public required string ContentDigest { get; init; }
/// <summary>
/// Signature bytes (base64).
/// </summary>
public required string Signature { get; init; }
/// <summary>
/// Signing algorithm used.
/// </summary>
public required string Algorithm { get; init; }
/// <summary>
/// Key ID used for signing.
/// </summary>
public string? KeyId { get; init; }
/// <summary>
/// Signer identity.
/// </summary>
public string? SignerIdentity { get; init; }
/// <summary>
/// Signature timestamp (UTC ISO-8601).
/// </summary>
public string? SignedAt { get; init; }
/// <summary>
/// Timestamp token (if requested).
/// </summary>
public string? TimestampToken { get; init; }
/// <summary>
/// Certificate chain (PEM).
/// </summary>
public string? CertificateChain { get; init; }
}
/// <summary>
/// Compiles AI-generated rules into versioned, signed policy bundles.
/// Sprint: SPRINT_20251226_017_AI_policy_copilot
/// Task: POLICY-13
/// </summary>
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<PolicyBundleCompiler> _logger;
private readonly TimeProvider _timeProvider;
public PolicyBundleCompiler(
IPolicyRuleGenerator ruleGenerator,
IPolicyBundleSigner? signer,
ILogger<PolicyBundleCompiler> 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<PolicyCompilationResult> CompileAsync(
PolicyCompilationRequest request,
CancellationToken cancellationToken = default)
{
_logger.LogInformation("Compiling policy bundle '{Name}' with {RuleCount} rules",
request.Name, request.Rules.Count);
var errors = new List<string>();
var warnings = new List<string>();
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<PolicyValidationReport> ValidateAsync(
PolicyBundle bundle,
CancellationToken cancellationToken = default)
{
var syntaxErrors = new List<string>();
var semanticWarnings = new List<string>();
var conflicts = new List<RuleConflict>();
// 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<SignedPolicyBundle> 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<IReadOnlyDictionary<SecurityAtom, K4Value>, 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;
};
}
/// <summary>
/// Extract referenced atoms from a lattice expression for overlap detection.
/// </summary>
private static HashSet<SecurityAtom> ExtractAtomsFromExpression(string expression)
{
var atoms = new HashSet<SecurityAtom>();
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<LatticeRule> rules,
IReadOnlyList<PolicyTestCase> testCases)
{
var failures = new List<TestFailure>();
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<LatticeRule> 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);
}
}
/// <summary>
/// Interface for signing policy bundles.
/// </summary>
public interface IPolicyBundleSigner
{
/// <summary>
/// Signs content and returns signature.
/// </summary>
Task<PolicySignature> SignAsync(
string contentDigest,
PolicySigningOptions options,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Policy signature result.
/// </summary>
public sealed record PolicySignature
{
/// <summary>
/// Signature bytes (base64).
/// </summary>
public required string SignatureBase64 { get; init; }
/// <summary>
/// Signing algorithm.
/// </summary>
public required string Algorithm { get; init; }
/// <summary>
/// Certificate chain (PEM).
/// </summary>
public string? CertificateChain { get; init; }
}