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

@@ -5,14 +5,13 @@ namespace StellaOps.Concelier.WebService.Contracts;
public sealed record AdvisoryStructuredFieldResponse(
string AdvisoryKey,
string Fingerprint,
int Total,
bool Truncated,
IReadOnlyList<AdvisoryStructuredFieldEntry> Entries);
public sealed record AdvisoryStructuredFieldEntry(
string Type,
string DocumentId,
string FieldPath,
string ChunkId,
AdvisoryStructuredFieldContent Content,
AdvisoryStructuredFieldProvenance Provenance);
@@ -65,6 +64,8 @@ public sealed record AdvisoryStructuredAffectedContent(
string? Status);
public sealed record AdvisoryStructuredFieldProvenance(
string DocumentId,
string ObservationPath,
string Source,
string Kind,
string? Value,

View File

@@ -1,16 +1,19 @@
using System.Collections.Immutable;
using StellaOps.Concelier.Models.Observations;
using StellaOps.Concelier.Models.Observations;
using StellaOps.Concelier.RawModels;
namespace StellaOps.Concelier.WebService.Contracts;
public sealed record AdvisoryObservationQueryResponse(
ImmutableArray<AdvisoryObservation> Observations,
AdvisoryObservationLinksetAggregateResponse Linkset,
string? NextCursor,
bool HasMore);
public sealed record AdvisoryObservationLinksetAggregateResponse(
ImmutableArray<string> Aliases,
ImmutableArray<string> Purls,
ImmutableArray<string> Cpes,
ImmutableArray<AdvisoryObservationReference> References);
public sealed record AdvisoryObservationQueryResponse(
ImmutableArray<AdvisoryObservation> Observations,
AdvisoryObservationLinksetAggregateResponse Linkset,
string? NextCursor,
bool HasMore);
public sealed record AdvisoryObservationLinksetAggregateResponse(
ImmutableArray<string> Aliases,
ImmutableArray<string> Purls,
ImmutableArray<string> Cpes,
ImmutableArray<AdvisoryObservationReference> References,
ImmutableArray<string> Scopes,
ImmutableArray<RawRelationship> Relationships);

View File

@@ -45,18 +45,26 @@ public sealed record AdvisoryIdentifiersRequest(
[property: JsonPropertyName("primary")] string Primary,
[property: JsonPropertyName("aliases")] IReadOnlyList<string>? Aliases);
public sealed record AdvisoryLinksetRequest(
[property: JsonPropertyName("aliases")] IReadOnlyList<string>? Aliases,
[property: JsonPropertyName("purls")] IReadOnlyList<string>? PackageUrls,
[property: JsonPropertyName("cpes")] IReadOnlyList<string>? Cpes,
[property: JsonPropertyName("references")] IReadOnlyList<AdvisoryLinksetReferenceRequest>? References,
[property: JsonPropertyName("reconciledFrom")] IReadOnlyList<string>? ReconciledFrom,
[property: JsonPropertyName("notes")] IDictionary<string, string>? Notes);
public sealed record AdvisoryLinksetReferenceRequest(
[property: JsonPropertyName("type")] string Type,
[property: JsonPropertyName("url")] string Url,
[property: JsonPropertyName("source")] string? Source);
public sealed record AdvisoryLinksetRequest(
[property: JsonPropertyName("aliases")] IReadOnlyList<string>? Aliases,
[property: JsonPropertyName("scopes")] IReadOnlyList<string>? Scopes,
[property: JsonPropertyName("relationships")] IReadOnlyList<AdvisoryLinksetRelationshipRequest>? Relationships,
[property: JsonPropertyName("purls")] IReadOnlyList<string>? PackageUrls,
[property: JsonPropertyName("cpes")] IReadOnlyList<string>? Cpes,
[property: JsonPropertyName("references")] IReadOnlyList<AdvisoryLinksetReferenceRequest>? References,
[property: JsonPropertyName("reconciledFrom")] IReadOnlyList<string>? ReconciledFrom,
[property: JsonPropertyName("notes")] IDictionary<string, string>? Notes);
public sealed record AdvisoryLinksetRelationshipRequest(
[property: JsonPropertyName("type")] string Type,
[property: JsonPropertyName("source")] string Source,
[property: JsonPropertyName("target")] string Target,
[property: JsonPropertyName("provenance")] string? Provenance);
public sealed record AdvisoryLinksetReferenceRequest(
[property: JsonPropertyName("type")] string Type,
[property: JsonPropertyName("url")] string Url,
[property: JsonPropertyName("source")] string? Source);
public sealed record AdvisoryIngestResponse(
[property: JsonPropertyName("id")] string Id,

View File

@@ -68,6 +68,8 @@ internal static class AdvisoryRawRequestMapper
var linkset = new RawLinkset
{
Aliases = NormalizeStrings(linksetRequest?.Aliases),
Scopes = NormalizeStrings(linksetRequest?.Scopes),
Relationships = NormalizeRelationships(linksetRequest?.Relationships),
PackageUrls = NormalizeStrings(linksetRequest?.PackageUrls),
Cpes = NormalizeStrings(linksetRequest?.Cpes),
References = NormalizeReferences(linksetRequest?.References),
@@ -135,7 +137,7 @@ internal static class AdvisoryRawRequestMapper
if (references is null)
{
return ImmutableArray<RawReference>.Empty;
}
}
var builder = ImmutableArray.CreateBuilder<RawReference>();
foreach (var reference in references)
@@ -151,10 +153,38 @@ internal static class AdvisoryRawRequestMapper
}
builder.Add(new RawReference(reference.Type.Trim(), reference.Url.Trim(), string.IsNullOrWhiteSpace(reference.Source) ? null : reference.Source.Trim()));
}
return builder.Count == 0 ? ImmutableArray<RawReference>.Empty : builder.ToImmutable();
}
}
return builder.Count == 0 ? ImmutableArray<RawReference>.Empty : builder.ToImmutable();
}
private static ImmutableArray<RawRelationship> NormalizeRelationships(IEnumerable<AdvisoryLinksetRelationshipRequest>? relationships)
{
if (relationships is null)
{
return ImmutableArray<RawRelationship>.Empty;
}
var builder = ImmutableArray.CreateBuilder<RawRelationship>();
foreach (var relationship in relationships)
{
if (relationship is null
|| string.IsNullOrWhiteSpace(relationship.Type)
|| string.IsNullOrWhiteSpace(relationship.Source)
|| string.IsNullOrWhiteSpace(relationship.Target))
{
continue;
}
builder.Add(new RawRelationship(
relationship.Type.Trim(),
relationship.Source.Trim(),
relationship.Target.Trim(),
string.IsNullOrWhiteSpace(relationship.Provenance) ? null : relationship.Provenance.Trim()));
}
return builder.Count == 0 ? ImmutableArray<RawRelationship>.Empty : builder.ToImmutable();
}
private static JsonElement NormalizeRawContent(JsonElement element)
{

View File

@@ -438,7 +438,9 @@ var observationsEndpoint = app.MapGet("/concelier/observations", async (
result.Linkset.Aliases,
result.Linkset.Purls,
result.Linkset.Cpes,
result.Linkset.References),
result.Linkset.References,
result.Linkset.Scopes,
result.Linkset.Relationships),
result.NextCursor,
result.HasMore);
@@ -861,6 +863,7 @@ var advisoryChunksEndpoint = app.MapGet("/advisories/{advisoryKey}/chunks", asyn
var formatFilter = BuildFilterSet(context.Request.Query["format"]);
var resolution = await ResolveAdvisoryAsync(
tenant,
normalizedKey,
advisoryStore,
aliasStore,
@@ -891,6 +894,7 @@ var advisoryChunksEndpoint = app.MapGet("/advisories/{advisoryKey}/chunks", asyn
var observations = observationResult.Observations.ToArray();
var buildOptions = new AdvisoryChunkBuildOptions(
advisory.AdvisoryKey,
fingerprint,
chunkLimit,
observationLimit,
sectionFilter,
@@ -1319,11 +1323,17 @@ IResult? EnsureTenantAuthorized(HttpContext context, string tenant)
}
async Task<(Advisory Advisory, ImmutableArray<string> Aliases, string Fingerprint)?> ResolveAdvisoryAsync(
string tenant,
string advisoryKey,
IAdvisoryStore advisoryStore,
IAliasStore aliasStore,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(tenant))
{
return null;
}
ArgumentNullException.ThrowIfNull(advisoryStore);
ArgumentNullException.ThrowIfNull(aliasStore);

View File

@@ -12,6 +12,7 @@ namespace StellaOps.Concelier.WebService.Services;
internal sealed record AdvisoryChunkBuildOptions(
string AdvisoryKey,
string Fingerprint,
int ChunkLimit,
int ObservationLimit,
ImmutableHashSet<string> SectionFilter,
@@ -56,9 +57,7 @@ internal sealed class AdvisoryChunkBuilder
var vendorIndex = new ObservationIndex(observations);
var chunkLimit = Math.Max(1, options.ChunkLimit);
var entries = new List<AdvisoryStructuredFieldEntry>(chunkLimit);
var total = 0;
var truncated = false;
var entries = new List<AdvisoryStructuredFieldEntry>();
var sectionFilter = options.SectionFilter ?? ImmutableHashSet<string>.Empty;
foreach (var section in SectionOrder)
@@ -82,31 +81,25 @@ internal sealed class AdvisoryChunkBuilder
continue;
}
total += bucket.Count;
if (entries.Count >= chunkLimit)
{
truncated = true;
continue;
}
var remaining = chunkLimit - entries.Count;
if (bucket.Count <= remaining)
{
entries.AddRange(bucket);
}
else
{
entries.AddRange(bucket.Take(remaining));
truncated = true;
}
entries.AddRange(bucket);
}
var ordered = entries
.OrderBy(static entry => entry.Type, StringComparer.Ordinal)
.ThenBy(static entry => entry.Provenance.ObservationPath, StringComparer.Ordinal)
.ThenBy(static entry => entry.Provenance.DocumentId, StringComparer.Ordinal)
.ToArray();
var total = ordered.Length;
var truncated = total > chunkLimit;
var limited = truncated ? ordered.Take(chunkLimit).ToArray() : ordered;
var response = new AdvisoryStructuredFieldResponse(
options.AdvisoryKey,
options.Fingerprint,
total,
truncated,
entries);
limited);
var telemetry = new AdvisoryChunkTelemetrySummary(
vendorIndex.SourceCount,
@@ -284,11 +277,11 @@ internal sealed class AdvisoryChunkBuilder
return new AdvisoryStructuredFieldEntry(
type,
documentId,
fieldPath,
chunkId,
content,
new AdvisoryStructuredFieldProvenance(
documentId,
fieldPath,
provenance.Source,
provenance.Kind,
provenance.Value,