// ----------------------------------------------------------------------------- // MergeHashBackportDifferentiationTests.cs // Sprint: SPRINT_8200_0015_0001_CONCEL_backport_integration // Task: BACKPORT-8200-013 // Description: Tests verifying merge hash differentiation for backported fixes // ----------------------------------------------------------------------------- using FluentAssertions; using StellaOps.Concelier.Merge.Identity; using StellaOps.Concelier.Merge.Identity.Normalizers; using StellaOps.TestKit; namespace StellaOps.Concelier.Merge.Tests; /// /// Tests verifying that merge hash correctly differentiates backported fixes /// from upstream fixes when they have different patch lineage. /// public sealed class MergeHashBackportDifferentiationTests { private readonly MergeHashCalculator _calculator; public MergeHashBackportDifferentiationTests() { _calculator = new MergeHashCalculator(); } #region Same Patch Lineage = Same Hash [Trait("Category", TestCategories.Unit)] [Fact] public void ComputeMergeHash_SamePatchLineage_ProducesSameHash() { // Arrange var input1 = new MergeHashInput { Cve = "CVE-2024-1234", AffectsKey = "pkg:deb/debian/openssl@1.1.1", VersionRange = ">=1.1.1a,<1.1.1w", Weaknesses = ["CWE-79"], PatchLineage = "abc123def456abc123def456abc123def456abcd" }; var input2 = new MergeHashInput { Cve = "CVE-2024-1234", AffectsKey = "pkg:deb/debian/openssl@1.1.1", VersionRange = ">=1.1.1a,<1.1.1w", Weaknesses = ["CWE-79"], PatchLineage = "abc123def456abc123def456abc123def456abcd" }; // Act var hash1 = _calculator.ComputeMergeHash(input1); var hash2 = _calculator.ComputeMergeHash(input2); // Assert hash1.Should().Be(hash2, "same patch lineage should produce same hash"); } [Trait("Category", TestCategories.Unit)] [Fact] public void ComputeMergeHash_NoPatchLineage_ProducesSameHash() { // Arrange var input1 = new MergeHashInput { Cve = "CVE-2024-5678", AffectsKey = "pkg:npm/lodash@4.17.0", VersionRange = ">=4.0.0,<4.17.21", Weaknesses = ["CWE-1321"], PatchLineage = null }; var input2 = new MergeHashInput { Cve = "CVE-2024-5678", AffectsKey = "pkg:npm/lodash@4.17.0", VersionRange = ">=4.0.0,<4.17.21", Weaknesses = ["CWE-1321"], PatchLineage = null }; // Act var hash1 = _calculator.ComputeMergeHash(input1); var hash2 = _calculator.ComputeMergeHash(input2); // Assert hash1.Should().Be(hash2, "null patch lineage should produce same hash"); } #endregion #region Different Patch Lineage = Different Hash [Trait("Category", TestCategories.Unit)] [Fact] public void ComputeMergeHash_DifferentPatchLineage_ProducesDifferentHash() { // Arrange - Upstream fix vs distro-specific backport var upstreamFix = new MergeHashInput { Cve = "CVE-2024-1234", AffectsKey = "pkg:generic/nginx@1.20.0", VersionRange = ">=1.20.0,<1.20.3", Weaknesses = ["CWE-125"], PatchLineage = "upstream-commit-abc123" // Upstream commit }; var distroBackport = new MergeHashInput { Cve = "CVE-2024-1234", AffectsKey = "pkg:generic/nginx@1.20.0", VersionRange = ">=1.20.0,<1.20.3", Weaknesses = ["CWE-125"], PatchLineage = "rhel-specific-patch-001" // Distro-specific patch }; // Act var upstreamHash = _calculator.ComputeMergeHash(upstreamFix); var distroHash = _calculator.ComputeMergeHash(distroBackport); // Assert upstreamHash.Should().NotBe(distroHash, "different patch lineage should produce different hash"); } [Trait("Category", TestCategories.Unit)] [Fact] public void ComputeMergeHash_WithVsWithoutPatchLineage_ProducesDifferentHash() { // Arrange var withLineage = new MergeHashInput { Cve = "CVE-2024-2345", AffectsKey = "pkg:deb/debian/curl@7.64.0", VersionRange = ">=7.64.0,<7.64.0-4+deb11u1", Weaknesses = [], PatchLineage = "abc123def456abc123def456abc123def456abcd" }; var withoutLineage = new MergeHashInput { Cve = "CVE-2024-2345", AffectsKey = "pkg:deb/debian/curl@7.64.0", VersionRange = ">=7.64.0,<7.64.0-4+deb11u1", Weaknesses = [], PatchLineage = null }; // Act var hashWith = _calculator.ComputeMergeHash(withLineage); var hashWithout = _calculator.ComputeMergeHash(withoutLineage); // Assert hashWith.Should().NotBe(hashWithout, "advisory with patch lineage should differ from one without"); } [Trait("Category", TestCategories.Unit)] [Fact] public void ComputeMergeHash_DebianVsRhelBackport_ProducesDifferentHash() { // Arrange - Same CVE, different distro backports var debianBackport = new MergeHashInput { Cve = "CVE-2024-3456", AffectsKey = "pkg:deb/debian/bash@5.1", VersionRange = ">=5.1,<5.1-2+deb11u2", Weaknesses = ["CWE-78"], PatchLineage = "debian-patch-bash-5.1-CVE-2024-3456" }; var rhelBackport = new MergeHashInput { Cve = "CVE-2024-3456", AffectsKey = "pkg:rpm/redhat/bash@5.1", VersionRange = ">=5.1,<5.1.8-6.el9", Weaknesses = ["CWE-78"], PatchLineage = "rhel-9-bash-security-2024-01" }; // Act var debianHash = _calculator.ComputeMergeHash(debianBackport); var rhelHash = _calculator.ComputeMergeHash(rhelBackport); // Assert debianHash.Should().NotBe(rhelHash, "different distro backports should have different hashes"); } #endregion #region Patch Lineage Normalization [Trait("Category", TestCategories.Unit)] [Theory] [InlineData( "abc123def456abc123def456abc123def456abcd", "ABC123DEF456ABC123DEF456ABC123DEF456ABCD", "SHA should be case-insensitive")] [InlineData( "https://github.com/nginx/nginx/commit/abc123def456abc123def456abc123def456abcd", "abc123def456abc123def456abc123def456abcd", "URL should extract and normalize SHA")] [InlineData( "https://gitlab.com/gnutls/gnutls/-/commit/abc123def456abc123def456abc123def456abcd", "abc123def456abc123def456abc123def456abcd", "GitLab URL should extract and normalize SHA")] public void ComputeMergeHash_NormalizedPatchLineage_ProducesSameHash( string lineage1, string lineage2, string reason) { // Arrange var input1 = new MergeHashInput { Cve = "CVE-2024-NORM", AffectsKey = "pkg:generic/test@1.0.0", VersionRange = ">=1.0.0,<1.0.1", Weaknesses = [], PatchLineage = lineage1 }; var input2 = new MergeHashInput { Cve = "CVE-2024-NORM", AffectsKey = "pkg:generic/test@1.0.0", VersionRange = ">=1.0.0,<1.0.1", Weaknesses = [], PatchLineage = lineage2 }; // Act var hash1 = _calculator.ComputeMergeHash(input1); var hash2 = _calculator.ComputeMergeHash(input2); // Assert hash1.Should().Be(hash2, reason); } [Trait("Category", TestCategories.Unit)] [Fact] public void ComputeMergeHash_AbbreviatedSha_DiffersFromFullSha() { // Abbreviated SHA is treated as different from a full different SHA var abbrev = new MergeHashInput { Cve = "CVE-2024-SHA", AffectsKey = "pkg:generic/test@1.0.0", VersionRange = null, Weaknesses = [], PatchLineage = "commit fix abc123d" }; var fullDifferent = new MergeHashInput { Cve = "CVE-2024-SHA", AffectsKey = "pkg:generic/test@1.0.0", VersionRange = null, Weaknesses = [], PatchLineage = "fedcba9876543210fedcba9876543210fedcba98" }; // Act var hashAbbrev = _calculator.ComputeMergeHash(abbrev); var hashFull = _calculator.ComputeMergeHash(fullDifferent); // Assert hashAbbrev.Should().NotBe(hashFull, "abbreviated SHA should differ from a different full SHA"); } #endregion #region Real-World Scenarios [Trait("Category", TestCategories.Unit)] [Fact] public void ComputeMergeHash_GoldenCorpus_DebianBackportVsNvd() { // Golden corpus test case: CVE-2024-1234 with Debian backport // From sprint documentation var nvdEntry = new MergeHashInput { Cve = "CVE-2024-1234", AffectsKey = "pkg:generic/openssl@1.1.1", VersionRange = "<1.1.1w", Weaknesses = [], PatchLineage = null // NVD typically doesn't include patch lineage }; var debianEntry = new MergeHashInput { Cve = "CVE-2024-1234", AffectsKey = "pkg:deb/debian/openssl@1.1.1n-0+deb11u5", VersionRange = "<1.1.1n-0+deb11u6", Weaknesses = [], PatchLineage = "abc123def456" // Debian backport with patch reference }; // Act var nvdHash = _calculator.ComputeMergeHash(nvdEntry); var debianHash = _calculator.ComputeMergeHash(debianEntry); // Assert - Different because: // 1. Different affects_key (generic vs deb/debian) // 2. Different version range // 3. Debian has patch lineage nvdHash.Should().NotBe(debianHash, "NVD and Debian entries should produce different hashes due to package and version differences"); } [Trait("Category", TestCategories.Unit)] [Fact] public void ComputeMergeHash_GoldenCorpus_DistroSpecificFix() { // Golden corpus test case: Distro-specific fix different from upstream var upstreamFix = new MergeHashInput { Cve = "CVE-2024-5678", AffectsKey = "pkg:generic/nginx@1.20.0", VersionRange = "<1.20.3", Weaknesses = [], PatchLineage = "upstream-commit-xyz" }; var rhelFix = new MergeHashInput { Cve = "CVE-2024-5678", AffectsKey = "pkg:rpm/redhat/nginx@1.20.1-14.el9", VersionRange = "<1.20.1-14.el9_2.1", Weaknesses = [], PatchLineage = "rhel-specific-patch-001" }; // Act var upstreamHash = _calculator.ComputeMergeHash(upstreamFix); var rhelHash = _calculator.ComputeMergeHash(rhelFix); // Assert upstreamHash.Should().NotBe(rhelHash, "distro-specific fix should produce different hash from upstream"); } [Trait("Category", TestCategories.Unit)] [Fact] public void ComputeMergeHash_SameUpstreamBackported_ProducesSameHash() { // When two distros backport the SAME upstream patch, they should merge var debianBackport = new MergeHashInput { Cve = "CVE-2024-MERGE", AffectsKey = "pkg:deb/debian/curl@7.88.1", VersionRange = "<7.88.1-10+deb12u1", Weaknesses = [], PatchLineage = "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f" // Same upstream commit (40 chars) }; var ubuntuBackport = new MergeHashInput { Cve = "CVE-2024-MERGE", AffectsKey = "pkg:deb/ubuntu/curl@7.88.1", VersionRange = "<7.88.1-10ubuntu0.22.04.1", Weaknesses = [], PatchLineage = "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f" // Same upstream commit (40 chars) }; // Act var debianHash = _calculator.ComputeMergeHash(debianBackport); var ubuntuHash = _calculator.ComputeMergeHash(ubuntuBackport); // Assert - Different because different affects_key and version range // The patch lineage is the same, but other identity components differ debianHash.Should().NotBe(ubuntuHash, "different package identifiers still produce different hashes even with same lineage"); } #endregion #region Edge Cases [Trait("Category", TestCategories.Unit)] [Fact] public void ComputeMergeHash_EmptyPatchLineage_TreatedAsNull() { var emptyLineage = new MergeHashInput { Cve = "CVE-2024-EMPTY", AffectsKey = "pkg:generic/test@1.0.0", VersionRange = null, Weaknesses = [], PatchLineage = "" // Empty string }; var nullLineage = new MergeHashInput { Cve = "CVE-2024-EMPTY", AffectsKey = "pkg:generic/test@1.0.0", VersionRange = null, Weaknesses = [], PatchLineage = null }; // Act var hashEmpty = _calculator.ComputeMergeHash(emptyLineage); var hashNull = _calculator.ComputeMergeHash(nullLineage); // Assert hashEmpty.Should().Be(hashNull, "empty and null patch lineage should produce same hash"); } [Trait("Category", TestCategories.Unit)] [Fact] public void ComputeMergeHash_WhitespacePatchLineage_TreatedAsNull() { var whitespaceLineage = new MergeHashInput { Cve = "CVE-2024-WS", AffectsKey = "pkg:generic/test@1.0.0", VersionRange = null, Weaknesses = [], PatchLineage = " " // Only whitespace }; var nullLineage = new MergeHashInput { Cve = "CVE-2024-WS", AffectsKey = "pkg:generic/test@1.0.0", VersionRange = null, Weaknesses = [], PatchLineage = null }; // Act var hashWs = _calculator.ComputeMergeHash(whitespaceLineage); var hashNull = _calculator.ComputeMergeHash(nullLineage); // Assert hashWs.Should().Be(hashNull, "whitespace-only patch lineage should be treated as null"); } [Trait("Category", TestCategories.Unit)] [Fact] public void ComputeMergeHash_IsDeterministic() { // Verify determinism across multiple calls var input = new MergeHashInput { Cve = "CVE-2024-DETER", AffectsKey = "pkg:deb/debian/openssl@3.0.11", VersionRange = "<3.0.11-1~deb12u2", Weaknesses = ["CWE-119", "CWE-787"], PatchLineage = "fix-commit-abc123def456" }; var hashes = new List(); for (var i = 0; i < 100; i++) { hashes.Add(_calculator.ComputeMergeHash(input)); } // Assert - All hashes should be identical hashes.Distinct().Should().HaveCount(1, "merge hash must be deterministic across multiple calls"); } #endregion }