save progress
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
// <copyright file="CveSymbolMappingServiceTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Reachability.Core.CveMapping;
|
||||
using StellaOps.Reachability.Core.Symbols;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Reachability.Core.Tests.CveMapping;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="CveSymbolMappingService"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class CveSymbolMappingServiceTests
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly SymbolCanonicalizer _canonicalizer;
|
||||
private readonly CveSymbolMappingService _service;
|
||||
|
||||
public CveSymbolMappingServiceTests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 9, 12, 0, 0, TimeSpan.Zero));
|
||||
_canonicalizer = new SymbolCanonicalizer();
|
||||
_service = new CveSymbolMappingService(
|
||||
_canonicalizer,
|
||||
_timeProvider,
|
||||
NullLogger<CveSymbolMappingService>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetMappingAsync_NotFound_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
var result = await _service.GetMappingAsync("CVE-2021-44228", CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IngestMapping_ThenGetMapping_ReturnsMapping()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = CreateTestSymbol("org.apache.log4j.core.lookup", "jndilookup", "lookup");
|
||||
var mapping = CveSymbolMapping.Create(
|
||||
"CVE-2021-44228",
|
||||
[VulnerableSymbol.Create(symbol, VulnerabilityType.Sink, 0.9)],
|
||||
MappingSource.PatchAnalysis,
|
||||
0.9,
|
||||
_timeProvider);
|
||||
|
||||
// Act
|
||||
await _service.IngestMappingAsync(mapping, CancellationToken.None);
|
||||
var result = await _service.GetMappingAsync("CVE-2021-44228", CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.CveId.Should().Be("CVE-2021-44228");
|
||||
result.Symbols.Should().HaveCount(1);
|
||||
result.Source.Should().Be(MappingSource.PatchAnalysis);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IngestMapping_CaseInsensitiveCveId()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = CreateTestSymbol("org.example", "service", "method");
|
||||
var mapping = CveSymbolMapping.Create(
|
||||
"cve-2023-12345",
|
||||
[VulnerableSymbol.Create(symbol, VulnerabilityType.Unknown, 0.5)],
|
||||
MappingSource.ManualCuration,
|
||||
0.5,
|
||||
_timeProvider);
|
||||
|
||||
await _service.IngestMappingAsync(mapping, CancellationToken.None);
|
||||
|
||||
// Act - Query with different cases
|
||||
var result1 = await _service.GetMappingAsync("CVE-2023-12345", CancellationToken.None);
|
||||
var result2 = await _service.GetMappingAsync("cve-2023-12345", CancellationToken.None);
|
||||
var result3 = await _service.GetMappingAsync("Cve-2023-12345", CancellationToken.None);
|
||||
|
||||
// Assert - All queries return the same mapping (lookup is case-insensitive)
|
||||
result1.Should().NotBeNull();
|
||||
result2.Should().NotBeNull();
|
||||
result3.Should().NotBeNull();
|
||||
// The stored CVE ID retains original case from mapping creation
|
||||
result1!.CveId.Should().Be(result2!.CveId);
|
||||
result1.CveId.Should().Be(result3!.CveId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetMappingsBatchAsync_ReturnsOnlyFound()
|
||||
{
|
||||
// Arrange
|
||||
var symbol1 = CreateTestSymbol("org.example", "a", "method1");
|
||||
var symbol2 = CreateTestSymbol("org.example", "b", "method2");
|
||||
|
||||
var mapping1 = CveSymbolMapping.Create(
|
||||
"CVE-2021-0001",
|
||||
[VulnerableSymbol.Create(symbol1, VulnerabilityType.Sink, 0.8)],
|
||||
MappingSource.PatchAnalysis,
|
||||
0.8,
|
||||
_timeProvider);
|
||||
|
||||
var mapping2 = CveSymbolMapping.Create(
|
||||
"CVE-2021-0002",
|
||||
[VulnerableSymbol.Create(symbol2, VulnerabilityType.TaintSource, 0.7)],
|
||||
MappingSource.OsvDatabase,
|
||||
0.7,
|
||||
_timeProvider);
|
||||
|
||||
await _service.IngestMappingAsync(mapping1, CancellationToken.None);
|
||||
await _service.IngestMappingAsync(mapping2, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var result = await _service.GetMappingsBatchAsync(
|
||||
["CVE-2021-0001", "CVE-2021-0002", "CVE-2021-9999"],
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(2);
|
||||
result.Should().ContainKey("CVE-2021-0001");
|
||||
result.Should().ContainKey("CVE-2021-0002");
|
||||
result.Should().NotContainKey("CVE-2021-9999");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IngestMapping_MergesDuplicates()
|
||||
{
|
||||
// Arrange
|
||||
var symbol1 = CreateTestSymbol("org.example", "service", "method1");
|
||||
var symbol2 = CreateTestSymbol("org.example", "service", "method2");
|
||||
|
||||
var mapping1 = CveSymbolMapping.Create(
|
||||
"CVE-2021-0001",
|
||||
[VulnerableSymbol.Create(symbol1, VulnerabilityType.Sink, 0.8)],
|
||||
MappingSource.PatchAnalysis,
|
||||
0.8,
|
||||
_timeProvider);
|
||||
|
||||
var mapping2 = CveSymbolMapping.Create(
|
||||
"CVE-2021-0001",
|
||||
[VulnerableSymbol.Create(symbol2, VulnerabilityType.TaintSource, 0.7)],
|
||||
MappingSource.OsvDatabase,
|
||||
0.7,
|
||||
_timeProvider);
|
||||
|
||||
// Act
|
||||
await _service.IngestMappingAsync(mapping1, CancellationToken.None);
|
||||
await _service.IngestMappingAsync(mapping2, CancellationToken.None);
|
||||
|
||||
var result = await _service.GetMappingAsync("CVE-2021-0001", CancellationToken.None);
|
||||
|
||||
// Assert - Should have both symbols merged
|
||||
result.Should().NotBeNull();
|
||||
result!.Symbols.Should().HaveCount(2);
|
||||
result.Confidence.Should().Be(0.8); // Max of both
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCvesForSymbolAsync_ReturnsMatchingCves()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = CreateTestSymbol("org.apache.log4j.core.lookup", "jndilookup", "lookup");
|
||||
|
||||
var mapping1 = CveSymbolMapping.Create(
|
||||
"CVE-2021-44228",
|
||||
[VulnerableSymbol.Create(symbol, VulnerabilityType.Sink, 0.9)],
|
||||
MappingSource.PatchAnalysis,
|
||||
0.9,
|
||||
_timeProvider);
|
||||
|
||||
var mapping2 = CveSymbolMapping.Create(
|
||||
"CVE-2021-45046",
|
||||
[VulnerableSymbol.Create(symbol, VulnerabilityType.Sink, 0.85)],
|
||||
MappingSource.PatchAnalysis,
|
||||
0.85,
|
||||
_timeProvider);
|
||||
|
||||
await _service.IngestMappingAsync(mapping1, CancellationToken.None);
|
||||
await _service.IngestMappingAsync(mapping2, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var result = await _service.GetCvesForSymbolAsync(symbol.CanonicalId, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(2);
|
||||
result.Should().Contain("CVE-2021-44228");
|
||||
result.Should().Contain("CVE-2021-45046");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchBySymbolAsync_FindsMatchingMappings()
|
||||
{
|
||||
// Arrange
|
||||
var symbol1 = CreateTestSymbol("org.apache.log4j.core.lookup", "jndilookup", "lookup");
|
||||
var symbol2 = CreateTestSymbol("org.springframework.beans", "beanwrapper", "setPropertyValue");
|
||||
|
||||
var mapping1 = CveSymbolMapping.Create(
|
||||
"CVE-2021-44228",
|
||||
[VulnerableSymbol.Create(symbol1, VulnerabilityType.Sink, 0.9)],
|
||||
MappingSource.PatchAnalysis,
|
||||
0.9,
|
||||
_timeProvider);
|
||||
|
||||
var mapping2 = CveSymbolMapping.Create(
|
||||
"CVE-2022-22965",
|
||||
[VulnerableSymbol.Create(symbol2, VulnerabilityType.Sink, 0.9)],
|
||||
MappingSource.PatchAnalysis,
|
||||
0.9,
|
||||
_timeProvider);
|
||||
|
||||
await _service.IngestMappingAsync(mapping1, CancellationToken.None);
|
||||
await _service.IngestMappingAsync(mapping2, CancellationToken.None);
|
||||
|
||||
// Act - Search for log4j
|
||||
var result = await _service.SearchBySymbolAsync("log4j", 10, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(1);
|
||||
result[0].CveId.Should().Be("CVE-2021-44228");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchBySymbolAsync_WildcardMatching()
|
||||
{
|
||||
// Arrange
|
||||
var symbol1 = CreateTestSymbol("org.apache.log4j.core.lookup", "jndilookup", "lookup");
|
||||
var symbol2 = CreateTestSymbol("org.apache.log4j.core.appender", "socketappender", "send");
|
||||
|
||||
var mapping1 = CveSymbolMapping.Create(
|
||||
"CVE-2021-44228",
|
||||
[VulnerableSymbol.Create(symbol1, VulnerabilityType.Sink, 0.9)],
|
||||
MappingSource.PatchAnalysis,
|
||||
0.9,
|
||||
_timeProvider);
|
||||
|
||||
var mapping2 = CveSymbolMapping.Create(
|
||||
"CVE-2021-44832",
|
||||
[VulnerableSymbol.Create(symbol2, VulnerabilityType.Sink, 0.8)],
|
||||
MappingSource.PatchAnalysis,
|
||||
0.8,
|
||||
_timeProvider);
|
||||
|
||||
await _service.IngestMappingAsync(mapping1, CancellationToken.None);
|
||||
await _service.IngestMappingAsync(mapping2, CancellationToken.None);
|
||||
|
||||
// Act - Wildcard search
|
||||
var result = await _service.SearchBySymbolAsync("*.log4j.*", 10, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IngestMapping_InvalidCveId_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = CreateTestSymbol("org.example", "service", "method");
|
||||
|
||||
// Act & Assert - "invalid" is not a valid CVE format
|
||||
var act = () => CveSymbolMapping.Create(
|
||||
"invalid",
|
||||
[VulnerableSymbol.Create(symbol, VulnerabilityType.Unknown, 0.5)],
|
||||
MappingSource.ManualCuration,
|
||||
0.5,
|
||||
_timeProvider);
|
||||
|
||||
// The service should validate on ingest
|
||||
var mapping = act(); // Create doesn't validate format
|
||||
|
||||
var ingestAct = async () => await _service.IngestMappingAsync(mapping, CancellationToken.None);
|
||||
ingestAct.Should().ThrowAsync<ArgumentException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Clear_RemovesAllMappings()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = CreateTestSymbol("org.example", "service", "method");
|
||||
var mapping = CveSymbolMapping.Create(
|
||||
"CVE-2021-0001",
|
||||
[VulnerableSymbol.Create(symbol, VulnerabilityType.Sink, 0.8)],
|
||||
MappingSource.PatchAnalysis,
|
||||
0.8,
|
||||
_timeProvider);
|
||||
|
||||
await _service.IngestMappingAsync(mapping, CancellationToken.None);
|
||||
_service.MappingCount.Should().Be(1);
|
||||
|
||||
// Act
|
||||
_service.Clear();
|
||||
|
||||
// Assert
|
||||
_service.MappingCount.Should().Be(0);
|
||||
}
|
||||
|
||||
private CanonicalSymbol CreateTestSymbol(string ns, string type, string method)
|
||||
{
|
||||
return CanonicalSymbol.Create(ns, type, method, "()", SymbolSource.JavaAsm);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
// <copyright file="CveSymbolMappingTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Reachability.Core.CveMapping;
|
||||
using StellaOps.Reachability.Core.Symbols;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Reachability.Core.Tests.CveMapping;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="CveSymbolMapping"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class CveSymbolMappingTests
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
|
||||
public CveSymbolMappingTests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 9, 12, 0, 0, TimeSpan.Zero));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_SetsAllProperties()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = CreateTestSymbol("org.example", "service", "method");
|
||||
var vulnSymbol = VulnerableSymbol.Create(symbol, VulnerabilityType.Sink, 0.9);
|
||||
|
||||
// Act
|
||||
var mapping = CveSymbolMapping.Create(
|
||||
"CVE-2021-44228",
|
||||
[vulnSymbol],
|
||||
MappingSource.PatchAnalysis,
|
||||
0.85,
|
||||
_timeProvider,
|
||||
patchCommitUrl: "https://github.com/apache/logging-log4j2/commit/abc123",
|
||||
osvAdvisoryId: "GHSA-jfh8-c2jp-5v3q");
|
||||
|
||||
// Assert
|
||||
mapping.CveId.Should().Be("CVE-2021-44228");
|
||||
mapping.Symbols.Should().HaveCount(1);
|
||||
mapping.Source.Should().Be(MappingSource.PatchAnalysis);
|
||||
mapping.Confidence.Should().Be(0.85);
|
||||
mapping.ExtractedAt.Should().Be(_timeProvider.GetUtcNow());
|
||||
mapping.PatchCommitUrl.Should().Be("https://github.com/apache/logging-log4j2/commit/abc123");
|
||||
mapping.OsvAdvisoryId.Should().Be("GHSA-jfh8-c2jp-5v3q");
|
||||
mapping.ContentDigest.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_ClampsConfidence()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = CreateTestSymbol("org.example", "service", "method");
|
||||
var vulnSymbol = VulnerableSymbol.Create(symbol, VulnerabilityType.Unknown, 0.5);
|
||||
|
||||
// Act
|
||||
var mapping1 = CveSymbolMapping.Create(
|
||||
"CVE-2021-0001",
|
||||
[vulnSymbol],
|
||||
MappingSource.ManualCuration,
|
||||
1.5, // Over 1.0
|
||||
_timeProvider);
|
||||
|
||||
var mapping2 = CveSymbolMapping.Create(
|
||||
"CVE-2021-0002",
|
||||
[vulnSymbol],
|
||||
MappingSource.ManualCuration,
|
||||
-0.5, // Below 0.0
|
||||
_timeProvider);
|
||||
|
||||
// Assert
|
||||
mapping1.Confidence.Should().Be(1.0);
|
||||
mapping2.Confidence.Should().Be(0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContentDigest_IsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = CreateTestSymbol("org.example", "service", "method");
|
||||
var vulnSymbol = VulnerableSymbol.Create(symbol, VulnerabilityType.Sink, 0.9);
|
||||
|
||||
// Act - Create two mappings with same content
|
||||
var mapping1 = CveSymbolMapping.Create(
|
||||
"CVE-2021-44228",
|
||||
[vulnSymbol],
|
||||
MappingSource.PatchAnalysis,
|
||||
0.85,
|
||||
_timeProvider);
|
||||
|
||||
var mapping2 = CveSymbolMapping.Create(
|
||||
"CVE-2021-44228",
|
||||
[vulnSymbol],
|
||||
MappingSource.PatchAnalysis,
|
||||
0.85,
|
||||
_timeProvider);
|
||||
|
||||
// Assert - Same content digest
|
||||
mapping1.ContentDigest.Should().Be(mapping2.ContentDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContentDigest_DiffersForDifferentSymbols()
|
||||
{
|
||||
// Arrange
|
||||
var symbol1 = CreateTestSymbol("org.example", "service", "method1");
|
||||
var symbol2 = CreateTestSymbol("org.example", "service", "method2");
|
||||
|
||||
// Act
|
||||
var mapping1 = CveSymbolMapping.Create(
|
||||
"CVE-2021-44228",
|
||||
[VulnerableSymbol.Create(symbol1, VulnerabilityType.Sink, 0.9)],
|
||||
MappingSource.PatchAnalysis,
|
||||
0.85,
|
||||
_timeProvider);
|
||||
|
||||
var mapping2 = CveSymbolMapping.Create(
|
||||
"CVE-2021-44228",
|
||||
[VulnerableSymbol.Create(symbol2, VulnerabilityType.Sink, 0.9)],
|
||||
MappingSource.PatchAnalysis,
|
||||
0.85,
|
||||
_timeProvider);
|
||||
|
||||
// Assert - Different content digest
|
||||
mapping1.ContentDigest.Should().NotBe(mapping2.ContentDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_CombinesSymbols()
|
||||
{
|
||||
// Arrange
|
||||
var symbol1 = CreateTestSymbol("org.example", "service", "method1");
|
||||
var symbol2 = CreateTestSymbol("org.example", "service", "method2");
|
||||
|
||||
var mapping1 = CveSymbolMapping.Create(
|
||||
"CVE-2021-44228",
|
||||
[VulnerableSymbol.Create(symbol1, VulnerabilityType.Sink, 0.8)],
|
||||
MappingSource.PatchAnalysis,
|
||||
0.8,
|
||||
_timeProvider);
|
||||
|
||||
var mapping2 = CveSymbolMapping.Create(
|
||||
"CVE-2021-44228",
|
||||
[VulnerableSymbol.Create(symbol2, VulnerabilityType.TaintSource, 0.7)],
|
||||
MappingSource.OsvDatabase,
|
||||
0.7,
|
||||
_timeProvider);
|
||||
|
||||
// Act
|
||||
var merged = mapping1.Merge(mapping2, _timeProvider);
|
||||
|
||||
// Assert
|
||||
merged.CveId.Should().Be("CVE-2021-44228");
|
||||
merged.Symbols.Should().HaveCount(2);
|
||||
merged.Confidence.Should().Be(0.8); // Max of both
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_DifferentCves_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = CreateTestSymbol("org.example", "service", "method");
|
||||
|
||||
var mapping1 = CveSymbolMapping.Create(
|
||||
"CVE-2021-44228",
|
||||
[VulnerableSymbol.Create(symbol, VulnerabilityType.Sink, 0.8)],
|
||||
MappingSource.PatchAnalysis,
|
||||
0.8,
|
||||
_timeProvider);
|
||||
|
||||
var mapping2 = CveSymbolMapping.Create(
|
||||
"CVE-2022-22965",
|
||||
[VulnerableSymbol.Create(symbol, VulnerabilityType.Sink, 0.7)],
|
||||
MappingSource.PatchAnalysis,
|
||||
0.7,
|
||||
_timeProvider);
|
||||
|
||||
// Act & Assert
|
||||
var act = () => mapping1.Merge(mapping2, _timeProvider);
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithMessage("*different CVEs*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_DeduplicatesSymbols()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = CreateTestSymbol("org.example", "service", "method");
|
||||
|
||||
var mapping1 = CveSymbolMapping.Create(
|
||||
"CVE-2021-44228",
|
||||
[VulnerableSymbol.Create(symbol, VulnerabilityType.Sink, 0.8)],
|
||||
MappingSource.PatchAnalysis,
|
||||
0.8,
|
||||
_timeProvider);
|
||||
|
||||
var mapping2 = CveSymbolMapping.Create(
|
||||
"CVE-2021-44228",
|
||||
[VulnerableSymbol.Create(symbol, VulnerabilityType.Sink, 0.9)], // Same symbol, different confidence
|
||||
MappingSource.OsvDatabase,
|
||||
0.9,
|
||||
_timeProvider);
|
||||
|
||||
// Act
|
||||
var merged = mapping1.Merge(mapping2, _timeProvider);
|
||||
|
||||
// Assert - Should not duplicate
|
||||
merged.Symbols.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_CombinesPurls()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = CreateTestSymbol("org.example", "service", "method");
|
||||
|
||||
var mapping1 = CveSymbolMapping.Create(
|
||||
"CVE-2021-44228",
|
||||
[VulnerableSymbol.Create(symbol, VulnerabilityType.Sink, 0.8)],
|
||||
MappingSource.PatchAnalysis,
|
||||
0.8,
|
||||
_timeProvider,
|
||||
affectedPurls: ["pkg:maven/org.apache.logging.log4j/log4j-core@2.14.0"]);
|
||||
|
||||
var mapping2 = CveSymbolMapping.Create(
|
||||
"CVE-2021-44228",
|
||||
[VulnerableSymbol.Create(symbol, VulnerabilityType.Sink, 0.9)],
|
||||
MappingSource.OsvDatabase,
|
||||
0.9,
|
||||
_timeProvider,
|
||||
affectedPurls: ["pkg:maven/org.apache.logging.log4j/log4j-api@2.14.0"]);
|
||||
|
||||
// Act
|
||||
var merged = mapping1.Merge(mapping2, _timeProvider);
|
||||
|
||||
// Assert
|
||||
merged.AffectedPurls.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
private CanonicalSymbol CreateTestSymbol(string ns, string type, string method)
|
||||
{
|
||||
return CanonicalSymbol.Create(ns, type, method, "()", SymbolSource.JavaAsm);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// <copyright file="VulnerableSymbolTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Reachability.Core.CveMapping;
|
||||
using StellaOps.Reachability.Core.Symbols;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Reachability.Core.Tests.CveMapping;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="VulnerableSymbol"/> and <see cref="LineRange"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class VulnerableSymbolTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_SetsRequiredFields()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = CanonicalSymbol.Create("org.example", "service", "method", "()", SymbolSource.JavaAsm);
|
||||
|
||||
// Act
|
||||
var vulnSymbol = VulnerableSymbol.Create(symbol, VulnerabilityType.Sink, 0.85);
|
||||
|
||||
// Assert
|
||||
vulnSymbol.Symbol.Should().Be(symbol);
|
||||
vulnSymbol.Type.Should().Be(VulnerabilityType.Sink);
|
||||
vulnSymbol.Confidence.Should().Be(0.85);
|
||||
vulnSymbol.Condition.Should().BeNull();
|
||||
vulnSymbol.Evidence.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_ClampsConfidence()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = CanonicalSymbol.Create("org.example", "service", "method", "()", SymbolSource.JavaAsm);
|
||||
|
||||
// Act
|
||||
var over = VulnerableSymbol.Create(symbol, VulnerabilityType.Unknown, 1.5);
|
||||
var under = VulnerableSymbol.Create(symbol, VulnerabilityType.Unknown, -0.5);
|
||||
|
||||
// Assert
|
||||
over.Confidence.Should().Be(1.0);
|
||||
under.Confidence.Should().Be(0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithOptionalFields()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = CanonicalSymbol.Create("org.example", "service", "method", "(string)", SymbolSource.Roslyn);
|
||||
|
||||
// Act
|
||||
var vulnSymbol = new VulnerableSymbol
|
||||
{
|
||||
Symbol = symbol,
|
||||
Type = VulnerabilityType.SqlInjection,
|
||||
Confidence = 0.95,
|
||||
Condition = "When user input is passed directly",
|
||||
Evidence = "Modified in commit abc123",
|
||||
SourceFile = "src/main/java/Example.java",
|
||||
LineRange = new LineRange(42, 58)
|
||||
};
|
||||
|
||||
// Assert
|
||||
vulnSymbol.Condition.Should().Be("When user input is passed directly");
|
||||
vulnSymbol.Evidence.Should().Be("Modified in commit abc123");
|
||||
vulnSymbol.SourceFile.Should().Be("src/main/java/Example.java");
|
||||
vulnSymbol.LineRange.Should().Be(new LineRange(42, 58));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LineRange_Length_CalculatesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var range = new LineRange(10, 20);
|
||||
|
||||
// Act & Assert
|
||||
range.Length.Should().Be(11); // 10,11,12,13,14,15,16,17,18,19,20 = 11 lines
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LineRange_Contains_WorksCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var range = new LineRange(10, 20);
|
||||
|
||||
// Act & Assert
|
||||
range.Contains(10).Should().BeTrue();
|
||||
range.Contains(15).Should().BeTrue();
|
||||
range.Contains(20).Should().BeTrue();
|
||||
range.Contains(9).Should().BeFalse();
|
||||
range.Contains(21).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(VulnerabilityType.Sink)]
|
||||
[InlineData(VulnerabilityType.TaintSource)]
|
||||
[InlineData(VulnerabilityType.GadgetEntry)]
|
||||
[InlineData(VulnerabilityType.DeserializationTarget)]
|
||||
[InlineData(VulnerabilityType.AuthBypass)]
|
||||
[InlineData(VulnerabilityType.CryptoWeakness)]
|
||||
[InlineData(VulnerabilityType.RceEntry)]
|
||||
[InlineData(VulnerabilityType.SqlInjection)]
|
||||
[InlineData(VulnerabilityType.PathTraversal)]
|
||||
[InlineData(VulnerabilityType.Ssrf)]
|
||||
[InlineData(VulnerabilityType.Xss)]
|
||||
public void VulnerabilityType_AllValuesSupported(VulnerabilityType type)
|
||||
{
|
||||
// Arrange
|
||||
var symbol = CanonicalSymbol.Create("org.example", "service", "method", "()", SymbolSource.JavaAsm);
|
||||
|
||||
// Act
|
||||
var vulnSymbol = VulnerableSymbol.Create(symbol, type, 0.5);
|
||||
|
||||
// Assert
|
||||
vulnSymbol.Type.Should().Be(type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>StellaOps.Reachability.Core.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
<PackageReference Include="coverlet.collector" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Reachability.Core\StellaOps.Reachability.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,215 @@
|
||||
// <copyright file="DotNetSymbolNormalizerTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Reachability.Core.Symbols;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Reachability.Core.Tests.Symbols;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="DotNetSymbolNormalizer"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class DotNetSymbolNormalizerTests
|
||||
{
|
||||
private readonly DotNetSymbolNormalizer _normalizer = new();
|
||||
|
||||
[Fact]
|
||||
public void SupportedSources_ContainsDotNetSources()
|
||||
{
|
||||
_normalizer.SupportedSources.Should().Contain(SymbolSource.Roslyn);
|
||||
_normalizer.SupportedSources.Should().Contain(SymbolSource.ILMetadata);
|
||||
_normalizer.SupportedSources.Should().Contain(SymbolSource.EtwClr);
|
||||
_normalizer.SupportedSources.Should().Contain(SymbolSource.EventPipe);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(SymbolSource.Roslyn, true)]
|
||||
[InlineData(SymbolSource.ILMetadata, true)]
|
||||
[InlineData(SymbolSource.JavaAsm, false)]
|
||||
[InlineData(SymbolSource.Unknown, false)]
|
||||
public void CanNormalize_ReturnsCorrectResult(SymbolSource source, bool expected)
|
||||
{
|
||||
_normalizer.CanNormalize(source).Should().Be(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_FullSignature_ParsesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var raw = new RawSymbol(
|
||||
"System.Void StellaOps.Scanner.Core.SbomGenerator::GenerateAsync(System.Threading.CancellationToken)",
|
||||
SymbolSource.ILMetadata);
|
||||
|
||||
// Act
|
||||
var canonical = _normalizer.Normalize(raw);
|
||||
|
||||
// Assert
|
||||
canonical.Should().NotBeNull();
|
||||
canonical!.Namespace.Should().Be("stellaops.scanner.core");
|
||||
canonical.Type.Should().Be("sbomgenerator");
|
||||
canonical.Method.Should().Be("generateasync");
|
||||
canonical.Signature.Should().Be("(cancellationtoken)");
|
||||
canonical.Source.Should().Be(SymbolSource.ILMetadata);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_FullSignature_MultipleParams_ParsesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var raw = new RawSymbol(
|
||||
"System.Boolean MyApp.Services.UserService::ValidateUser(System.String, System.Int32, System.String)",
|
||||
SymbolSource.ILMetadata);
|
||||
|
||||
// Act
|
||||
var canonical = _normalizer.Normalize(raw);
|
||||
|
||||
// Assert
|
||||
canonical.Should().NotBeNull();
|
||||
canonical!.Namespace.Should().Be("myapp.services");
|
||||
canonical.Type.Should().Be("userservice");
|
||||
canonical.Method.Should().Be("validateuser");
|
||||
canonical.Signature.Should().Be("(string, int32, string)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_RoslynFormat_ParsesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var raw = new RawSymbol(
|
||||
"StellaOps.Scanner.Core.SbomGenerator.GenerateAsync(CancellationToken)",
|
||||
SymbolSource.Roslyn);
|
||||
|
||||
// Act
|
||||
var canonical = _normalizer.Normalize(raw);
|
||||
|
||||
// Assert
|
||||
canonical.Should().NotBeNull();
|
||||
canonical!.Namespace.Should().Be("stellaops.scanner.core");
|
||||
canonical.Type.Should().Be("sbomgenerator");
|
||||
canonical.Method.Should().Be("generateasync");
|
||||
canonical.Signature.Should().Be("(cancellationtoken)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_RoslynFormat_NoParams_ParsesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var raw = new RawSymbol(
|
||||
"StellaOps.Scanner.Core.SbomGenerator.GenerateAsync",
|
||||
SymbolSource.Roslyn);
|
||||
|
||||
// Act
|
||||
var canonical = _normalizer.Normalize(raw);
|
||||
|
||||
// Assert
|
||||
canonical.Should().NotBeNull();
|
||||
canonical!.Namespace.Should().Be("stellaops.scanner.core");
|
||||
canonical.Type.Should().Be("sbomgenerator");
|
||||
canonical.Method.Should().Be("generateasync");
|
||||
canonical.Signature.Should().Be("()");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_SimpleDoubleColon_ParsesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var raw = new RawSymbol(
|
||||
"StellaOps.Scanner.Core.SbomGenerator::GenerateAsync",
|
||||
SymbolSource.ILMetadata);
|
||||
|
||||
// Act
|
||||
var canonical = _normalizer.Normalize(raw);
|
||||
|
||||
// Assert
|
||||
canonical.Should().NotBeNull();
|
||||
canonical!.Namespace.Should().Be("stellaops.scanner.core");
|
||||
canonical.Type.Should().Be("sbomgenerator");
|
||||
canonical.Method.Should().Be("generateasync");
|
||||
canonical.Signature.Should().Be("()");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_InvalidSymbol_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var raw = new RawSymbol("not a valid symbol", SymbolSource.Roslyn);
|
||||
|
||||
// Act
|
||||
var canonical = _normalizer.Normalize(raw);
|
||||
|
||||
// Assert
|
||||
canonical.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_EmptyValue_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var raw = new RawSymbol("", SymbolSource.Roslyn);
|
||||
|
||||
// Act
|
||||
var result = _normalizer.TryNormalize(raw, out var canonical, out var error);
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
canonical.Should().BeNull();
|
||||
error.Should().Contain("empty");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_PreservesPurl()
|
||||
{
|
||||
// Arrange
|
||||
var raw = new RawSymbol(
|
||||
"StellaOps.Scanner.Core.SbomGenerator.GenerateAsync",
|
||||
SymbolSource.Roslyn,
|
||||
"pkg:nuget/StellaOps.Scanner.Core@1.0.0");
|
||||
|
||||
// Act
|
||||
var canonical = _normalizer.Normalize(raw);
|
||||
|
||||
// Assert
|
||||
canonical.Should().NotBeNull();
|
||||
canonical!.Purl.Should().Be("pkg:nuget/stellaops.scanner.core@1.0.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_PreservesOriginalSymbol()
|
||||
{
|
||||
// Arrange
|
||||
var original = "StellaOps.Scanner.Core.SbomGenerator.GenerateAsync";
|
||||
var raw = new RawSymbol(original, SymbolSource.Roslyn);
|
||||
|
||||
// Act
|
||||
var canonical = _normalizer.Normalize(raw);
|
||||
|
||||
// Assert
|
||||
canonical.Should().NotBeNull();
|
||||
canonical!.OriginalSymbol.Should().Be(original);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_GeneratesStableCanonicalId()
|
||||
{
|
||||
// Arrange
|
||||
var raw1 = new RawSymbol(
|
||||
"System.Void StellaOps.Scanner.Core.SbomGenerator::GenerateAsync(System.Threading.CancellationToken)",
|
||||
SymbolSource.ILMetadata);
|
||||
var raw2 = new RawSymbol(
|
||||
"StellaOps.Scanner.Core.SbomGenerator.GenerateAsync(CancellationToken)",
|
||||
SymbolSource.Roslyn);
|
||||
|
||||
// Act
|
||||
var canonical1 = _normalizer.Normalize(raw1);
|
||||
var canonical2 = _normalizer.Normalize(raw2);
|
||||
|
||||
// Assert
|
||||
canonical1.Should().NotBeNull();
|
||||
canonical2.Should().NotBeNull();
|
||||
canonical1!.CanonicalId.Should().Be(canonical2!.CanonicalId,
|
||||
"Same symbol from different sources should have same canonical ID");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
// <copyright file="JavaSymbolNormalizerTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Reachability.Core.Symbols;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Reachability.Core.Tests.Symbols;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="JavaSymbolNormalizer"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class JavaSymbolNormalizerTests
|
||||
{
|
||||
private readonly JavaSymbolNormalizer _normalizer = new();
|
||||
|
||||
[Fact]
|
||||
public void SupportedSources_ContainsJavaSources()
|
||||
{
|
||||
_normalizer.SupportedSources.Should().Contain(SymbolSource.JavaAsm);
|
||||
_normalizer.SupportedSources.Should().Contain(SymbolSource.JavaJfr);
|
||||
_normalizer.SupportedSources.Should().Contain(SymbolSource.JavaJvmti);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(SymbolSource.JavaAsm, true)]
|
||||
[InlineData(SymbolSource.JavaJfr, true)]
|
||||
[InlineData(SymbolSource.Roslyn, false)]
|
||||
[InlineData(SymbolSource.Unknown, false)]
|
||||
public void CanNormalize_ReturnsCorrectResult(SymbolSource source, bool expected)
|
||||
{
|
||||
_normalizer.CanNormalize(source).Should().Be(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_AsmFormat_ParsesCorrectly()
|
||||
{
|
||||
// Arrange - ASM bytecode format
|
||||
var raw = new RawSymbol(
|
||||
"org/apache/log4j/core/lookup/JndiLookup.lookup(Ljava/lang/String;)Ljava/lang/String;",
|
||||
SymbolSource.JavaAsm);
|
||||
|
||||
// Act
|
||||
var canonical = _normalizer.Normalize(raw);
|
||||
|
||||
// Assert
|
||||
canonical.Should().NotBeNull();
|
||||
canonical!.Namespace.Should().Be("org.apache.log4j.core.lookup");
|
||||
canonical.Type.Should().Be("jndilookup");
|
||||
canonical.Method.Should().Be("lookup");
|
||||
canonical.Signature.Should().Be("(string)");
|
||||
canonical.Source.Should().Be(SymbolSource.JavaAsm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_AsmFormat_MultipleParams_ParsesCorrectly()
|
||||
{
|
||||
// Arrange - Multiple parameters
|
||||
var raw = new RawSymbol(
|
||||
"com/example/Service.process(Ljava/lang/String;IZ)V",
|
||||
SymbolSource.JavaAsm);
|
||||
|
||||
// Act
|
||||
var canonical = _normalizer.Normalize(raw);
|
||||
|
||||
// Assert
|
||||
canonical.Should().NotBeNull();
|
||||
canonical!.Namespace.Should().Be("com.example");
|
||||
canonical.Type.Should().Be("service");
|
||||
canonical.Method.Should().Be("process");
|
||||
canonical.Signature.Should().Be("(string, int, boolean)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_AsmFormat_ArrayParam_ParsesCorrectly()
|
||||
{
|
||||
// Arrange - Array parameter
|
||||
var raw = new RawSymbol(
|
||||
"com/example/Service.processBytes([B)V",
|
||||
SymbolSource.JavaAsm);
|
||||
|
||||
// Act
|
||||
var canonical = _normalizer.Normalize(raw);
|
||||
|
||||
// Assert
|
||||
canonical.Should().NotBeNull();
|
||||
canonical!.Signature.Should().Be("(byte[])");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_JfrFormat_ParsesCorrectly()
|
||||
{
|
||||
// Arrange - JFR format (simpler)
|
||||
var raw = new RawSymbol(
|
||||
"org.apache.log4j.core.lookup.JndiLookup.lookup(String)",
|
||||
SymbolSource.JavaJfr);
|
||||
|
||||
// Act
|
||||
var canonical = _normalizer.Normalize(raw);
|
||||
|
||||
// Assert
|
||||
canonical.Should().NotBeNull();
|
||||
canonical!.Namespace.Should().Be("org.apache.log4j.core.lookup");
|
||||
canonical.Type.Should().Be("jndilookup");
|
||||
canonical.Method.Should().Be("lookup");
|
||||
canonical.Signature.Should().Be("(string)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_PatchFormat_ParsesCorrectly()
|
||||
{
|
||||
// Arrange - Patch analysis format with #
|
||||
var raw = new RawSymbol(
|
||||
"org.apache.logging.log4j.core.lookup.JndiLookup#lookup",
|
||||
SymbolSource.PatchAnalysis);
|
||||
|
||||
// Act
|
||||
var canonical = _normalizer.Normalize(raw);
|
||||
|
||||
// Assert
|
||||
canonical.Should().NotBeNull();
|
||||
canonical!.Namespace.Should().Be("org.apache.logging.log4j.core.lookup");
|
||||
canonical.Type.Should().Be("jndilookup");
|
||||
canonical.Method.Should().Be("lookup");
|
||||
canonical.Signature.Should().Be("()");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_InvalidSymbol_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var raw = new RawSymbol("not a valid symbol", SymbolSource.JavaAsm);
|
||||
|
||||
// Act
|
||||
var canonical = _normalizer.Normalize(raw);
|
||||
|
||||
// Assert
|
||||
canonical.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_EmptyValue_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var raw = new RawSymbol("", SymbolSource.JavaAsm);
|
||||
|
||||
// Act
|
||||
var result = _normalizer.TryNormalize(raw, out var canonical, out var error);
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
canonical.Should().BeNull();
|
||||
error.Should().Contain("empty");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_PreservesPurl()
|
||||
{
|
||||
// Arrange
|
||||
var raw = new RawSymbol(
|
||||
"org/apache/log4j/core/lookup/JndiLookup.lookup(Ljava/lang/String;)Ljava/lang/String;",
|
||||
SymbolSource.JavaAsm,
|
||||
"pkg:maven/org.apache.logging.log4j/log4j-core@2.14.0");
|
||||
|
||||
// Act
|
||||
var canonical = _normalizer.Normalize(raw);
|
||||
|
||||
// Assert
|
||||
canonical.Should().NotBeNull();
|
||||
canonical!.Purl.Should().Be("pkg:maven/org.apache.logging.log4j/log4j-core@2.14.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_CrossSourceMatching_SameCanonicalId()
|
||||
{
|
||||
// Arrange - Same symbol from ASM and JFR
|
||||
var asmRaw = new RawSymbol(
|
||||
"org/apache/log4j/core/lookup/JndiLookup.lookup(Ljava/lang/String;)Ljava/lang/String;",
|
||||
SymbolSource.JavaAsm);
|
||||
var jfrRaw = new RawSymbol(
|
||||
"org.apache.log4j.core.lookup.JndiLookup.lookup(String)",
|
||||
SymbolSource.JavaJfr);
|
||||
|
||||
// Act
|
||||
var asmCanonical = _normalizer.Normalize(asmRaw);
|
||||
var jfrCanonical = _normalizer.Normalize(jfrRaw);
|
||||
|
||||
// Assert
|
||||
asmCanonical.Should().NotBeNull();
|
||||
jfrCanonical.Should().NotBeNull();
|
||||
asmCanonical!.CanonicalId.Should().Be(jfrCanonical!.CanonicalId,
|
||||
"Same symbol from ASM and JFR should have same canonical ID");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_JvmDescriptor_AllPrimitives()
|
||||
{
|
||||
// Arrange - All primitive types
|
||||
var raw = new RawSymbol(
|
||||
"com/example/Util.test(BCDFIJSZ)V",
|
||||
SymbolSource.JavaAsm);
|
||||
|
||||
// Act
|
||||
var canonical = _normalizer.Normalize(raw);
|
||||
|
||||
// Assert
|
||||
canonical.Should().NotBeNull();
|
||||
canonical!.Signature.Should().Be("(byte, char, double, float, int, long, short, boolean)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_JvmDescriptor_NestedArray()
|
||||
{
|
||||
// Arrange - 2D array
|
||||
var raw = new RawSymbol(
|
||||
"com/example/Matrix.multiply([[D)[[D",
|
||||
SymbolSource.JavaAsm);
|
||||
|
||||
// Act
|
||||
var canonical = _normalizer.Normalize(raw);
|
||||
|
||||
// Assert
|
||||
canonical.Should().NotBeNull();
|
||||
canonical!.Signature.Should().Be("(double[][])");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_JvmDescriptor_ObjectArray()
|
||||
{
|
||||
// Arrange - Object array
|
||||
var raw = new RawSymbol(
|
||||
"com/example/Util.process([Ljava/lang/String;)V",
|
||||
SymbolSource.JavaAsm);
|
||||
|
||||
// Act
|
||||
var canonical = _normalizer.Normalize(raw);
|
||||
|
||||
// Assert
|
||||
canonical.Should().NotBeNull();
|
||||
canonical!.Signature.Should().Be("(string[])");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
// <copyright file="SymbolCanonicalizerTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Reachability.Core.Symbols;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Reachability.Core.Tests.Symbols;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="SymbolCanonicalizer"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class SymbolCanonicalizerTests
|
||||
{
|
||||
private readonly SymbolCanonicalizer _canonicalizer = new();
|
||||
|
||||
[Fact]
|
||||
public void Canonicalize_DotNetSymbol_UsesCorrectNormalizer()
|
||||
{
|
||||
// Arrange
|
||||
var raw = new RawSymbol(
|
||||
"StellaOps.Scanner.Core.SbomGenerator.GenerateAsync(CancellationToken)",
|
||||
SymbolSource.Roslyn);
|
||||
|
||||
// Act
|
||||
var canonical = _canonicalizer.Canonicalize(raw);
|
||||
|
||||
// Assert
|
||||
canonical.Should().NotBeNull();
|
||||
canonical!.Namespace.Should().Be("stellaops.scanner.core");
|
||||
canonical.Type.Should().Be("sbomgenerator");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Canonicalize_JavaSymbol_UsesCorrectNormalizer()
|
||||
{
|
||||
// Arrange
|
||||
var raw = new RawSymbol(
|
||||
"org/apache/log4j/core/lookup/JndiLookup.lookup(Ljava/lang/String;)Ljava/lang/String;",
|
||||
SymbolSource.JavaAsm);
|
||||
|
||||
// Act
|
||||
var canonical = _canonicalizer.Canonicalize(raw);
|
||||
|
||||
// Assert
|
||||
canonical.Should().NotBeNull();
|
||||
canonical!.Namespace.Should().Be("org.apache.log4j.core.lookup");
|
||||
canonical.Type.Should().Be("jndilookup");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Canonicalize_UnknownSource_TriesFallback()
|
||||
{
|
||||
// Arrange - Unknown source but valid .NET format
|
||||
var raw = new RawSymbol(
|
||||
"StellaOps.Scanner.Core.SbomGenerator.GenerateAsync",
|
||||
SymbolSource.Unknown);
|
||||
|
||||
// Act
|
||||
var canonical = _canonicalizer.Canonicalize(raw);
|
||||
|
||||
// Assert
|
||||
canonical.Should().NotBeNull();
|
||||
canonical!.Method.Should().Be("generateasync");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanonicalizeBatch_ReturnsOnlySuccessful()
|
||||
{
|
||||
// Arrange
|
||||
var symbols = new[]
|
||||
{
|
||||
new RawSymbol("StellaOps.Scanner.Core.A.MethodA", SymbolSource.Roslyn),
|
||||
new RawSymbol("invalid symbol", SymbolSource.Roslyn),
|
||||
new RawSymbol("StellaOps.Scanner.Core.B.MethodB", SymbolSource.Roslyn)
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _canonicalizer.CanonicalizeBatch(symbols);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(2);
|
||||
result.Select(c => c.Method).Should().Contain("methoda");
|
||||
result.Select(c => c.Method).Should().Contain("methodb");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanonicalizeBatchWithErrors_ReturnsAllResults()
|
||||
{
|
||||
// Arrange
|
||||
var symbols = new[]
|
||||
{
|
||||
new RawSymbol("StellaOps.Scanner.Core.A.MethodA", SymbolSource.Roslyn),
|
||||
new RawSymbol("invalid symbol", SymbolSource.Roslyn),
|
||||
new RawSymbol("StellaOps.Scanner.Core.B.MethodB", SymbolSource.Roslyn)
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _canonicalizer.CanonicalizeBatchWithErrors(symbols);
|
||||
|
||||
// Assert
|
||||
result.Successful.Should().HaveCount(2);
|
||||
result.Failed.Should().HaveCount(1);
|
||||
result.Failed[0].Raw.Value.Should().Be("invalid symbol");
|
||||
result.Failed[0].Error.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Match_DelegatestoMatcher()
|
||||
{
|
||||
// Arrange
|
||||
var a = CanonicalSymbol.Create(
|
||||
"org.example", "service", "process", "(string)", SymbolSource.Roslyn);
|
||||
var b = CanonicalSymbol.Create(
|
||||
"org.example", "service", "process", "(string)", SymbolSource.JavaAsm);
|
||||
|
||||
// Act
|
||||
var result = _canonicalizer.Match(a, b);
|
||||
|
||||
// Assert
|
||||
result.MatchType.Should().Be(SymbolMatchType.Exact);
|
||||
result.Confidence.Should().Be(1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CrossSourceMatching_Log4jExample()
|
||||
{
|
||||
// Arrange - Log4Shell vulnerability symbols from different sources
|
||||
var asmSymbol = new RawSymbol(
|
||||
"org/apache/log4j/core/lookup/JndiLookup.lookup(Ljava/lang/String;)Ljava/lang/String;",
|
||||
SymbolSource.JavaAsm,
|
||||
"pkg:maven/org.apache.logging.log4j/log4j-core@2.14.0");
|
||||
|
||||
var jfrSymbol = new RawSymbol(
|
||||
"org.apache.log4j.core.lookup.JndiLookup.lookup(String)",
|
||||
SymbolSource.JavaJfr,
|
||||
"pkg:maven/org.apache.logging.log4j/log4j-core@2.14.0");
|
||||
|
||||
// Act
|
||||
var asmCanonical = _canonicalizer.Canonicalize(asmSymbol);
|
||||
var jfrCanonical = _canonicalizer.Canonicalize(jfrSymbol);
|
||||
var matchResult = _canonicalizer.Match(asmCanonical!, jfrCanonical!);
|
||||
|
||||
// Assert
|
||||
asmCanonical.Should().NotBeNull();
|
||||
jfrCanonical.Should().NotBeNull();
|
||||
matchResult.MatchType.Should().Be(SymbolMatchType.Exact);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CrossSourceMatching_DotNetExample()
|
||||
{
|
||||
// Arrange - Same .NET symbol from different sources
|
||||
var roslynSymbol = new RawSymbol(
|
||||
"StellaOps.Scanner.Core.SbomGenerator.GenerateAsync(CancellationToken)",
|
||||
SymbolSource.Roslyn);
|
||||
|
||||
var ilSymbol = new RawSymbol(
|
||||
"System.Threading.Tasks.Task StellaOps.Scanner.Core.SbomGenerator::GenerateAsync(System.Threading.CancellationToken)",
|
||||
SymbolSource.ILMetadata);
|
||||
|
||||
// Act
|
||||
var roslynCanonical = _canonicalizer.Canonicalize(roslynSymbol);
|
||||
var ilCanonical = _canonicalizer.Canonicalize(ilSymbol);
|
||||
var matchResult = _canonicalizer.Match(roslynCanonical!, ilCanonical!);
|
||||
|
||||
// Assert
|
||||
roslynCanonical.Should().NotBeNull();
|
||||
ilCanonical.Should().NotBeNull();
|
||||
matchResult.MatchType.Should().Be(SymbolMatchType.Exact);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Determinism_SameInputProducesSameOutput()
|
||||
{
|
||||
// Arrange
|
||||
var raw = new RawSymbol(
|
||||
"org/apache/log4j/core/lookup/JndiLookup.lookup(Ljava/lang/String;)Ljava/lang/String;",
|
||||
SymbolSource.JavaAsm);
|
||||
|
||||
// Act - Run multiple times
|
||||
var results = Enumerable.Range(0, 100)
|
||||
.Select(_ => _canonicalizer.Canonicalize(raw))
|
||||
.ToList();
|
||||
|
||||
// Assert - All should be identical
|
||||
var first = results[0];
|
||||
results.Should().AllSatisfy(r =>
|
||||
{
|
||||
r.Should().NotBeNull();
|
||||
r!.CanonicalId.Should().Be(first!.CanonicalId);
|
||||
r.Namespace.Should().Be(first.Namespace);
|
||||
r.Type.Should().Be(first.Type);
|
||||
r.Method.Should().Be(first.Method);
|
||||
r.Signature.Should().Be(first.Signature);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
// <copyright file="SymbolMatcherTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Reachability.Core.Symbols;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Reachability.Core.Tests.Symbols;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="SymbolMatcher"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class SymbolMatcherTests
|
||||
{
|
||||
private readonly SymbolMatcher _matcher = new();
|
||||
|
||||
[Fact]
|
||||
public void Match_ExactCanonicalId_ReturnsExact()
|
||||
{
|
||||
// Arrange
|
||||
var a = CanonicalSymbol.Create(
|
||||
"org.example", "service", "process", "(string)", SymbolSource.Roslyn);
|
||||
var b = CanonicalSymbol.Create(
|
||||
"org.example", "service", "process", "(string)", SymbolSource.JavaAsm);
|
||||
|
||||
// Act
|
||||
var result = _matcher.Match(a, b);
|
||||
|
||||
// Assert
|
||||
result.MatchType.Should().Be(SymbolMatchType.Exact);
|
||||
result.Confidence.Should().Be(1.0);
|
||||
result.IsMatch.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Match_SameMethodDifferentSignature_ReturnsFuzzy()
|
||||
{
|
||||
// Arrange - Same method but different overload
|
||||
var a = CanonicalSymbol.Create(
|
||||
"org.example", "service", "process", "(string)", SymbolSource.Roslyn);
|
||||
var b = CanonicalSymbol.Create(
|
||||
"org.example", "service", "process", "(string, int)", SymbolSource.Roslyn);
|
||||
|
||||
// Act
|
||||
var result = _matcher.Match(a, b);
|
||||
|
||||
// Assert
|
||||
result.MatchType.Should().Be(SymbolMatchType.Fuzzy);
|
||||
result.Confidence.Should().BeGreaterThanOrEqualTo(0.7);
|
||||
result.IsMatch.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Match_DifferentMethod_ReturnsNoMatch()
|
||||
{
|
||||
// Arrange
|
||||
var a = CanonicalSymbol.Create(
|
||||
"org.example", "service", "process", "(string)", SymbolSource.Roslyn);
|
||||
var b = CanonicalSymbol.Create(
|
||||
"org.example", "service", "validate", "(string)", SymbolSource.Roslyn);
|
||||
|
||||
// Act
|
||||
var result = _matcher.Match(a, b);
|
||||
|
||||
// Assert
|
||||
result.MatchType.Should().Be(SymbolMatchType.NoMatch);
|
||||
result.IsMatch.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Match_DifferentPurl_ReturnsNoMatch()
|
||||
{
|
||||
// Arrange - Different packages
|
||||
var a = CanonicalSymbol.Create(
|
||||
"org.example", "service", "process", "(string)", SymbolSource.Roslyn, "pkg:npm/pkg-a@1.0.0");
|
||||
var b = CanonicalSymbol.Create(
|
||||
"org.example", "service", "process", "(string)", SymbolSource.Roslyn, "pkg:npm/pkg-b@1.0.0");
|
||||
|
||||
// Act
|
||||
var result = _matcher.Match(a, b);
|
||||
|
||||
// Assert
|
||||
result.MatchType.Should().Be(SymbolMatchType.NoMatch);
|
||||
result.Reason.Should().Contain("PURL");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Match_DifferentPurl_IgnoredWhenDisabled()
|
||||
{
|
||||
// Arrange
|
||||
var a = CanonicalSymbol.Create(
|
||||
"org.example", "service", "process", "(string)", SymbolSource.Roslyn, "pkg:npm/pkg-a@1.0.0");
|
||||
var b = CanonicalSymbol.Create(
|
||||
"org.example", "service", "process", "(string)", SymbolSource.Roslyn, "pkg:npm/pkg-b@1.0.0");
|
||||
var options = new SymbolMatchOptions { ConsiderPurl = false };
|
||||
|
||||
// Act
|
||||
var result = _matcher.Match(a, b, options);
|
||||
|
||||
// Assert - Even with ConsiderPurl=false, canonical IDs differ due to different PURLs in the hash
|
||||
// So we get a fuzzy match (same namespace/type/method) rather than no match
|
||||
result.MatchType.Should().Be(SymbolMatchType.Fuzzy);
|
||||
result.IsMatch.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Match_SimilarNamespace_ReturnsFuzzy()
|
||||
{
|
||||
// Arrange - Similar namespace (logging variation)
|
||||
var a = CanonicalSymbol.Create(
|
||||
"org.apache.log4j.core.lookup", "jndilookup", "lookup", "(string)", SymbolSource.JavaAsm);
|
||||
var b = CanonicalSymbol.Create(
|
||||
"org.apache.logging.log4j.core.lookup", "jndilookup", "lookup", "(string)", SymbolSource.JavaAsm);
|
||||
|
||||
// Act with lenient options
|
||||
var result = _matcher.Match(a, b, SymbolMatchOptions.Lenient);
|
||||
|
||||
// Assert
|
||||
result.MatchType.Should().Be(SymbolMatchType.Fuzzy);
|
||||
result.Confidence.Should().BeGreaterThan(0.5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Match_StrictOptions_RequiresHigherConfidence()
|
||||
{
|
||||
// Arrange - Similar but not identical
|
||||
var a = CanonicalSymbol.Create(
|
||||
"org.example.services", "userservice", "validate", "(string)", SymbolSource.Roslyn);
|
||||
var b = CanonicalSymbol.Create(
|
||||
"org.example.svc", "uservalidator", "validate", "(string)", SymbolSource.Roslyn);
|
||||
|
||||
// Act
|
||||
var defaultResult = _matcher.Match(a, b);
|
||||
var strictResult = _matcher.Match(a, b, SymbolMatchOptions.Strict);
|
||||
|
||||
// Assert - Strict should reject matches that default accepts
|
||||
// (or at least have different confidence thresholds)
|
||||
strictResult.Confidence.Should().BeLessThanOrEqualTo(defaultResult.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Match_CompatibleTypes_MatchesSuccessfully()
|
||||
{
|
||||
// Arrange - System.String vs string
|
||||
var a = CanonicalSymbol.Create(
|
||||
"org.example", "service", "process", "(string)", SymbolSource.Roslyn);
|
||||
var b = CanonicalSymbol.Create(
|
||||
"org.example", "service", "process", "(system.string)", SymbolSource.ILMetadata);
|
||||
|
||||
// Act - These should be fuzzy match since signatures differ but types are compatible
|
||||
var result = _matcher.Match(a, b);
|
||||
|
||||
// Assert
|
||||
result.MatchType.Should().Be(SymbolMatchType.Fuzzy);
|
||||
result.Confidence.Should().BeGreaterThanOrEqualTo(0.7);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Match_Symmetric_SameResultBothDirections()
|
||||
{
|
||||
// Arrange
|
||||
var a = CanonicalSymbol.Create(
|
||||
"org.example", "service", "process", "(string, int)", SymbolSource.Roslyn);
|
||||
var b = CanonicalSymbol.Create(
|
||||
"org.example", "service", "process", "(string)", SymbolSource.Roslyn);
|
||||
|
||||
// Act
|
||||
var resultAB = _matcher.Match(a, b);
|
||||
var resultBA = _matcher.Match(b, a);
|
||||
|
||||
// Assert
|
||||
resultAB.MatchType.Should().Be(resultBA.MatchType);
|
||||
resultAB.Confidence.Should().Be(resultBA.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Match_EmptySignatures_Matches()
|
||||
{
|
||||
// Arrange - Both have empty signatures
|
||||
var a = CanonicalSymbol.Create(
|
||||
"org.example", "service", "run", "()", SymbolSource.Roslyn);
|
||||
var b = CanonicalSymbol.Create(
|
||||
"org.example", "service", "run", "()", SymbolSource.JavaAsm);
|
||||
|
||||
// Act
|
||||
var result = _matcher.Match(a, b);
|
||||
|
||||
// Assert
|
||||
result.MatchType.Should().Be(SymbolMatchType.Exact);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user