470 lines
15 KiB
C#
470 lines
15 KiB
C#
// -----------------------------------------------------------------------------
|
|
// 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;
|
|
|
|
/// <summary>
|
|
/// Tests verifying that merge hash correctly differentiates backported fixes
|
|
/// from upstream fixes when they have different patch lineage.
|
|
/// </summary>
|
|
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<string>();
|
|
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
|
|
}
|