save dev progress

This commit is contained in:
StellaOps Bot
2025-12-26 00:32:35 +02:00
parent aa70af062e
commit ed3079543c
142 changed files with 23771 additions and 232 deletions

View File

@@ -0,0 +1,503 @@
// -----------------------------------------------------------------------------
// SbomParserTests.cs
// Sprint: SPRINT_8200_0013_0003_SCAN_sbom_intersection_scoring
// Task: SBOM-8200-007
// Description: Unit tests for SBOM parsing and PURL extraction
// Supports CycloneDX 1.4-1.7 and SPDX 2.2-2.3, 3.0
// -----------------------------------------------------------------------------
using System.Text;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
using StellaOps.Concelier.SbomIntegration.Models;
using StellaOps.Concelier.SbomIntegration.Parsing;
using Xunit;
namespace StellaOps.Concelier.SbomIntegration.Tests;
public class SbomParserTests
{
private readonly SbomParser _parser;
public SbomParserTests()
{
var loggerMock = new Mock<ILogger<SbomParser>>();
_parser = new SbomParser(loggerMock.Object);
}
#region CycloneDX Tests
[Fact]
public async Task ParseAsync_CycloneDX_ExtractsPurls()
{
// Arrange
var cycloneDxContent = """
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"version": 1,
"metadata": {
"component": {
"type": "application",
"name": "myapp",
"version": "1.0.0"
}
},
"components": [
{
"type": "library",
"name": "lodash",
"version": "4.17.21",
"purl": "pkg:npm/lodash@4.17.21"
},
{
"type": "library",
"name": "express",
"version": "4.18.2",
"purl": "pkg:npm/express@4.18.2"
}
]
}
""";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(cycloneDxContent));
// Act
var result = await _parser.ParseAsync(stream, SbomFormat.CycloneDX);
// Assert
result.Should().NotBeNull();
result.PrimaryName.Should().Be("myapp");
result.PrimaryVersion.Should().Be("1.0.0");
result.Purls.Should().HaveCount(2);
result.Purls.Should().Contain("pkg:npm/lodash@4.17.21");
result.Purls.Should().Contain("pkg:npm/express@4.18.2");
}
[Fact]
public async Task ParseAsync_CycloneDX_HandlesNestedComponents()
{
// Arrange
var cycloneDxContent = """
{
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"components": [
{
"type": "library",
"name": "parent",
"version": "1.0.0",
"purl": "pkg:npm/parent@1.0.0",
"components": [
{
"type": "library",
"name": "child",
"version": "2.0.0",
"purl": "pkg:npm/child@2.0.0"
}
]
}
]
}
""";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(cycloneDxContent));
// Act
var result = await _parser.ParseAsync(stream, SbomFormat.CycloneDX);
// Assert
result.Purls.Should().Contain("pkg:npm/parent@1.0.0");
result.Purls.Should().Contain("pkg:npm/child@2.0.0");
}
[Fact]
public async Task ParseAsync_CycloneDX_SkipsComponentsWithoutPurl()
{
// Arrange
var cycloneDxContent = """
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"components": [
{
"type": "library",
"name": "with-purl",
"version": "1.0.0",
"purl": "pkg:npm/with-purl@1.0.0"
},
{
"type": "library",
"name": "without-purl",
"version": "1.0.0"
}
]
}
""";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(cycloneDxContent));
// Act
var result = await _parser.ParseAsync(stream, SbomFormat.CycloneDX);
// Assert
result.Purls.Should().HaveCount(1);
result.Purls.Should().Contain("pkg:npm/with-purl@1.0.0");
result.UnresolvedComponents.Should().HaveCount(1);
result.UnresolvedComponents[0].Name.Should().Be("without-purl");
}
[Fact]
public async Task ParseAsync_CycloneDX_DeduplicatesPurls()
{
// Arrange
var cycloneDxContent = """
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"components": [
{
"type": "library",
"purl": "pkg:npm/lodash@4.17.21"
},
{
"type": "library",
"purl": "pkg:npm/lodash@4.17.21"
}
]
}
""";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(cycloneDxContent));
// Act
var result = await _parser.ParseAsync(stream, SbomFormat.CycloneDX);
// Assert
result.Purls.Should().HaveCount(1);
}
[Fact]
public async Task ParseAsync_CycloneDX17_ExtractsPurls()
{
// Arrange - CycloneDX 1.7 format
var cycloneDxContent = """
{
"bomFormat": "CycloneDX",
"specVersion": "1.7",
"version": 1,
"metadata": {
"component": {
"type": "application",
"name": "myapp",
"version": "2.0.0"
}
},
"components": [
{
"type": "library",
"name": "axios",
"version": "1.6.0",
"purl": "pkg:npm/axios@1.6.0"
}
]
}
""";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(cycloneDxContent));
// Act
var result = await _parser.ParseAsync(stream, SbomFormat.CycloneDX);
// Assert
result.Should().NotBeNull();
result.PrimaryName.Should().Be("myapp");
result.Purls.Should().Contain("pkg:npm/axios@1.6.0");
}
#endregion
#region SPDX Tests
[Fact]
public async Task ParseAsync_SPDX_ExtractsPurls()
{
// Arrange
var spdxContent = """
{
"spdxVersion": "SPDX-2.3",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "myapp-sbom",
"packages": [
{
"SPDXID": "SPDXRef-Package-npm-lodash",
"name": "lodash",
"versionInfo": "4.17.21",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:npm/lodash@4.17.21"
}
]
},
{
"SPDXID": "SPDXRef-Package-npm-express",
"name": "express",
"versionInfo": "4.18.2",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:npm/express@4.18.2"
}
]
}
]
}
""";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(spdxContent));
// Act
var result = await _parser.ParseAsync(stream, SbomFormat.SPDX);
// Assert
result.Purls.Should().HaveCount(2);
result.Purls.Should().Contain("pkg:npm/lodash@4.17.21");
result.Purls.Should().Contain("pkg:npm/express@4.18.2");
}
[Fact]
public async Task ParseAsync_SPDX_IgnoresNonPurlExternalRefs()
{
// Arrange
var spdxContent = """
{
"spdxVersion": "SPDX-2.3",
"packages": [
{
"SPDXID": "SPDXRef-Package",
"name": "mypackage",
"externalRefs": [
{
"referenceCategory": "SECURITY",
"referenceType": "cpe23Type",
"referenceLocator": "cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*"
},
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:npm/mypackage@1.0.0"
}
]
}
]
}
""";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(spdxContent));
// Act
var result = await _parser.ParseAsync(stream, SbomFormat.SPDX);
// Assert
result.Purls.Should().HaveCount(1);
result.Purls.Should().Contain("pkg:npm/mypackage@1.0.0");
result.Cpes.Should().HaveCount(1);
result.Cpes.Should().Contain("cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*");
}
#endregion
#region Format Detection Tests
[Theory]
[InlineData("1.4")]
[InlineData("1.5")]
[InlineData("1.6")]
[InlineData("1.7")]
public async Task DetectFormatAsync_CycloneDX_DetectsAllVersions(string specVersion)
{
// Arrange
var content = $$"""
{
"bomFormat": "CycloneDX",
"specVersion": "{{specVersion}}",
"components": []
}
""";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
// Act
var result = await _parser.DetectFormatAsync(stream);
// Assert
result.IsDetected.Should().BeTrue();
result.Format.Should().Be(SbomFormat.CycloneDX);
result.SpecVersion.Should().Be(specVersion);
}
[Fact]
public async Task DetectFormatAsync_SPDX2_DetectsFormat()
{
// Arrange
var content = """
{
"spdxVersion": "SPDX-2.3",
"packages": []
}
""";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
// Act
var result = await _parser.DetectFormatAsync(stream);
// Assert
result.IsDetected.Should().BeTrue();
result.Format.Should().Be(SbomFormat.SPDX);
result.SpecVersion.Should().Be("SPDX-2.3");
}
[Fact]
public async Task DetectFormatAsync_UnknownFormat_ReturnsNotDetected()
{
// Arrange
var content = """
{
"unknownField": "value"
}
""";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
// Act
var result = await _parser.DetectFormatAsync(stream);
// Assert
result.IsDetected.Should().BeFalse();
}
[Fact]
public async Task DetectFormatAsync_InvalidJson_ReturnsNotDetected()
{
// Arrange
var content = "not valid json {{{";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
// Act
var result = await _parser.DetectFormatAsync(stream);
// Assert
result.IsDetected.Should().BeFalse();
}
#endregion
#region PURL Ecosystem Tests
[Theory]
[InlineData("pkg:npm/lodash@4.17.21")]
[InlineData("pkg:pypi/requests@2.28.0")]
[InlineData("pkg:maven/org.apache.commons/commons-lang3@3.12.0")]
[InlineData("pkg:nuget/Newtonsoft.Json@13.0.1")]
[InlineData("pkg:cargo/serde@1.0.150")]
[InlineData("pkg:golang/github.com/gin-gonic/gin@1.9.0")]
[InlineData("pkg:gem/rails@7.0.4")]
[InlineData("pkg:deb/debian/openssl@1.1.1n-0+deb11u3")]
[InlineData("pkg:rpm/fedora/kernel@5.19.0-43.fc37")]
[InlineData("pkg:apk/alpine/openssl@1.1.1q-r0")]
public async Task ParseAsync_CycloneDX_SupportsVariousEcosystems(string purl)
{
// Arrange
var content = $$"""
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"components": [
{
"type": "library",
"purl": "{{purl}}"
}
]
}
""";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
// Act
var result = await _parser.ParseAsync(stream, SbomFormat.CycloneDX);
// Assert
result.Purls.Should().Contain(purl);
}
#endregion
#region Edge Cases
[Fact]
public async Task ParseAsync_EmptyComponents_ReturnsEmptyPurls()
{
// Arrange
var content = """
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"components": []
}
""";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
// Act
var result = await _parser.ParseAsync(stream, SbomFormat.CycloneDX);
// Assert
result.Purls.Should().BeEmpty();
result.TotalComponents.Should().Be(0);
}
[Fact]
public async Task ParseAsync_NullStream_ThrowsArgumentNullException()
{
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(() =>
_parser.ParseAsync(null!, SbomFormat.CycloneDX));
}
[Fact]
public async Task ParseAsync_ExtractsCpes()
{
// Arrange
var content = """
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"components": [
{
"type": "library",
"name": "openssl",
"cpe": "cpe:2.3:a:openssl:openssl:1.1.1:*:*:*:*:*:*:*",
"purl": "pkg:deb/debian/openssl@1.1.1"
}
]
}
""";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
// Act
var result = await _parser.ParseAsync(stream, SbomFormat.CycloneDX);
// Assert
result.Cpes.Should().HaveCount(1);
result.Cpes.Should().Contain("cpe:2.3:a:openssl:openssl:1.1.1:*:*:*:*:*:*:*");
}
#endregion
}