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

@@ -29,6 +29,11 @@ public sealed class SignalsOptions
/// Air-gap configuration.
/// </summary>
public SignalsAirGapOptions AirGap { get; } = new();
/// <summary>
/// Reachability scoring configuration.
/// </summary>
public SignalsScoringOptions Scoring { get; } = new();
/// <summary>
/// Validates configured options.
@@ -39,5 +44,6 @@ public sealed class SignalsOptions
Mongo.Validate();
Storage.Validate();
AirGap.Validate();
Scoring.Validate();
}
}

View File

@@ -0,0 +1,71 @@
using System;
namespace StellaOps.Signals.Options;
/// <summary>
/// Configurable weights used by reachability scoring.
/// </summary>
public sealed class SignalsScoringOptions
{
/// <summary>
/// Confidence assigned when a path exists from entry point to target.
/// </summary>
public double ReachableConfidence { get; set; } = 0.75;
/// <summary>
/// Confidence assigned when no path exists from entry point to target.
/// </summary>
public double UnreachableConfidence { get; set; } = 0.25;
/// <summary>
/// Bonus applied when runtime evidence matches the discovered path.
/// </summary>
public double RuntimeBonus { get; set; } = 0.15;
/// <summary>
/// Maximum confidence permitted after bonuses are applied.
/// </summary>
public double MaxConfidence { get; set; } = 0.99;
/// <summary>
/// Minimum confidence permitted after penalties are applied.
/// </summary>
public double MinConfidence { get; set; } = 0.05;
public void Validate()
{
EnsurePercent(nameof(ReachableConfidence), ReachableConfidence);
EnsurePercent(nameof(UnreachableConfidence), UnreachableConfidence);
EnsurePercent(nameof(RuntimeBonus), RuntimeBonus);
EnsurePercent(nameof(MaxConfidence), MaxConfidence);
EnsurePercent(nameof(MinConfidence), MinConfidence);
if (MinConfidence > UnreachableConfidence)
{
throw new ArgumentException("MinConfidence must be less than or equal to UnreachableConfidence.");
}
if (UnreachableConfidence > ReachableConfidence)
{
throw new ArgumentException("UnreachableConfidence must be less than or equal to ReachableConfidence.");
}
if (ReachableConfidence > MaxConfidence)
{
throw new ArgumentException("ReachableConfidence must be less than or equal to MaxConfidence.");
}
if (MinConfidence >= MaxConfidence)
{
throw new ArgumentException("MinConfidence must be less than MaxConfidence.");
}
}
private static void EnsurePercent(string name, double value)
{
if (double.IsNaN(value) || value < 0.0 || value > 1.0)
{
throw new ArgumentOutOfRangeException(name, value, "Value must be between 0 and 1.");
}
}
}

View File

@@ -4,33 +4,32 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Signals.Models;
using StellaOps.Signals.Persistence;
using StellaOps.Signals.Options;
namespace StellaOps.Signals.Services;
public sealed class ReachabilityScoringService : IReachabilityScoringService
{
private const double ReachableConfidence = 0.75;
private const double UnreachableConfidence = 0.25;
private const double RuntimeBonus = 0.15;
private const double MaxConfidence = 0.99;
private const double MinConfidence = 0.05;
private readonly ICallgraphRepository callgraphRepository;
private readonly IReachabilityFactRepository factRepository;
private readonly TimeProvider timeProvider;
private readonly SignalsScoringOptions scoringOptions;
private readonly ILogger<ReachabilityScoringService> logger;
public ReachabilityScoringService(
ICallgraphRepository callgraphRepository,
IReachabilityFactRepository factRepository,
TimeProvider timeProvider,
IOptions<SignalsOptions> options,
ILogger<ReachabilityScoringService> logger)
{
this.callgraphRepository = callgraphRepository ?? throw new ArgumentNullException(nameof(callgraphRepository));
this.factRepository = factRepository ?? throw new ArgumentNullException(nameof(factRepository));
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
this.scoringOptions = options?.Value?.Scoring ?? throw new ArgumentNullException(nameof(options));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -89,16 +88,16 @@ public sealed class ReachabilityScoringService : IReachabilityScoringService
{
var path = FindPath(entryPoints, target, graph.Adjacency);
var reachable = path is not null;
var confidence = reachable ? ReachableConfidence : UnreachableConfidence;
var confidence = reachable ? scoringOptions.ReachableConfidence : scoringOptions.UnreachableConfidence;
var runtimeEvidence = runtimeHits.Where(hit => path?.Contains(hit, StringComparer.Ordinal) == true)
.ToList();
if (runtimeEvidence.Count > 0)
{
confidence = Math.Min(MaxConfidence, confidence + RuntimeBonus);
confidence = Math.Min(scoringOptions.MaxConfidence, confidence + scoringOptions.RuntimeBonus);
}
confidence = Math.Clamp(confidence, MinConfidence, MaxConfidence);
confidence = Math.Clamp(confidence, scoringOptions.MinConfidence, scoringOptions.MaxConfidence);
states.Add(new ReachabilityStateDocument
{

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();