Files
git.stella-ops.org/src/Graph/StellaOps.Graph.Api/Services/InMemoryOverlayService.cs
StellaOps Bot 1c782897f7
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
up
2025-11-26 07:47:08 +02:00

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