// // Copyright (c) StellaOps. Licensed under the BUSL-1.1. // using StellaOps.Spdx3.Model; using StellaOps.Spdx3.Model.Security; using System.Collections.Immutable; namespace StellaOps.VexLens.Spdx3; /// /// Builds combined SPDX 3.0.1 documents containing Software and Security profiles. /// Sprint: SPRINT_20260107_004_004 Task SP-009 /// /// /// This builder merges an SBOM (Software profile) with VEX data (Security profile) /// into a single coherent document, linking VulnAssessmentRelationships to /// Package elements. /// public sealed class CombinedSbomVexBuilder { private readonly TimeProvider _timeProvider; private readonly List _elements = new(); private readonly HashSet _profiles = new(); private readonly List _creationInfos = new(); private string? _documentSpdxId; private string? _documentName; private Spdx3Document? _sbom; /// /// Initializes a new instance of the class. /// /// Time provider for timestamps. public CombinedSbomVexBuilder(TimeProvider timeProvider) { _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); } /// /// Sets the document SPDX ID. /// /// The document's unique IRI identifier. /// This builder for chaining. public CombinedSbomVexBuilder WithDocumentId(string spdxId) { ArgumentException.ThrowIfNullOrWhiteSpace(spdxId); _documentSpdxId = spdxId; return this; } /// /// Sets the document name. /// /// Human-readable document name. /// This builder for chaining. public CombinedSbomVexBuilder WithName(string name) { ArgumentException.ThrowIfNullOrWhiteSpace(name); _documentName = name; return this; } /// /// Adds elements from a Software profile SBOM. /// /// The source SBOM document. /// This builder for chaining. 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; } /// /// Adds VEX statements as Security profile elements. /// /// The VEX consensus. /// Prefix for generating SPDX IDs. /// This builder for chaining. 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; } /// /// Adds VEX statements with automatic linking to SBOM packages. /// /// The VEX statements. /// Prefix for generating SPDX IDs. /// Map from PURL to SPDX Package ID. /// This builder for chaining. public CombinedSbomVexBuilder WithLinkedSecurityProfile( IEnumerable statements, string spdxIdPrefix, IReadOnlyDictionary? 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; } /// /// Adds creation information for the combined document. /// /// The creation information. /// This builder for chaining. public CombinedSbomVexBuilder WithCreationInfo(Spdx3CreationInfo creationInfo) { ArgumentNullException.ThrowIfNull(creationInfo); _creationInfos.Add(creationInfo); return this; } /// /// Builds the combined SPDX 3.0.1 document. /// /// The combined document. /// If required fields are missing. 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.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 BuildPurlMapping() { var mapping = new Dictionary(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 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; } /// /// Creates a new builder with the given time provider. /// /// Time provider for timestamps. /// A new builder instance. public static CombinedSbomVexBuilder Create(TimeProvider timeProvider) { return new CombinedSbomVexBuilder(timeProvider); } /// /// Creates a new builder using the system time provider. /// /// A new builder instance. public static CombinedSbomVexBuilder Create() { return new CombinedSbomVexBuilder(TimeProvider.System); } } /// /// Extension methods for combining SPDX 3.0.1 SBOM with VEX data. /// public static class CombinedSbomVexExtensions { /// /// Combines an SBOM with VEX data into a single document. /// /// The source SBOM. /// The VEX consensus. /// The combined document ID. /// Prefix for generated IDs. /// Time provider for timestamps. /// The combined document. 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(); } }