release orchestrator v1 draft and build fixes

This commit is contained in:
master
2026-01-12 12:24:17 +02:00
parent f3de858c59
commit 9873f80830
1598 changed files with 240385 additions and 5944 deletions

View File

@@ -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;

View File

@@ -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";
}

View File

@@ -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;

View File

@@ -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; }

View File

@@ -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)