Files
git.stella-ops.org/src/Concelier/__Tests/StellaOps.Concelier.Merge.Tests/MergeHashBackportDifferentiationTests.cs

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
}