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