213 lines
7.4 KiB
C#
213 lines
7.4 KiB
C#
// -----------------------------------------------------------------------------
|
|
// 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;
|
|
|
|
/// <summary>
|
|
/// Implementation of policy lock generator.
|
|
/// Freezes policy rules for deterministic verdict computation.
|
|
/// </summary>
|
|
public sealed class PolicyLockGenerator : IPolicyLockGenerator
|
|
{
|
|
private readonly ILogger<PolicyLockGenerator> _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<PolicyLockGenerator> logger, TimeProvider? timeProvider = null)
|
|
{
|
|
_logger = logger;
|
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
|
}
|
|
|
|
public async ValueTask<PolicyLock> 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<PolicyLock> 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<PolicyLockValidation> ValidateAsync(
|
|
PolicyLock policyLock,
|
|
CancellationToken ct = default)
|
|
{
|
|
_logger.LogDebug("Validating policy lock {Version}", policyLock.PolicyVersion);
|
|
|
|
// Basic validation
|
|
var errors = new List<string>();
|
|
|
|
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<string>();
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generate rule hashes for current policy configuration.
|
|
/// </summary>
|
|
private async ValueTask<IReadOnlyDictionary<string, string>> 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<string, string>
|
|
{
|
|
["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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generate rule hashes for a specific policy version.
|
|
/// </summary>
|
|
private async ValueTask<IReadOnlyDictionary<string, string>> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compute deterministic hash for a rule.
|
|
/// Uses SHA256 of canonical JSON representation.
|
|
/// </summary>
|
|
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()}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validate hash format (sha256:hex).
|
|
/// </summary>
|
|
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));
|
|
}
|
|
}
|