//
// 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();
}
}