289 lines
9.8 KiB
C#
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();
|
|
}
|
|
}
|