feat: Add initial implementation of Vulnerability Resolver Jobs
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:
master
2025-11-18 07:52:15 +02:00
parent e69b57d467
commit 8355e2ff75
299 changed files with 13293 additions and 2444 deletions

View File

@@ -1,17 +1,202 @@
var builder = WebApplication.CreateBuilder(args);
builder.Configuration
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables("SBOM_");
builder.Services.AddOptions();
builder.Services.AddLogging();
// TODO: register SBOM projection services, repositories, and Authority integration.
var app = builder.Build();
app.MapGet("/healthz", () => Results.Ok(new { status = "ok" }));
app.MapGet("/readyz", () => Results.Ok(new { status = "warming" }));
app.Run();
using System.Diagnostics;
using System.Globalization;
using System.Diagnostics.Metrics;
using Microsoft.AspNetCore.Mvc;
using StellaOps.SbomService.Models;
using StellaOps.SbomService.Services;
using StellaOps.SbomService.Observability;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables("SBOM_");
builder.Services.AddOptions();
builder.Services.AddLogging();
// Register SBOM query services (InMemory seed; replace with Mongo-backed repository later).
builder.Services.AddSingleton<ISbomQueryService, InMemorySbomQueryService>();
var app = builder.Build();
app.MapGet("/healthz", () => Results.Ok(new { status = "ok" }));
app.MapGet("/readyz", () => Results.Ok(new { status = "warming" }));
app.MapGet("/console/sboms", async Task<IResult> (
[FromServices] ISbomQueryService service,
[FromQuery] string? artifact,
[FromQuery] string? license,
[FromQuery] string? scope,
[FromQuery(Name = "assetTag")] string? assetTag,
[FromQuery] string? cursor,
[FromQuery] int? limit,
CancellationToken cancellationToken) =>
{
if (limit is { } requestedLimit && (requestedLimit <= 0 || requestedLimit > 200))
{
return Results.BadRequest(new { error = "limit must be between 1 and 200" });
}
if (cursor is { Length: > 0 } && !int.TryParse(cursor, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
{
return Results.BadRequest(new { error = "cursor must be an integer offset" });
}
var offset = cursor is null ? 0 : int.Parse(cursor, CultureInfo.InvariantCulture);
var pageSize = limit ?? 50;
var start = Stopwatch.GetTimestamp();
var result = await service.GetConsoleCatalogAsync(
new SbomCatalogQuery(artifact?.Trim(), license?.Trim(), scope?.Trim(), assetTag?.Trim(), pageSize, offset),
cancellationToken);
var elapsedSeconds = Stopwatch.GetElapsedTime(start).TotalSeconds;
SbomMetrics.PathsLatencySeconds.Record(elapsedSeconds, new TagList
{
{ "scope", scope ?? string.Empty },
{ "env", string.Empty }
});
SbomMetrics.PathsQueryTotal.Add(1, new TagList
{
{ "cache_hit", result.CacheHit },
{ "scope", scope ?? string.Empty }
});
return Results.Ok(result.Result);
});
app.MapGet("/components/lookup", async Task<IResult> (
[FromServices] ISbomQueryService service,
[FromQuery] string? purl,
[FromQuery] string? artifact,
[FromQuery] string? cursor,
[FromQuery] int? limit,
CancellationToken cancellationToken) =>
{
if (string.IsNullOrWhiteSpace(purl))
{
return Results.BadRequest(new { error = "purl is required" });
}
if (limit is { } requestedLimit && (requestedLimit <= 0 || requestedLimit > 200))
{
return Results.BadRequest(new { error = "limit must be between 1 and 200" });
}
if (cursor is { Length: > 0 } && !int.TryParse(cursor, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
{
return Results.BadRequest(new { error = "cursor must be an integer offset" });
}
var offset = cursor is null ? 0 : int.Parse(cursor, CultureInfo.InvariantCulture);
var pageSize = limit ?? 50;
var start = Stopwatch.GetTimestamp();
var result = await service.GetComponentLookupAsync(
new ComponentLookupQuery(purl.Trim(), artifact?.Trim(), pageSize, offset),
cancellationToken);
var elapsedSeconds = Stopwatch.GetElapsedTime(start).TotalSeconds;
SbomMetrics.PathsLatencySeconds.Record(elapsedSeconds, new TagList
{
{ "scope", string.Empty },
{ "env", string.Empty }
});
SbomMetrics.PathsQueryTotal.Add(1, new TagList
{
{ "cache_hit", result.CacheHit },
{ "scope", string.Empty }
});
return Results.Ok(result.Result);
});
app.MapGet("/sbom/paths", async Task<IResult> (
[FromServices] ISbomQueryService service,
[FromQuery] string? purl,
[FromQuery] string? artifact,
[FromQuery] string? scope,
[FromQuery(Name = "env")] string? environment,
[FromQuery] string? cursor,
[FromQuery] int? limit,
CancellationToken cancellationToken) =>
{
if (string.IsNullOrWhiteSpace(purl))
{
return Results.BadRequest(new { error = "purl is required" });
}
if (limit is { } requestedLimit && (requestedLimit <= 0 || requestedLimit > 200))
{
return Results.BadRequest(new { error = "limit must be between 1 and 200" });
}
if (cursor is { Length: > 0 } && !int.TryParse(cursor, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
{
return Results.BadRequest(new { error = "cursor must be an integer offset" });
}
var offset = cursor is null ? 0 : int.Parse(cursor, CultureInfo.InvariantCulture);
var pageSize = limit ?? 50;
var start = Stopwatch.GetTimestamp();
var result = await service.GetPathsAsync(
new SbomPathQuery(purl.Trim(), artifact?.Trim(), scope?.Trim(), environment?.Trim(), pageSize, offset),
cancellationToken);
var elapsedSeconds = Stopwatch.GetElapsedTime(start).TotalSeconds;
SbomMetrics.PathsLatencySeconds.Record(elapsedSeconds, new TagList
{
{ "scope", scope ?? string.Empty },
{ "env", environment ?? string.Empty }
});
SbomMetrics.PathsQueryTotal.Add(1, new TagList
{
{ "cache_hit", result.CacheHit },
{ "scope", scope ?? string.Empty }
});
return Results.Ok(result.Result);
});
app.MapGet("/sbom/versions", async Task<IResult> (
[FromServices] ISbomQueryService service,
[FromQuery] string? artifact,
[FromQuery] string? cursor,
[FromQuery] int? limit,
CancellationToken cancellationToken) =>
{
if (string.IsNullOrWhiteSpace(artifact))
{
return Results.BadRequest(new { error = "artifact is required" });
}
if (limit is { } requestedLimit && (requestedLimit <= 0 || requestedLimit > 200))
{
return Results.BadRequest(new { error = "limit must be between 1 and 200" });
}
if (cursor is { Length: > 0 } && !int.TryParse(cursor, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
{
return Results.BadRequest(new { error = "cursor must be an integer offset" });
}
var offset = cursor is null ? 0 : int.Parse(cursor, CultureInfo.InvariantCulture);
var pageSize = limit ?? 50;
var start = Stopwatch.GetTimestamp();
var result = await service.GetTimelineAsync(
new SbomTimelineQuery(artifact.Trim(), pageSize, offset),
cancellationToken);
var elapsedSeconds = Stopwatch.GetElapsedTime(start).TotalSeconds;
SbomMetrics.TimelineLatencySeconds.Record(elapsedSeconds, new TagList { { "artifact", artifact } });
SbomMetrics.TimelineQueryTotal.Add(1, new TagList { { "artifact", artifact }, { "cache_hit", result.CacheHit } });
return Results.Ok(result.Result);
});
app.Run();
public partial class Program;