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

@@ -2,6 +2,7 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using StellaOps.Scanner.Emit.Spdx;
using StellaOps.Scanner.WebService.Constants;
using StellaOps.Scanner.WebService.Domain;
using StellaOps.Scanner.WebService.Infrastructure;
@@ -23,6 +24,17 @@ internal static class ExportEndpoints
{
ArgumentNullException.ThrowIfNull(scansGroup);
// GET /scans/{scanId}/exports/sbom - SPDX 3.0.1 SBOM export with format and profile selection
// Sprint: SPRINT_20260107_004_002 Task SG-010
scansGroup.MapGet("/{scanId}/exports/sbom", HandleExportSbomAsync)
.WithName("scanner.scans.exports.sbom")
.WithTags("Exports", "SBOM")
.Produces(StatusCodes.Status200OK, contentType: "application/spdx+json")
.Produces(StatusCodes.Status200OK, contentType: "application/ld+json")
.Produces(StatusCodes.Status200OK, contentType: "application/vnd.cyclonedx+json")
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.ScansRead);
// GET /scans/{scanId}/exports/sarif
scansGroup.MapGet("/{scanId}/exports/sarif", HandleExportSarifAsync)
.WithName("scanner.scans.exports.sarif")
@@ -185,4 +197,142 @@ internal static class ExportEndpoints
var json = JsonSerializer.Serialize(vexDocument, SerializerOptions);
return Results.Content(json, "application/json", System.Text.Encoding.UTF8, StatusCodes.Status200OK);
}
/// <summary>
/// Handles SBOM export with format and profile selection.
/// Sprint: SPRINT_20260107_004_002 Tasks SG-010, SG-012
/// </summary>
/// <param name="scanId">The scan identifier.</param>
/// <param name="format">SBOM format: spdx3, spdx2, cyclonedx (default: spdx2 for backward compatibility).</param>
/// <param name="profile">SPDX 3.0.1 profile: software, lite (default: software). Only applies to spdx3 format.</param>
/// <param name="coordinator">The scan coordinator service.</param>
/// <param name="sbomExportService">The SBOM export service.</param>
/// <param name="context">The HTTP context.</param>
/// <param name="cancellationToken">Cancellation token.</param>
private static async Task<IResult> HandleExportSbomAsync(
string scanId,
string? format,
string? profile,
IScanCoordinator coordinator,
ISbomExportService sbomExportService,
HttpContext context,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(coordinator);
ArgumentNullException.ThrowIfNull(sbomExportService);
if (!ScanId.TryParse(scanId, out var parsed))
{
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
}
var snapshot = await coordinator.GetAsync(parsed, cancellationToken).ConfigureAwait(false);
if (snapshot is null)
{
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
StatusCodes.Status404NotFound,
detail: "Requested scan could not be located.");
}
// SG-012: Format selection logic with fallback to SPDX 2.3 for backward compatibility
var selectedFormat = SelectSbomFormat(format);
var selectedProfile = SelectSpdx3Profile(profile);
var exportResult = await sbomExportService.ExportAsync(
parsed,
selectedFormat,
selectedProfile,
cancellationToken).ConfigureAwait(false);
if (exportResult is null || exportResult.Bytes is null || exportResult.Bytes.Length == 0)
{
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"No SBOM data available",
StatusCodes.Status404NotFound,
detail: "No SBOM data available for export.");
}
// Set appropriate content-type header based on format
var contentType = selectedFormat switch
{
SbomExportFormat.Spdx3 => "application/ld+json; profile=\"https://spdx.org/rdf/3.0.1/terms/Software/ProfileIdentifierType/" + selectedProfile.ToString().ToLowerInvariant() + "\"",
SbomExportFormat.Spdx2 => "application/spdx+json; version=2.3",
SbomExportFormat.CycloneDx => "application/vnd.cyclonedx+json; version=1.7",
_ => "application/json"
};
context.Response.Headers["X-StellaOps-Format"] = selectedFormat.ToString().ToLowerInvariant();
if (selectedFormat == SbomExportFormat.Spdx3)
{
context.Response.Headers["X-StellaOps-Profile"] = selectedProfile.ToString().ToLowerInvariant();
}
return Results.Bytes(exportResult.Bytes, contentType);
}
/// <summary>
/// Selects SBOM format with fallback to SPDX 2.3 for backward compatibility.
/// Sprint: SPRINT_20260107_004_002 Task SG-012
/// </summary>
private static SbomExportFormat SelectSbomFormat(string? format)
{
if (string.IsNullOrWhiteSpace(format))
{
// Default to SPDX 2.3 for backward compatibility
return SbomExportFormat.Spdx2;
}
return format.ToLowerInvariant() switch
{
"spdx3" or "spdx-3" or "spdx3.0" or "spdx-3.0.1" => SbomExportFormat.Spdx3,
"spdx2" or "spdx-2" or "spdx2.3" or "spdx-2.3" or "spdx" => SbomExportFormat.Spdx2,
"cyclonedx" or "cdx" or "cdx17" or "cyclonedx-1.7" => SbomExportFormat.CycloneDx,
_ => SbomExportFormat.Spdx2 // Fallback for unknown formats
};
}
/// <summary>
/// Selects SPDX 3.0.1 profile with default to Software.
/// </summary>
private static Spdx3ProfileType SelectSpdx3Profile(string? profile)
{
if (string.IsNullOrWhiteSpace(profile))
{
return Spdx3ProfileType.Software;
}
return profile.ToLowerInvariant() switch
{
"lite" => Spdx3ProfileType.Lite,
"build" => Spdx3ProfileType.Build,
"security" => Spdx3ProfileType.Security,
"software" or _ => Spdx3ProfileType.Software
};
}
}
/// <summary>
/// SBOM export format enumeration.
/// Sprint: SPRINT_20260107_004_002 Task SG-012
/// </summary>
public enum SbomExportFormat
{
/// <summary>SPDX 2.3 JSON format (default for backward compatibility).</summary>
Spdx2,
/// <summary>SPDX 3.0.1 JSON-LD format with profile support.</summary>
Spdx3,
/// <summary>CycloneDX 1.7 JSON format.</summary>
CycloneDx
}