tests fixes and sprints work
This commit is contained in:
@@ -38,6 +38,36 @@ public sealed class ScannerWorkerOptions
|
||||
|
||||
public VerdictPushOptions VerdictPush { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Options for service security analysis.
|
||||
/// Sprint: SPRINT_20260119_016 - Service Security Analysis
|
||||
/// </summary>
|
||||
public ServiceSecurityOptions ServiceSecurity { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Options for crypto (CBOM) analysis.
|
||||
/// Sprint: SPRINT_20260119_017 - CBOM Crypto Analysis
|
||||
/// </summary>
|
||||
public CryptoAnalysisOptions CryptoAnalysis { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Options for AI/ML supply chain analysis.
|
||||
/// Sprint: SPRINT_20260119_018 - AI/ML Supply Chain Security
|
||||
/// </summary>
|
||||
public AiMlSecurityOptions AiMlSecurity { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Options for build provenance verification.
|
||||
/// Sprint: SPRINT_20260119_019 - Build Provenance Verification
|
||||
/// </summary>
|
||||
public BuildProvenanceOptions BuildProvenance { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Options for SBOM dependency reachability analysis.
|
||||
/// Sprint: SPRINT_20260119_022 - Dependency reachability inference
|
||||
/// </summary>
|
||||
public ReachabilityOptions Reachability { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Options for secrets leak detection scanning.
|
||||
/// Sprint: SPRINT_20251229_046_BE - Secrets Leak Detection
|
||||
@@ -318,6 +348,181 @@ public sealed class ScannerWorkerOptions
|
||||
public bool AllowAnonymousFallback { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for service security analysis.
|
||||
/// </summary>
|
||||
public sealed class ServiceSecurityOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enable service security analysis.
|
||||
/// When disabled, the service security stage will be skipped.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to the service security policy file (YAML or JSON).
|
||||
/// </summary>
|
||||
public string? PolicyPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Metadata key used to locate the SBOM file path.
|
||||
/// </summary>
|
||||
public string SbomPathMetadataKey { get; set; } = ScanMetadataKeys.SbomPath;
|
||||
|
||||
/// <summary>
|
||||
/// Metadata key used to identify the SBOM format.
|
||||
/// </summary>
|
||||
public string SbomFormatMetadataKey { get; set; } = ScanMetadataKeys.SbomFormat;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for crypto (CBOM) analysis.
|
||||
/// </summary>
|
||||
public sealed class CryptoAnalysisOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enable crypto analysis.
|
||||
/// When disabled, the crypto analysis stage will be skipped.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to the crypto policy file (YAML or JSON).
|
||||
/// </summary>
|
||||
public string? PolicyPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Require FIPS compliance checks even if policy does not specify a framework.
|
||||
/// </summary>
|
||||
public bool RequireFips { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable post-quantum analysis regardless of policy configuration.
|
||||
/// </summary>
|
||||
public bool EnablePostQuantumAnalysis { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Metadata key used to locate the SBOM file path.
|
||||
/// </summary>
|
||||
public string SbomPathMetadataKey { get; set; } = ScanMetadataKeys.SbomPath;
|
||||
|
||||
/// <summary>
|
||||
/// Metadata key used to identify the SBOM format.
|
||||
/// </summary>
|
||||
public string SbomFormatMetadataKey { get; set; } = ScanMetadataKeys.SbomFormat;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for AI/ML supply chain analysis.
|
||||
/// </summary>
|
||||
public sealed class AiMlSecurityOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enable AI/ML security analysis.
|
||||
/// When disabled, the AI/ML stage will be skipped.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to AI governance policy file (YAML or JSON).
|
||||
/// </summary>
|
||||
public string? PolicyPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Require explicit risk assessment even if policy does not specify it.
|
||||
/// </summary>
|
||||
public bool RequireRiskAssessment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable model binary analysis (requires ML embedding services).
|
||||
/// </summary>
|
||||
public bool EnableBinaryAnalysis { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Metadata key used to locate the SBOM file path.
|
||||
/// </summary>
|
||||
public string SbomPathMetadataKey { get; set; } = ScanMetadataKeys.SbomPath;
|
||||
|
||||
/// <summary>
|
||||
/// Metadata key used to identify the SBOM format.
|
||||
/// </summary>
|
||||
public string SbomFormatMetadataKey { get; set; } = ScanMetadataKeys.SbomFormat;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for build provenance verification.
|
||||
/// </summary>
|
||||
public sealed class BuildProvenanceOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enable build provenance verification.
|
||||
/// When disabled, the build provenance stage will be skipped.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to the build provenance policy file (YAML or JSON).
|
||||
/// </summary>
|
||||
public string? PolicyPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Trigger reproducibility verification (rebuild) when possible.
|
||||
/// </summary>
|
||||
public bool VerifyReproducibility { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Require reproducible build verification for compliance.
|
||||
/// </summary>
|
||||
public bool RequireReproducible { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Metadata key used to locate the SBOM file path.
|
||||
/// </summary>
|
||||
public string SbomPathMetadataKey { get; set; } = ScanMetadataKeys.SbomPath;
|
||||
|
||||
/// <summary>
|
||||
/// Metadata key used to identify the SBOM format.
|
||||
/// </summary>
|
||||
public string SbomFormatMetadataKey { get; set; } = ScanMetadataKeys.SbomFormat;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for SBOM dependency reachability analysis.
|
||||
/// </summary>
|
||||
public sealed class ReachabilityOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enable dependency reachability analysis.
|
||||
/// When disabled, the reachability stage will be skipped.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to the reachability policy file (YAML or JSON).
|
||||
/// </summary>
|
||||
public string? PolicyPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Metadata key used to locate the SBOM file path.
|
||||
/// </summary>
|
||||
public string SbomPathMetadataKey { get; set; } = ScanMetadataKeys.SbomPath;
|
||||
|
||||
/// <summary>
|
||||
/// Metadata key used to identify the SBOM format.
|
||||
/// </summary>
|
||||
public string SbomFormatMetadataKey { get; set; } = ScanMetadataKeys.SbomFormat;
|
||||
|
||||
/// <summary>
|
||||
/// Metadata key used to locate an optional RichGraph call graph.
|
||||
/// </summary>
|
||||
public string CallGraphPathMetadataKey { get; set; } = ScanMetadataKeys.ReachabilityCallGraphPath;
|
||||
|
||||
/// <summary>
|
||||
/// Include unreachable vulnerabilities in downstream matches and reports.
|
||||
/// </summary>
|
||||
public bool IncludeUnreachableVulnerabilities { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for secrets leak detection scanning.
|
||||
/// Sprint: SPRINT_20251229_046_BE - Secrets Leak Detection
|
||||
|
||||
@@ -105,6 +105,71 @@ public sealed class ScannerWorkerOptionsValidator : IValidateOptions<ScannerWork
|
||||
}
|
||||
}
|
||||
|
||||
if (options.ServiceSecurity.Enabled)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(options.ServiceSecurity.SbomPathMetadataKey))
|
||||
{
|
||||
failures.Add("Scanner.Worker:ServiceSecurity:SbomPathMetadataKey must be provided when ServiceSecurity is enabled.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.ServiceSecurity.SbomFormatMetadataKey))
|
||||
{
|
||||
failures.Add("Scanner.Worker:ServiceSecurity:SbomFormatMetadataKey must be provided when ServiceSecurity is enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
if (options.CryptoAnalysis.Enabled)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(options.CryptoAnalysis.SbomPathMetadataKey))
|
||||
{
|
||||
failures.Add("Scanner.Worker:CryptoAnalysis:SbomPathMetadataKey must be provided when CryptoAnalysis is enabled.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.CryptoAnalysis.SbomFormatMetadataKey))
|
||||
{
|
||||
failures.Add("Scanner.Worker:CryptoAnalysis:SbomFormatMetadataKey must be provided when CryptoAnalysis is enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
if (options.AiMlSecurity.Enabled)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(options.AiMlSecurity.SbomPathMetadataKey))
|
||||
{
|
||||
failures.Add("Scanner.Worker:AiMlSecurity:SbomPathMetadataKey must be provided when AiMlSecurity is enabled.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.AiMlSecurity.SbomFormatMetadataKey))
|
||||
{
|
||||
failures.Add("Scanner.Worker:AiMlSecurity:SbomFormatMetadataKey must be provided when AiMlSecurity is enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
if (options.BuildProvenance.Enabled)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(options.BuildProvenance.SbomPathMetadataKey))
|
||||
{
|
||||
failures.Add("Scanner.Worker:BuildProvenance:SbomPathMetadataKey must be provided when BuildProvenance is enabled.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.BuildProvenance.SbomFormatMetadataKey))
|
||||
{
|
||||
failures.Add("Scanner.Worker:BuildProvenance:SbomFormatMetadataKey must be provided when BuildProvenance is enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
if (options.Reachability.Enabled)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(options.Reachability.SbomPathMetadataKey))
|
||||
{
|
||||
failures.Add("Scanner.Worker:Reachability:SbomPathMetadataKey must be provided when Reachability is enabled.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.Reachability.SbomFormatMetadataKey))
|
||||
{
|
||||
failures.Add("Scanner.Worker:Reachability:SbomFormatMetadataKey must be provided when Reachability is enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
if (options.Telemetry.EnableTelemetry)
|
||||
{
|
||||
if (!options.Telemetry.EnableMetrics && !options.Telemetry.EnableTracing)
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Concelier.SbomIntegration.Models;
|
||||
using StellaOps.Concelier.SbomIntegration.Parsing;
|
||||
using StellaOps.Scanner.AiMlSecurity;
|
||||
using StellaOps.Scanner.AiMlSecurity.Policy;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Worker.Options;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Processing.AiMlSecurity;
|
||||
|
||||
internal sealed class AiMlSecurityStageExecutor : IScanStageExecutor
|
||||
{
|
||||
private readonly IAiMlSecurityAnalyzer _analyzer;
|
||||
private readonly IAiGovernancePolicyLoader _policyLoader;
|
||||
private readonly IParsedSbomParser _parsedSbomParser;
|
||||
private readonly ISbomParser _sbomParser;
|
||||
private readonly ScannerWorkerOptions _options;
|
||||
private readonly ILogger<AiMlSecurityStageExecutor> _logger;
|
||||
|
||||
public AiMlSecurityStageExecutor(
|
||||
IAiMlSecurityAnalyzer analyzer,
|
||||
IAiGovernancePolicyLoader policyLoader,
|
||||
IParsedSbomParser parsedSbomParser,
|
||||
ISbomParser sbomParser,
|
||||
IOptions<ScannerWorkerOptions> options,
|
||||
ILogger<AiMlSecurityStageExecutor> logger)
|
||||
{
|
||||
_analyzer = analyzer ?? throw new ArgumentNullException(nameof(analyzer));
|
||||
_policyLoader = policyLoader ?? throw new ArgumentNullException(nameof(policyLoader));
|
||||
_parsedSbomParser = parsedSbomParser ?? throw new ArgumentNullException(nameof(parsedSbomParser));
|
||||
_sbomParser = sbomParser ?? throw new ArgumentNullException(nameof(sbomParser));
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public string StageName => ScanStageNames.AiMlSecurity;
|
||||
|
||||
public async ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
var options = _options.AiMlSecurity;
|
||||
if (!options.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sbomPath = ResolveSbomPath(context, options);
|
||||
if (string.IsNullOrWhiteSpace(sbomPath))
|
||||
{
|
||||
_logger.LogDebug("No SBOM path provided; skipping AI/ML analysis for job {JobId}.", context.JobId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(sbomPath))
|
||||
{
|
||||
_logger.LogWarning("SBOM path {Path} not found; skipping AI/ML analysis for job {JobId}.", sbomPath, context.JobId);
|
||||
return;
|
||||
}
|
||||
|
||||
var policy = await _policyLoader.LoadAsync(options.PolicyPath, cancellationToken).ConfigureAwait(false);
|
||||
policy = ApplyOptions(policy, options);
|
||||
|
||||
await using var stream = File.OpenRead(sbomPath);
|
||||
var format = ResolveFormat(context, options);
|
||||
if (format is null)
|
||||
{
|
||||
var detected = await _sbomParser.DetectFormatAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
if (!detected.IsDetected)
|
||||
{
|
||||
_logger.LogWarning("Unable to detect SBOM format for {Path}; skipping AI/ML analysis.", sbomPath);
|
||||
return;
|
||||
}
|
||||
|
||||
format = detected.Format;
|
||||
}
|
||||
|
||||
var parsed = await _parsedSbomParser.ParseAsync(stream, format.Value, cancellationToken).ConfigureAwait(false);
|
||||
var hasModelCards = parsed.Components.Any(component => component.ModelCard is not null);
|
||||
if (!hasModelCards)
|
||||
{
|
||||
_logger.LogDebug("SBOM at {Path} contains no model cards; skipping AI/ML analysis.", sbomPath);
|
||||
return;
|
||||
}
|
||||
|
||||
var report = await _analyzer.AnalyzeAsync(parsed.Components, policy, cancellationToken).ConfigureAwait(false);
|
||||
context.Analysis.Set(ScanAnalysisKeys.AiMlSecurityReport, report);
|
||||
context.Analysis.Set(ScanAnalysisKeys.AiMlPolicyVersion, policy.Version ?? "default");
|
||||
|
||||
_logger.LogInformation(
|
||||
"AI/ML analysis completed for job {JobId}: {FindingCount} findings.",
|
||||
context.JobId,
|
||||
report.Summary.TotalFindings);
|
||||
}
|
||||
|
||||
private static AiGovernancePolicy ApplyOptions(
|
||||
AiGovernancePolicy policy,
|
||||
ScannerWorkerOptions.AiMlSecurityOptions options)
|
||||
{
|
||||
if (options.RequireRiskAssessment && !policy.RequireRiskAssessment)
|
||||
{
|
||||
policy = policy with { RequireRiskAssessment = true };
|
||||
}
|
||||
|
||||
return policy;
|
||||
}
|
||||
|
||||
private static string? ResolveSbomPath(ScanJobContext context, ScannerWorkerOptions.AiMlSecurityOptions options)
|
||||
{
|
||||
return TryGetMetadata(context, options.SbomPathMetadataKey)
|
||||
?? TryGetMetadata(context, ScanMetadataKeys.SbomPath)
|
||||
?? TryGetMetadata(context, "sbomPath");
|
||||
}
|
||||
|
||||
private static SbomFormat? ResolveFormat(ScanJobContext context, ScannerWorkerOptions.AiMlSecurityOptions options)
|
||||
{
|
||||
var format = TryGetMetadata(context, options.SbomFormatMetadataKey)
|
||||
?? TryGetMetadata(context, ScanMetadataKeys.SbomFormat);
|
||||
if (string.IsNullOrWhiteSpace(format))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalized = format.Trim().ToLowerInvariant();
|
||||
if (normalized.Contains("spdx", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return SbomFormat.SPDX;
|
||||
}
|
||||
|
||||
return SbomFormat.CycloneDX;
|
||||
}
|
||||
|
||||
private static string? TryGetMetadata(ScanJobContext context, string? key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (context.Lease.Metadata.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value.Trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Concelier.SbomIntegration.Models;
|
||||
using StellaOps.Concelier.SbomIntegration.Parsing;
|
||||
using StellaOps.Scanner.BuildProvenance.Analyzers;
|
||||
using StellaOps.Scanner.BuildProvenance.Policy;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Worker.Options;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Processing.BuildProvenance;
|
||||
|
||||
internal sealed class BuildProvenanceStageExecutor : IScanStageExecutor
|
||||
{
|
||||
private readonly IBuildProvenanceVerifier _verifier;
|
||||
private readonly IBuildProvenancePolicyLoader _policyLoader;
|
||||
private readonly IParsedSbomParser _parsedSbomParser;
|
||||
private readonly ISbomParser _sbomParser;
|
||||
private readonly ScannerWorkerOptions _options;
|
||||
private readonly ILogger<BuildProvenanceStageExecutor> _logger;
|
||||
|
||||
public BuildProvenanceStageExecutor(
|
||||
IBuildProvenanceVerifier verifier,
|
||||
IBuildProvenancePolicyLoader policyLoader,
|
||||
IParsedSbomParser parsedSbomParser,
|
||||
ISbomParser sbomParser,
|
||||
IOptions<ScannerWorkerOptions> options,
|
||||
ILogger<BuildProvenanceStageExecutor> logger)
|
||||
{
|
||||
_verifier = verifier ?? throw new ArgumentNullException(nameof(verifier));
|
||||
_policyLoader = policyLoader ?? throw new ArgumentNullException(nameof(policyLoader));
|
||||
_parsedSbomParser = parsedSbomParser ?? throw new ArgumentNullException(nameof(parsedSbomParser));
|
||||
_sbomParser = sbomParser ?? throw new ArgumentNullException(nameof(sbomParser));
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public string StageName => ScanStageNames.BuildProvenance;
|
||||
|
||||
public async ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
var options = _options.BuildProvenance;
|
||||
if (!options.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sbomPath = ResolveSbomPath(context, options);
|
||||
if (string.IsNullOrWhiteSpace(sbomPath))
|
||||
{
|
||||
_logger.LogDebug("No SBOM path provided; skipping build provenance for job {JobId}.", context.JobId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(sbomPath))
|
||||
{
|
||||
_logger.LogWarning("SBOM path {Path} not found; skipping build provenance for job {JobId}.", sbomPath, context.JobId);
|
||||
return;
|
||||
}
|
||||
|
||||
var policy = await _policyLoader.LoadAsync(options.PolicyPath, cancellationToken).ConfigureAwait(false);
|
||||
policy = ApplyOptions(policy, options);
|
||||
|
||||
await using var stream = File.OpenRead(sbomPath);
|
||||
var format = ResolveFormat(context, options);
|
||||
if (format is null)
|
||||
{
|
||||
var detected = await _sbomParser.DetectFormatAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
if (!detected.IsDetected)
|
||||
{
|
||||
_logger.LogWarning("Unable to detect SBOM format for {Path}; skipping build provenance.", sbomPath);
|
||||
return;
|
||||
}
|
||||
|
||||
format = detected.Format;
|
||||
}
|
||||
|
||||
var parsed = await _parsedSbomParser.ParseAsync(stream, format.Value, cancellationToken).ConfigureAwait(false);
|
||||
if (parsed.BuildInfo is null && parsed.Formulation is null)
|
||||
{
|
||||
_logger.LogDebug("SBOM at {Path} contains no build provenance; skipping build provenance verification.", sbomPath);
|
||||
return;
|
||||
}
|
||||
|
||||
var report = await _verifier.VerifyAsync(parsed, policy, cancellationToken).ConfigureAwait(false);
|
||||
context.Analysis.Set(ScanAnalysisKeys.BuildProvenanceReport, report);
|
||||
context.Analysis.Set(ScanAnalysisKeys.BuildProvenancePolicyVersion, policy.Version ?? "default");
|
||||
|
||||
_logger.LogInformation(
|
||||
"Build provenance verification completed for job {JobId}: {FindingCount} findings.",
|
||||
context.JobId,
|
||||
report.Summary.TotalFindings);
|
||||
}
|
||||
|
||||
private static BuildProvenancePolicy ApplyOptions(
|
||||
BuildProvenancePolicy policy,
|
||||
ScannerWorkerOptions.BuildProvenanceOptions options)
|
||||
{
|
||||
var repro = policy.Reproducibility;
|
||||
if (!options.VerifyReproducibility && repro.VerifyOnDemand)
|
||||
{
|
||||
repro = repro with { VerifyOnDemand = false };
|
||||
}
|
||||
|
||||
if (options.RequireReproducible && !repro.RequireReproducible)
|
||||
{
|
||||
repro = repro with { RequireReproducible = true };
|
||||
}
|
||||
|
||||
return policy with { Reproducibility = repro };
|
||||
}
|
||||
|
||||
private static string? ResolveSbomPath(ScanJobContext context, ScannerWorkerOptions.BuildProvenanceOptions options)
|
||||
{
|
||||
return TryGetMetadata(context, options.SbomPathMetadataKey)
|
||||
?? TryGetMetadata(context, ScanMetadataKeys.SbomPath)
|
||||
?? TryGetMetadata(context, "sbomPath");
|
||||
}
|
||||
|
||||
private static SbomFormat? ResolveFormat(ScanJobContext context, ScannerWorkerOptions.BuildProvenanceOptions options)
|
||||
{
|
||||
var format = TryGetMetadata(context, options.SbomFormatMetadataKey)
|
||||
?? TryGetMetadata(context, ScanMetadataKeys.SbomFormat);
|
||||
if (string.IsNullOrWhiteSpace(format))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalized = format.Trim().ToLowerInvariant();
|
||||
if (normalized.Contains("spdx", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return SbomFormat.SPDX;
|
||||
}
|
||||
|
||||
return SbomFormat.CycloneDX;
|
||||
}
|
||||
|
||||
private static string? TryGetMetadata(ScanJobContext context, string? key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (context.Lease.Metadata.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value.Trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Concelier.SbomIntegration.Models;
|
||||
using StellaOps.Concelier.SbomIntegration.Parsing;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.CryptoAnalysis;
|
||||
using StellaOps.Scanner.CryptoAnalysis.Policy;
|
||||
using StellaOps.Scanner.Worker.Options;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Processing.CryptoAnalysis;
|
||||
|
||||
internal sealed class CryptoAnalysisStageExecutor : IScanStageExecutor
|
||||
{
|
||||
private readonly ICryptoAnalyzer _analyzer;
|
||||
private readonly ICryptoPolicyLoader _policyLoader;
|
||||
private readonly IParsedSbomParser _parsedSbomParser;
|
||||
private readonly ISbomParser _sbomParser;
|
||||
private readonly ScannerWorkerOptions _options;
|
||||
private readonly ILogger<CryptoAnalysisStageExecutor> _logger;
|
||||
|
||||
public CryptoAnalysisStageExecutor(
|
||||
ICryptoAnalyzer analyzer,
|
||||
ICryptoPolicyLoader policyLoader,
|
||||
IParsedSbomParser parsedSbomParser,
|
||||
ISbomParser sbomParser,
|
||||
IOptions<ScannerWorkerOptions> options,
|
||||
ILogger<CryptoAnalysisStageExecutor> logger)
|
||||
{
|
||||
_analyzer = analyzer ?? throw new ArgumentNullException(nameof(analyzer));
|
||||
_policyLoader = policyLoader ?? throw new ArgumentNullException(nameof(policyLoader));
|
||||
_parsedSbomParser = parsedSbomParser ?? throw new ArgumentNullException(nameof(parsedSbomParser));
|
||||
_sbomParser = sbomParser ?? throw new ArgumentNullException(nameof(sbomParser));
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public string StageName => ScanStageNames.CryptoAnalysis;
|
||||
|
||||
public async ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
var options = _options.CryptoAnalysis;
|
||||
if (!options.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sbomPath = ResolveSbomPath(context, options);
|
||||
if (string.IsNullOrWhiteSpace(sbomPath))
|
||||
{
|
||||
_logger.LogDebug("No SBOM path provided; skipping crypto analysis for job {JobId}.", context.JobId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(sbomPath))
|
||||
{
|
||||
_logger.LogWarning("SBOM path {Path} not found; skipping crypto analysis for job {JobId}.", sbomPath, context.JobId);
|
||||
return;
|
||||
}
|
||||
|
||||
var policy = await _policyLoader.LoadAsync(options.PolicyPath, cancellationToken).ConfigureAwait(false);
|
||||
policy = ApplyOptions(policy, options);
|
||||
|
||||
await using var stream = File.OpenRead(sbomPath);
|
||||
var format = ResolveFormat(context, options);
|
||||
if (format is null)
|
||||
{
|
||||
var detected = await _sbomParser.DetectFormatAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
if (!detected.IsDetected)
|
||||
{
|
||||
_logger.LogWarning("Unable to detect SBOM format for {Path}; skipping crypto analysis.", sbomPath);
|
||||
return;
|
||||
}
|
||||
|
||||
format = detected.Format;
|
||||
}
|
||||
|
||||
var parsed = await _parsedSbomParser.ParseAsync(stream, format.Value, cancellationToken).ConfigureAwait(false);
|
||||
var componentsWithCrypto = parsed.Components
|
||||
.Where(component => component.CryptoProperties is not null)
|
||||
.ToImmutableArray();
|
||||
if (componentsWithCrypto.IsDefaultOrEmpty)
|
||||
{
|
||||
_logger.LogDebug("SBOM at {Path} contains no crypto properties; skipping crypto analysis.", sbomPath);
|
||||
return;
|
||||
}
|
||||
|
||||
var report = await _analyzer.AnalyzeAsync(componentsWithCrypto, policy, cancellationToken).ConfigureAwait(false);
|
||||
context.Analysis.Set(ScanAnalysisKeys.CryptoAnalysisReport, report);
|
||||
context.Analysis.Set(ScanAnalysisKeys.CryptoPolicyVersion, policy.Version ?? "default");
|
||||
|
||||
_logger.LogInformation(
|
||||
"Crypto analysis completed for job {JobId}: {FindingCount} findings.",
|
||||
context.JobId,
|
||||
report.Summary.TotalFindings);
|
||||
}
|
||||
|
||||
private static CryptoPolicy ApplyOptions(CryptoPolicy policy, ScannerWorkerOptions.CryptoAnalysisOptions options)
|
||||
{
|
||||
if (options.RequireFips)
|
||||
{
|
||||
var frameworks = policy.ComplianceFrameworks.IsDefault
|
||||
? ImmutableArray<string>.Empty
|
||||
: policy.ComplianceFrameworks;
|
||||
|
||||
if (!frameworks.Any(framework => framework.Contains("FIPS", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
frameworks = frameworks.Add("FIPS-140-3");
|
||||
}
|
||||
|
||||
policy = policy with { ComplianceFrameworks = frameworks };
|
||||
}
|
||||
|
||||
if (options.EnablePostQuantumAnalysis && !policy.PostQuantum.Enabled)
|
||||
{
|
||||
policy = policy with { PostQuantum = policy.PostQuantum with { Enabled = true } };
|
||||
}
|
||||
|
||||
return policy;
|
||||
}
|
||||
|
||||
private static string? ResolveSbomPath(ScanJobContext context, ScannerWorkerOptions.CryptoAnalysisOptions options)
|
||||
{
|
||||
return TryGetMetadata(context, options.SbomPathMetadataKey)
|
||||
?? TryGetMetadata(context, ScanMetadataKeys.SbomPath)
|
||||
?? TryGetMetadata(context, "sbomPath");
|
||||
}
|
||||
|
||||
private static SbomFormat? ResolveFormat(ScanJobContext context, ScannerWorkerOptions.CryptoAnalysisOptions options)
|
||||
{
|
||||
var format = TryGetMetadata(context, options.SbomFormatMetadataKey)
|
||||
?? TryGetMetadata(context, ScanMetadataKeys.SbomFormat);
|
||||
if (string.IsNullOrWhiteSpace(format))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalized = format.Trim().ToLowerInvariant();
|
||||
if (normalized.Contains("spdx", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return SbomFormat.SPDX;
|
||||
}
|
||||
|
||||
return SbomFormat.CycloneDX;
|
||||
}
|
||||
|
||||
private static string? TryGetMetadata(ScanJobContext context, string? key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (context.Lease.Metadata.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value.Trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using StellaOps.Concelier.SbomIntegration;
|
||||
using StellaOps.Concelier.SbomIntegration.Models;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Processing.Reachability;
|
||||
|
||||
internal sealed class NullSbomAdvisoryMatcher : ISbomAdvisoryMatcher
|
||||
{
|
||||
public Task<IReadOnlyList<SbomAdvisoryMatch>> MatchAsync(
|
||||
Guid sbomId,
|
||||
string sbomDigest,
|
||||
IEnumerable<string> purls,
|
||||
IReadOnlyDictionary<string, bool>? reachabilityMap = null,
|
||||
IReadOnlyDictionary<string, bool>? deploymentMap = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<IReadOnlyList<SbomAdvisoryMatch>>(Array.Empty<SbomAdvisoryMatch>());
|
||||
|
||||
public Task<IReadOnlyList<Guid>> FindAffectingCanonicalIdsAsync(
|
||||
string purl,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<IReadOnlyList<Guid>>(Array.Empty<Guid>());
|
||||
|
||||
public Task<SbomAdvisoryMatch?> CheckMatchAsync(
|
||||
string purl,
|
||||
Guid canonicalId,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult<SbomAdvisoryMatch?>(null);
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Attestor;
|
||||
using StellaOps.Concelier.Core.Canonical;
|
||||
using StellaOps.Concelier.SbomIntegration;
|
||||
using StellaOps.Concelier.SbomIntegration.Models;
|
||||
using StellaOps.Concelier.SbomIntegration.Parsing;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Reachability.Dependencies;
|
||||
using StellaOps.Scanner.Reachability.Dependencies.Reporting;
|
||||
using StellaOps.Scanner.Worker.Options;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Processing.Reachability;
|
||||
|
||||
internal sealed class SbomReachabilityStageExecutor : IScanStageExecutor
|
||||
{
|
||||
private readonly IParsedSbomParser _parsedSbomParser;
|
||||
private readonly ISbomParser _sbomParser;
|
||||
private readonly IReachabilityPolicyLoader _policyLoader;
|
||||
private readonly ISbomAdvisoryMatcher _advisoryMatcher;
|
||||
private readonly DependencyReachabilityReporter _reporter;
|
||||
private readonly ICryptoHash _cryptoHash;
|
||||
private readonly ScannerWorkerOptions _options;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<SbomReachabilityStageExecutor> _logger;
|
||||
|
||||
public SbomReachabilityStageExecutor(
|
||||
IParsedSbomParser parsedSbomParser,
|
||||
ISbomParser sbomParser,
|
||||
IReachabilityPolicyLoader policyLoader,
|
||||
ISbomAdvisoryMatcher advisoryMatcher,
|
||||
DependencyReachabilityReporter reporter,
|
||||
ICryptoHash cryptoHash,
|
||||
IOptions<ScannerWorkerOptions> options,
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<SbomReachabilityStageExecutor> logger)
|
||||
{
|
||||
_parsedSbomParser = parsedSbomParser ?? throw new ArgumentNullException(nameof(parsedSbomParser));
|
||||
_sbomParser = sbomParser ?? throw new ArgumentNullException(nameof(sbomParser));
|
||||
_policyLoader = policyLoader ?? throw new ArgumentNullException(nameof(policyLoader));
|
||||
_advisoryMatcher = advisoryMatcher ?? throw new ArgumentNullException(nameof(advisoryMatcher));
|
||||
_reporter = reporter ?? throw new ArgumentNullException(nameof(reporter));
|
||||
_cryptoHash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash));
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public string StageName => ScanStageNames.ReachabilityAnalysis;
|
||||
|
||||
public async ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
var options = _options.Reachability;
|
||||
if (!options.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sbomPath = ResolveSbomPath(context, options);
|
||||
if (string.IsNullOrWhiteSpace(sbomPath))
|
||||
{
|
||||
_logger.LogDebug("No SBOM path provided; skipping reachability analysis for job {JobId}.", context.JobId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(sbomPath))
|
||||
{
|
||||
_logger.LogWarning("SBOM path {Path} not found; skipping reachability analysis for job {JobId}.", sbomPath, context.JobId);
|
||||
return;
|
||||
}
|
||||
|
||||
var policy = await _policyLoader.LoadAsync(options.PolicyPath, cancellationToken).ConfigureAwait(false);
|
||||
var sbomDigest = await ComputeDigestAsync(sbomPath, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await using var stream = File.OpenRead(sbomPath);
|
||||
var format = ResolveFormat(context, options);
|
||||
if (format is null)
|
||||
{
|
||||
var detected = await _sbomParser.DetectFormatAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
if (!detected.IsDetected)
|
||||
{
|
||||
_logger.LogWarning("Unable to detect SBOM format for {Path}; skipping reachability analysis.", sbomPath);
|
||||
return;
|
||||
}
|
||||
|
||||
format = detected.Format;
|
||||
}
|
||||
|
||||
var parsed = await _parsedSbomParser.ParseAsync(stream, format.Value, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var callGraph = await LoadCallGraphAsync(context, options, cancellationToken).ConfigureAwait(false);
|
||||
var combiner = new ReachGraphReachabilityCombiner();
|
||||
var reachabilityReport = combiner.Analyze(parsed, callGraph, policy);
|
||||
|
||||
var purlReachability = BuildPurlReachabilityMap(parsed, reachabilityReport.ComponentReachability);
|
||||
var matcherReachability = BuildReachabilityMapForMatcher(purlReachability, policy);
|
||||
|
||||
var purls = parsed.Components
|
||||
.Select(component => component.Purl)
|
||||
.Where(purl => !string.IsNullOrWhiteSpace(purl))
|
||||
.Select(purl => purl!.Trim())
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(purl => purl, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
IReadOnlyList<SbomAdvisoryMatch> matches = [];
|
||||
if (purls.Length > 0)
|
||||
{
|
||||
matches = await _advisoryMatcher.MatchAsync(
|
||||
Guid.Empty,
|
||||
sbomDigest,
|
||||
purls,
|
||||
matcherReachability,
|
||||
deploymentMap: null,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var advisorySummaries = await LoadAdvisorySummariesAsync(matches, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var severityMap = advisorySummaries.ToDictionary(
|
||||
entry => entry.Key,
|
||||
entry => entry.Value.Severity);
|
||||
|
||||
var filter = new VulnerabilityReachabilityFilter();
|
||||
var filterResult = filter.Apply(matches, purlReachability, policy, severityMap);
|
||||
|
||||
var report = _reporter.BuildReport(parsed, reachabilityReport, filterResult, advisorySummaries, policy);
|
||||
var graphViz = _reporter.ExportGraphViz(reachabilityReport.Graph, reachabilityReport.ComponentReachability, BuildComponentPurlLookup(parsed));
|
||||
var toolVersion = typeof(SbomReachabilityStageExecutor).Assembly.GetName().Version?.ToString() ?? "unknown";
|
||||
var sarif = await _reporter.ExportSarifAsync(
|
||||
report,
|
||||
toolVersion,
|
||||
includeFiltered: options.IncludeUnreachableVulnerabilities || policy.Reporting.ShowFilteredVulnerabilities,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
context.Analysis.Set(ScanAnalysisKeys.DependencyReachabilityReport, report);
|
||||
context.Analysis.Set(ScanAnalysisKeys.DependencyReachabilityDot, graphViz);
|
||||
context.Analysis.Set(ScanAnalysisKeys.DependencyReachabilitySarif, sarif);
|
||||
|
||||
var vulnerabilityMatches = BuildVulnerabilityMatches(
|
||||
filterResult,
|
||||
advisorySummaries,
|
||||
includeFiltered: options.IncludeUnreachableVulnerabilities);
|
||||
context.Analysis.Set(ScanAnalysisKeys.VulnerabilityMatches, vulnerabilityMatches);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Dependency reachability completed for job {JobId}: components={Total} reachable={Reachable} unreachable={Unreachable} vulnerabilities={Vulns} filtered={Filtered} reduction={Reduction:0.##}%.",
|
||||
context.JobId,
|
||||
report.Summary.ComponentStatistics.TotalComponents,
|
||||
report.Summary.ComponentStatistics.ReachableComponents,
|
||||
report.Summary.ComponentStatistics.UnreachableComponents,
|
||||
report.Summary.VulnerabilityStatistics.TotalVulnerabilities,
|
||||
report.Summary.VulnerabilityStatistics.FilteredVulnerabilities,
|
||||
report.Summary.FalsePositiveReductionPercent);
|
||||
}
|
||||
|
||||
private static List<VulnerabilityMatch> BuildVulnerabilityMatches(
|
||||
VulnerabilityReachabilityFilterResult filterResult,
|
||||
IReadOnlyDictionary<Guid, DependencyReachabilityAdvisorySummary> advisorySummaries,
|
||||
bool includeFiltered)
|
||||
{
|
||||
var matches = new List<VulnerabilityMatch>();
|
||||
foreach (var adjustment in filterResult.Adjustments)
|
||||
{
|
||||
if (adjustment.IsFiltered && !includeFiltered)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
advisorySummaries.TryGetValue(adjustment.Match.CanonicalId, out var advisory);
|
||||
var severity = adjustment.AdjustedSeverity
|
||||
?? advisory?.Severity
|
||||
?? adjustment.OriginalSeverity
|
||||
?? "unknown";
|
||||
var vulnId = advisory?.VulnerabilityId ?? adjustment.Match.CanonicalId.ToString();
|
||||
var isReachable = adjustment.EffectiveStatus is ReachabilityStatus.Reachable or
|
||||
ReachabilityStatus.PotentiallyReachable;
|
||||
|
||||
matches.Add(new VulnerabilityMatch(
|
||||
VulnId: vulnId,
|
||||
ComponentRef: adjustment.Match.Purl,
|
||||
IsReachable: isReachable,
|
||||
Severity: severity));
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
private static Dictionary<string, string?> BuildComponentPurlLookup(ParsedSbom sbom)
|
||||
{
|
||||
var lookup = new Dictionary<string, string?>(StringComparer.Ordinal);
|
||||
foreach (var component in sbom.Components)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(component.BomRef))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
lookup[component.BomRef] = component.Purl;
|
||||
}
|
||||
|
||||
return lookup;
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyDictionary<Guid, DependencyReachabilityAdvisorySummary>> LoadAdvisorySummariesAsync(
|
||||
IReadOnlyList<SbomAdvisoryMatch> matches,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (matches.Count == 0)
|
||||
{
|
||||
return new Dictionary<Guid, DependencyReachabilityAdvisorySummary>();
|
||||
}
|
||||
|
||||
var canonicalService = _serviceProvider.GetService<ICanonicalAdvisoryService>();
|
||||
if (canonicalService is null)
|
||||
{
|
||||
return new Dictionary<Guid, DependencyReachabilityAdvisorySummary>();
|
||||
}
|
||||
|
||||
var summaries = new Dictionary<Guid, DependencyReachabilityAdvisorySummary>();
|
||||
foreach (var canonicalId in matches.Select(match => match.CanonicalId).Distinct().OrderBy(id => id))
|
||||
{
|
||||
var advisory = await canonicalService.GetByIdAsync(canonicalId, cancellationToken).ConfigureAwait(false);
|
||||
if (advisory is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
summaries[canonicalId] = new DependencyReachabilityAdvisorySummary
|
||||
{
|
||||
CanonicalId = canonicalId,
|
||||
VulnerabilityId = advisory.Cve,
|
||||
Severity = advisory.Severity,
|
||||
Title = advisory.Title,
|
||||
Summary = advisory.Summary,
|
||||
AffectedVersions = advisory.VersionRange?.RangeExpression
|
||||
};
|
||||
}
|
||||
|
||||
return summaries;
|
||||
}
|
||||
|
||||
private async Task<string> ComputeDigestAsync(string sbomPath, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var digestStream = File.OpenRead(sbomPath);
|
||||
var hex = await _cryptoHash.ComputeHashHexAsync(digestStream, HashAlgorithms.Sha256, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return $"sha256:{hex}";
|
||||
}
|
||||
|
||||
private static Dictionary<string, ReachabilityStatus> BuildPurlReachabilityMap(
|
||||
ParsedSbom sbom,
|
||||
IReadOnlyDictionary<string, ReachabilityStatus> componentReachability)
|
||||
{
|
||||
var map = new Dictionary<string, ReachabilityStatus>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var component in sbom.Components)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(component.Purl) || string.IsNullOrWhiteSpace(component.BomRef))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var status = componentReachability.TryGetValue(component.BomRef, out var value)
|
||||
? value
|
||||
: ReachabilityStatus.Unknown;
|
||||
|
||||
if (map.TryGetValue(component.Purl, out var existing))
|
||||
{
|
||||
map[component.Purl] = MaxStatus(existing, status);
|
||||
}
|
||||
else
|
||||
{
|
||||
map[component.Purl] = status;
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
private static Dictionary<string, bool> BuildReachabilityMapForMatcher(
|
||||
IReadOnlyDictionary<string, ReachabilityStatus> purlReachability,
|
||||
ReachabilityPolicy policy)
|
||||
{
|
||||
var map = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var entry in purlReachability.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
||||
{
|
||||
var effective = entry.Value == ReachabilityStatus.Unknown
|
||||
? policy.Confidence.MarkUnknownAs
|
||||
: entry.Value;
|
||||
map[entry.Key] = effective is ReachabilityStatus.Reachable or ReachabilityStatus.PotentiallyReachable;
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
private static ReachabilityStatus MaxStatus(ReachabilityStatus left, ReachabilityStatus right)
|
||||
{
|
||||
static int Rank(ReachabilityStatus status) => status switch
|
||||
{
|
||||
ReachabilityStatus.Reachable => 3,
|
||||
ReachabilityStatus.PotentiallyReachable => 2,
|
||||
ReachabilityStatus.Unknown => 1,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
return Rank(left) >= Rank(right) ? left : right;
|
||||
}
|
||||
|
||||
private async Task<RichGraph?> LoadCallGraphAsync(
|
||||
ScanJobContext context,
|
||||
ScannerWorkerOptions.ReachabilityOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var path = ResolveCallGraphPath(context, options);
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
_logger.LogWarning("Call graph path {Path} not found; reachability analysis will be SBOM-only.", path);
|
||||
return null;
|
||||
}
|
||||
|
||||
await using var stream = File.OpenRead(path);
|
||||
var reader = new RichGraphReader();
|
||||
try
|
||||
{
|
||||
return await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to read call graph from {Path}; reachability analysis will be SBOM-only.", path);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ResolveSbomPath(ScanJobContext context, ScannerWorkerOptions.ReachabilityOptions options)
|
||||
{
|
||||
return TryGetMetadata(context, options.SbomPathMetadataKey)
|
||||
?? TryGetMetadata(context, ScanMetadataKeys.SbomPath)
|
||||
?? TryGetMetadata(context, "sbomPath");
|
||||
}
|
||||
|
||||
private static string? ResolveCallGraphPath(ScanJobContext context, ScannerWorkerOptions.ReachabilityOptions options)
|
||||
{
|
||||
return TryGetMetadata(context, options.CallGraphPathMetadataKey)
|
||||
?? TryGetMetadata(context, ScanMetadataKeys.ReachabilityCallGraphPath)
|
||||
?? TryGetMetadata(context, "reachability.callgraph.path")
|
||||
?? TryGetMetadata(context, "reachability.richgraph.path");
|
||||
}
|
||||
|
||||
private static SbomFormat? ResolveFormat(ScanJobContext context, ScannerWorkerOptions.ReachabilityOptions options)
|
||||
{
|
||||
var format = TryGetMetadata(context, options.SbomFormatMetadataKey)
|
||||
?? TryGetMetadata(context, ScanMetadataKeys.SbomFormat);
|
||||
if (string.IsNullOrWhiteSpace(format))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalized = format.Trim().ToLowerInvariant();
|
||||
if (normalized.Contains("spdx", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return SbomFormat.SPDX;
|
||||
}
|
||||
|
||||
return SbomFormat.CycloneDX;
|
||||
}
|
||||
|
||||
private static string? TryGetMetadata(ScanJobContext context, string? key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (context.Lease.Metadata.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value.Trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,16 @@ public static class ScanStageNames
|
||||
public const string PullLayers = "pull-layers";
|
||||
public const string BuildFilesystem = "build-filesystem";
|
||||
public const string ExecuteAnalyzers = "execute-analyzers";
|
||||
// Sprint: SPRINT_20260119_016 - Service Security Analysis
|
||||
public const string ServiceSecurity = "service-security";
|
||||
// Sprint: SPRINT_20260119_017 - CBOM Crypto Analysis
|
||||
public const string CryptoAnalysis = "crypto-analysis";
|
||||
// Sprint: SPRINT_20260119_018 - AI/ML Supply Chain Security
|
||||
public const string AiMlSecurity = "ai-ml-security";
|
||||
// Sprint: SPRINT_20260119_019 - Build Provenance Verification
|
||||
public const string BuildProvenance = "build-provenance";
|
||||
// Sprint: SPRINT_20260119_022 - Dependency Reachability
|
||||
public const string ReachabilityAnalysis = "reachability-analysis";
|
||||
public const string EpssEnrichment = "epss-enrichment";
|
||||
public const string ComposeArtifacts = "compose-artifacts";
|
||||
public const string EmitReports = "emit-reports";
|
||||
@@ -36,9 +46,14 @@ public static class ScanStageNames
|
||||
PullLayers,
|
||||
BuildFilesystem,
|
||||
ExecuteAnalyzers,
|
||||
ServiceSecurity,
|
||||
CryptoAnalysis,
|
||||
AiMlSecurity,
|
||||
BuildProvenance,
|
||||
ScanSecrets,
|
||||
BinaryLookup,
|
||||
EpssEnrichment,
|
||||
ReachabilityAnalysis,
|
||||
VexGate,
|
||||
ComposeArtifacts,
|
||||
Entropy,
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Concelier.SbomIntegration.Models;
|
||||
using StellaOps.Concelier.SbomIntegration.Parsing;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.ServiceSecurity;
|
||||
using StellaOps.Scanner.ServiceSecurity.Policy;
|
||||
using StellaOps.Scanner.Worker.Options;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Processing.ServiceSecurity;
|
||||
|
||||
internal sealed class ServiceSecurityStageExecutor : IScanStageExecutor
|
||||
{
|
||||
private readonly IServiceSecurityAnalyzer _analyzer;
|
||||
private readonly IServiceSecurityPolicyLoader _policyLoader;
|
||||
private readonly IParsedSbomParser _parsedSbomParser;
|
||||
private readonly ISbomParser _sbomParser;
|
||||
private readonly ScannerWorkerOptions _options;
|
||||
private readonly ILogger<ServiceSecurityStageExecutor> _logger;
|
||||
|
||||
public ServiceSecurityStageExecutor(
|
||||
IServiceSecurityAnalyzer analyzer,
|
||||
IServiceSecurityPolicyLoader policyLoader,
|
||||
IParsedSbomParser parsedSbomParser,
|
||||
ISbomParser sbomParser,
|
||||
IOptions<ScannerWorkerOptions> options,
|
||||
ILogger<ServiceSecurityStageExecutor> logger)
|
||||
{
|
||||
_analyzer = analyzer ?? throw new ArgumentNullException(nameof(analyzer));
|
||||
_policyLoader = policyLoader ?? throw new ArgumentNullException(nameof(policyLoader));
|
||||
_parsedSbomParser = parsedSbomParser ?? throw new ArgumentNullException(nameof(parsedSbomParser));
|
||||
_sbomParser = sbomParser ?? throw new ArgumentNullException(nameof(sbomParser));
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public string StageName => ScanStageNames.ServiceSecurity;
|
||||
|
||||
public async ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
var options = _options.ServiceSecurity;
|
||||
if (!options.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sbomPath = ResolveSbomPath(context, options);
|
||||
if (string.IsNullOrWhiteSpace(sbomPath))
|
||||
{
|
||||
_logger.LogDebug("No SBOM path provided; skipping service security analysis for job {JobId}.", context.JobId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(sbomPath))
|
||||
{
|
||||
_logger.LogWarning("SBOM path {Path} not found; skipping service security analysis for job {JobId}.", sbomPath, context.JobId);
|
||||
return;
|
||||
}
|
||||
|
||||
var policy = await _policyLoader.LoadAsync(options.PolicyPath, cancellationToken).ConfigureAwait(false);
|
||||
await using var stream = File.OpenRead(sbomPath);
|
||||
|
||||
var format = ResolveFormat(context, options);
|
||||
if (format is null)
|
||||
{
|
||||
var detected = await _sbomParser.DetectFormatAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
if (!detected.IsDetected)
|
||||
{
|
||||
_logger.LogWarning("Unable to detect SBOM format for {Path}; skipping service analysis.", sbomPath);
|
||||
return;
|
||||
}
|
||||
|
||||
format = detected.Format;
|
||||
}
|
||||
|
||||
var parsed = await _parsedSbomParser.ParseAsync(stream, format.Value, cancellationToken).ConfigureAwait(false);
|
||||
if (parsed.Services.IsDefaultOrEmpty)
|
||||
{
|
||||
_logger.LogDebug("SBOM at {Path} contains no services; skipping service analysis.", sbomPath);
|
||||
return;
|
||||
}
|
||||
|
||||
var report = await _analyzer.AnalyzeAsync(parsed.Services, policy, cancellationToken).ConfigureAwait(false);
|
||||
context.Analysis.Set(ScanAnalysisKeys.ServiceSecurityReport, report);
|
||||
context.Analysis.Set(ScanAnalysisKeys.ServiceSecurityPolicyVersion, policy.Version ?? "default");
|
||||
|
||||
_logger.LogInformation(
|
||||
"Service security analysis completed for job {JobId}: {FindingCount} findings.",
|
||||
context.JobId,
|
||||
report.Summary.TotalFindings);
|
||||
}
|
||||
|
||||
private static string? ResolveSbomPath(ScanJobContext context, ScannerWorkerOptions.ServiceSecurityOptions options)
|
||||
{
|
||||
return TryGetMetadata(context, options.SbomPathMetadataKey)
|
||||
?? TryGetMetadata(context, ScanMetadataKeys.SbomPath)
|
||||
?? TryGetMetadata(context, "sbomPath");
|
||||
}
|
||||
|
||||
private static SbomFormat? ResolveFormat(ScanJobContext context, ScannerWorkerOptions.ServiceSecurityOptions options)
|
||||
{
|
||||
var format = TryGetMetadata(context, options.SbomFormatMetadataKey)
|
||||
?? TryGetMetadata(context, ScanMetadataKeys.SbomFormat);
|
||||
if (string.IsNullOrWhiteSpace(format))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalized = format.Trim().ToLowerInvariant();
|
||||
if (normalized.Contains("spdx", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return SbomFormat.SPDX;
|
||||
}
|
||||
|
||||
return SbomFormat.CycloneDX;
|
||||
}
|
||||
|
||||
private static string? TryGetMetadata(ScanJobContext context, string? key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (context.Lease.Metadata.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value.Trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -325,6 +325,8 @@ internal sealed class SurfaceManifestPublisher : ISurfaceManifestPublisher
|
||||
ArtifactDocumentFormat.EntryTraceGraphJson => "entrytrace.graph",
|
||||
ArtifactDocumentFormat.ComponentFragmentJson => "layer.fragments",
|
||||
ArtifactDocumentFormat.ObservationJson => "observation.json",
|
||||
ArtifactDocumentFormat.SarifJson => "sarif-json",
|
||||
ArtifactDocumentFormat.GraphVizDot => "graphviz-dot",
|
||||
ArtifactDocumentFormat.SurfaceManifestJson => "surface.manifest",
|
||||
ArtifactDocumentFormat.CompositionRecipeJson => "composition.recipe",
|
||||
ArtifactDocumentFormat.CycloneDxJson => "cdx-json",
|
||||
|
||||
@@ -10,11 +10,17 @@ using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Canonical.Json;
|
||||
using StellaOps.Scanner.AiMlSecurity.Models;
|
||||
using StellaOps.Scanner.CryptoAnalysis.Models;
|
||||
using StellaOps.Scanner.BuildProvenance.Models;
|
||||
using StellaOps.Scanner.Analyzers.Lang;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Core.Entropy;
|
||||
using StellaOps.Scanner.EntryTrace;
|
||||
using StellaOps.Scanner.EntryTrace.Serialization;
|
||||
using StellaOps.Scanner.Reachability.Dependencies.Reporting;
|
||||
using StellaOps.Scanner.Sarif.Models;
|
||||
using StellaOps.Scanner.ServiceSecurity.Models;
|
||||
using StellaOps.Scanner.Surface.Env;
|
||||
using StellaOps.Scanner.Surface.FS;
|
||||
using StellaOps.Scanner.Storage.Catalog;
|
||||
@@ -255,6 +261,177 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
|
||||
}));
|
||||
}
|
||||
|
||||
if (context.Analysis.TryGet<ServiceSecurityReport>(ScanAnalysisKeys.ServiceSecurityReport, out var serviceReport)
|
||||
&& serviceReport is not null)
|
||||
{
|
||||
var reportBytes = SerializeCanonical(serviceReport);
|
||||
var metadata = new Dictionary<string, string>
|
||||
{
|
||||
["findingCount"] = serviceReport.Summary.TotalFindings.ToString(CultureInfoInvariant)
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(serviceReport.PolicyVersion))
|
||||
{
|
||||
metadata["policyVersion"] = serviceReport.PolicyVersion!;
|
||||
}
|
||||
|
||||
payloads.Add(new SurfaceManifestPayload(
|
||||
ArtifactDocumentType.SurfaceObservation,
|
||||
ArtifactDocumentFormat.ObservationJson,
|
||||
Kind: "service-security.report",
|
||||
MediaType: "application/json",
|
||||
Content: reportBytes,
|
||||
View: "service-security",
|
||||
Metadata: metadata));
|
||||
}
|
||||
|
||||
if (context.Analysis.TryGet<CryptoAnalysisReport>(ScanAnalysisKeys.CryptoAnalysisReport, out var cryptoReport)
|
||||
&& cryptoReport is not null)
|
||||
{
|
||||
var reportBytes = SerializeCanonical(cryptoReport);
|
||||
var metadata = new Dictionary<string, string>
|
||||
{
|
||||
["findingCount"] = cryptoReport.Summary.TotalFindings.ToString(CultureInfoInvariant)
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(cryptoReport.PolicyVersion))
|
||||
{
|
||||
metadata["policyVersion"] = cryptoReport.PolicyVersion!;
|
||||
}
|
||||
|
||||
if (!cryptoReport.ComplianceStatus.Frameworks.IsDefaultOrEmpty)
|
||||
{
|
||||
metadata["frameworks"] = string.Join(
|
||||
",",
|
||||
cryptoReport.ComplianceStatus.Frameworks
|
||||
.Select(framework => framework.Framework)
|
||||
.OrderBy(framework => framework, StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (cryptoReport.QuantumReadiness.TotalAlgorithms > 0)
|
||||
{
|
||||
metadata["quantumReadinessScore"] = cryptoReport.QuantumReadiness.Score.ToString(CultureInfoInvariant);
|
||||
}
|
||||
|
||||
payloads.Add(new SurfaceManifestPayload(
|
||||
ArtifactDocumentType.SurfaceObservation,
|
||||
ArtifactDocumentFormat.ObservationJson,
|
||||
Kind: "crypto-analysis.report",
|
||||
MediaType: "application/json",
|
||||
Content: reportBytes,
|
||||
View: "crypto-analysis",
|
||||
Metadata: metadata));
|
||||
}
|
||||
|
||||
if (context.Analysis.TryGet<AiMlSecurityReport>(ScanAnalysisKeys.AiMlSecurityReport, out var aiReport)
|
||||
&& aiReport is not null)
|
||||
{
|
||||
var reportBytes = SerializeCanonical(aiReport);
|
||||
var metadata = new Dictionary<string, string>
|
||||
{
|
||||
["findingCount"] = aiReport.Summary.TotalFindings.ToString(CultureInfoInvariant),
|
||||
["modelCount"] = aiReport.Summary.ModelCount.ToString(CultureInfoInvariant),
|
||||
["datasetCount"] = aiReport.Summary.DatasetCount.ToString(CultureInfoInvariant)
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(aiReport.PolicyVersion))
|
||||
{
|
||||
metadata["policyVersion"] = aiReport.PolicyVersion!;
|
||||
}
|
||||
|
||||
if (!aiReport.ComplianceStatus.Frameworks.IsDefaultOrEmpty)
|
||||
{
|
||||
metadata["frameworks"] = string.Join(
|
||||
",",
|
||||
aiReport.ComplianceStatus.Frameworks
|
||||
.Select(framework => framework.Framework)
|
||||
.OrderBy(framework => framework, StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
payloads.Add(new SurfaceManifestPayload(
|
||||
ArtifactDocumentType.SurfaceObservation,
|
||||
ArtifactDocumentFormat.ObservationJson,
|
||||
Kind: "ai-ml-security.report",
|
||||
MediaType: "application/json",
|
||||
Content: reportBytes,
|
||||
View: "ai-ml-security",
|
||||
Metadata: metadata));
|
||||
}
|
||||
|
||||
if (context.Analysis.TryGet<BuildProvenanceReport>(ScanAnalysisKeys.BuildProvenanceReport, out var provenanceReport)
|
||||
&& provenanceReport is not null)
|
||||
{
|
||||
var reportBytes = SerializeCanonical(provenanceReport);
|
||||
var metadata = new Dictionary<string, string>
|
||||
{
|
||||
["findingCount"] = provenanceReport.Summary.TotalFindings.ToString(CultureInfoInvariant),
|
||||
["slsaLevel"] = provenanceReport.AchievedLevel.ToString(),
|
||||
["reproducibility"] = provenanceReport.ReproducibilityStatus.State.ToString()
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(provenanceReport.PolicyVersion))
|
||||
{
|
||||
metadata["policyVersion"] = provenanceReport.PolicyVersion!;
|
||||
}
|
||||
|
||||
payloads.Add(new SurfaceManifestPayload(
|
||||
ArtifactDocumentType.SurfaceObservation,
|
||||
ArtifactDocumentFormat.ObservationJson,
|
||||
Kind: "build-provenance.report",
|
||||
MediaType: "application/json",
|
||||
Content: reportBytes,
|
||||
View: "build-provenance",
|
||||
Metadata: metadata));
|
||||
}
|
||||
|
||||
if (context.Analysis.TryGet<DependencyReachabilityReport>(ScanAnalysisKeys.DependencyReachabilityReport, out var reachabilityReport)
|
||||
&& reachabilityReport is not null)
|
||||
{
|
||||
var reportBytes = SerializeCanonical(reachabilityReport);
|
||||
var metadata = new Dictionary<string, string>
|
||||
{
|
||||
["componentCount"] = reachabilityReport.Summary.ComponentStatistics.TotalComponents.ToString(CultureInfoInvariant),
|
||||
["vulnerabilityCount"] = reachabilityReport.Summary.VulnerabilityStatistics.TotalVulnerabilities.ToString(CultureInfoInvariant),
|
||||
["filteredCount"] = reachabilityReport.Summary.VulnerabilityStatistics.FilteredVulnerabilities.ToString(CultureInfoInvariant),
|
||||
["reductionPercent"] = reachabilityReport.Summary.FalsePositiveReductionPercent.ToString("0.####", CultureInfoInvariant),
|
||||
["analysisMode"] = reachabilityReport.AnalysisMode.ToString()
|
||||
};
|
||||
|
||||
payloads.Add(new SurfaceManifestPayload(
|
||||
ArtifactDocumentType.SurfaceObservation,
|
||||
ArtifactDocumentFormat.ObservationJson,
|
||||
Kind: "reachability.report",
|
||||
MediaType: "application/json",
|
||||
Content: reportBytes,
|
||||
View: "reachability",
|
||||
Metadata: metadata));
|
||||
}
|
||||
|
||||
if (context.Analysis.TryGet<SarifLog>(ScanAnalysisKeys.DependencyReachabilitySarif, out var reachabilitySarif)
|
||||
&& reachabilitySarif is not null)
|
||||
{
|
||||
var sarifBytes = SerializeCanonical(reachabilitySarif);
|
||||
payloads.Add(new SurfaceManifestPayload(
|
||||
ArtifactDocumentType.SurfaceObservation,
|
||||
ArtifactDocumentFormat.SarifJson,
|
||||
Kind: "reachability.report.sarif",
|
||||
MediaType: "application/sarif+json",
|
||||
Content: sarifBytes,
|
||||
View: "reachability"));
|
||||
}
|
||||
|
||||
if (context.Analysis.TryGet<string>(ScanAnalysisKeys.DependencyReachabilityDot, out var reachabilityDot)
|
||||
&& !string.IsNullOrWhiteSpace(reachabilityDot))
|
||||
{
|
||||
payloads.Add(new SurfaceManifestPayload(
|
||||
ArtifactDocumentType.SurfaceObservation,
|
||||
ArtifactDocumentFormat.GraphVizDot,
|
||||
Kind: "reachability.graph.dot",
|
||||
MediaType: "text/vnd.graphviz",
|
||||
Content: Encoding.UTF8.GetBytes(reachabilityDot),
|
||||
View: "reachability"));
|
||||
}
|
||||
|
||||
return payloads;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Attestor;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Gate;
|
||||
using StellaOps.Scanner.Worker.Metrics;
|
||||
@@ -140,6 +141,19 @@ public sealed class VexGateStageExecutor : IScanStageExecutor
|
||||
}
|
||||
}
|
||||
|
||||
if (context.Analysis.TryGet<IReadOnlyList<VulnerabilityMatch>>(ScanAnalysisKeys.VulnerabilityMatches, out var matches)
|
||||
&& matches is not null)
|
||||
{
|
||||
foreach (var match in matches)
|
||||
{
|
||||
var gateFinding = ConvertToGateFinding(match);
|
||||
if (gateFinding is not null)
|
||||
{
|
||||
findings.Add(gateFinding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return findings;
|
||||
}
|
||||
|
||||
@@ -215,6 +229,7 @@ public sealed class VexGateStageExecutor : IScanStageExecutor
|
||||
else
|
||||
{
|
||||
var vulnIdProperty = findingType.GetProperty("VulnerabilityId");
|
||||
vulnIdProperty ??= findingType.GetProperty("VulnId");
|
||||
if (vulnIdProperty?.GetValue(finding) is string vid && !string.IsNullOrWhiteSpace(vid))
|
||||
{
|
||||
vulnId = vid;
|
||||
@@ -242,6 +257,15 @@ public sealed class VexGateStageExecutor : IScanStageExecutor
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(purl))
|
||||
{
|
||||
var componentRefProperty = findingType.GetProperty("ComponentRef");
|
||||
if (componentRefProperty?.GetValue(finding) is string componentRef)
|
||||
{
|
||||
purl = componentRef;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract finding ID
|
||||
string findingId;
|
||||
var idProperty = findingType.GetProperty("FindingId") ?? findingType.GetProperty("Id");
|
||||
|
||||
@@ -6,9 +6,13 @@ using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Auth.Client;
|
||||
using StellaOps.Concelier.SbomIntegration;
|
||||
using StellaOps.Concelier.SbomIntegration.Parsing;
|
||||
using StellaOps.Configuration;
|
||||
using StellaOps.Scanner.Cache;
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Reachability.Dependencies;
|
||||
using StellaOps.Scanner.Reachability.Dependencies.Reporting;
|
||||
using StellaOps.Scanner.Reachability.Gates;
|
||||
using StellaOps.Scanner.Analyzers.OS.Plugin;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Plugin;
|
||||
@@ -22,12 +26,18 @@ using StellaOps.Scanner.Surface.Env;
|
||||
using StellaOps.Scanner.Surface.FS;
|
||||
using StellaOps.Scanner.Surface.Secrets;
|
||||
using StellaOps.Scanner.Surface.Validation;
|
||||
using StellaOps.Scanner.CryptoAnalysis;
|
||||
using StellaOps.Scanner.ServiceSecurity;
|
||||
using StellaOps.Scanner.Worker.Diagnostics;
|
||||
using StellaOps.Scanner.Worker.Hosting;
|
||||
using StellaOps.Scanner.Worker.Options;
|
||||
using StellaOps.Scanner.Worker.Processing;
|
||||
using StellaOps.Scanner.Worker.Processing.AiMlSecurity;
|
||||
using StellaOps.Scanner.Worker.Processing.BuildProvenance;
|
||||
using StellaOps.Scanner.Worker.Processing.Entropy;
|
||||
using StellaOps.Scanner.Worker.Processing.Secrets;
|
||||
using StellaOps.Scanner.Worker.Processing.ServiceSecurity;
|
||||
using StellaOps.Scanner.Worker.Processing.CryptoAnalysis;
|
||||
using StellaOps.Scanner.Worker.Determinism;
|
||||
using StellaOps.Scanner.Analyzers.Secrets;
|
||||
using StellaOps.Scanner.Worker.Extensions;
|
||||
@@ -35,6 +45,10 @@ using StellaOps.Scanner.Worker.Processing.Surface;
|
||||
using StellaOps.Scanner.Storage.Extensions;
|
||||
using StellaOps.Scanner.Storage;
|
||||
using StellaOps.Scanner.Storage.Services;
|
||||
using StellaOps.BinaryIndex.ML;
|
||||
using StellaOps.Scanner.AiMlSecurity;
|
||||
using StellaOps.Scanner.BuildProvenance;
|
||||
using StellaOps.Scanner.Sarif;
|
||||
using Reachability = StellaOps.Scanner.Worker.Processing.Reachability;
|
||||
using ReachabilityEvidenceStageExecutor = StellaOps.Scanner.Worker.Processing.Reachability.ReachabilityEvidenceStageExecutor;
|
||||
using GateDetectors = StellaOps.Scanner.Reachability.Gates.Detectors;
|
||||
@@ -177,6 +191,58 @@ builder.Services.AddSingleton<IScanStageExecutor, Reachability.ReachabilityBuild
|
||||
builder.Services.AddSingleton<IScanStageExecutor, Reachability.ReachabilityPublishStageExecutor>();
|
||||
builder.Services.AddSingleton<IScanStageExecutor, EntropyStageExecutor>();
|
||||
|
||||
// Service Security Analysis (Sprint: SPRINT_20260119_016)
|
||||
if (workerOptions.ServiceSecurity.Enabled)
|
||||
{
|
||||
builder.Services.TryAddSingleton<ISbomParser, SbomParser>();
|
||||
builder.Services.TryAddSingleton<IParsedSbomParser, ParsedSbomParser>();
|
||||
builder.Services.AddServiceSecurity();
|
||||
builder.Services.AddSingleton<IScanStageExecutor, ServiceSecurityStageExecutor>();
|
||||
}
|
||||
|
||||
// CBOM Crypto Analysis (Sprint: SPRINT_20260119_017)
|
||||
if (workerOptions.CryptoAnalysis.Enabled)
|
||||
{
|
||||
builder.Services.TryAddSingleton<ISbomParser, SbomParser>();
|
||||
builder.Services.TryAddSingleton<IParsedSbomParser, ParsedSbomParser>();
|
||||
builder.Services.AddCryptoAnalysis();
|
||||
builder.Services.AddSingleton<IScanStageExecutor, CryptoAnalysisStageExecutor>();
|
||||
}
|
||||
|
||||
// AI/ML Supply Chain Security (Sprint: SPRINT_20260119_018)
|
||||
if (workerOptions.AiMlSecurity.Enabled)
|
||||
{
|
||||
builder.Services.TryAddSingleton<ISbomParser, SbomParser>();
|
||||
builder.Services.TryAddSingleton<IParsedSbomParser, ParsedSbomParser>();
|
||||
builder.Services.AddAiMlSecurity();
|
||||
if (workerOptions.AiMlSecurity.EnableBinaryAnalysis)
|
||||
{
|
||||
builder.Services.AddMlServices();
|
||||
}
|
||||
builder.Services.AddSingleton<IScanStageExecutor, AiMlSecurityStageExecutor>();
|
||||
}
|
||||
|
||||
// Build Provenance Verification (Sprint: SPRINT_20260119_019)
|
||||
if (workerOptions.BuildProvenance.Enabled)
|
||||
{
|
||||
builder.Services.TryAddSingleton<ISbomParser, SbomParser>();
|
||||
builder.Services.TryAddSingleton<IParsedSbomParser, ParsedSbomParser>();
|
||||
builder.Services.AddBuildProvenance();
|
||||
builder.Services.AddSingleton<IScanStageExecutor, BuildProvenanceStageExecutor>();
|
||||
}
|
||||
|
||||
// SBOM Dependency Reachability (Sprint: SPRINT_20260119_022)
|
||||
if (workerOptions.Reachability.Enabled)
|
||||
{
|
||||
builder.Services.TryAddSingleton<ISbomParser, SbomParser>();
|
||||
builder.Services.TryAddSingleton<IParsedSbomParser, ParsedSbomParser>();
|
||||
builder.Services.TryAddSingleton<IReachabilityPolicyLoader, ReachabilityPolicyLoader>();
|
||||
builder.Services.TryAddSingleton<ISbomAdvisoryMatcher, Reachability.NullSbomAdvisoryMatcher>();
|
||||
builder.Services.TryAddSingleton<ISarifExportService, SarifExportService>();
|
||||
builder.Services.TryAddSingleton<DependencyReachabilityReporter>();
|
||||
builder.Services.AddSingleton<IScanStageExecutor, Reachability.SbomReachabilityStageExecutor>();
|
||||
}
|
||||
|
||||
// Secrets Leak Detection (Sprint: SPRINT_20251229_046_BE)
|
||||
if (workerOptions.Secrets.Enabled)
|
||||
{
|
||||
|
||||
@@ -35,7 +35,14 @@
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Surface.FS/StellaOps.Scanner.Surface.FS.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Storage/StellaOps.Scanner.Storage.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Emit/StellaOps.Scanner.Emit.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.ServiceSecurity/StellaOps.Scanner.ServiceSecurity.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Sarif/StellaOps.Scanner.Sarif.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.CryptoAnalysis/StellaOps.Scanner.CryptoAnalysis.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.AiMlSecurity/StellaOps.Scanner.AiMlSecurity.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.BuildProvenance/StellaOps.Scanner.BuildProvenance.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Scanner.Analyzers.Native/StellaOps.Scanner.Analyzers.Native.csproj" />
|
||||
<ProjectReference Include="../../Concelier/__Libraries/StellaOps.Concelier.SbomIntegration/StellaOps.Concelier.SbomIntegration.csproj" />
|
||||
<ProjectReference Include="../../Concelier/__Libraries/StellaOps.Concelier.Core/StellaOps.Concelier.Core.csproj" />
|
||||
<ProjectReference Include="../../Unknowns/__Libraries/StellaOps.Unknowns.Core/StellaOps.Unknowns.Core.csproj" />
|
||||
<ProjectReference Include="../../BinaryIndex/__Libraries/StellaOps.BinaryIndex.Core/StellaOps.BinaryIndex.Core.csproj" />
|
||||
<ProjectReference Include="../../BinaryIndex/__Libraries/StellaOps.BinaryIndex.Persistence/StellaOps.BinaryIndex.Persistence.csproj" />
|
||||
@@ -43,6 +50,7 @@
|
||||
<ProjectReference Include="../../BinaryIndex/__Libraries/StellaOps.BinaryIndex.Disassembly/StellaOps.BinaryIndex.Disassembly.csproj" />
|
||||
<ProjectReference Include="../../BinaryIndex/__Libraries/StellaOps.BinaryIndex.Disassembly.Abstractions/StellaOps.BinaryIndex.Disassembly.Abstractions.csproj" />
|
||||
<ProjectReference Include="../../BinaryIndex/__Libraries/StellaOps.BinaryIndex.Normalization/StellaOps.BinaryIndex.Normalization.csproj" />
|
||||
<ProjectReference Include="../../BinaryIndex/__Libraries/StellaOps.BinaryIndex.ML/StellaOps.BinaryIndex.ML.csproj" />
|
||||
<ProjectReference Include="../../Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/StellaOps.Attestor.Core.csproj" />
|
||||
<ProjectReference Include="../../Signals/StellaOps.Signals/StellaOps.Signals.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user