184 lines
6.3 KiB
C#
184 lines
6.3 KiB
C#
// <copyright file="VexToSpdx3Mapper.cs" company="StellaOps">
|
|
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
|
// </copyright>
|
|
|
|
using System.Collections.Immutable;
|
|
using StellaOps.Spdx3.Model;
|
|
using StellaOps.Spdx3.Model.Security;
|
|
|
|
namespace StellaOps.VexLens.Spdx3;
|
|
|
|
/// <summary>
|
|
/// Maps VEX consensus results to SPDX 3.0.1 Security profile documents.
|
|
/// Sprint: SPRINT_20260107_004_004 Task SP-006
|
|
/// </summary>
|
|
public sealed class VexToSpdx3Mapper : IVexToSpdx3Mapper
|
|
{
|
|
private readonly TimeProvider _timeProvider;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="VexToSpdx3Mapper"/> class.
|
|
/// </summary>
|
|
/// <param name="timeProvider">Time provider for timestamps.</param>
|
|
public VexToSpdx3Mapper(TimeProvider timeProvider)
|
|
{
|
|
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance using the system time provider.
|
|
/// </summary>
|
|
public VexToSpdx3Mapper() : this(TimeProvider.System)
|
|
{
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<Spdx3Document> MapConsensusAsync(
|
|
VexConsensus consensus,
|
|
VexToSpdx3Options options,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(consensus);
|
|
ArgumentNullException.ThrowIfNull(options);
|
|
|
|
// Filter statements if filters are specified
|
|
var statements = FilterStatements(consensus.Statements, options);
|
|
|
|
// Map statements to SPDX 3.0.1 elements
|
|
var mappingResult = MapStatements(statements, options.SpdxIdPrefix);
|
|
|
|
// Build creation info
|
|
var creationInfo = new Spdx3CreationInfo
|
|
{
|
|
Id = $"{options.SpdxIdPrefix}/creationInfo/{consensus.DocumentId}",
|
|
SpecVersion = Spdx3CreationInfo.Spdx301Version,
|
|
Created = consensus.Timestamp ?? _timeProvider.GetUtcNow(),
|
|
CreatedBy = consensus.Author is not null
|
|
? ImmutableArray.Create(consensus.Author)
|
|
: ImmutableArray<string>.Empty,
|
|
CreatedUsing = ImmutableArray.Create(options.ToolId),
|
|
Profile = ImmutableArray.Create(
|
|
Spdx3ProfileIdentifier.Core,
|
|
Spdx3ProfileIdentifier.Security),
|
|
DataLicense = Spdx3CreationInfo.Spdx301DataLicense
|
|
};
|
|
|
|
// Include CVSS assessments if requested
|
|
var allElements = mappingResult.AllElements.ToList();
|
|
|
|
if (options.IncludeCvss)
|
|
{
|
|
var cvssAssessments = BuildCvssAssessments(statements, options.SpdxIdPrefix);
|
|
allElements.AddRange(cvssAssessments);
|
|
}
|
|
|
|
if (options.IncludeEpss)
|
|
{
|
|
var epssAssessments = BuildEpssAssessments(statements, options.SpdxIdPrefix);
|
|
allElements.AddRange(epssAssessments);
|
|
}
|
|
|
|
var document = new Spdx3Document(
|
|
elements: allElements,
|
|
creationInfos: new[] { creationInfo },
|
|
profiles: new[] { Spdx3ProfileIdentifier.Core, Spdx3ProfileIdentifier.Security });
|
|
|
|
return Task.FromResult(document);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public VexMappingResult MapStatements(
|
|
IEnumerable<OpenVexStatement> statements,
|
|
string spdxIdPrefix)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(statements);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(spdxIdPrefix);
|
|
|
|
var vulnerabilities = new List<Spdx3Element>();
|
|
var assessments = new List<Spdx3Element>();
|
|
var seenVulnerabilities = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var statement in statements)
|
|
{
|
|
// Create vulnerability element if not already created
|
|
if (!seenVulnerabilities.Contains(statement.VulnerabilityId))
|
|
{
|
|
var vulnerability = BuildVulnerability(statement, spdxIdPrefix);
|
|
vulnerabilities.Add(vulnerability);
|
|
seenVulnerabilities.Add(statement.VulnerabilityId);
|
|
}
|
|
|
|
// Create VEX assessment relationship
|
|
var assessment = VexStatusMapper.MapToSpdx3(statement, spdxIdPrefix);
|
|
assessments.Add(assessment);
|
|
}
|
|
|
|
return new VexMappingResult
|
|
{
|
|
Vulnerabilities = vulnerabilities,
|
|
Assessments = assessments
|
|
};
|
|
}
|
|
|
|
private static IEnumerable<OpenVexStatement> FilterStatements(
|
|
IReadOnlyList<OpenVexStatement> statements,
|
|
VexToSpdx3Options options)
|
|
{
|
|
IEnumerable<OpenVexStatement> result = statements;
|
|
|
|
if (options.ProductFilter is { Count: > 0 })
|
|
{
|
|
var productSet = new HashSet<string>(options.ProductFilter, StringComparer.OrdinalIgnoreCase);
|
|
result = result.Where(s => productSet.Contains(s.ProductId));
|
|
}
|
|
|
|
if (options.VulnerabilityFilter is { Count: > 0 })
|
|
{
|
|
var vulnSet = new HashSet<string>(options.VulnerabilityFilter, StringComparer.OrdinalIgnoreCase);
|
|
result = result.Where(s => vulnSet.Contains(s.VulnerabilityId));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static Spdx3Vulnerability BuildVulnerability(OpenVexStatement statement, string spdxIdPrefix)
|
|
{
|
|
return new VulnerabilityElementBuilder(spdxIdPrefix)
|
|
.WithVulnerabilityId(statement.VulnerabilityId)
|
|
.WithPublishedTime(statement.Timestamp)
|
|
.Build();
|
|
}
|
|
|
|
private static IEnumerable<Spdx3Element> BuildCvssAssessments(
|
|
IEnumerable<OpenVexStatement> statements,
|
|
string spdxIdPrefix)
|
|
{
|
|
foreach (var statement in statements.Where(s => s.CvssV3 is not null))
|
|
{
|
|
var cvss = CvssMapper.MapToSpdx3(
|
|
statement.CvssV3!,
|
|
statement.VulnerabilityId,
|
|
statement.ProductId,
|
|
spdxIdPrefix);
|
|
|
|
yield return cvss;
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<Spdx3Element> BuildEpssAssessments(
|
|
IEnumerable<OpenVexStatement> statements,
|
|
string spdxIdPrefix)
|
|
{
|
|
foreach (var statement in statements.Where(s => s.Epss is not null))
|
|
{
|
|
var epss = CvssMapper.MapEpssToSpdx3(
|
|
statement.Epss!,
|
|
statement.VulnerabilityId,
|
|
statement.ProductId,
|
|
spdxIdPrefix);
|
|
|
|
yield return epss;
|
|
}
|
|
}
|
|
}
|