up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,380 @@
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Engine.SelectionJoin;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.SelectionJoin;
|
||||
|
||||
public sealed class SelectionJoinTests
|
||||
{
|
||||
#region PurlEquivalence Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData("pkg:npm/lodash@4.17.21", "pkg:npm/lodash")]
|
||||
[InlineData("pkg:maven/org.apache.commons/commons-lang3@3.12.0", "pkg:maven/org.apache.commons/commons-lang3")]
|
||||
[InlineData("pkg:pypi/requests@2.28.0", "pkg:pypi/requests")]
|
||||
[InlineData("pkg:gem/rails@7.0.0", "pkg:gem/rails")]
|
||||
[InlineData("pkg:nuget/Newtonsoft.Json@13.0.1", "pkg:nuget/Newtonsoft.Json")]
|
||||
public void ExtractPackageKey_RemovesVersion(string purl, string expectedKey)
|
||||
{
|
||||
var key = PurlEquivalence.ExtractPackageKey(purl);
|
||||
|
||||
key.Should().Be(expectedKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractPackageKey_HandlesNoVersion()
|
||||
{
|
||||
var purl = "pkg:npm/lodash";
|
||||
|
||||
var key = PurlEquivalence.ExtractPackageKey(purl);
|
||||
|
||||
key.Should().Be("pkg:npm/lodash");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractPackageKey_HandlesScopedPackages()
|
||||
{
|
||||
var purl = "pkg:npm/@scope/package@1.0.0";
|
||||
|
||||
var key = PurlEquivalence.ExtractPackageKey(purl);
|
||||
|
||||
key.Should().Be("pkg:npm/@scope/package");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("pkg:npm/lodash@4.17.21", "npm")]
|
||||
[InlineData("pkg:maven/org.apache/commons@1.0", "maven")]
|
||||
[InlineData("pkg:pypi/requests@2.28", "pypi")]
|
||||
public void ExtractEcosystem_ReturnsCorrectEcosystem(string purl, string expected)
|
||||
{
|
||||
var ecosystem = PurlEquivalence.ExtractEcosystem(purl);
|
||||
|
||||
ecosystem.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeMatchConfidence_ExactMatch_Returns1()
|
||||
{
|
||||
var confidence = PurlEquivalence.ComputeMatchConfidence(
|
||||
"pkg:npm/lodash@4.17.21",
|
||||
"pkg:npm/lodash@4.17.21");
|
||||
|
||||
confidence.Should().Be(1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeMatchConfidence_PackageKeyMatch_Returns08()
|
||||
{
|
||||
var confidence = PurlEquivalence.ComputeMatchConfidence(
|
||||
"pkg:npm/lodash@4.17.21",
|
||||
"pkg:npm/lodash@4.17.20");
|
||||
|
||||
confidence.Should().Be(0.8);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PurlEquivalenceTable Tests
|
||||
|
||||
[Fact]
|
||||
public void FromGroups_CreatesEquivalentMappings()
|
||||
{
|
||||
var groups = new[]
|
||||
{
|
||||
new[] { "pkg:npm/lodash", "pkg:npm/lodash-es" }
|
||||
};
|
||||
|
||||
var table = PurlEquivalenceTable.FromGroups(groups);
|
||||
|
||||
table.AreEquivalent("pkg:npm/lodash", "pkg:npm/lodash-es").Should().BeTrue();
|
||||
table.GroupCount.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCanonical_ReturnsFirstLexicographically()
|
||||
{
|
||||
var groups = new[]
|
||||
{
|
||||
new[] { "pkg:npm/b-package", "pkg:npm/a-package" }
|
||||
};
|
||||
|
||||
var table = PurlEquivalenceTable.FromGroups(groups);
|
||||
|
||||
// "a-package" is lexicographically first
|
||||
table.GetCanonical("pkg:npm/b-package").Should().Be("pkg:npm/a-package");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetEquivalents_ReturnsAllEquivalentPurls()
|
||||
{
|
||||
var groups = new[]
|
||||
{
|
||||
new[] { "pkg:npm/a", "pkg:npm/b", "pkg:npm/c" }
|
||||
};
|
||||
|
||||
var table = PurlEquivalenceTable.FromGroups(groups);
|
||||
var equivalents = table.GetEquivalents("pkg:npm/b");
|
||||
|
||||
equivalents.Should().HaveCount(3);
|
||||
equivalents.Should().Contain("pkg:npm/a");
|
||||
equivalents.Should().Contain("pkg:npm/b");
|
||||
equivalents.Should().Contain("pkg:npm/c");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Empty_HasNoMappings()
|
||||
{
|
||||
var table = PurlEquivalenceTable.Empty;
|
||||
|
||||
table.GroupCount.Should().Be(0);
|
||||
table.TotalEntries.Should().Be(0);
|
||||
table.AreEquivalent("pkg:npm/a", "pkg:npm/b").Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SelectionJoinService Tests
|
||||
|
||||
[Fact]
|
||||
public void ResolveTuples_MatchesByExactPurl()
|
||||
{
|
||||
var service = new SelectionJoinService();
|
||||
var input = new SelectionJoinBatchInput(
|
||||
TenantId: "test-tenant",
|
||||
BatchId: "batch-1",
|
||||
Components: [
|
||||
new SbomComponentInput(
|
||||
Purl: "pkg:npm/lodash@4.17.21",
|
||||
Name: "lodash",
|
||||
Version: "4.17.21",
|
||||
Ecosystem: "npm",
|
||||
Metadata: ImmutableDictionary<string, string>.Empty)
|
||||
],
|
||||
Advisories: [
|
||||
new AdvisoryLinksetInput(
|
||||
AdvisoryId: "GHSA-test-001",
|
||||
Source: "github",
|
||||
Purls: ["pkg:npm/lodash@4.17.21"],
|
||||
Cpes: ImmutableArray<string>.Empty,
|
||||
Aliases: ["CVE-2021-12345"],
|
||||
Confidence: 1.0)
|
||||
],
|
||||
VexLinksets: ImmutableArray<VexLinksetInput>.Empty,
|
||||
EquivalenceTable: null,
|
||||
Options: new SelectionJoinOptions());
|
||||
|
||||
var result = service.ResolveTuples(input);
|
||||
|
||||
result.Tuples.Should().ContainSingle();
|
||||
result.Tuples[0].MatchType.Should().Be(SelectionMatchType.ExactPurl);
|
||||
result.Tuples[0].Component.Purl.Should().Be("pkg:npm/lodash@4.17.21");
|
||||
result.Statistics.ExactPurlMatches.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveTuples_MatchesByPackageKey()
|
||||
{
|
||||
var service = new SelectionJoinService();
|
||||
var input = new SelectionJoinBatchInput(
|
||||
TenantId: "test-tenant",
|
||||
BatchId: "batch-1",
|
||||
Components: [
|
||||
new SbomComponentInput("pkg:npm/lodash@4.17.21", "lodash", "4.17.21", "npm",
|
||||
ImmutableDictionary<string, string>.Empty)
|
||||
],
|
||||
Advisories: [
|
||||
new AdvisoryLinksetInput("GHSA-test-001", "github",
|
||||
Purls: ["pkg:npm/lodash@4.17.20"], // Different version
|
||||
Cpes: ImmutableArray<string>.Empty,
|
||||
Aliases: ["CVE-2021-12345"],
|
||||
Confidence: 1.0)
|
||||
],
|
||||
VexLinksets: ImmutableArray<VexLinksetInput>.Empty,
|
||||
EquivalenceTable: null,
|
||||
Options: new SelectionJoinOptions());
|
||||
|
||||
var result = service.ResolveTuples(input);
|
||||
|
||||
result.Tuples.Should().ContainSingle();
|
||||
result.Tuples[0].MatchType.Should().Be(SelectionMatchType.PackageKeyMatch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveTuples_AppliesVexOverlay()
|
||||
{
|
||||
var service = new SelectionJoinService();
|
||||
var input = new SelectionJoinBatchInput(
|
||||
TenantId: "test-tenant",
|
||||
BatchId: "batch-1",
|
||||
Components: [
|
||||
new SbomComponentInput("pkg:npm/lodash@4.17.21", "lodash", "4.17.21", "npm",
|
||||
ImmutableDictionary<string, string>.Empty)
|
||||
],
|
||||
Advisories: [
|
||||
new AdvisoryLinksetInput("GHSA-test-001", "github",
|
||||
Purls: ["pkg:npm/lodash@4.17.21"],
|
||||
Cpes: ImmutableArray<string>.Empty,
|
||||
Aliases: ["CVE-2021-12345"],
|
||||
Confidence: 1.0)
|
||||
],
|
||||
VexLinksets: [
|
||||
new VexLinksetInput("vex-1", "CVE-2021-12345", "pkg:npm/lodash@4.17.21",
|
||||
"not_affected", "vulnerable_code_not_in_execute_path", VexConfidenceLevel.High)
|
||||
],
|
||||
EquivalenceTable: null,
|
||||
Options: new SelectionJoinOptions());
|
||||
|
||||
var result = service.ResolveTuples(input);
|
||||
|
||||
result.Tuples.Should().ContainSingle();
|
||||
result.Tuples[0].Vex.Should().NotBeNull();
|
||||
result.Tuples[0].Vex!.Status.Should().Be("not_affected");
|
||||
result.Statistics.VexOverlays.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveTuples_ProducesDeterministicOrdering()
|
||||
{
|
||||
var service = new SelectionJoinService();
|
||||
var input = new SelectionJoinBatchInput(
|
||||
TenantId: "test-tenant",
|
||||
BatchId: "batch-1",
|
||||
Components: [
|
||||
new SbomComponentInput("pkg:npm/z-package@1.0.0", "z", "1.0.0", "npm",
|
||||
ImmutableDictionary<string, string>.Empty),
|
||||
new SbomComponentInput("pkg:npm/a-package@1.0.0", "a", "1.0.0", "npm",
|
||||
ImmutableDictionary<string, string>.Empty),
|
||||
new SbomComponentInput("pkg:npm/m-package@1.0.0", "m", "1.0.0", "npm",
|
||||
ImmutableDictionary<string, string>.Empty)
|
||||
],
|
||||
Advisories: [
|
||||
new AdvisoryLinksetInput("ADV-001", "test",
|
||||
Purls: ["pkg:npm/z-package", "pkg:npm/a-package", "pkg:npm/m-package"],
|
||||
Cpes: ImmutableArray<string>.Empty,
|
||||
Aliases: ["CVE-2021-001"],
|
||||
Confidence: 1.0)
|
||||
],
|
||||
VexLinksets: ImmutableArray<VexLinksetInput>.Empty,
|
||||
EquivalenceTable: null,
|
||||
Options: new SelectionJoinOptions());
|
||||
|
||||
var result = service.ResolveTuples(input);
|
||||
|
||||
// Should be sorted by component PURL
|
||||
result.Tuples.Should().HaveCount(3);
|
||||
result.Tuples[0].Component.Purl.Should().Be("pkg:npm/a-package@1.0.0");
|
||||
result.Tuples[1].Component.Purl.Should().Be("pkg:npm/m-package@1.0.0");
|
||||
result.Tuples[2].Component.Purl.Should().Be("pkg:npm/z-package@1.0.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveTuples_HandlesMultipleAdvisories()
|
||||
{
|
||||
var service = new SelectionJoinService();
|
||||
var input = new SelectionJoinBatchInput(
|
||||
TenantId: "test-tenant",
|
||||
BatchId: "batch-1",
|
||||
Components: [
|
||||
new SbomComponentInput("pkg:npm/lodash@4.17.21", "lodash", "4.17.21", "npm",
|
||||
ImmutableDictionary<string, string>.Empty)
|
||||
],
|
||||
Advisories: [
|
||||
new AdvisoryLinksetInput("ADV-001", "test",
|
||||
Purls: ["pkg:npm/lodash@4.17.21"],
|
||||
Cpes: ImmutableArray<string>.Empty,
|
||||
Aliases: ["CVE-2021-001"],
|
||||
Confidence: 1.0),
|
||||
new AdvisoryLinksetInput("ADV-002", "test",
|
||||
Purls: ["pkg:npm/lodash@4.17.21"],
|
||||
Cpes: ImmutableArray<string>.Empty,
|
||||
Aliases: ["CVE-2021-002"],
|
||||
Confidence: 1.0)
|
||||
],
|
||||
VexLinksets: ImmutableArray<VexLinksetInput>.Empty,
|
||||
EquivalenceTable: null,
|
||||
Options: new SelectionJoinOptions());
|
||||
|
||||
var result = service.ResolveTuples(input);
|
||||
|
||||
result.Tuples.Should().HaveCount(2);
|
||||
result.Tuples.Should().Contain(t => t.Advisory.AdvisoryId == "ADV-001");
|
||||
result.Tuples.Should().Contain(t => t.Advisory.AdvisoryId == "ADV-002");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveTuples_ReturnsStatistics()
|
||||
{
|
||||
var service = new SelectionJoinService();
|
||||
var input = new SelectionJoinBatchInput(
|
||||
TenantId: "test-tenant",
|
||||
BatchId: "batch-1",
|
||||
Components: [
|
||||
new SbomComponentInput("pkg:npm/a@1.0.0", "a", "1.0.0", "npm",
|
||||
ImmutableDictionary<string, string>.Empty),
|
||||
new SbomComponentInput("pkg:npm/b@1.0.0", "b", "1.0.0", "npm",
|
||||
ImmutableDictionary<string, string>.Empty)
|
||||
],
|
||||
Advisories: [
|
||||
new AdvisoryLinksetInput("ADV-001", "test",
|
||||
Purls: ["pkg:npm/a"],
|
||||
Cpes: ImmutableArray<string>.Empty,
|
||||
Aliases: ["CVE-001"],
|
||||
Confidence: 1.0)
|
||||
],
|
||||
VexLinksets: ImmutableArray<VexLinksetInput>.Empty,
|
||||
EquivalenceTable: null,
|
||||
Options: new SelectionJoinOptions());
|
||||
|
||||
var result = service.ResolveTuples(input);
|
||||
|
||||
result.Statistics.TotalComponents.Should().Be(2);
|
||||
result.Statistics.TotalAdvisories.Should().Be(1);
|
||||
result.Statistics.MatchedTuples.Should().Be(1);
|
||||
result.UnmatchedComponents.Should().ContainSingle(c => c.Purl == "pkg:npm/b@1.0.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveTuples_HandlesEmptyInput()
|
||||
{
|
||||
var service = new SelectionJoinService();
|
||||
var input = new SelectionJoinBatchInput(
|
||||
TenantId: "test-tenant",
|
||||
BatchId: "batch-1",
|
||||
Components: ImmutableArray<SbomComponentInput>.Empty,
|
||||
Advisories: ImmutableArray<AdvisoryLinksetInput>.Empty,
|
||||
VexLinksets: ImmutableArray<VexLinksetInput>.Empty,
|
||||
EquivalenceTable: null,
|
||||
Options: new SelectionJoinOptions());
|
||||
|
||||
var result = service.ResolveTuples(input);
|
||||
|
||||
result.Tuples.Should().BeEmpty();
|
||||
result.Statistics.TotalComponents.Should().Be(0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SelectionJoinTuple Tests
|
||||
|
||||
[Fact]
|
||||
public void CreateTupleId_IsDeterministic()
|
||||
{
|
||||
var id1 = SelectionJoinTuple.CreateTupleId("tenant1", "pkg:npm/lodash@4.17.21", "CVE-2021-12345");
|
||||
var id2 = SelectionJoinTuple.CreateTupleId("tenant1", "pkg:npm/lodash@4.17.21", "CVE-2021-12345");
|
||||
|
||||
id1.Should().Be(id2);
|
||||
id1.Should().StartWith("tuple:sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateTupleId_NormalizesInput()
|
||||
{
|
||||
var id1 = SelectionJoinTuple.CreateTupleId("TENANT1", "PKG:NPM/LODASH@4.17.21", "CVE-2021-12345");
|
||||
var id2 = SelectionJoinTuple.CreateTupleId("tenant1", "pkg:npm/lodash@4.17.21", "CVE-2021-12345");
|
||||
|
||||
id1.Should().Be(id2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user