save progress

This commit is contained in:
master
2026-01-09 18:27:36 +02:00
parent e608752924
commit a21d3dbc1f
361 changed files with 63068 additions and 1192 deletions

View File

@@ -3,6 +3,7 @@
// </copyright>
using System.Globalization;
using StellaOps.Spdx3.Model;
using StellaOps.Spdx3.Model.Security;
namespace StellaOps.VexLens.Spdx3;
@@ -41,7 +42,7 @@ public static class CvssMapper
AssessedElement = assessedElementSpdxId,
From = vulnerabilitySpdxId,
To = [assessedElementSpdxId],
RelationshipType = "hasAssessmentFor",
RelationshipType = Spdx3RelationshipType.HasAssessmentFor,
Score = cvssData.BaseScore,
Severity = MapSeverity(cvssData.BaseScore),
VectorString = cvssData.VectorString,
@@ -79,7 +80,7 @@ public static class CvssMapper
AssessedElement = assessedElementSpdxId,
From = vulnerabilitySpdxId,
To = [assessedElementSpdxId],
RelationshipType = "hasAssessmentFor",
RelationshipType = Spdx3RelationshipType.HasAssessmentFor,
Probability = epssData.Probability,
Percentile = epssData.Percentile,
PublishedTime = epssData.ScoreDate,
@@ -165,70 +166,6 @@ public static class CvssMapper
}
}
/// <summary>
/// CVSS v3 data input model.
/// Sprint: SPRINT_20260107_004_004 Task SP-007
/// </summary>
public sealed record CvssV3Data
{
/// <summary>
/// Gets or sets the CVSS v3 base score (0.0-10.0).
/// </summary>
public decimal? BaseScore { get; init; }
/// <summary>
/// Gets or sets the CVSS v3 vector string.
/// </summary>
public string? VectorString { get; init; }
/// <summary>
/// Gets or sets the temporal score.
/// </summary>
public decimal? TemporalScore { get; init; }
/// <summary>
/// Gets or sets the environmental score.
/// </summary>
public decimal? EnvironmentalScore { get; init; }
/// <summary>
/// Gets or sets when the score was published.
/// </summary>
public DateTimeOffset? PublishedTime { get; init; }
/// <summary>
/// Gets or sets when the score was modified.
/// </summary>
public DateTimeOffset? ModifiedTime { get; init; }
/// <summary>
/// Gets or sets the source of the CVSS data.
/// </summary>
public string? Source { get; init; }
}
/// <summary>
/// EPSS data input model.
/// Sprint: SPRINT_20260107_004_004 Task SP-007
/// </summary>
public sealed record EpssData
{
/// <summary>
/// Gets or sets the EPSS probability (0.0-1.0).
/// </summary>
public decimal? Probability { get; init; }
/// <summary>
/// Gets or sets the EPSS percentile (0.0-1.0).
/// </summary>
public decimal? Percentile { get; init; }
/// <summary>
/// Gets or sets the date of the EPSS score.
/// </summary>
public DateTimeOffset? ScoreDate { get; init; }
}
/// <summary>
/// Parsed CVSS v3 vector components.
/// Sprint: SPRINT_20260107_004_004 Task SP-007
@@ -262,3 +199,4 @@ public sealed record CvssVectorComponents
/// <summary>Gets or sets the Availability Impact (A).</summary>
public string? AvailabilityImpact { get; init; }
}

View File

@@ -249,33 +249,45 @@ public enum VexJustification
/// <summary>
/// CVSS v3 scoring data.
/// Sprint: SPRINT_20260107_004_004 Task SP-007
/// </summary>
public sealed record CvssV3Data
{
/// <summary>Gets the CVSS v3 base score (0.0-10.0).</summary>
public required double BaseScore { get; init; }
public decimal? BaseScore { get; init; }
/// <summary>Gets the CVSS v3 vector string.</summary>
public required string VectorString { get; init; }
public string? VectorString { get; init; }
/// <summary>Gets the temporal score if available.</summary>
public double? TemporalScore { get; init; }
public decimal? TemporalScore { get; init; }
/// <summary>Gets the environmental score if available.</summary>
public double? EnvironmentalScore { get; init; }
public decimal? EnvironmentalScore { get; init; }
/// <summary>Gets when the score was published.</summary>
public DateTimeOffset? PublishedTime { get; init; }
/// <summary>Gets when the score was modified.</summary>
public DateTimeOffset? ModifiedTime { get; init; }
/// <summary>Gets the source of the CVSS data.</summary>
public string? Source { get; init; }
}
/// <summary>
/// EPSS (Exploit Prediction Scoring System) data.
/// Sprint: SPRINT_20260107_004_004 Task SP-007
/// </summary>
public sealed record EpssData
{
/// <summary>Gets the EPSS probability (0.0-1.0).</summary>
public required double Probability { get; init; }
public decimal? Probability { get; init; }
/// <summary>Gets the EPSS percentile (0.0-1.0).</summary>
public required double Percentile { get; init; }
public decimal? Percentile { get; init; }
/// <summary>Gets when the score was assessed.</summary>
public DateTimeOffset? AssessedOn { get; init; }
/// <summary>Gets the date of the EPSS score.</summary>
public DateTimeOffset? ScoreDate { get; init; }
}

