feat: Add initial implementation of Vulnerability Resolver Jobs
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Created project for StellaOps.Scanner.Analyzers.Native.Tests with necessary dependencies. - Documented roles and guidelines in AGENTS.md for Scheduler module. - Implemented IResolverJobService interface and InMemoryResolverJobService for handling resolver jobs. - Added ResolverBacklogNotifier and ResolverBacklogService for monitoring job metrics. - Developed API endpoints for managing resolver jobs and retrieving metrics. - Defined models for resolver job requests and responses. - Integrated dependency injection for resolver job services. - Implemented ImpactIndexSnapshot for persisting impact index data. - Introduced SignalsScoringOptions for configurable scoring weights in reachability scoring. - Added unit tests for ReachabilityScoringService and RuntimeFactsIngestionService. - Created dotnet-filter.sh script to handle command-line arguments for dotnet. - Established nuget-prime project for managing package downloads.
This commit is contained in:
@@ -0,0 +1,361 @@
|
||||
using System.Globalization;
|
||||
using StellaOps.SbomService.Models;
|
||||
|
||||
namespace StellaOps.SbomService.Services;
|
||||
|
||||
internal sealed class InMemorySbomQueryService : ISbomQueryService
|
||||
{
|
||||
private readonly IReadOnlyList<PathRecord> _paths;
|
||||
private readonly IReadOnlyList<TimelineRecord> _timelines;
|
||||
private readonly IReadOnlyList<CatalogRecord> _catalog;
|
||||
private readonly IReadOnlyList<ComponentLookupRecord> _components;
|
||||
private readonly ConcurrentDictionary<string, object> _cache = new();
|
||||
|
||||
public InMemorySbomQueryService()
|
||||
{
|
||||
// Deterministic seed data for early contract testing; replace with Mongo-backed implementation later.
|
||||
_paths = SeedPaths();
|
||||
_timelines = SeedTimelines();
|
||||
_catalog = SeedCatalog();
|
||||
_components = SeedComponents();
|
||||
}
|
||||
|
||||
public Task<QueryResult<SbomPathResult>> 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<SbomPathResult>(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<SbomPathResult>(result, false));
|
||||
}
|
||||
|
||||
public Task<QueryResult<SbomTimelineResult>> 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<SbomTimelineResult>(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<SbomTimelineResult>(result, false));
|
||||
}
|
||||
|
||||
public Task<QueryResult<SbomCatalogResult>> 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<SbomCatalogResult>(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<SbomCatalogResult>(result, false));
|
||||
}
|
||||
|
||||
public Task<QueryResult<ComponentLookupResult>> 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 Task.FromResult(new QueryResult<ComponentLookupResult>(cachedResult, true));
|
||||
}
|
||||
|
||||
var filtered = _components
|
||||
.Where(c => c.Purl.Equals(query.Purl, StringComparison.OrdinalIgnoreCase))
|
||||
.Where(c => query.Artifact is null || c.Artifact.Equals(query.Artifact, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(c => c.Artifact)
|
||||
.ThenBy(c => c.Purl)
|
||||
.ToList();
|
||||
|
||||
var page = filtered
|
||||
.Skip(query.Offset)
|
||||
.Take(query.Limit)
|
||||
.Select(c => new ComponentNeighbor(c.NeighborPurl, c.Relationship, c.License, c.Scope, c.RuntimeFlag))
|
||||
.ToList();
|
||||
|
||||
string? nextCursor = query.Offset + query.Limit < filtered.Count
|
||||
? (query.Offset + query.Limit).ToString(CultureInfo.InvariantCulture)
|
||||
: null;
|
||||
|
||||
var result = new ComponentLookupResult(query.Purl, query.Artifact, page, nextCursor, CacheHint: "seeded");
|
||||
_cache[cacheKey] = result;
|
||||
return Task.FromResult(new QueryResult<ComponentLookupResult>(result, false));
|
||||
}
|
||||
|
||||
private static IReadOnlyList<PathRecord> SeedPaths()
|
||||
{
|
||||
return new List<PathRecord>
|
||||
{
|
||||
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<TimelineRecord> SeedTimelines()
|
||||
{
|
||||
return new List<TimelineRecord>
|
||||
{
|
||||
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<CatalogRecord> SeedCatalog()
|
||||
{
|
||||
return new List<CatalogRecord>
|
||||
{
|
||||
new(
|
||||
Artifact: "ghcr.io/stellaops/sample-api",
|
||||
SbomVersion: "2025.11.16.1",
|
||||
Digest: "sha256:112",
|
||||
License: "MIT",
|
||||
Scope: "runtime",
|
||||
AssetTags: new Dictionary<string, string>
|
||||
{
|
||||
["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<string, string>
|
||||
{
|
||||
["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<string, string>
|
||||
{
|
||||
["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<ComponentLookupRecord> SeedComponents()
|
||||
{
|
||||
return new List<ComponentLookupRecord>
|
||||
{
|
||||
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<SbomPathNode> 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<string, string> 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);
|
||||
}
|
||||
Reference in New Issue
Block a user