using StellaOps.Graph.Api.Contracts; namespace StellaOps.Graph.Api.Services; public sealed class InMemoryGraphRepository { private static readonly string[] CompatibilityKinds = ["artifact", "component", "vuln"]; private static readonly DateTimeOffset FixedSnapshotAt = new(2026, 4, 4, 0, 0, 0, TimeSpan.Zero); private readonly List _nodes; private readonly List _edges; private readonly Dictionary Nodes, List Edges)> _snapshots; public InMemoryGraphRepository(IEnumerable? seed = null, IEnumerable? edges = null) { _nodes = seed?.ToList() ?? new List { new() { Id = "gn:acme:component:example", Kind = "component", Tenant = "acme", Attributes = new() { ["purl"] = "pkg:npm/example@1.0.0", ["ecosystem"] = "npm", ["displayName"] = "example", ["version"] = "1.0.0" } }, new() { Id = "gn:acme:component:widget", Kind = "component", Tenant = "acme", Attributes = new() { ["purl"] = "pkg:npm/widget@2.0.0", ["ecosystem"] = "npm", ["displayName"] = "widget", ["version"] = "2.0.0" } }, new() { Id = "gn:acme:artifact:sha256:abc", Kind = "artifact", Tenant = "acme", Attributes = new() { ["digest"] = "sha256:abc", ["ecosystem"] = "container", ["displayName"] = "auth-service", ["version"] = "2026.04.04" } }, new() { Id = "gn:acme:sbom:sha256:sbom-a", Kind = "sbom", Tenant = "acme", Attributes = new() { ["sbom_digest"] = "sha256:sbom-a", ["artifact_digest"] = "sha256:abc", ["format"] = "cyclonedx" } }, new() { Id = "gn:acme:sbom:sha256:sbom-b", Kind = "sbom", Tenant = "acme", Attributes = new() { ["sbom_digest"] = "sha256:sbom-b", ["artifact_digest"] = "sha256:abc", ["format"] = "spdx" } }, new() { Id = "gn:acme:component:gamma", Kind = "component", Tenant = "acme", Attributes = new() { ["purl"] = "pkg:nuget/Gamma@3.1.4", ["ecosystem"] = "nuget", ["displayName"] = "gamma", ["version"] = "3.1.4" } }, new() { Id = "gn:acme:vuln:CVE-2026-1234", Kind = "vuln", Tenant = "acme", Attributes = new() { ["displayName"] = "CVE-2026-1234", ["severity"] = "critical", ["cveId"] = "CVE-2026-1234" } }, new() { Id = "gn:acme:vuln:CVE-2026-5678", Kind = "vuln", Tenant = "acme", Attributes = new() { ["displayName"] = "CVE-2026-5678", ["severity"] = "high", ["cveId"] = "CVE-2026-5678" } }, new() { Id = "gn:bravo:component:widget", Kind = "component", Tenant = "bravo", Attributes = new() { ["purl"] = "pkg:npm/widget@2.0.0", ["ecosystem"] = "npm", ["displayName"] = "widget", ["version"] = "2.0.0" } }, new() { Id = "gn:bravo:artifact:sha256:def", Kind = "artifact", Tenant = "bravo", Attributes = new() { ["digest"] = "sha256:def", ["ecosystem"] = "container", ["displayName"] = "transform-service", ["version"] = "2026.04.04" } }, new() { Id = "gn:bravo:vuln:CVE-2026-9001", Kind = "vuln", Tenant = "bravo", Attributes = new() { ["displayName"] = "CVE-2026-9001", ["severity"] = "medium", ["cveId"] = "CVE-2026-9001" } }, }; _edges = edges?.ToList() ?? new List { new() { Id = "ge:acme:artifact->component", Kind = "builds", Tenant = "acme", Source = "gn:acme:artifact:sha256:abc", Target = "gn:acme:component:example", Attributes = new() { ["reason"] = "sbom" } }, new() { Id = "ge:acme:component->component", Kind = "depends_on", Tenant = "acme", Source = "gn:acme:component:example", Target = "gn:acme:component:widget", Attributes = new() { ["scope"] = "runtime" } }, new() { Id = "ge:acme:component->component:gamma", Kind = "depends_on", Tenant = "acme", Source = "gn:acme:component:widget", Target = "gn:acme:component:gamma", Attributes = new() { ["scope"] = "runtime" } }, new() { Id = "ge:acme:component->vuln:widget", Kind = "affects", Tenant = "acme", Source = "gn:acme:component:widget", Target = "gn:acme:vuln:CVE-2026-1234", Attributes = new() { ["scope"] = "runtime" } }, new() { Id = "ge:acme:component->vuln:gamma", Kind = "affects", Tenant = "acme", Source = "gn:acme:component:gamma", Target = "gn:acme:vuln:CVE-2026-5678", Attributes = new() { ["scope"] = "runtime" } }, new() { Id = "ge:acme:sbom->artifact", Kind = "SBOM_VERSION_OF", Tenant = "acme", Source = "gn:acme:sbom:sha256:sbom-b", Target = "gn:acme:artifact:sha256:abc", Attributes = new() { ["relationship"] = "version_of" } }, new() { Id = "ge:acme:sbom->sbom", Kind = "SBOM_LINEAGE_PARENT", Tenant = "acme", Source = "gn:acme:sbom:sha256:sbom-b", Target = "gn:acme:sbom:sha256:sbom-a", Attributes = new() { ["relationship"] = "parent" } }, new() { Id = "ge:bravo:artifact->component", Kind = "builds", Tenant = "bravo", Source = "gn:bravo:artifact:sha256:def", Target = "gn:bravo:component:widget", Attributes = new() { ["reason"] = "sbom" } }, new() { Id = "ge:bravo:component->vuln:widget", Kind = "affects", Tenant = "bravo", Source = "gn:bravo:component:widget", Target = "gn:bravo:vuln:CVE-2026-9001", Attributes = new() { ["scope"] = "runtime" } }, }; // Drop edges whose endpoints aren't present in the current node set to avoid invalid graph seeds in tests. var nodeIds = _nodes.Select(n => n.Id).ToHashSet(StringComparer.Ordinal); _edges = _edges.Where(e => nodeIds.Contains(e.Source) && nodeIds.Contains(e.Target)).ToList(); _snapshots = SeedSnapshots(); } public IEnumerable Query(string tenant, GraphSearchRequest request) { var queryable = _nodes .Where(n => n.Tenant.Equals(tenant, StringComparison.Ordinal)) .Where(n => request.Kinds is null || request.Kinds.Length == 0 || request.Kinds.Contains(n.Kind, StringComparer.OrdinalIgnoreCase)); if (!string.IsNullOrWhiteSpace(request.Query)) { queryable = queryable.Where(n => MatchesQuery(n, request.Query!)); } if (request.Filters is not null) { queryable = queryable.Where(n => FiltersMatch(n, request.Filters!)); } return queryable; } public (IReadOnlyList Nodes, IReadOnlyList Edges) QueryGraph(string tenant, GraphQueryRequest request) { var nodes = Query(tenant, new GraphSearchRequest { Kinds = request.Kinds, Query = request.Query, Filters = request.Filters, Limit = request.Limit, Cursor = request.Cursor }).ToList(); var nodeIds = nodes.Select(n => n.Id).ToHashSet(StringComparer.Ordinal); var edges = request.IncludeEdges ? _edges.Where(e => e.Tenant.Equals(tenant, StringComparison.Ordinal) && nodeIds.Contains(e.Source) && nodeIds.Contains(e.Target)) .OrderBy(e => e.Id, StringComparer.Ordinal) .ToList() : new List(); return (nodes, edges); } public (IReadOnlyList Nodes, IReadOnlyList Edges) GetLineage(string tenant, GraphLineageRequest request) { ArgumentNullException.ThrowIfNull(request); var maxDepth = request.MaxDepth ?? 3; if (maxDepth < 1) { maxDepth = 1; } var allowedKinds = BuildLineageKindFilter(request.RelationshipKinds); var tenantNodes = _nodes .Where(n => n.Tenant.Equals(tenant, StringComparison.Ordinal)) .ToList(); if (tenantNodes.Count == 0) { return (Array.Empty(), Array.Empty()); } var nodeById = tenantNodes.ToDictionary(n => n.Id, StringComparer.Ordinal); var seedIds = new HashSet(StringComparer.Ordinal); if (!string.IsNullOrWhiteSpace(request.ArtifactDigest)) { var digest = request.ArtifactDigest.Trim(); foreach (var node in tenantNodes.Where(n => HasAttribute(n, "artifact_digest", digest))) { seedIds.Add(node.Id); } } if (!string.IsNullOrWhiteSpace(request.SbomDigest)) { var digest = request.SbomDigest.Trim(); foreach (var node in tenantNodes.Where(n => HasAttribute(n, "sbom_digest", digest))) { seedIds.Add(node.Id); } } if (seedIds.Count == 0) { return (Array.Empty(), Array.Empty()); } var tenantEdges = _edges .Where(e => e.Tenant.Equals(tenant, StringComparison.Ordinal)) .Where(e => IsLineageEdgeAllowed(e, allowedKinds)) .ToList(); var adjacency = new Dictionary>(StringComparer.Ordinal); foreach (var edge in tenantEdges) { AddAdjacency(adjacency, edge.Source, edge); AddAdjacency(adjacency, edge.Target, edge); } var visitedNodes = new HashSet(seedIds, StringComparer.Ordinal); var visitedEdges = new HashSet(StringComparer.Ordinal); var frontier = new HashSet(seedIds, StringComparer.Ordinal); for (var depth = 0; depth < maxDepth && frontier.Count > 0; depth++) { var next = new HashSet(StringComparer.Ordinal); foreach (var nodeId in frontier) { if (!adjacency.TryGetValue(nodeId, out var edges)) { continue; } foreach (var edge in edges) { if (visitedEdges.Add(edge.Id)) { var other = string.Equals(edge.Source, nodeId, StringComparison.Ordinal) ? edge.Target : edge.Source; if (visitedNodes.Add(other)) { next.Add(other); } } } } frontier = next; } var resultNodes = new List(); foreach (var nodeId in visitedNodes.OrderBy(id => id, StringComparer.Ordinal)) { if (nodeById.TryGetValue(nodeId, out var node)) { resultNodes.Add(node); } } var resultEdges = tenantEdges .Where(edge => visitedEdges.Contains(edge.Id)) .Where(edge => visitedNodes.Contains(edge.Source) && visitedNodes.Contains(edge.Target)) .OrderBy(edge => edge.Id, StringComparer.Ordinal) .ToList(); return (resultNodes, resultEdges); } public (IReadOnlyList Nodes, IReadOnlyList Edges)? GetSnapshot(string tenant, string snapshotId) { if (_snapshots.TryGetValue($"{tenant}:{snapshotId}", out var snap)) { return (snap.Nodes, snap.Edges); } return null; } public IReadOnlyList GetCompatibilityNodes(string tenant) => _nodes .Where(node => node.Tenant.Equals(tenant, StringComparison.Ordinal)) .Where(node => CompatibilityKinds.Contains(node.Kind, StringComparer.OrdinalIgnoreCase)) .OrderBy(node => node.Id, StringComparer.Ordinal) .ToList(); public IReadOnlyList GetCompatibilityEdges(string tenant) { var nodeIds = GetCompatibilityNodes(tenant).Select(node => node.Id).ToHashSet(StringComparer.Ordinal); return _edges .Where(edge => edge.Tenant.Equals(tenant, StringComparison.Ordinal)) .Where(edge => nodeIds.Contains(edge.Source) && nodeIds.Contains(edge.Target)) .OrderBy(edge => edge.Id, StringComparer.Ordinal) .ToList(); } public static string BuildGraphId(string tenant) => $"graph::{tenant}::main"; public bool GraphExists(string tenant, string graphId) => string.Equals(BuildGraphId(tenant), graphId, StringComparison.OrdinalIgnoreCase); public NodeTile? GetNode(string tenant, string nodeId) => _nodes.FirstOrDefault(node => node.Tenant.Equals(tenant, StringComparison.Ordinal) && node.Id.Equals(nodeId, StringComparison.Ordinal)); public (IReadOnlyList Incoming, IReadOnlyList Outgoing) GetAdjacency(string tenant, string nodeId) { var edges = GetCompatibilityEdges(tenant); var incoming = edges.Where(edge => edge.Target.Equals(nodeId, StringComparison.Ordinal)).ToList(); var outgoing = edges.Where(edge => edge.Source.Equals(nodeId, StringComparison.Ordinal)).ToList(); return (incoming, outgoing); } public (IReadOnlyList Nodes, IReadOnlyList Edges)? FindCompatibilityPath(string tenant, string sourceId, string targetId, int maxDepth) { var nodes = GetCompatibilityNodes(tenant).ToDictionary(node => node.Id, StringComparer.Ordinal); if (!nodes.ContainsKey(sourceId) || !nodes.ContainsKey(targetId)) { return null; } var edges = GetCompatibilityEdges(tenant); var adjacency = new Dictionary>(StringComparer.Ordinal); foreach (var edge in edges) { if (!adjacency.TryGetValue(edge.Source, out var list)) { list = new List(); adjacency[edge.Source] = list; } list.Add(edge); } var queue = new Queue<(string NodeId, List PathEdges)>(); var visited = new HashSet(StringComparer.Ordinal) { sourceId }; queue.Enqueue((sourceId, new List())); while (queue.Count > 0) { var (nodeId, pathEdges) = queue.Dequeue(); if (nodeId.Equals(targetId, StringComparison.Ordinal)) { var pathNodes = BuildNodeListFromEdges(nodes, sourceId, pathEdges); return (pathNodes, pathEdges); } if (pathEdges.Count >= maxDepth || !adjacency.TryGetValue(nodeId, out var outgoing)) { continue; } foreach (var edge in outgoing.OrderBy(edge => edge.Id, StringComparer.Ordinal)) { if (!visited.Add(edge.Target)) { continue; } var nextEdges = new List(pathEdges.Count + 1); nextEdges.AddRange(pathEdges); nextEdges.Add(edge); queue.Enqueue((edge.Target, nextEdges)); } } return null; } public DateTimeOffset GetSnapshotTimestamp() => FixedSnapshotAt; private Dictionary Nodes, List Edges)> SeedSnapshots() { var dict = new Dictionary, List)>(StringComparer.Ordinal); dict["acme:snapA"] = (new List(_nodes), new List(_edges)); var updatedNodes = new List(_nodes.Select(n => n with { Attributes = new Dictionary(n.Attributes) })); var widget = updatedNodes.FirstOrDefault(n => n.Id == "gn:acme:component:widget"); if (widget is null) { // Custom seeds may not include the default widget node; skip optional snapshot wiring in that case. return dict; } widget.Attributes["purl"] = "pkg:npm/widget@2.1.0"; widget.Attributes["version"] = "2.1.0"; updatedNodes.Add(new NodeTile { Id = "gn:acme:component:newlib", Kind = "component", Tenant = "acme", Attributes = new() { ["purl"] = "pkg:npm/newlib@1.0.0", ["ecosystem"] = "npm", ["displayName"] = "newlib", ["version"] = "1.0.0" } }); var updatedEdges = new List(_edges) { new() { Id = "ge:acme:component->component:new", Kind = "depends_on", Tenant = "acme", Source = widget.Id, Target = "gn:acme:component:newlib", Attributes = new() { ["scope"] = "runtime" } } }; dict["acme:snapB"] = (updatedNodes, updatedEdges); return dict; } private static bool HasAttribute(NodeTile node, string key, string expected) { if (!node.Attributes.TryGetValue(key, out var value) || value is null) { return false; } return string.Equals(value.ToString(), expected, StringComparison.OrdinalIgnoreCase); } private static HashSet BuildLineageKindFilter(string[]? relationshipKinds) { if (relationshipKinds is null || relationshipKinds.Length == 0) { return new HashSet(StringComparer.OrdinalIgnoreCase); } var set = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var value in relationshipKinds) { if (string.IsNullOrWhiteSpace(value)) { continue; } set.Add(value.Trim()); } return set; } private static bool IsLineageEdgeAllowed(EdgeTile edge, HashSet allowedKinds) { if (allowedKinds.Count == 0) { return edge.Kind.StartsWith("SBOM_LINEAGE_", StringComparison.OrdinalIgnoreCase) || string.Equals(edge.Kind, "SBOM_VERSION_OF", StringComparison.OrdinalIgnoreCase); } if (allowedKinds.Contains(edge.Kind)) { return true; } if (edge.Attributes.TryGetValue("relationship", out var relationship) && relationship is not null) { return allowedKinds.Contains(relationship.ToString() ?? string.Empty); } return false; } private static void AddAdjacency(Dictionary> adjacency, string nodeId, EdgeTile edge) { if (!adjacency.TryGetValue(nodeId, out var list)) { list = new List(); adjacency[nodeId] = list; } list.Add(edge); } private static bool MatchesQuery(NodeTile node, string query) { var q = query.ToLowerInvariant(); return node.Id.ToLowerInvariant().Contains(q) || node.Attributes.Values.OfType().Any(v => v.Contains(q, StringComparison.OrdinalIgnoreCase)); } private static bool FiltersMatch(NodeTile node, IReadOnlyDictionary filters) { foreach (var kvp in filters) { if (!node.Attributes.TryGetValue(kvp.Key, out var value)) { return false; } if (kvp.Value is null) { continue; } if (!kvp.Value.ToString()!.Equals(value?.ToString(), StringComparison.OrdinalIgnoreCase)) { return false; } } return true; } private static IReadOnlyList BuildNodeListFromEdges( IDictionary nodes, string sourceId, IReadOnlyList edges) { var list = new List(); if (nodes.TryGetValue(sourceId, out var firstNode)) { list.Add(firstNode); } foreach (var edge in edges) { if (nodes.TryGetValue(edge.Target, out var node)) { list.Add(node); } } return list; } } internal static class CursorCodec { public static string Encode(int offset) => Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(offset.ToString())); public static int Decode(string? token) { if (string.IsNullOrWhiteSpace(token)) return 0; try { var text = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(token)); return int.TryParse(text, out var value) ? value : 0; } catch { return 0; } } }