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:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user