View File

@@ -2,6 +2,7 @@
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using StellaOps.Spdx3.Model;
using StellaOps.Spdx3.Model.Security;
namespace StellaOps.VexLens.Spdx3;
@@ -81,7 +82,7 @@ public static class VexStatusMapper
AssessedElement = statement.ProductId,
From = statement.VulnerabilityId,
To = [statement.ProductId],
RelationshipType = "affects",
RelationshipType = Spdx3RelationshipType.Affects,
VexVersion = "1.0.0",
StatusNotes = statement.StatusNotes,
ActionStatement = statement.ActionStatement,
@@ -104,7 +105,7 @@ public static class VexStatusMapper
AssessedElement = statement.ProductId,
From = statement.VulnerabilityId,
To = [statement.ProductId],
RelationshipType = "doesNotAffect",
RelationshipType = Spdx3RelationshipType.DoesNotAffect,
VexVersion = "1.0.0",
StatusNotes = statusNotes,
JustificationType = MapJustification(statement.Justification),
@@ -125,7 +126,7 @@ public static class VexStatusMapper
AssessedElement = statement.ProductId,
From = statement.VulnerabilityId,
To = [statement.ProductId],
RelationshipType = "fixedIn",
RelationshipType = Spdx3RelationshipType.FixedIn,
VexVersion = "1.0.0",
StatusNotes = statement.StatusNotes,
PublishedTime = statement.Timestamp,
@@ -144,7 +145,7 @@ public static class VexStatusMapper
AssessedElement = statement.ProductId,
From = statement.VulnerabilityId,
To = [statement.ProductId],
RelationshipType = "underInvestigationFor",
RelationshipType = Spdx3RelationshipType.UnderInvestigationFor,
VexVersion = "1.0.0",
StatusNotes = statement.StatusNotes,
PublishedTime = statement.Timestamp,
@@ -182,101 +183,3 @@ public static class VexStatusMapper
return $"{statusNotes}. Impact: {impactStatement}";
}
}
/// <summary>
/// Represents an OpenVEX statement for mapping purposes.
/// Sprint: SPRINT_20260107_004_004 Task SP-002
/// </summary>
public sealed record OpenVexStatement
{
/// <summary>
/// Gets or sets the vulnerability ID (e.g., CVE-2026-1234).
/// </summary>
public required string VulnerabilityId { get; init; }
/// <summary>
/// Gets or sets the product ID (PURL or SPDX ID).
/// </summary>
public required string ProductId { get; init; }
/// <summary>
/// Gets or sets the VEX status.
/// </summary>
public required VexStatus Status { get; init; }
/// <summary>
/// Gets or sets the statement timestamp.
/// </summary>
public DateTimeOffset? Timestamp { get; init; }
/// <summary>
/// Gets or sets the supplier ID.
/// </summary>
public string? Supplier { get; init; }
/// <summary>
/// Gets or sets the justification (for not_affected).
/// </summary>
public VexJustification? Justification { get; init; }
/// <summary>
/// Gets or sets the status notes.
/// </summary>
public string? StatusNotes { get; init; }
/// <summary>
/// Gets or sets the impact statement.
/// </summary>
public string? ImpactStatement { get; init; }
/// <summary>
/// Gets or sets the action statement (for affected).
/// </summary>
public string? ActionStatement { get; init; }
/// <summary>
/// Gets or sets the action statement deadline.
/// </summary>
public DateTimeOffset? ActionStatementTime { get; init; }
}
/// <summary>
/// OpenVEX status values.
/// Sprint: SPRINT_20260107_004_004 Task SP-002
/// </summary>
public enum VexStatus
{
/// <summary>Product is affected by the vulnerability.</summary>
Affected,
/// <summary>Product is not affected by the vulnerability.</summary>
NotAffected,
/// <summary>Vulnerability has been fixed in this version.</summary>
Fixed,
/// <summary>Vulnerability impact is under investigation.</summary>
UnderInvestigation
}
/// <summary>
/// OpenVEX justification values for not_affected status.
/// Sprint: SPRINT_20260107_004_004 Task SP-002
/// </summary>
public enum VexJustification
{
/// <summary>Component is not present in the product.</summary>
ComponentNotPresent,
/// <summary>Vulnerable code is not present.</summary>
VulnerableCodeNotPresent,
/// <summary>Vulnerable code cannot be controlled by adversary.</summary>
VulnerableCodeCannotBeControlledByAdversary,
/// <summary>Vulnerable code is not in execute path.</summary>
VulnerableCodeNotInExecutePath,
/// <summary>Inline mitigations already exist.</summary>
InlineMitigationsAlreadyExist
}

