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