Files
git.stella-ops.org/src/__Libraries/StellaOps.Verdict/PolicyLockGenerator.cs
2026-01-06 19:07:48 +02:00

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));
}
}