Files
git.stella-ops.org/src/VexLens/__Libraries/StellaOps.VexLens.Spdx3/VulnerabilityElementBuilder.cs
2026-01-09 18:27:46 +02:00

215 lines
7.9 KiB
C#

// <copyright file="VulnerabilityElementBuilder.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using System.Collections.Immutable;
using System.Globalization;
using StellaOps.Spdx3.Model;
using StellaOps.Spdx3.Model.Security;
namespace StellaOps.VexLens.Spdx3;
/// <summary>
/// Builds SPDX 3.0.1 Vulnerability elements from CVE and other vulnerability data.
/// Sprint: SPRINT_20260107_004_004 Task SP-004
/// </summary>
public sealed class VulnerabilityElementBuilder
{
private readonly string _spdxIdPrefix;
private readonly List<Spdx3ExternalIdentifier> _externalIdentifiers = new();
private readonly List<Spdx3ExternalRef> _externalRefs = new();
private string? _vulnerabilityId;
private string? _description;
private DateTimeOffset? _publishedTime;
private DateTimeOffset? _modifiedTime;
/// <summary>
/// Initializes a new instance of the <see cref="VulnerabilityElementBuilder"/> class.
/// </summary>
/// <param name="spdxIdPrefix">Prefix for generating SPDX IDs.</param>
public VulnerabilityElementBuilder(string spdxIdPrefix)
{
ArgumentException.ThrowIfNullOrWhiteSpace(spdxIdPrefix);
_spdxIdPrefix = spdxIdPrefix;
}
/// <summary>
/// Sets the vulnerability ID (e.g., CVE-2026-1234).
/// </summary>
/// <param name="vulnerabilityId">The vulnerability ID.</param>
/// <returns>This builder for fluent chaining.</returns>
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;
}
/// <summary>
/// Sets the vulnerability description.
/// </summary>
/// <param name="description">The description text.</param>
/// <returns>This builder for fluent chaining.</returns>
public VulnerabilityElementBuilder WithDescription(string? description)
{
_description = description;
return this;
}
/// <summary>
/// Sets the published time.
/// </summary>
/// <param name="publishedTime">When the vulnerability was published.</param>
/// <returns>This builder for fluent chaining.</returns>
public VulnerabilityElementBuilder WithPublishedTime(DateTimeOffset? publishedTime)
{
_publishedTime = publishedTime;
return this;
}
/// <summary>
/// Sets the modified time.
/// </summary>
/// <param name="modifiedTime">When the vulnerability was last modified.</param>
/// <returns>This builder for fluent chaining.</returns>
public VulnerabilityElementBuilder WithModifiedTime(DateTimeOffset? modifiedTime)
{
_modifiedTime = modifiedTime;
return this;
}
/// <summary>
/// Adds a reference to NVD.
/// </summary>
/// <param name="cveId">The CVE ID for NVD lookup.</param>
/// <returns>This builder for fluent chaining.</returns>
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;
}
/// <summary>
/// Adds a reference to OSV.
/// </summary>
/// <param name="vulnerabilityId">The vulnerability ID for OSV lookup.</param>
/// <returns>This builder for fluent chaining.</returns>
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;
}
/// <summary>
/// Adds a custom external reference.
/// </summary>
/// <param name="refType">The reference type.</param>
/// <param name="locator">The reference URL.</param>
/// <returns>This builder for fluent chaining.</returns>
public VulnerabilityElementBuilder WithExternalRef(Spdx3ExternalRefType refType, string locator)
{
ArgumentException.ThrowIfNullOrWhiteSpace(locator);
_externalRefs.Add(new Spdx3ExternalRef
{
ExternalRefType = refType,
Locator = ImmutableArray.Create(locator)
});
return this;
}
/// <summary>
/// Builds the SPDX 3.0.1 Vulnerability element.
/// </summary>
/// <returns>The constructed Vulnerability element.</returns>
/// <exception cref="InvalidOperationException">If vulnerability ID is not set.</exception>
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()
};
}
/// <summary>
/// Creates a Vulnerability element from a CVE ID with NVD reference.
/// </summary>
/// <param name="cveId">The CVE ID.</param>
/// <param name="spdxIdPrefix">Prefix for SPDX ID generation.</param>
/// <param name="description">Optional description.</param>
/// <returns>The constructed Vulnerability element.</returns>
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}";
}
}