using System.Text.Json.Nodes; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using StellaOps.Findings.Ledger.Domain; using StellaOps.Findings.Ledger.Options; namespace StellaOps.Findings.Ledger.Infrastructure.Policy; internal sealed record PolicyEvaluationCacheKey(string TenantId, string PolicyVersion, Guid EventId, string? ProjectionHash); internal sealed class PolicyEvaluationCache : IDisposable { private readonly IMemoryCache _cache; private readonly ILogger _logger; private bool _disposed; public PolicyEvaluationCache( LedgerServiceOptions.PolicyEngineOptions options, ILogger logger) { ArgumentNullException.ThrowIfNull(options); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _cache = new MemoryCache(new MemoryCacheOptions { SizeLimit = options.Cache.SizeLimit }); EntryLifetime = options.Cache.EntryLifetime; } public TimeSpan EntryLifetime { get; } public bool TryGet(PolicyEvaluationCacheKey key, out PolicyEvaluationResult result) { ArgumentNullException.ThrowIfNull(key); if (_cache.TryGetValue(key, out PolicyEvaluationResult? cached) && cached is not null) { _logger.LogTrace("Policy evaluation cache hit for tenant {Tenant} finding {Finding} policy {Policy}", key.TenantId, key.EventId, key.PolicyVersion); result = Clone(cached); return true; } result = null!; return false; } public void Set(PolicyEvaluationCacheKey key, PolicyEvaluationResult value) { ArgumentNullException.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(value); var entryOptions = new MemoryCacheEntryOptions() .SetSize(1) .SetAbsoluteExpiration(EntryLifetime); _cache.Set(key, Clone(value), entryOptions); } private static PolicyEvaluationResult Clone(PolicyEvaluationResult result) { var labelsClone = result.Labels is null ? new JsonObject() : (JsonObject)result.Labels.DeepClone(); var rationaleClone = result.Rationale is null ? new JsonArray() : CloneArray(result.Rationale); return new PolicyEvaluationResult( result.Status, result.Severity, labelsClone, result.ExplainRef, rationaleClone); } private static JsonArray CloneArray(JsonArray source) { var clone = new JsonArray(); foreach (var item in source) { clone.Add(item?.DeepClone()); } return clone; } public void Dispose() { if (_disposed) { return; } _cache.Dispose(); _disposed = true; } }