release orchestrator v1 draft and build fixes
This commit is contained in:
@@ -4,9 +4,9 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.EntryTrace;
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using ScanAnalysisKeys = StellaOps.Scanner.Core.Contracts.ScanAnalysisKeys;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Processing.Reachability;
|
||||
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ReachabilityEvidenceStageExecutor.cs
|
||||
// Sprint: EVID-001 - Reachability Evidence Pipeline
|
||||
// Task: EVID-001-005
|
||||
// Description: Scan stage executor that generates reachability evidence for CVEs.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Reachability.Jobs;
|
||||
using StellaOps.Scanner.Reachability.Services;
|
||||
using StellaOps.Scanner.Reachability.Vex;
|
||||
using CoreScanAnalysisKeys = StellaOps.Scanner.Core.Contracts.ScanAnalysisKeys;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Processing.Reachability;
|
||||
|
||||
/// <summary>
|
||||
/// Scan stage executor that generates reachability evidence for vulnerability findings.
|
||||
/// Analyzes CVE reachability using the 3-layer model and emits VEX statements.
|
||||
/// </summary>
|
||||
public sealed class ReachabilityEvidenceStageExecutor : IScanStageExecutor
|
||||
{
|
||||
private readonly IReachabilityEvidenceJobExecutor _jobExecutor;
|
||||
private readonly ICveSymbolMappingService _mappingService;
|
||||
private readonly IVexStatusDeterminer _vexDeterminer;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<ReachabilityEvidenceStageExecutor> _logger;
|
||||
|
||||
public ReachabilityEvidenceStageExecutor(
|
||||
IReachabilityEvidenceJobExecutor jobExecutor,
|
||||
ICveSymbolMappingService mappingService,
|
||||
IVexStatusDeterminer vexDeterminer,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<ReachabilityEvidenceStageExecutor> logger)
|
||||
{
|
||||
_jobExecutor = jobExecutor ?? throw new ArgumentNullException(nameof(jobExecutor));
|
||||
_mappingService = mappingService ?? throw new ArgumentNullException(nameof(mappingService));
|
||||
_vexDeterminer = vexDeterminer ?? throw new ArgumentNullException(nameof(vexDeterminer));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public string StageName => "reachability-evidence";
|
||||
|
||||
public async ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
// Extract CVE findings that have symbol mappings
|
||||
var cveFindings = await ExtractEligibleCveFindingsAsync(context, cancellationToken);
|
||||
|
||||
if (cveFindings.Count == 0)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"No eligible CVE findings with symbol mappings for job {JobId}",
|
||||
context.JobId);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Generating reachability evidence for {CveCount} CVEs in job {JobId}",
|
||||
cveFindings.Count,
|
||||
context.JobId);
|
||||
|
||||
var results = new List<ReachabilityEvidenceJobResult>();
|
||||
var vexStatements = new List<VexStatement>();
|
||||
|
||||
foreach (var (cveId, purl) in cveFindings)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await ProcessCveAsync(context, cveId, purl, cancellationToken);
|
||||
|
||||
if (result is not null)
|
||||
{
|
||||
results.Add(result);
|
||||
|
||||
// Generate VEX statement if we have a verdict
|
||||
if (result.Stack is not null)
|
||||
{
|
||||
var productId = $"{context.ImageDigest}:{purl}";
|
||||
var evidenceUris = result.EvidenceUri is not null
|
||||
? new[] { result.EvidenceUri }
|
||||
: Array.Empty<string>();
|
||||
|
||||
var vexStatement = _vexDeterminer.CreateStatement(
|
||||
result.Stack,
|
||||
productId,
|
||||
evidenceUris);
|
||||
|
||||
vexStatements.Add(vexStatement);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex,
|
||||
"Failed to process reachability for CVE {CveId} PURL {Purl} in job {JobId}",
|
||||
cveId, purl, context.JobId);
|
||||
}
|
||||
}
|
||||
|
||||
// Store results in analysis context
|
||||
if (results.Count > 0)
|
||||
{
|
||||
context.Analysis.Set(ReachabilityAnalysisKeys.ReachabilityEvidenceResults, results);
|
||||
_logger.LogInformation(
|
||||
"Reachability evidence generated for {Count} CVEs in job {JobId}",
|
||||
results.Count,
|
||||
context.JobId);
|
||||
}
|
||||
|
||||
if (vexStatements.Count > 0)
|
||||
{
|
||||
context.Analysis.Set(ReachabilityAnalysisKeys.VexStatements, vexStatements);
|
||||
_logger.LogInformation(
|
||||
"Generated {Count} VEX statements for job {JobId}",
|
||||
vexStatements.Count,
|
||||
context.JobId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<(string CveId, string Purl)>> ExtractEligibleCveFindingsAsync(
|
||||
ScanJobContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var eligibleFindings = new List<(string CveId, string Purl)>();
|
||||
var seenCves = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Extract CVE+PURL pairs from findings
|
||||
var cvePurlPairs = ExtractCvePurlPairs(context);
|
||||
|
||||
foreach (var (cveId, purl) in cvePurlPairs)
|
||||
{
|
||||
if (seenCves.Contains(cveId))
|
||||
continue;
|
||||
|
||||
// Check if we have symbol mappings for this CVE
|
||||
var hasMapping = await _mappingService.HasMappingAsync(cveId, cancellationToken);
|
||||
if (hasMapping)
|
||||
{
|
||||
eligibleFindings.Add((cveId, purl));
|
||||
seenCves.Add(cveId);
|
||||
}
|
||||
}
|
||||
|
||||
return eligibleFindings;
|
||||
}
|
||||
|
||||
private async Task<ReachabilityEvidenceJobResult?> ProcessCveAsync(
|
||||
ScanJobContext context,
|
||||
string cveId,
|
||||
string purl,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var imageDigest = context.ImageDigest;
|
||||
if (string.IsNullOrEmpty(imageDigest))
|
||||
{
|
||||
_logger.LogWarning("Cannot process reachability for CVE {CveId}: no image digest available", cveId);
|
||||
return null;
|
||||
}
|
||||
|
||||
var jobId = ReachabilityEvidenceJob.ComputeJobId(
|
||||
imageDigest,
|
||||
cveId,
|
||||
purl);
|
||||
|
||||
var job = new ReachabilityEvidenceJob
|
||||
{
|
||||
JobId = jobId,
|
||||
ImageDigest = imageDigest,
|
||||
CveId = cveId,
|
||||
Purl = purl,
|
||||
SourceCommit = context.Analysis.TryGet<string>(ReachabilityAnalysisKeys.SourceCommit, out var commit)
|
||||
? commit
|
||||
: null,
|
||||
Options = new ReachabilityJobOptions
|
||||
{
|
||||
IncludeL2 = false, // Requires binary paths not available in scan context
|
||||
IncludeL3 = false, // Requires container ID
|
||||
MaxPathsPerSink = 5,
|
||||
MaxDepth = 256
|
||||
},
|
||||
QueuedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
_logger.LogDebug(
|
||||
"Executing reachability evidence job {JobId} for CVE {CveId}",
|
||||
jobId, cveId);
|
||||
|
||||
var result = await _jobExecutor.ExecuteAsync(job, cancellationToken);
|
||||
|
||||
if (result.Status == JobStatus.Completed && result.Stack is not null)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Reachability verdict for CVE {CveId}: {Verdict}",
|
||||
cveId, result.Stack.Verdict);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<(string CveId, string Purl)> ExtractCvePurlPairs(ScanJobContext context)
|
||||
{
|
||||
var pairs = new List<(string CveId, string Purl)>();
|
||||
|
||||
// Extract from OS package analyzer results
|
||||
if (context.Analysis.TryGet<object>(CoreScanAnalysisKeys.OsPackageAnalyzers, out var osResults) &&
|
||||
osResults is System.Collections.IDictionary osDictionary)
|
||||
{
|
||||
foreach (var analyzerResult in osDictionary.Values)
|
||||
{
|
||||
if (analyzerResult is not null)
|
||||
{
|
||||
ExtractPairsFromAnalyzerResult(analyzerResult, pairs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract from language analyzer results
|
||||
if (context.Analysis.TryGet<object>(CoreScanAnalysisKeys.LanguageAnalyzerResults, out var langResults) &&
|
||||
langResults is System.Collections.IDictionary langDictionary)
|
||||
{
|
||||
foreach (var analyzerResult in langDictionary.Values)
|
||||
{
|
||||
if (analyzerResult is not null)
|
||||
{
|
||||
ExtractPairsFromAnalyzerResult(analyzerResult, pairs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pairs;
|
||||
}
|
||||
|
||||
private static void ExtractPairsFromAnalyzerResult(
|
||||
object analyzerResult,
|
||||
List<(string CveId, string Purl)> pairs)
|
||||
{
|
||||
var resultType = analyzerResult.GetType();
|
||||
|
||||
// Try to get Vulnerabilities property
|
||||
var vulnsProperty = resultType.GetProperty("Vulnerabilities");
|
||||
if (vulnsProperty?.GetValue(analyzerResult) is IEnumerable<object> vulns)
|
||||
{
|
||||
foreach (var vuln in vulns)
|
||||
{
|
||||
ExtractPairFromFinding(vuln, pairs);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get Findings property
|
||||
var findingsProperty = resultType.GetProperty("Findings");
|
||||
if (findingsProperty?.GetValue(analyzerResult) is IEnumerable<object> findingsList)
|
||||
{
|
||||
foreach (var finding in findingsList)
|
||||
{
|
||||
ExtractPairFromFinding(finding, pairs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExtractPairFromFinding(
|
||||
object finding,
|
||||
List<(string CveId, string Purl)> pairs)
|
||||
{
|
||||
var findingType = finding.GetType();
|
||||
string? cveId = null;
|
||||
string? purl = null;
|
||||
|
||||
// Try CveId property
|
||||
var cveIdProperty = findingType.GetProperty("CveId");
|
||||
if (cveIdProperty?.GetValue(finding) is string cve && !string.IsNullOrWhiteSpace(cve))
|
||||
{
|
||||
cveId = cve;
|
||||
}
|
||||
|
||||
// Try VulnerabilityId property
|
||||
if (cveId is null)
|
||||
{
|
||||
var vulnIdProperty = findingType.GetProperty("VulnerabilityId");
|
||||
if (vulnIdProperty?.GetValue(finding) is string vulnId &&
|
||||
!string.IsNullOrWhiteSpace(vulnId) &&
|
||||
vulnId.StartsWith("CVE-", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
cveId = vulnId;
|
||||
}
|
||||
}
|
||||
|
||||
// Try Purl property
|
||||
var purlProperty = findingType.GetProperty("Purl");
|
||||
if (purlProperty?.GetValue(finding) is string p && !string.IsNullOrWhiteSpace(p))
|
||||
{
|
||||
purl = p;
|
||||
}
|
||||
|
||||
// Try PackageUrl property
|
||||
if (purl is null)
|
||||
{
|
||||
var packageUrlProperty = findingType.GetProperty("PackageUrl");
|
||||
if (packageUrlProperty?.GetValue(finding) is string pkg && !string.IsNullOrWhiteSpace(pkg))
|
||||
{
|
||||
purl = pkg;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(cveId) && !string.IsNullOrWhiteSpace(purl))
|
||||
{
|
||||
pairs.Add((cveId, purl));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analysis keys for reachability evidence stage.
|
||||
/// </summary>
|
||||
public static class ReachabilityAnalysisKeys
|
||||
{
|
||||
public const string ReachabilityEvidenceResults = "reachability.evidence.results";
|
||||
public const string VexStatements = "reachability.vex.statements";
|
||||
public const string SourceCommit = "source.commit";
|
||||
}
|
||||
@@ -2,8 +2,8 @@ using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using ScanAnalysisKeys = StellaOps.Scanner.Core.Contracts.ScanAnalysisKeys;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Processing.Reachability;
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace StellaOps.Scanner.Worker.Processing;
|
||||
|
||||
public sealed class ScanJobContext
|
||||
{
|
||||
private const string ImageDigestMetadataKey = "image.digest";
|
||||
|
||||
public ScanJobContext(IScanJobLease lease, TimeProvider timeProvider, DateTimeOffset startUtc, CancellationToken cancellationToken)
|
||||
{
|
||||
Lease = lease ?? throw new ArgumentNullException(nameof(lease));
|
||||
@@ -27,6 +29,12 @@ public sealed class ScanJobContext
|
||||
|
||||
public string ScanId => Lease.ScanId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the OCI image digest from job metadata, if available.
|
||||
/// </summary>
|
||||
public string? ImageDigest =>
|
||||
Lease.Metadata.TryGetValue(ImageDigestMetadataKey, out var digest) ? digest : null;
|
||||
|
||||
public string? ReplayBundlePath { get; set; }
|
||||
|
||||
public ScanAnalysisStore Analysis { get; }
|
||||
|
||||
@@ -35,6 +35,7 @@ using StellaOps.Scanner.Storage.Extensions;
|
||||
using StellaOps.Scanner.Storage;
|
||||
using StellaOps.Scanner.Storage.Services;
|
||||
using Reachability = StellaOps.Scanner.Worker.Processing.Reachability;
|
||||
using ReachabilityEvidenceStageExecutor = StellaOps.Scanner.Worker.Processing.Reachability.ReachabilityEvidenceStageExecutor;
|
||||
using GateDetectors = StellaOps.Scanner.Reachability.Gates.Detectors;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
@@ -127,6 +128,10 @@ if (!string.IsNullOrWhiteSpace(connectionString))
|
||||
builder.Services.AddSingleton<IScanStageExecutor, SurfaceManifestStageExecutor>();
|
||||
builder.Services.AddSingleton<IDsseEnvelopeSigner, HmacDsseEnvelopeSigner>();
|
||||
|
||||
// Reachability Evidence Pipeline (Sprint: EVID-001)
|
||||
builder.Services.AddReachabilityEvidence(connectionString);
|
||||
builder.Services.AddSingleton<IScanStageExecutor, ReachabilityEvidenceStageExecutor>();
|
||||
|
||||
// EPSS ingestion job (Sprint: SPRINT_3410_0001_0001)
|
||||
builder.Services.AddOptions<EpssIngestOptions>()
|
||||
.BindConfiguration(EpssIngestOptions.SectionName)
|
||||
|
||||
Reference in New Issue
Block a user