// // Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later. // using System.Collections.Immutable; using System.Globalization; using StellaOps.Spdx3.Model; using StellaOps.Spdx3.Model.Security; namespace StellaOps.VexLens.Spdx3; /// /// Builds SPDX 3.0.1 Vulnerability elements from CVE and other vulnerability data. /// Sprint: SPRINT_20260107_004_004 Task SP-004 /// public sealed class VulnerabilityElementBuilder { private readonly string _spdxIdPrefix; private readonly List _externalIdentifiers = new(); private readonly List _externalRefs = new(); private string? _vulnerabilityId; private string? _description; private DateTimeOffset? _publishedTime; private DateTimeOffset? _modifiedTime; /// /// Initializes a new instance of the class. /// /// Prefix for generating SPDX IDs. public VulnerabilityElementBuilder(string spdxIdPrefix) { ArgumentException.ThrowIfNullOrWhiteSpace(spdxIdPrefix); _spdxIdPrefix = spdxIdPrefix; } /// /// Sets the vulnerability ID (e.g., CVE-2026-1234). /// /// The vulnerability ID. /// This builder for fluent chaining. public VulnerabilityElementBuilder WithVulnerabilityId(string vulnerabilityId) { ArgumentException.ThrowIfNullOrWhiteSpace(vulnerabilityId); _vulnerabilityId = vulnerabilityId; // Auto-detect identifier type and add external identifier if (vulnerabilityId.StartsWith("CVE-", StringComparison.OrdinalIgnoreCase)) { _externalIdentifiers.Add(new Spdx3ExternalIdentifier { ExternalIdentifierType = Spdx3ExternalIdentifierType.Cve, Identifier = vulnerabilityId, Comment = $"https://nvd.nist.gov/vuln/detail/{vulnerabilityId}" }); } else if (vulnerabilityId.StartsWith("GHSA-", StringComparison.OrdinalIgnoreCase)) { _externalIdentifiers.Add(new Spdx3ExternalIdentifier { ExternalIdentifierType = Spdx3ExternalIdentifierType.SecurityOther, Identifier = vulnerabilityId, Comment = $"GitHub Security Advisory: https://github.com/advisories/{vulnerabilityId}" }); } else if (vulnerabilityId.StartsWith("OSV-", StringComparison.OrdinalIgnoreCase)) { _externalIdentifiers.Add(new Spdx3ExternalIdentifier { ExternalIdentifierType = Spdx3ExternalIdentifierType.SecurityOther, Identifier = vulnerabilityId, Comment = $"OSV Vulnerability: https://osv.dev/vulnerability/{vulnerabilityId}" }); } return this; } /// /// Sets the vulnerability description. /// /// The description text. /// This builder for fluent chaining. public VulnerabilityElementBuilder WithDescription(string? description) { _description = description; return this; } /// /// Sets the published time. /// /// When the vulnerability was published. /// This builder for fluent chaining. public VulnerabilityElementBuilder WithPublishedTime(DateTimeOffset? publishedTime) { _publishedTime = publishedTime; return this; } /// /// Sets the modified time. /// /// When the vulnerability was last modified. /// This builder for fluent chaining. public VulnerabilityElementBuilder WithModifiedTime(DateTimeOffset? modifiedTime) { _modifiedTime = modifiedTime; return this; } /// /// Adds a reference to NVD. /// /// The CVE ID for NVD lookup. /// This builder for fluent chaining. public VulnerabilityElementBuilder WithNvdReference(string cveId) { ArgumentException.ThrowIfNullOrWhiteSpace(cveId); _externalRefs.Add(new Spdx3ExternalRef { ExternalRefType = Spdx3ExternalRefType.SecurityAdvisory, Locator = ImmutableArray.Create($"https://nvd.nist.gov/vuln/detail/{cveId}") }); return this; } /// /// Adds a reference to OSV. /// /// The vulnerability ID for OSV lookup. /// This builder for fluent chaining. public VulnerabilityElementBuilder WithOsvReference(string vulnerabilityId) { ArgumentException.ThrowIfNullOrWhiteSpace(vulnerabilityId); _externalRefs.Add(new Spdx3ExternalRef { ExternalRefType = Spdx3ExternalRefType.SecurityAdvisory, Locator = ImmutableArray.Create($"https://osv.dev/vulnerability/{vulnerabilityId}") }); return this; } /// /// Adds a custom external reference. /// /// The reference type. /// The reference URL. /// This builder for fluent chaining. public VulnerabilityElementBuilder WithExternalRef(Spdx3ExternalRefType refType, string locator) { ArgumentException.ThrowIfNullOrWhiteSpace(locator); _externalRefs.Add(new Spdx3ExternalRef { ExternalRefType = refType, Locator = ImmutableArray.Create(locator) }); return this; } /// /// Builds the SPDX 3.0.1 Vulnerability element. /// /// The constructed Vulnerability element. /// If vulnerability ID is not set. public Spdx3Vulnerability Build() { if (string.IsNullOrWhiteSpace(_vulnerabilityId)) { throw new InvalidOperationException("Vulnerability ID is required. Call WithVulnerabilityId() first."); } return new Spdx3Vulnerability { SpdxId = GenerateSpdxId(), Type = Spdx3Vulnerability.TypeName, Name = _vulnerabilityId, Description = _description, PublishedTime = _publishedTime, ModifiedTime = _modifiedTime, ExternalIdentifiers = _externalIdentifiers.ToImmutableArray(), ExternalRefs = _externalRefs.ToImmutableArray() }; } /// /// Creates a Vulnerability element from a CVE ID with NVD reference. /// /// The CVE ID. /// Prefix for SPDX ID generation. /// Optional description. /// The constructed Vulnerability element. public static Spdx3Vulnerability FromCve( string cveId, string spdxIdPrefix, string? description = null) { return new VulnerabilityElementBuilder(spdxIdPrefix) .WithVulnerabilityId(cveId) .WithDescription(description) .WithNvdReference(cveId) .Build(); } private string GenerateSpdxId() { // Generate a deterministic SPDX ID from the vulnerability ID using var sha = System.Security.Cryptography.SHA256.Create(); var input = _vulnerabilityId ?? string.Empty; var hash = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(input)); var shortHash = Convert.ToHexStringLower(hash)[..12]; return $"{_spdxIdPrefix.TrimEnd('/')}/vulnerability/{shortHash}"; } }