new advisories work and features gaps work
This commit is contained in:
@@ -1,395 +0,0 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user