774 lines
24 KiB
C#
774 lines
24 KiB
C#
using System.Globalization;
|
|
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", CultureInfo.InvariantCulture)
|
|
};
|
|
}
|
|
|
|
// 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 = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture),
|
|
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", CultureInfo.InvariantCulture)
|
|
};
|
|
}
|
|
|
|
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", CultureInfo.InvariantCulture),
|
|
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; }
|
|
}
|
|
|