View File

@@ -156,9 +156,9 @@ public sealed class VexToSpdx3Mapper : IVexToSpdx3Mapper
foreach (var statement in statements.Where(s => s.CvssV3 is not null))
{
var cvss = CvssMapper.MapToSpdx3(
statement.CvssV3!,
statement.VulnerabilityId,
statement.ProductId,
statement.CvssV3!,
spdxIdPrefix);
yield return cvss;
@@ -172,9 +172,9 @@ public sealed class VexToSpdx3Mapper : IVexToSpdx3Mapper
foreach (var statement in statements.Where(s => s.Epss is not null))
{
var epss = CvssMapper.MapEpssToSpdx3(
statement.Epss!,
statement.VulnerabilityId,
statement.ProductId,
statement.Epss!,
spdxIdPrefix);
yield return epss;

View File

@@ -49,27 +49,27 @@ public sealed class VulnerabilityElementBuilder
{
_externalIdentifiers.Add(new Spdx3ExternalIdentifier
{
ExternalIdentifierType = "cve",
ExternalIdentifierType = Spdx3ExternalIdentifierType.Cve,
Identifier = vulnerabilityId,
IdentifierLocator = ImmutableArray.Create($"https://nvd.nist.gov/vuln/detail/{vulnerabilityId}")
Comment = $"https://nvd.nist.gov/vuln/detail/{vulnerabilityId}"
});
}
else if (vulnerabilityId.StartsWith("GHSA-", StringComparison.OrdinalIgnoreCase))
{
_externalIdentifiers.Add(new Spdx3ExternalIdentifier
{
ExternalIdentifierType = "ghsa",
ExternalIdentifierType = Spdx3ExternalIdentifierType.SecurityOther,
Identifier = vulnerabilityId,
IdentifierLocator = ImmutableArray.Create($"https://github.com/advisories/{vulnerabilityId}")
Comment = $"GitHub Security Advisory: https://github.com/advisories/{vulnerabilityId}"
});
}
else if (vulnerabilityId.StartsWith("OSV-", StringComparison.OrdinalIgnoreCase))
{
_externalIdentifiers.Add(new Spdx3ExternalIdentifier
{
ExternalIdentifierType = "osv",
ExternalIdentifierType = Spdx3ExternalIdentifierType.SecurityOther,
Identifier = vulnerabilityId,
IdentifierLocator = ImmutableArray.Create($"https://osv.dev/vulnerability/{vulnerabilityId}")
Comment = $"OSV Vulnerability: https://osv.dev/vulnerability/{vulnerabilityId}"
});
}
@@ -119,7 +119,7 @@ public sealed class VulnerabilityElementBuilder
ArgumentException.ThrowIfNullOrWhiteSpace(cveId);
_externalRefs.Add(new Spdx3ExternalRef
{
ExternalRefType = "securityAdvisory",
ExternalRefType = Spdx3ExternalRefType.SecurityAdvisory,
Locator = ImmutableArray.Create($"https://nvd.nist.gov/vuln/detail/{cveId}")
});
return this;
@@ -135,7 +135,7 @@ public sealed class VulnerabilityElementBuilder
ArgumentException.ThrowIfNullOrWhiteSpace(vulnerabilityId);
_externalRefs.Add(new Spdx3ExternalRef
{
ExternalRefType = "securityAdvisory",
ExternalRefType = Spdx3ExternalRefType.SecurityAdvisory,
Locator = ImmutableArray.Create($"https://osv.dev/vulnerability/{vulnerabilityId}")
});
return this;
@@ -147,9 +147,8 @@ public sealed class VulnerabilityElementBuilder
/// <param name="refType">The reference type.</param>
/// <param name="locator">The reference URL.</param>
/// <returns>This builder for fluent chaining.</returns>
public VulnerabilityElementBuilder WithExternalRef(string refType, string locator)
public VulnerabilityElementBuilder WithExternalRef(Spdx3ExternalRefType refType, string locator)
{
ArgumentException.ThrowIfNullOrWhiteSpace(refType);
ArgumentException.ThrowIfNullOrWhiteSpace(locator);
_externalRefs.Add(new Spdx3ExternalRef
{
@@ -213,57 +212,3 @@ public sealed class VulnerabilityElementBuilder
return $"{_spdxIdPrefix.TrimEnd('/')}/vulnerability/{shortHash}";
}
}
/// <summary>
/// SPDX 3.0.1 External Identifier.
/// Sprint: SPRINT_20260107_004_004 Task SP-004
/// </summary>
public sealed record Spdx3ExternalIdentifier
{
/// <summary>
/// Gets or sets the external identifier type (e.g., "cve", "ghsa", "osv").
/// </summary>
public required string ExternalIdentifierType { get; init; }
/// <summary>
/// Gets or sets the identifier value.
/// </summary>
public required string Identifier { get; init; }
/// <summary>
/// Gets or sets the locator URLs for the identifier.
/// </summary>
public ImmutableArray<string> IdentifierLocator { get; init; } = ImmutableArray<string>.Empty;
/// <summary>
/// Gets or sets issuing authority of the identifier.
/// </summary>
public string? IssuingAuthority { get; init; }
}
/// <summary>
/// SPDX 3.0.1 External Reference.
/// Sprint: SPRINT_20260107_004_004 Task SP-004
/// </summary>
public sealed record Spdx3ExternalRef
{
/// <summary>
/// Gets or sets the external reference type (e.g., "securityAdvisory").
/// </summary>
public required string ExternalRefType { get; init; }
/// <summary>
/// Gets or sets the locator URLs.
/// </summary>
public ImmutableArray<string> Locator { get; init; } = ImmutableArray<string>.Empty;
/// <summary>
/// Gets or sets the content type of the referenced resource.
/// </summary>
public string? ContentType { get; init; }
/// <summary>
/// Gets or sets a comment about the reference.
/// </summary>
public string? Comment { get; init; }
}

View File

@@ -0,0 +1,395 @@
// -----------------------------------------------------------------------------
// SecurityProfileIntegrationTests.cs
// Sprint: SPRINT_20260107_004_004_BE_spdx3_security_profile
// Task: SP-013 - Integration tests for SPDX 3.0.1 Security profile
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.Text.Json;
using FluentAssertions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Spdx3.Model;
using StellaOps.Spdx3.Model.Security;
using StellaOps.Vex.OpenVex;
using Xunit;
namespace StellaOps.VexLens.Spdx3.Tests.Integration;
/// <summary>
/// Integration tests for SPDX 3.0.1 Security profile end-to-end flows.
/// These tests verify the complete VEX-to-SPDX 3.0.1 pipeline.
/// </summary>
[Trait("Category", "Integration")]
public sealed class SecurityProfileIntegrationTests
{
private static readonly DateTimeOffset FixedTimestamp =
new(2026, 1, 9, 12, 0, 0, TimeSpan.Zero);
[Fact]
public async Task EndToEnd_VexConsensusToSpdx3_ProducesValidSecurityProfile()
{
// Arrange: Create a realistic VEX consensus result
var vexConsensus = new VexConsensus
{
ConsensusId = "consensus-001",
ComponentPurl = "pkg:npm/lodash@4.17.21",
CveId = "CVE-2021-23337",
FinalStatus = VexStatus.Affected,
FinalJustification = null,
ConfidenceScore = 0.95,
StatementCount = 3,
Timestamp = FixedTimestamp,
ActionStatement = "Upgrade to lodash@4.17.22 or later",
ActionStatementTime = FixedTimestamp.AddDays(30),
StatusNotes = "Prototype pollution vulnerability in defaultsDeep function"
};
var timeProvider = new FakeTimeProvider(FixedTimestamp);
var mapper = new VexToSpdx3Mapper(timeProvider);
// Act: Map VEX consensus to SPDX 3.0.1
var securityElements = await mapper.MapConsensusAsync(
vexConsensus,
CancellationToken.None);
// Assert: Verify all elements are created correctly
securityElements.Should().NotBeNull();
securityElements.Vulnerability.Should().NotBeNull();
securityElements.Assessment.Should().NotBeNull();
var vuln = securityElements.Vulnerability;
vuln.ExternalIdentifiers.Should().Contain(id =>
id.Identifier == "CVE-2021-23337" && id.IdentifierType == "cve");
var assessment = securityElements.Assessment as Spdx3VexAffectedVulnAssessmentRelationship;
assessment.Should().NotBeNull();
assessment!.StatusNotes.Should().Contain("Prototype pollution");
assessment.ActionStatement.Should().Be("Upgrade to lodash@4.17.22 or later");
}
[Fact]
public async Task CombinedSbomVex_GeneratesValidDocument()
{
// Arrange: Create Software profile SBOM
var sbomDocument = new Spdx3Document
{
SpdxId = "urn:stellaops:sbom:myapp-001",
Name = "MyApp SBOM with VEX",
Namespaces = ImmutableArray.Create("https://stellaops.org/spdx/"),
ProfileConformance = ImmutableArray.Create(Spdx3Profile.Software),
Elements = ImmutableArray.Create<Spdx3Element>(
new Spdx3Package
{
SpdxId = "urn:stellaops:pkg:lodash-4.17.21",
Name = "lodash",
PackageVersion = "4.17.21",
PackageUrl = "pkg:npm/lodash@4.17.21"
},
new Spdx3Package
{
SpdxId = "urn:stellaops:pkg:express-4.18.2",
Name = "express",
PackageVersion = "4.18.2",
PackageUrl = "pkg:npm/express@4.18.2"
}
)
};
// Arrange: Create VEX statements
var vexStatements = new[]
{
new OpenVexStatement
{
StatementId = "stmt-001",
Vulnerability = new VulnerabilityReference { Name = "CVE-2021-23337" },
Products = ImmutableArray.Create(new ProductReference { Id = "pkg:npm/lodash@4.17.21" }),
Status = VexStatus.Affected,
ActionStatement = "Upgrade to 4.17.22",
Timestamp = FixedTimestamp
},
new OpenVexStatement
{
StatementId = "stmt-002",
Vulnerability = new VulnerabilityReference { Name = "CVE-2024-1234" },
Products = ImmutableArray.Create(new ProductReference { Id = "pkg:npm/express@4.18.2" }),
Status = VexStatus.NotAffected,
Justification = VexJustification.VulnerableCodeNotPresent,
ImpactStatement = "The vulnerable code path is not used",
Timestamp = FixedTimestamp
}
};
var timeProvider = new FakeTimeProvider(FixedTimestamp);
var mapper = new VexToSpdx3Mapper(timeProvider);
// Act: Build combined document
var builder = new CombinedSbomVexBuilder(mapper);
var combinedDoc = await builder
.WithSoftwareDocument(sbomDocument)
.WithVexStatements(vexStatements)
.BuildAsync(CancellationToken.None);
// Assert: Combined document has both profiles
combinedDoc.Should().NotBeNull();
combinedDoc.ProfileConformance.Should().Contain(Spdx3Profile.Software);
combinedDoc.ProfileConformance.Should().Contain(Spdx3Profile.Security);
// Assert: Contains packages, vulnerabilities, and assessments
combinedDoc.Elements.OfType<Spdx3Package>().Should().HaveCount(2);
combinedDoc.Elements.OfType<Spdx3Vulnerability>().Should().HaveCount(2);
combinedDoc.Elements.OfType<Spdx3VulnAssessmentRelationship>().Should().HaveCount(2);
// Assert: Affected assessment has action
var affectedAssessment = combinedDoc.Elements
.OfType<Spdx3VexAffectedVulnAssessmentRelationship>()
.FirstOrDefault();
affectedAssessment.Should().NotBeNull();
affectedAssessment!.ActionStatement.Should().Be("Upgrade to 4.17.22");
// Assert: Not affected assessment has justification
var notAffectedAssessment = combinedDoc.Elements
.OfType<Spdx3VexNotAffectedVulnAssessmentRelationship>()
.FirstOrDefault();
notAffectedAssessment.Should().NotBeNull();
notAffectedAssessment!.Justification.Should().Be(Spdx3VexJustification.VulnerableCodeNotPresent);
}
[Fact]
public void ParseExternalSecurityProfile_ValidDocument_ExtractsAllElements()
{
// Arrange: External SPDX 3.0.1 Security profile JSON
var externalJson = """
{
"@context": "https://spdx.org/rdf/3.0.1/terms/",
"@graph": [
{
"@type": "security_Vulnerability",
"spdxId": "urn:external:vuln:CVE-2024-5678",
"name": "CVE-2024-5678",
"summary": "Remote code execution in XML parser",
"externalIdentifier": [
{
"identifierType": "cve",
"identifier": "CVE-2024-5678"
}
],
"security_publishedTime": "2024-03-15T10:00:00Z",
"security_modifiedTime": "2024-03-20T14:30:00Z"
},
{
"@type": "security_VexAffectedVulnAssessmentRelationship",
"spdxId": "urn:external:vex:assessment-001",
"from": "urn:external:vuln:CVE-2024-5678",
"to": ["urn:external:pkg:xml-parser-1.0.0"],
"relationshipType": "affects",
"security_assessedElement": "urn:external:pkg:xml-parser-1.0.0",
"security_publishedTime": "2024-03-16T09:00:00Z",
"security_statusNotes": "Affected when parsing untrusted XML",
"security_actionStatement": "Upgrade to xml-parser@2.0.0"
}
]
}
""";
// Act: Parse the external document
var parser = new Spdx3Parser();
var parseResult = parser.Parse(externalJson);
// Assert: Document parses successfully
parseResult.IsSuccess.Should().BeTrue();
parseResult.Document.Should().NotBeNull();
// Assert: Vulnerability element parsed
var vulnerabilities = parseResult.Document!.Elements
.OfType<Spdx3Vulnerability>()
.ToList();
vulnerabilities.Should().HaveCount(1);
var vuln = vulnerabilities[0];
vuln.SpdxId.Should().Be("urn:external:vuln:CVE-2024-5678");
vuln.Name.Should().Be("CVE-2024-5678");
vuln.Summary.Should().Contain("Remote code execution");
// Assert: VEX assessment parsed
var assessments = parseResult.Document.Elements
.OfType<Spdx3VexAffectedVulnAssessmentRelationship>()
.ToList();
assessments.Should().HaveCount(1);
var assessment = assessments[0];
assessment.From.Should().Be("urn:external:vuln:CVE-2024-5678");
assessment.StatusNotes.Should().Contain("untrusted XML");
assessment.ActionStatement.Should().Be("Upgrade to xml-parser@2.0.0");
}
[Fact]
public async Task AllVexStatuses_MapCorrectly()
{
// Arrange: Create VEX statements for each status
var timeProvider = new FakeTimeProvider(FixedTimestamp);
var mapper = new VexToSpdx3Mapper(timeProvider);
var statuses = new[]
{
(VexStatus.Affected, typeof(Spdx3VexAffectedVulnAssessmentRelationship)),
(VexStatus.NotAffected, typeof(Spdx3VexNotAffectedVulnAssessmentRelationship)),
(VexStatus.Fixed, typeof(Spdx3VexFixedVulnAssessmentRelationship)),
(VexStatus.UnderInvestigation, typeof(Spdx3VexUnderInvestigationVulnAssessmentRelationship))
};
foreach (var (status, expectedType) in statuses)
{
var statement = new OpenVexStatement
{
StatementId = $"stmt-{status}",
Vulnerability = new VulnerabilityReference { Name = $"CVE-{status}" },
Products = ImmutableArray.Create(new ProductReference { Id = "pkg:test/pkg@1.0.0" }),
Status = status,
Justification = status == VexStatus.NotAffected
? VexJustification.VulnerableCodeNotPresent
: null,
Timestamp = FixedTimestamp
};
// Act
var elements = await mapper.MapStatementAsync(statement, CancellationToken.None);
// Assert
elements.Assessment.Should().NotBeNull();
elements.Assessment.GetType().Should().Be(expectedType,
$"Status {status} should map to {expectedType.Name}");
}
}
[Fact]
public async Task CvssAndEpssData_IncludedInDocument()
{
// Arrange
var timeProvider = new FakeTimeProvider(FixedTimestamp);
var cvssMapper = new CvssMapper();
var cvssData = new CvssV3Data
{
VectorString = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
BaseScore = 9.8,
BaseSeverity = "CRITICAL"
};
var epssData = new EpssData
{
Score = 0.97,
Percentile = 99.5,
AssessmentDate = FixedTimestamp
};
// Act
var cvssRelationship = cvssMapper.MapCvssToSpdx3(
"urn:test:vuln:CVE-2024-9999",
"urn:test:pkg:target",
cvssData);
var epssRelationship = cvssMapper.MapEpssToSpdx3(
"urn:test:vuln:CVE-2024-9999",
"urn:test:pkg:target",
epssData);
// Assert: CVSS relationship
cvssRelationship.Should().NotBeNull();
cvssRelationship.Score.Should().Be(9.8);
cvssRelationship.Severity.Should().Be(Spdx3CvssSeverity.Critical);
cvssRelationship.VectorString.Should().Be("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H");
// Assert: EPSS relationship
epssRelationship.Should().NotBeNull();
epssRelationship.Probability.Should().Be(0.97);
epssRelationship.Percentile.Should().Be(99.5);
}
[Fact]
public void RoundTrip_SerializeAndParse_PreservesAllData()
{
// Arrange: Create a complete Security profile document
var originalDoc = new Spdx3Document
{
SpdxId = "urn:stellaops:security:roundtrip-001",
Name = "Security Profile Round-Trip Test",
Namespaces = ImmutableArray.Create("https://stellaops.org/spdx/"),
ProfileConformance = ImmutableArray.Create(Spdx3Profile.Security),
Elements = ImmutableArray.Create<Spdx3Element>(
new Spdx3Vulnerability
{
SpdxId = "urn:stellaops:vuln:CVE-2024-RT",
Name = "CVE-2024-RT",
Summary = "Round-trip test vulnerability",
PublishedTime = FixedTimestamp.AddDays(-30),
ModifiedTime = FixedTimestamp,
ExternalIdentifiers = ImmutableArray.Create(new Spdx3ExternalIdentifier
{
IdentifierType = "cve",
Identifier = "CVE-2024-RT"
})
},
new Spdx3VexAffectedVulnAssessmentRelationship
{
SpdxId = "urn:stellaops:vex:rt-assessment-001",
From = "urn:stellaops:vuln:CVE-2024-RT",
To = ImmutableArray.Create("urn:stellaops:pkg:rt-pkg"),
RelationshipType = Spdx3RelationshipType.Affects,
AssessedElement = "urn:stellaops:pkg:rt-pkg",
PublishedTime = FixedTimestamp,
StatusNotes = "Affected in all versions",
ActionStatement = "No patch available yet",
ActionStatementTime = FixedTimestamp.AddDays(14)
}
)
};
// Act: Serialize and parse
var serializer = new Spdx3JsonSerializer();
var json = serializer.Serialize(originalDoc);
var parser = new Spdx3Parser();
var parseResult = parser.Parse(json);
// Assert: Parsing succeeded
parseResult.IsSuccess.Should().BeTrue();
var parsedDoc = parseResult.Document;
// Assert: All data preserved
parsedDoc.Should().NotBeNull();
parsedDoc!.SpdxId.Should().Be(originalDoc.SpdxId);
parsedDoc.Name.Should().Be(originalDoc.Name);
parsedDoc.ProfileConformance.Should().BeEquivalentTo(originalDoc.ProfileConformance);
// Assert: Vulnerability preserved
var parsedVuln = parsedDoc.Elements.OfType<Spdx3Vulnerability>().FirstOrDefault();
parsedVuln.Should().NotBeNull();
parsedVuln!.Name.Should().Be("CVE-2024-RT");
parsedVuln.Summary.Should().Be("Round-trip test vulnerability");
// Assert: Assessment preserved
var parsedAssessment = parsedDoc.Elements
.OfType<Spdx3VexAffectedVulnAssessmentRelationship>()
.FirstOrDefault();
parsedAssessment.Should().NotBeNull();
parsedAssessment!.StatusNotes.Should().Be("Affected in all versions");
parsedAssessment.ActionStatement.Should().Be("No patch available yet");
}
}
/// <summary>
/// Simple JSON serializer for SPDX 3.0.1 documents (test implementation).
/// </summary>
file sealed class Spdx3JsonSerializer
{
private static readonly JsonSerializerOptions Options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
public string Serialize(Spdx3Document document)
{
return JsonSerializer.Serialize(document, Options);
}
}