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

@@ -4,6 +4,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Signals.Models;
using StellaOps.Signals.Persistence;
@@ -13,16 +14,19 @@ public sealed class RuntimeFactsIngestionService : IRuntimeFactsIngestionService
{
private readonly IReachabilityFactRepository factRepository;
private readonly TimeProvider timeProvider;
private readonly IReachabilityScoringService scoringService;
private readonly ILogger<RuntimeFactsIngestionService> logger;
public RuntimeFactsIngestionService(
IReachabilityFactRepository factRepository,
TimeProvider timeProvider,
IReachabilityScoringService scoringService,
ILogger<RuntimeFactsIngestionService> logger)
{
this.factRepository = factRepository ?? throw new ArgumentNullException(nameof(factRepository));
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.scoringService = scoringService ?? throw new ArgumentNullException(nameof(scoringService));
this.logger = logger ?? NullLogger<RuntimeFactsIngestionService>.Instance;
}
public async Task<RuntimeFactsIngestResponse> IngestAsync(RuntimeFactsIngestRequest request, CancellationToken cancellationToken)
@@ -47,9 +51,15 @@ public sealed class RuntimeFactsIngestionService : IRuntimeFactsIngestionService
var aggregated = AggregateRuntimeFacts(request.Events);
document.RuntimeFacts = MergeRuntimeFacts(document.RuntimeFacts, aggregated);
document.Metadata = MergeMetadata(document.Metadata, request.Metadata);
document.Metadata ??= new Dictionary<string, string?>(StringComparer.Ordinal);
document.Metadata.TryAdd("provenance.source", request.Metadata?.TryGetValue("source", out var source) == true ? source : "runtime");
document.Metadata["provenance.ingestedAt"] = document.ComputedAt.ToString("O");
document.Metadata["provenance.callgraphId"] = request.CallgraphId;
var persisted = await factRepository.UpsertAsync(document, cancellationToken).ConfigureAwait(false);
await RecomputeReachabilityAsync(persisted, aggregated, request, cancellationToken).ConfigureAwait(false);
logger.LogInformation(
"Stored {RuntimeFactCount} runtime fact(s) for subject {SubjectKey} (callgraph={CallgraphId}).",
persisted.RuntimeFacts?.Count ?? 0,
@@ -244,6 +254,67 @@ public sealed class RuntimeFactsIngestionService : IRuntimeFactsIngestionService
.ToList();
}
private async Task RecomputeReachabilityAsync(
ReachabilityFactDocument persisted,
List<RuntimeFactDocument> aggregatedRuntimeFacts,
RuntimeFactsIngestRequest request,
CancellationToken cancellationToken)
{
var targets = new HashSet<string>(StringComparer.Ordinal);
if (persisted.States is { Count: > 0 })
{
foreach (var state in persisted.States)
{
if (!string.IsNullOrWhiteSpace(state.Target))
{
targets.Add(state.Target.Trim());
}
}
}
foreach (var fact in aggregatedRuntimeFacts)
{
if (!string.IsNullOrWhiteSpace(fact.SymbolId))
{
targets.Add(fact.SymbolId.Trim());
}
}
var runtimeHits = aggregatedRuntimeFacts
.Select(f => f.SymbolId)
.Where(id => !string.IsNullOrWhiteSpace(id))
.Select(id => id.Trim())
.Distinct(StringComparer.Ordinal)
.ToList();
if (targets.Count == 0)
{
return;
}
var requestMetadata = MergeMetadata(persisted.Metadata, request.Metadata);
var recomputeRequest = new ReachabilityRecomputeRequest
{
CallgraphId = request.CallgraphId,
Subject = request.Subject,
EntryPoints = persisted.EntryPoints ?? new List<string>(),
Targets = targets.ToList(),
RuntimeHits = runtimeHits,
Metadata = requestMetadata
};
try
{
await scoringService.RecomputeAsync(recomputeRequest, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to recompute reachability after runtime ingestion for subject {SubjectKey}.", persisted.SubjectKey);
throw;
}
}
private static string? Normalize(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();