using System.Collections.Concurrent; namespace StellaOps.Policy.Engine.ReachabilityFacts; /// /// Store interface for reachability facts persistence. /// public interface IReachabilityFactsStore { /// /// Gets a single reachability fact by key. /// Task GetAsync( string tenantId, string componentPurl, string advisoryId, CancellationToken cancellationToken = default); /// /// Gets multiple reachability facts by keys. /// Task> GetBatchAsync( IReadOnlyList keys, CancellationToken cancellationToken = default); /// /// Queries reachability facts with filtering. /// Task> QueryAsync( ReachabilityFactsQuery query, CancellationToken cancellationToken = default); /// /// Saves or updates a reachability fact. /// Task SaveAsync( ReachabilityFact fact, CancellationToken cancellationToken = default); /// /// Saves multiple reachability facts. /// Task SaveBatchAsync( IReadOnlyList facts, CancellationToken cancellationToken = default); /// /// Deletes a reachability fact. /// Task DeleteAsync( string tenantId, string componentPurl, string advisoryId, CancellationToken cancellationToken = default); /// /// Gets the count of facts for a tenant. /// Task CountAsync( string tenantId, CancellationToken cancellationToken = default); } /// /// In-memory implementation of the reachability facts store for development and testing. /// public sealed class InMemoryReachabilityFactsStore : IReachabilityFactsStore { private readonly ConcurrentDictionary _facts = new(); private readonly TimeProvider _timeProvider; public InMemoryReachabilityFactsStore(TimeProvider? timeProvider = null) { _timeProvider = timeProvider ?? TimeProvider.System; } public Task GetAsync( string tenantId, string componentPurl, string advisoryId, CancellationToken cancellationToken = default) { var key = new ReachabilityFactKey(tenantId, componentPurl, advisoryId); _facts.TryGetValue(key, out var fact); return Task.FromResult(fact); } public Task> GetBatchAsync( IReadOnlyList keys, CancellationToken cancellationToken = default) { var result = new Dictionary(); foreach (var key in keys) { if (_facts.TryGetValue(key, out var fact)) { result[key] = fact; } } return Task.FromResult>(result); } public Task> QueryAsync( ReachabilityFactsQuery query, CancellationToken cancellationToken = default) { var now = _timeProvider.GetUtcNow(); var results = _facts.Values .Where(f => f.TenantId == query.TenantId) .Where(f => query.ComponentPurls == null || query.ComponentPurls.Contains(f.ComponentPurl)) .Where(f => query.AdvisoryIds == null || query.AdvisoryIds.Contains(f.AdvisoryId)) .Where(f => query.States == null || query.States.Contains(f.State)) .Where(f => !query.MinConfidence.HasValue || f.Confidence >= query.MinConfidence.Value) .Where(f => query.IncludeExpired || !f.ExpiresAt.HasValue || f.ExpiresAt > now) .OrderByDescending(f => f.ComputedAt) .Skip(query.Skip) .Take(query.Limit) .ToList(); return Task.FromResult>(results); } public Task SaveAsync(ReachabilityFact fact, CancellationToken cancellationToken = default) { var key = new ReachabilityFactKey(fact.TenantId, fact.ComponentPurl, fact.AdvisoryId); _facts[key] = fact; return Task.CompletedTask; } public Task SaveBatchAsync(IReadOnlyList facts, CancellationToken cancellationToken = default) { foreach (var fact in facts) { var key = new ReachabilityFactKey(fact.TenantId, fact.ComponentPurl, fact.AdvisoryId); _facts[key] = fact; } return Task.CompletedTask; } public Task DeleteAsync( string tenantId, string componentPurl, string advisoryId, CancellationToken cancellationToken = default) { var key = new ReachabilityFactKey(tenantId, componentPurl, advisoryId); _facts.TryRemove(key, out _); return Task.CompletedTask; } public Task CountAsync(string tenantId, CancellationToken cancellationToken = default) { var count = _facts.Values.Count(f => f.TenantId == tenantId); return Task.FromResult((long)count); } } /// /// Index definitions for MongoDB reachability_facts collection. /// public static class ReachabilityFactsIndexes { /// /// Primary compound index for efficient lookups. /// public const string PrimaryIndex = "tenant_component_advisory"; /// /// Index for querying by tenant and state. /// public const string TenantStateIndex = "tenant_state_computed"; /// /// Index for TTL expiration. /// public const string ExpirationIndex = "expires_at_ttl"; /// /// Gets the index definitions for creating MongoDB indexes. /// public static IReadOnlyList GetIndexDefinitions() { return new[] { new ReachabilityIndexDefinition( PrimaryIndex, new[] { "tenant_id", "component_purl", "advisory_id" }, Unique: true), new ReachabilityIndexDefinition( TenantStateIndex, new[] { "tenant_id", "state", "computed_at" }, Unique: false), new ReachabilityIndexDefinition( ExpirationIndex, new[] { "expires_at" }, Unique: false, ExpireAfterSeconds: 0), }; } } /// /// Index definition for MongoDB collection. /// public sealed record ReachabilityIndexDefinition( string Name, IReadOnlyList Fields, bool Unique, int? ExpireAfterSeconds = null);