116 lines
4.6 KiB
C#
116 lines
4.6 KiB
C#
using Microsoft.Extensions.Caching.Memory;
|
|
using StellaOps.Graph.Api.Contracts;
|
|
|
|
namespace StellaOps.Graph.Api.Services;
|
|
|
|
public sealed class InMemoryOverlayService : IOverlayService
|
|
{
|
|
private readonly IMemoryCache _cache;
|
|
private static readonly DateTimeOffset FixedTimestamp = new(2025, 11, 23, 0, 0, 0, TimeSpan.Zero);
|
|
private readonly IGraphMetrics _metrics;
|
|
|
|
public InMemoryOverlayService(IMemoryCache cache, IGraphMetrics metrics)
|
|
{
|
|
_cache = cache;
|
|
_metrics = metrics;
|
|
}
|
|
|
|
public Task<IDictionary<string, Dictionary<string, OverlayPayload>>> GetOverlaysAsync(string tenant, IEnumerable<string> nodeIds, bool sampleExplain, CancellationToken ct = default)
|
|
{
|
|
var result = new Dictionary<string, Dictionary<string, OverlayPayload>>(StringComparer.Ordinal);
|
|
var explainEmitted = false;
|
|
|
|
foreach (var nodeId in nodeIds)
|
|
{
|
|
var cacheKey = $"overlay:{tenant}:{nodeId}";
|
|
if (!_cache.TryGetValue(cacheKey, out Dictionary<string, OverlayPayload>? cachedBase))
|
|
{
|
|
_metrics.OverlayCacheMiss.Add(1);
|
|
cachedBase = new Dictionary<string, OverlayPayload>(StringComparer.Ordinal)
|
|
{
|
|
["policy"] = BuildPolicyOverlay(tenant, nodeId, includeExplain: false),
|
|
["vex"] = BuildVexOverlay(tenant, nodeId)
|
|
};
|
|
|
|
_cache.Set(cacheKey, cachedBase, new MemoryCacheEntryOptions
|
|
{
|
|
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
|
|
});
|
|
}
|
|
|
|
else
|
|
{
|
|
_metrics.OverlayCacheHit.Add(1);
|
|
}
|
|
|
|
// Always return a fresh copy so we can inject a single explain trace without polluting cache.
|
|
var overlays = new Dictionary<string, OverlayPayload>(cachedBase, StringComparer.Ordinal);
|
|
|
|
if (sampleExplain && !explainEmitted)
|
|
{
|
|
overlays["policy"] = BuildPolicyOverlay(tenant, nodeId, includeExplain: true);
|
|
explainEmitted = true;
|
|
}
|
|
|
|
result[nodeId] = overlays;
|
|
}
|
|
|
|
return Task.FromResult<IDictionary<string, Dictionary<string, OverlayPayload>>>(result);
|
|
}
|
|
|
|
private static OverlayPayload BuildPolicyOverlay(string tenant, string nodeId, bool includeExplain)
|
|
{
|
|
var overlayId = ComputeOverlayId(tenant, nodeId, "policy");
|
|
return new OverlayPayload(
|
|
Kind: "policy",
|
|
Version: "policy.overlay.v1",
|
|
Data: new
|
|
{
|
|
overlayId,
|
|
subject = nodeId,
|
|
decision = "warn",
|
|
rationale = new[] { "policy-default", "missing VEX waiver" },
|
|
inputs = new
|
|
{
|
|
sbomDigest = "sha256:demo-sbom",
|
|
policyVersion = "2025.11.23",
|
|
advisoriesDigest = "sha256:demo-advisories"
|
|
},
|
|
policyVersion = "2025.11.23",
|
|
createdAt = FixedTimestamp,
|
|
explainTrace = includeExplain
|
|
? new[]
|
|
{
|
|
"matched rule POLICY-ENGINE-30-001",
|
|
$"node {nodeId} lacks VEX waiver"
|
|
}
|
|
: null
|
|
});
|
|
}
|
|
|
|
private static OverlayPayload BuildVexOverlay(string tenant, string nodeId)
|
|
{
|
|
var overlayId = ComputeOverlayId(tenant, nodeId, "vex");
|
|
return new OverlayPayload(
|
|
Kind: "vex",
|
|
Version: "openvex.v1",
|
|
Data: new
|
|
{
|
|
overlayId,
|
|
subject = nodeId,
|
|
status = "not_affected",
|
|
justification = "component_not_present",
|
|
issued = FixedTimestamp,
|
|
impacts = Array.Empty<string>()
|
|
});
|
|
}
|
|
|
|
private static string ComputeOverlayId(string tenant, string nodeId, string overlayKind)
|
|
{
|
|
using var sha = System.Security.Cryptography.SHA256.Create();
|
|
var bytes = System.Text.Encoding.UTF8.GetBytes($"{tenant}|{nodeId}|{overlayKind}");
|
|
var hash = sha.ComputeHash(bytes);
|
|
return Convert.ToHexString(hash).ToLowerInvariant();
|
|
}
|
|
}
|