Files
git.stella-ops.org/src/VexLens/__Libraries/StellaOps.VexLens.Spdx3/CombinedSbomVexBuilder.cs
2026-02-01 21:37:40 +02:00

289 lines
9.8 KiB
C#

// <copyright file="CombinedSbomVexBuilder.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using StellaOps.Spdx3.Model;
using StellaOps.Spdx3.Model.Security;
using System.Collections.Immutable;
namespace StellaOps.VexLens.Spdx3;
/// <summary>
/// Builds combined SPDX 3.0.1 documents containing Software and Security profiles.
/// Sprint: SPRINT_20260107_004_004 Task SP-009
/// </summary>
/// <remarks>
/// This builder merges an SBOM (Software profile) with VEX data (Security profile)
/// into a single coherent document, linking VulnAssessmentRelationships to
/// Package elements.
/// </remarks>
public sealed class CombinedSbomVexBuilder
{
private readonly TimeProvider _timeProvider;
private readonly List<Spdx3Element> _elements = new();
private readonly HashSet<Spdx3ProfileIdentifier> _profiles = new();
private readonly List<Spdx3CreationInfo> _creationInfos = new();
private string? _documentSpdxId;
private string? _documentName;
private Spdx3Document? _sbom;
/// <summary>
/// Initializes a new instance of the <see cref="CombinedSbomVexBuilder"/> class.
/// </summary>
/// <param name="timeProvider">Time provider for timestamps.</param>
public CombinedSbomVexBuilder(TimeProvider timeProvider)
{
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
}
/// <summary>
/// Sets the document SPDX ID.
/// </summary>
/// <param name="spdxId">The document's unique IRI identifier.</param>
/// <returns>This builder for chaining.</returns>
public CombinedSbomVexBuilder WithDocumentId(string spdxId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(spdxId);
_documentSpdxId = spdxId;
return this;
}
/// <summary>
/// Sets the document name.
/// </summary>
/// <param name="name">Human-readable document name.</param>
/// <returns>This builder for chaining.</returns>
public CombinedSbomVexBuilder WithName(string name)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
_documentName = name;
return this;
}
/// <summary>
/// Adds elements from a Software profile SBOM.
/// </summary>
/// <param name="sbom">The source SBOM document.</param>
/// <returns>This builder for chaining.</returns>
public CombinedSbomVexBuilder WithSoftwareProfile(Spdx3Document sbom)
{
ArgumentNullException.ThrowIfNull(sbom);
_sbom = sbom;
// Add all elements from the SBOM
foreach (var element in sbom.Elements)
{
_elements.Add(element);
}
// Add Software and Core profiles
_profiles.Add(Spdx3ProfileIdentifier.Core);
_profiles.Add(Spdx3ProfileIdentifier.Software);
// Preserve existing profile conformance
foreach (var profile in sbom.Profiles)
{
_profiles.Add(profile);
}
return this;
}
/// <summary>
/// Adds VEX statements as Security profile elements.
/// </summary>
/// <param name="consensus">The VEX consensus.</param>
/// <param name="spdxIdPrefix">Prefix for generating SPDX IDs.</param>
/// <returns>This builder for chaining.</returns>
public CombinedSbomVexBuilder WithSecurityProfile(VexConsensus consensus, string spdxIdPrefix)
{
ArgumentNullException.ThrowIfNull(consensus);
ArgumentException.ThrowIfNullOrWhiteSpace(spdxIdPrefix);
var mapper = new VexToSpdx3Mapper(_timeProvider);
var mappingResult = mapper.MapStatements(consensus.Statements, spdxIdPrefix);
// Add vulnerability and assessment elements
foreach (var element in mappingResult.AllElements)
{
_elements.Add(element);
}
_profiles.Add(Spdx3ProfileIdentifier.Core);
_profiles.Add(Spdx3ProfileIdentifier.Security);
return this;
}
/// <summary>
/// Adds VEX statements with automatic linking to SBOM packages.
/// </summary>
/// <param name="statements">The VEX statements.</param>
/// <param name="spdxIdPrefix">Prefix for generating SPDX IDs.</param>
/// <param name="purlToSpdxIdMap">Map from PURL to SPDX Package ID.</param>
/// <returns>This builder for chaining.</returns>
public CombinedSbomVexBuilder WithLinkedSecurityProfile(
IEnumerable<OpenVexStatement> statements,
string spdxIdPrefix,
IReadOnlyDictionary<string, string>? purlToSpdxIdMap = null)
{
ArgumentNullException.ThrowIfNull(statements);
ArgumentException.ThrowIfNullOrWhiteSpace(spdxIdPrefix);
// Build PURL to SPDX ID map from SBOM if not provided
var mapping = purlToSpdxIdMap ?? BuildPurlMapping();
var mapper = new VexToSpdx3Mapper(_timeProvider);
// Map statements, potentially rewriting product IDs to SPDX Package IDs
var rewrittenStatements = statements.Select(s => RewriteProductId(s, mapping));
var mappingResult = mapper.MapStatements(rewrittenStatements, spdxIdPrefix);
foreach (var element in mappingResult.AllElements)
{
_elements.Add(element);
}
_profiles.Add(Spdx3ProfileIdentifier.Core);
_profiles.Add(Spdx3ProfileIdentifier.Security);
return this;
}
/// <summary>
/// Adds creation information for the combined document.
/// </summary>
/// <param name="creationInfo">The creation information.</param>
/// <returns>This builder for chaining.</returns>
public CombinedSbomVexBuilder WithCreationInfo(Spdx3CreationInfo creationInfo)
{
ArgumentNullException.ThrowIfNull(creationInfo);
_creationInfos.Add(creationInfo);
return this;
}
/// <summary>
/// Builds the combined SPDX 3.0.1 document.
/// </summary>
/// <returns>The combined document.</returns>
/// <exception cref="InvalidOperationException">If required fields are missing.</exception>
public Spdx3Document Build()
{
if (string.IsNullOrWhiteSpace(_documentSpdxId))
{
throw new InvalidOperationException("Document SPDX ID is required. Call WithDocumentId().");
}
// Create combined creation info if none provided
if (_creationInfos.Count == 0)
{
var defaultCreationInfo = new Spdx3CreationInfo
{
Id = $"{_documentSpdxId}/creationInfo",
SpecVersion = Spdx3CreationInfo.Spdx301Version,
Created = _timeProvider.GetUtcNow(),
CreatedBy = ImmutableArray<string>.Empty,
CreatedUsing = ImmutableArray.Create("StellaOps VexLens"),
Profile = _profiles.ToImmutableArray(),
DataLicense = Spdx3CreationInfo.Spdx301DataLicense
};
_creationInfos.Add(defaultCreationInfo);
}
return new Spdx3Document(
elements: _elements,
creationInfos: _creationInfos,
profiles: _profiles);
}
private Dictionary<string, string> BuildPurlMapping()
{
var mapping = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (_sbom is null)
{
return mapping;
}
foreach (var package in _sbom.Packages)
{
foreach (var identifier in package.ExternalIdentifier)
{
if (identifier.ExternalIdentifierType == Spdx3ExternalIdentifierType.PackageUrl)
{
mapping[identifier.Identifier] = package.SpdxId;
}
}
}
return mapping;
}
private static OpenVexStatement RewriteProductId(
OpenVexStatement statement,
IReadOnlyDictionary<string, string> mapping)
{
// If the product ID is a PURL and we have a mapping, rewrite it
if (mapping.TryGetValue(statement.ProductId, out var spdxId))
{
return statement with { ProductId = spdxId };
}
return statement;
}
/// <summary>
/// Creates a new builder with the given time provider.
/// </summary>
/// <param name="timeProvider">Time provider for timestamps.</param>
/// <returns>A new builder instance.</returns>
public static CombinedSbomVexBuilder Create(TimeProvider timeProvider)
{
return new CombinedSbomVexBuilder(timeProvider);
}
/// <summary>
/// Creates a new builder using the system time provider.
/// </summary>
/// <returns>A new builder instance.</returns>
public static CombinedSbomVexBuilder Create()
{
return new CombinedSbomVexBuilder(TimeProvider.System);
}
}
/// <summary>
/// Extension methods for combining SPDX 3.0.1 SBOM with VEX data.
/// </summary>
public static class CombinedSbomVexExtensions
{
/// <summary>
/// Combines an SBOM with VEX data into a single document.
/// </summary>
/// <param name="sbom">The source SBOM.</param>
/// <param name="consensus">The VEX consensus.</param>
/// <param name="documentId">The combined document ID.</param>
/// <param name="spdxIdPrefix">Prefix for generated IDs.</param>
/// <param name="timeProvider">Time provider for timestamps.</param>
/// <returns>The combined document.</returns>
public static Spdx3Document WithVexData(
this Spdx3Document sbom,
VexConsensus consensus,
string documentId,
string spdxIdPrefix,
TimeProvider? timeProvider = null)
{
ArgumentNullException.ThrowIfNull(sbom);
ArgumentNullException.ThrowIfNull(consensus);
return CombinedSbomVexBuilder.Create(timeProvider ?? TimeProvider.System)
.WithDocumentId(documentId)
.WithName("Combined SBOM and VEX Data")
.WithSoftwareProfile(sbom)
.WithSecurityProfile(consensus, spdxIdPrefix)
.Build();
}
}