214 lines
6.8 KiB
C#
214 lines
6.8 KiB
C#
using System.Collections.Concurrent;
|
|
|
|
namespace StellaOps.Policy.Engine.ReachabilityFacts;
|
|
|
|
/// <summary>
|
|
/// Store interface for reachability facts persistence.
|
|
/// </summary>
|
|
public interface IReachabilityFactsStore
|
|
{
|
|
/// <summary>
|
|
/// Gets a single reachability fact by key.
|
|
/// </summary>
|
|
Task<ReachabilityFact?> GetAsync(
|
|
string tenantId,
|
|
string componentPurl,
|
|
string advisoryId,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Gets multiple reachability facts by keys.
|
|
/// </summary>
|
|
Task<IReadOnlyDictionary<ReachabilityFactKey, ReachabilityFact>> GetBatchAsync(
|
|
IReadOnlyList<ReachabilityFactKey> keys,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Queries reachability facts with filtering.
|
|
/// </summary>
|
|
Task<IReadOnlyList<ReachabilityFact>> QueryAsync(
|
|
ReachabilityFactsQuery query,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Saves or updates a reachability fact.
|
|
/// </summary>
|
|
Task SaveAsync(
|
|
ReachabilityFact fact,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Saves multiple reachability facts.
|
|
/// </summary>
|
|
Task SaveBatchAsync(
|
|
IReadOnlyList<ReachabilityFact> facts,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Deletes a reachability fact.
|
|
/// </summary>
|
|
Task DeleteAsync(
|
|
string tenantId,
|
|
string componentPurl,
|
|
string advisoryId,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Gets the count of facts for a tenant.
|
|
/// </summary>
|
|
Task<long> CountAsync(
|
|
string tenantId,
|
|
CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
/// <summary>
|
|
/// In-memory implementation of the reachability facts store for development and testing.
|
|
/// </summary>
|
|
public sealed class InMemoryReachabilityFactsStore : IReachabilityFactsStore
|
|
{
|
|
private readonly ConcurrentDictionary<ReachabilityFactKey, ReachabilityFact> _facts = new();
|
|
private readonly TimeProvider _timeProvider;
|
|
|
|
public InMemoryReachabilityFactsStore(TimeProvider? timeProvider = null)
|
|
{
|
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
|
}
|
|
|
|
public Task<ReachabilityFact?> 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<IReadOnlyDictionary<ReachabilityFactKey, ReachabilityFact>> GetBatchAsync(
|
|
IReadOnlyList<ReachabilityFactKey> keys,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var result = new Dictionary<ReachabilityFactKey, ReachabilityFact>();
|
|
|
|
foreach (var key in keys)
|
|
{
|
|
if (_facts.TryGetValue(key, out var fact))
|
|
{
|
|
result[key] = fact;
|
|
}
|
|
}
|
|
|
|
return Task.FromResult<IReadOnlyDictionary<ReachabilityFactKey, ReachabilityFact>>(result);
|
|
}
|
|
|
|
public Task<IReadOnlyList<ReachabilityFact>> 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<IReadOnlyList<ReachabilityFact>>(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<ReachabilityFact> 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<long> CountAsync(string tenantId, CancellationToken cancellationToken = default)
|
|
{
|
|
var count = _facts.Values.Count(f => f.TenantId == tenantId);
|
|
return Task.FromResult((long)count);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Index definitions for MongoDB reachability_facts collection.
|
|
/// </summary>
|
|
public static class ReachabilityFactsIndexes
|
|
{
|
|
/// <summary>
|
|
/// Primary compound index for efficient lookups.
|
|
/// </summary>
|
|
public const string PrimaryIndex = "tenant_component_advisory";
|
|
|
|
/// <summary>
|
|
/// Index for querying by tenant and state.
|
|
/// </summary>
|
|
public const string TenantStateIndex = "tenant_state_computed";
|
|
|
|
/// <summary>
|
|
/// Index for TTL expiration.
|
|
/// </summary>
|
|
public const string ExpirationIndex = "expires_at_ttl";
|
|
|
|
/// <summary>
|
|
/// Gets the index definitions for creating MongoDB indexes.
|
|
/// </summary>
|
|
public static IReadOnlyList<ReachabilityIndexDefinition> 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),
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Index definition for MongoDB collection.
|
|
/// </summary>
|
|
public sealed record ReachabilityIndexDefinition(
|
|
string Name,
|
|
IReadOnlyList<string> Fields,
|
|
bool Unique,
|
|
int? ExpireAfterSeconds = null);
|