more audit work

This commit is contained in:
master
2026-01-08 10:21:51 +02:00
parent 43c02081ef
commit 51cf4bc16c
546 changed files with 36721 additions and 4003 deletions

View File

@@ -0,0 +1,214 @@
// <copyright file="SbomExportService.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using Microsoft.Extensions.Logging;
using StellaOps.Canonical.Json;
using StellaOps.Scanner.Emit.Composition;
using StellaOps.Scanner.Emit.Spdx;
using StellaOps.Scanner.WebService.Domain;
using StellaOps.Scanner.WebService.Endpoints;
namespace StellaOps.Scanner.WebService.Services;
/// <summary>
/// Service for exporting SBOMs in multiple formats (SPDX 2.3, SPDX 3.0.1, CycloneDX).
/// Sprint: SPRINT_20260107_004_002 Tasks SG-010, SG-012
/// </summary>
public sealed class SbomExportService : ISbomExportService
{
private readonly IScanCoordinator _coordinator;
private readonly ISpdxComposer _spdxComposer;
private readonly ILayerSbomService _layerSbomService;
private readonly TimeProvider _timeProvider;
private readonly ILogger<SbomExportService> _logger;
public SbomExportService(
IScanCoordinator coordinator,
ISpdxComposer spdxComposer,
ILayerSbomService layerSbomService,
TimeProvider timeProvider,
ILogger<SbomExportService> logger)
{
_coordinator = coordinator ?? throw new ArgumentNullException(nameof(coordinator));
_spdxComposer = spdxComposer ?? throw new ArgumentNullException(nameof(spdxComposer));
_layerSbomService = layerSbomService ?? throw new ArgumentNullException(nameof(layerSbomService));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
public async Task<SbomExportResult?> ExportAsync(
ScanId scanId,
SbomExportFormat format,
Spdx3ProfileType profile,
CancellationToken cancellationToken = default)
{
_logger.LogDebug(
"Exporting SBOM for scan {ScanId} with format {Format} and profile {Profile}",
scanId,
format,
profile);
var snapshot = await _coordinator.GetAsync(scanId, cancellationToken).ConfigureAwait(false);
if (snapshot is null)
{
_logger.LogWarning("Scan {ScanId} not found for SBOM export", scanId);
return null;
}
// Get layer fragments for SBOM composition
var layerFragments = await _layerSbomService.GetLayerFragmentsAsync(scanId, cancellationToken)
.ConfigureAwait(false);
if (layerFragments is null || layerFragments.Count == 0)
{
_logger.LogWarning("No layer fragments found for scan {ScanId}", scanId);
return null;
}
return format switch
{
SbomExportFormat.Spdx3 => await ExportSpdx3Async(snapshot, layerFragments, profile, cancellationToken)
.ConfigureAwait(false),
SbomExportFormat.Spdx2 => await ExportSpdx2Async(snapshot, cancellationToken)
.ConfigureAwait(false),
SbomExportFormat.CycloneDx => await ExportCycloneDxAsync(snapshot, cancellationToken)
.ConfigureAwait(false),
_ => await ExportSpdx2Async(snapshot, cancellationToken).ConfigureAwait(false)
};
}
private Task<SbomExportResult> ExportSpdx3Async(
ScanSnapshot snapshot,
IReadOnlyList<SbomLayerFragment> layerFragments,
Spdx3ProfileType profile,
CancellationToken cancellationToken)
{
_logger.LogDebug("Generating SPDX 3.0.1 SBOM with profile {Profile}", profile);
// Build composition request from layer fragments
var request = BuildCompositionRequest(snapshot, layerFragments);
var options = new SpdxCompositionOptions
{
ProfileType = profile,
IncludeFiles = profile != Spdx3ProfileType.Lite,
IncludeTagValue = false
};
var artifact = _spdxComposer.Compose(request, options, cancellationToken);
return Task.FromResult(new SbomExportResult(
artifact.JsonBytes,
SbomExportFormat.Spdx3,
profile,
artifact.JsonDigest,
artifact.ComponentCount));
}
private async Task<SbomExportResult> ExportSpdx2Async(
ScanSnapshot snapshot,
CancellationToken cancellationToken)
{
_logger.LogDebug("Generating SPDX 2.3 SBOM");
// For SPDX 2.3, we use the layer SBOM service's existing functionality
var sbomBytes = await _layerSbomService.GetComposedSbomAsync(
snapshot.ScanId,
"spdx",
cancellationToken).ConfigureAwait(false);
if (sbomBytes is null || sbomBytes.Length == 0)
{
_logger.LogWarning("No SPDX 2.3 SBOM available for scan {ScanId}", snapshot.ScanId);
return new SbomExportResult(
Array.Empty<byte>(),
SbomExportFormat.Spdx2,
null,
string.Empty,
0);
}
var digest = CanonJson.Sha256Hex(sbomBytes);
var componentCount = EstimateComponentCount(sbomBytes);
return new SbomExportResult(
sbomBytes,
SbomExportFormat.Spdx2,
null,
digest,
componentCount);
}
private async Task<SbomExportResult> ExportCycloneDxAsync(
ScanSnapshot snapshot,
CancellationToken cancellationToken)
{
_logger.LogDebug("Generating CycloneDX 1.7 SBOM");
var sbomBytes = await _layerSbomService.GetComposedSbomAsync(
snapshot.ScanId,
"cdx",
cancellationToken).ConfigureAwait(false);
if (sbomBytes is null || sbomBytes.Length == 0)
{
_logger.LogWarning("No CycloneDX SBOM available for scan {ScanId}", snapshot.ScanId);
return new SbomExportResult(
Array.Empty<byte>(),
SbomExportFormat.CycloneDx,
null,
string.Empty,
0);
}
var digest = CanonJson.Sha256Hex(sbomBytes);
var componentCount = EstimateComponentCount(sbomBytes);
return new SbomExportResult(
sbomBytes,
SbomExportFormat.CycloneDx,
null,
digest,
componentCount);
}
private SbomCompositionRequest BuildCompositionRequest(
ScanSnapshot snapshot,
IReadOnlyList<SbomLayerFragment> layerFragments)
{
// Convert SbomLayerFragment to the format expected by SpdxComposer
var fragments = layerFragments.Select(f => new Scanner.Core.Contracts.LayerSbomFragment
{
LayerDigest = f.LayerDigest,
Order = f.Order,
ComponentPurls = f.ComponentPurls.ToList()
}).ToList();
return new SbomCompositionRequest
{
Image = new Scanner.Core.Contracts.ImageReference
{
ImageDigest = snapshot.Target.Digest ?? string.Empty,
ImageRef = snapshot.Target.Reference ?? string.Empty
},
LayerFragments = fragments,
GeneratedAt = _timeProvider.GetUtcNow(),
GeneratorVersion = "StellaOps-Scanner/1.0"
};
}
private static int EstimateComponentCount(byte[] sbomBytes)
{
// Quick heuristic: count "purl" occurrences as proxy for component count
var content = System.Text.Encoding.UTF8.GetString(sbomBytes);
var count = 0;
var index = 0;
while ((index = content.IndexOf("\"purl\"", index, StringComparison.Ordinal)) != -1)
{
count++;
index += 6;
}
return Math.Max(count, 1);
}
}