tests fixes and sprints work

This commit is contained in:
master
2026-01-22 19:08:46 +02:00
parent c32fff8f86
commit 726d70dc7f
881 changed files with 134434 additions and 6228 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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",

View File

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

View File

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

View File

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

View File

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