215 lines
7.9 KiB
C#
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}";
|
|
}
|
|
}
|