using System.Collections.Concurrent; using System.Globalization; using StellaOps.SbomService.Models; using StellaOps.SbomService.Repositories; namespace StellaOps.SbomService.Services; internal sealed class InMemorySbomQueryService : ISbomQueryService { private readonly IReadOnlyList _paths; private readonly IReadOnlyList _timelines; private readonly IReadOnlyList _catalog; private readonly IComponentLookupRepository _componentLookupRepository; private readonly IProjectionRepository _projectionRepository; private readonly ConcurrentDictionary _cache = new(); public InMemorySbomQueryService(IComponentLookupRepository componentLookupRepository, IProjectionRepository projectionRepository) { _componentLookupRepository = componentLookupRepository; _projectionRepository = projectionRepository; // Deterministic seed data for early contract testing; replace with Mongo-backed implementation later. _paths = SeedPaths(); _timelines = SeedTimelines(); _catalog = SeedCatalog(); } public Task> GetPathsAsync(SbomPathQuery query, CancellationToken cancellationToken) { var cacheKey = $"paths|{query.Purl}|{query.Artifact}|{query.Scope}|{query.Environment}|{query.Offset}|{query.Limit}"; if (_cache.TryGetValue(cacheKey, out var cached) && cached is SbomPathResult cachedResult) { return Task.FromResult(new QueryResult(cachedResult, true)); } var filtered = _paths .Where(p => p.Purl.Equals(query.Purl, StringComparison.OrdinalIgnoreCase)) .Where(p => query.Artifact is null || p.Artifact.Equals(query.Artifact, StringComparison.OrdinalIgnoreCase)) .Where(p => query.Scope is null || string.Equals(p.Scope, query.Scope, StringComparison.OrdinalIgnoreCase)) .Where(p => query.Environment is null || string.Equals(p.Environment, query.Environment, StringComparison.OrdinalIgnoreCase)) .OrderBy(p => p.Artifact) .ThenBy(p => p.Environment) .ThenBy(p => p.Scope) .ThenBy(p => string.Join("->", p.Nodes.Select(n => n.Name))) .ToList(); var page = filtered .Skip(query.Offset) .Take(query.Limit) .Select(r => new SbomPath(r.Nodes, r.RuntimeFlag, r.BlastRadius, r.NearestSafeVersion)) .ToList(); string? nextCursor = query.Offset + query.Limit < filtered.Count ? (query.Offset + query.Limit).ToString(CultureInfo.InvariantCulture) : null; var result = new SbomPathResult( Purl: query.Purl, Artifact: query.Artifact, Scope: query.Scope, Environment: query.Environment, Paths: page, NextCursor: nextCursor); _cache[cacheKey] = result; return Task.FromResult(new QueryResult(result, false)); } public Task> GetTimelineAsync(SbomTimelineQuery query, CancellationToken cancellationToken) { var cacheKey = $"timeline|{query.Artifact}|{query.Offset}|{query.Limit}"; if (_cache.TryGetValue(cacheKey, out var cached) && cached is SbomTimelineResult cachedTimeline) { return Task.FromResult(new QueryResult(cachedTimeline, true)); } var filtered = _timelines .Where(t => t.Artifact.Equals(query.Artifact, StringComparison.OrdinalIgnoreCase)) .OrderByDescending(t => t.CreatedAt) .ThenByDescending(t => t.Version) .ToList(); var page = filtered .Skip(query.Offset) .Take(query.Limit) .Select(t => new SbomVersion(t.Version, t.Digest, t.CreatedAt, t.SourceBundleHash, t.Provenance)) .ToList(); string? nextCursor = query.Offset + query.Limit < filtered.Count ? (query.Offset + query.Limit).ToString(CultureInfo.InvariantCulture) : null; var result = new SbomTimelineResult(query.Artifact, page, nextCursor); _cache[cacheKey] = result; return Task.FromResult(new QueryResult(result, false)); } public Task> GetConsoleCatalogAsync(SbomCatalogQuery query, CancellationToken cancellationToken) { var cacheKey = $"catalog|{query.Artifact}|{query.License}|{query.Scope}|{query.AssetTag}|{query.Offset}|{query.Limit}"; if (_cache.TryGetValue(cacheKey, out var cached) && cached is SbomCatalogResult cachedCatalog) { return Task.FromResult(new QueryResult(cachedCatalog, true)); } var filtered = _catalog .Where(c => query.Artifact is null || c.Artifact.Contains(query.Artifact, StringComparison.OrdinalIgnoreCase)) .Where(c => query.License is null || string.Equals(c.License, query.License, StringComparison.OrdinalIgnoreCase)) .Where(c => query.Scope is null || string.Equals(c.Scope, query.Scope, StringComparison.OrdinalIgnoreCase)) .Where(c => query.AssetTag is null || c.AssetTags.ContainsKey(query.AssetTag)) .OrderByDescending(c => c.CreatedAt) .ThenBy(c => c.Artifact) .ToList(); var page = filtered .Skip(query.Offset) .Take(query.Limit) .Select(c => new SbomCatalogItem( c.Artifact, c.SbomVersion, c.Digest, c.License, c.Scope, c.AssetTags, c.CreatedAt, c.ProjectionHash, c.EvaluationMetadata)) .ToList(); string? nextCursor = query.Offset + query.Limit < filtered.Count ? (query.Offset + query.Limit).ToString(CultureInfo.InvariantCulture) : null; var result = new SbomCatalogResult(page, nextCursor); _cache[cacheKey] = result; return Task.FromResult(new QueryResult(result, false)); } public async Task> GetComponentLookupAsync(ComponentLookupQuery query, CancellationToken cancellationToken) { var cacheKey = $"component|{query.Purl}|{query.Artifact}|{query.Offset}|{query.Limit}"; if (_cache.TryGetValue(cacheKey, out var cached) && cached is ComponentLookupResult cachedResult) { return new QueryResult(cachedResult, true); } var page = await _componentLookupRepository.QueryAsync(query, cancellationToken); string? nextCursor = query.Offset + query.Limit < page.Count ? (query.Offset + query.Limit).ToString(CultureInfo.InvariantCulture) : null; var neighbors = page .Select(c => new ComponentNeighbor(c.NeighborPurl, c.Relationship, c.License, c.Scope, c.RuntimeFlag)) .ToList(); var result = new ComponentLookupResult(query.Purl, query.Artifact, neighbors, nextCursor, CacheHint: "seeded"); _cache[cacheKey] = result; return new QueryResult(result, false); } public async Task GetProjectionAsync(string snapshotId, string tenantId, CancellationToken cancellationToken) { var cacheKey = $"projection|{snapshotId}|{tenantId}"; if (_cache.TryGetValue(cacheKey, out var cached) && cached is SbomProjectionResult cachedProjection) { return cachedProjection; } var projection = await _projectionRepository.GetAsync(snapshotId, tenantId, cancellationToken); if (projection is not null) { _cache[cacheKey] = projection; } return projection; } private static IReadOnlyList SeedPaths() { return new List { new( Artifact: "ghcr.io/stellaops/sample-api@sha256:111", Purl: "pkg:npm/lodash@4.17.21", Scope: "runtime", Environment: "prod", RuntimeFlag: true, BlastRadius: "medium", NearestSafeVersion: "pkg:npm/lodash@4.17.22", Nodes: new[] { new SbomPathNode("sample-api", "artifact"), new SbomPathNode("express", "npm"), new SbomPathNode("lodash", "npm") }), new( Artifact: "ghcr.io/stellaops/sample-api@sha256:111", Purl: "pkg:npm/lodash@4.17.21", Scope: "build", Environment: "prod", RuntimeFlag: false, BlastRadius: "low", NearestSafeVersion: "pkg:npm/lodash@4.17.22", Nodes: new[] { new SbomPathNode("sample-api", "artifact"), new SbomPathNode("rollup", "npm"), new SbomPathNode("lodash", "npm") }), new( Artifact: "ghcr.io/stellaops/sample-api@sha256:222", Purl: "pkg:nuget/Newtonsoft.Json@13.0.2", Scope: "runtime", Environment: "staging", RuntimeFlag: true, BlastRadius: "high", NearestSafeVersion: "pkg:nuget/Newtonsoft.Json@13.0.3", Nodes: new[] { new SbomPathNode("sample-worker", "artifact"), new SbomPathNode("StellaOps.Core", "nuget"), new SbomPathNode("Newtonsoft.Json", "nuget") }) }; } private static IReadOnlyList SeedTimelines() { return new List { new( Artifact: "ghcr.io/stellaops/sample-api", Version: "2025.11.15.1", Digest: "sha256:111", SourceBundleHash: "sha256:bundle111", CreatedAt: new DateTimeOffset(2025, 11, 15, 12, 0, 0, TimeSpan.Zero), Provenance: "scanner:surface_bundle_mock_v1.tgz"), new( Artifact: "ghcr.io/stellaops/sample-api", Version: "2025.11.16.1", Digest: "sha256:112", SourceBundleHash: "sha256:bundle112", CreatedAt: new DateTimeOffset(2025, 11, 16, 12, 0, 0, TimeSpan.Zero), Provenance: "scanner:surface_bundle_mock_v1.tgz"), new( Artifact: "ghcr.io/stellaops/sample-worker", Version: "2025.11.12.0", Digest: "sha256:222", SourceBundleHash: "sha256:bundle222", CreatedAt: new DateTimeOffset(2025, 11, 12, 8, 0, 0, TimeSpan.Zero), Provenance: "upload:spdx:worker"), }; } private static IReadOnlyList SeedCatalog() { return new List { new( Artifact: "ghcr.io/stellaops/sample-api", SbomVersion: "2025.11.16.1", Digest: "sha256:112", License: "MIT", Scope: "runtime", AssetTags: new Dictionary { ["owner"] = "payments", ["criticality"] = "high", ["env"] = "prod" }, CreatedAt: new DateTimeOffset(2025, 11, 16, 12, 0, 0, TimeSpan.Zero), ProjectionHash: "sha256:proj112", EvaluationMetadata: "eval:passed:v1"), new( Artifact: "ghcr.io/stellaops/sample-api", SbomVersion: "2025.11.15.1", Digest: "sha256:111", License: "MIT", Scope: "runtime", AssetTags: new Dictionary { ["owner"] = "payments", ["criticality"] = "high", ["env"] = "prod" }, CreatedAt: new DateTimeOffset(2025, 11, 15, 12, 0, 0, TimeSpan.Zero), ProjectionHash: "sha256:proj111", EvaluationMetadata: "eval:passed:v1"), new( Artifact: "ghcr.io/stellaops/sample-worker", SbomVersion: "2025.11.12.0", Digest: "sha256:222", License: "Apache-2.0", Scope: "runtime", AssetTags: new Dictionary { ["owner"] = "platform", ["criticality"] = "medium", ["env"] = "staging" }, CreatedAt: new DateTimeOffset(2025, 11, 12, 8, 0, 0, TimeSpan.Zero), ProjectionHash: "sha256:proj222", EvaluationMetadata: "eval:pending:v1"), }; } private static IReadOnlyList SeedComponents() { return new List { new( Artifact: "ghcr.io/stellaops/sample-api", Purl: "pkg:npm/lodash@4.17.21", NeighborPurl: "pkg:npm/express@4.18.2", Relationship: "DEPENDS_ON", License: "MIT", Scope: "runtime", RuntimeFlag: true), new( Artifact: "ghcr.io/stellaops/sample-api", Purl: "pkg:npm/lodash@4.17.21", NeighborPurl: "pkg:npm/rollup@3.0.0", Relationship: "DEPENDS_ON", License: "MIT", Scope: "build", RuntimeFlag: false), new( Artifact: "ghcr.io/stellaops/sample-worker", Purl: "pkg:nuget/Newtonsoft.Json@13.0.2", NeighborPurl: "pkg:nuget/StellaOps.Core@1.0.0", Relationship: "DEPENDS_ON", License: "Apache-2.0", Scope: "runtime", RuntimeFlag: true) }; } private sealed record PathRecord( string Artifact, string Purl, string? Scope, string? Environment, bool RuntimeFlag, string? BlastRadius, string? NearestSafeVersion, IReadOnlyList Nodes); private sealed record TimelineRecord( string Artifact, string Version, string Digest, string SourceBundleHash, DateTimeOffset CreatedAt, string? Provenance); private sealed record CatalogRecord( string Artifact, string SbomVersion, string Digest, string? License, string Scope, IReadOnlyDictionary AssetTags, DateTimeOffset CreatedAt, string ProjectionHash, string EvaluationMetadata); private sealed record ComponentLookupRecord( string Artifact, string Purl, string NeighborPurl, string Relationship, string? License, string Scope, bool RuntimeFlag); }