245 lines
7.1 KiB
C#
245 lines
7.1 KiB
C#
// Licensed under BUSL-1.1. Copyright (C) 2026 StellaOps Contributors.
|
|
|
|
using System.Collections.Immutable;
|
|
using FluentAssertions;
|
|
using Xunit;
|
|
|
|
namespace StellaOps.BinaryIndex.Diff.Tests.Unit;
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class PatchDiffModelTests
|
|
{
|
|
[Fact]
|
|
public void PatchDiffResult_NoPatchDetected_CreatesCorrectResult()
|
|
{
|
|
// Arrange
|
|
var goldenSetId = "CVE-2024-1234";
|
|
var goldenSetDigest = "sha256:abcd1234";
|
|
var binaryDigest = "sha256:same1234";
|
|
var comparedAt = DateTimeOffset.UtcNow;
|
|
var duration = TimeSpan.FromMilliseconds(100);
|
|
var options = DiffOptions.Default;
|
|
|
|
// Act
|
|
var result = PatchDiffResult.NoPatchDetected(
|
|
goldenSetId, goldenSetDigest, binaryDigest,
|
|
comparedAt, duration, options);
|
|
|
|
// Assert
|
|
result.GoldenSetId.Should().Be(goldenSetId);
|
|
result.Verdict.Should().Be(PatchVerdict.NoPatchDetected);
|
|
result.Confidence.Should().Be(1.0m);
|
|
result.PreBinaryDigest.Should().Be(binaryDigest);
|
|
result.PostBinaryDigest.Should().Be(binaryDigest);
|
|
result.Evidence.Should().HaveCount(1);
|
|
result.Evidence[0].Type.Should().Be(DiffEvidenceType.IdenticalBinaries);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(PatchVerdict.Fixed)]
|
|
[InlineData(PatchVerdict.PartialFix)]
|
|
[InlineData(PatchVerdict.StillVulnerable)]
|
|
[InlineData(PatchVerdict.Inconclusive)]
|
|
[InlineData(PatchVerdict.NoPatchDetected)]
|
|
public void PatchVerdict_AllValuesAreDefined(PatchVerdict verdict)
|
|
{
|
|
// Assert
|
|
Enum.IsDefined(verdict).Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void FunctionDiffResult_FunctionRemoved_CreatesCorrectResult()
|
|
{
|
|
// Act
|
|
var result = FunctionDiffResult.FunctionRemoved("vulnerable_func");
|
|
|
|
// Assert
|
|
result.FunctionName.Should().Be("vulnerable_func");
|
|
result.PreStatus.Should().Be(FunctionStatus.Present);
|
|
result.PostStatus.Should().Be(FunctionStatus.Absent);
|
|
result.Verdict.Should().Be(FunctionPatchVerdict.FunctionRemoved);
|
|
}
|
|
|
|
[Fact]
|
|
public void FunctionDiffResult_NotFound_CreatesCorrectResult()
|
|
{
|
|
// Act
|
|
var result = FunctionDiffResult.NotFound("missing_func");
|
|
|
|
// Assert
|
|
result.FunctionName.Should().Be("missing_func");
|
|
result.PreStatus.Should().Be(FunctionStatus.Absent);
|
|
result.PostStatus.Should().Be(FunctionStatus.Absent);
|
|
result.Verdict.Should().Be(FunctionPatchVerdict.Inconclusive);
|
|
}
|
|
|
|
[Fact]
|
|
public void CfgDiffResult_StructureChanged_DetectsChange()
|
|
{
|
|
// Arrange
|
|
var diff = new CfgDiffResult
|
|
{
|
|
PreCfgHash = "hash1",
|
|
PostCfgHash = "hash2",
|
|
PreBlockCount = 5,
|
|
PostBlockCount = 6,
|
|
PreEdgeCount = 7,
|
|
PostEdgeCount = 9
|
|
};
|
|
|
|
// Assert
|
|
diff.StructureChanged.Should().BeTrue();
|
|
diff.BlockCountDelta.Should().Be(1);
|
|
diff.EdgeCountDelta.Should().Be(2);
|
|
}
|
|
|
|
[Fact]
|
|
public void CfgDiffResult_NoStructureChange_WhenHashesMatch()
|
|
{
|
|
// Arrange
|
|
var diff = new CfgDiffResult
|
|
{
|
|
PreCfgHash = "samehash",
|
|
PostCfgHash = "samehash",
|
|
PreBlockCount = 5,
|
|
PostBlockCount = 5,
|
|
PreEdgeCount = 7,
|
|
PostEdgeCount = 7
|
|
};
|
|
|
|
// Assert
|
|
diff.StructureChanged.Should().BeFalse();
|
|
diff.BlockCountDelta.Should().Be(0);
|
|
diff.EdgeCountDelta.Should().Be(0);
|
|
}
|
|
}
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class VulnerableEdgeDiffTests
|
|
{
|
|
[Fact]
|
|
public void Compute_AllEdgesRemoved_SetsFlag()
|
|
{
|
|
// Arrange
|
|
var preEdges = ImmutableArray.Create("bb0->bb1", "bb1->bb2");
|
|
var postEdges = ImmutableArray<string>.Empty;
|
|
|
|
// Act
|
|
var diff = VulnerableEdgeDiff.Compute(preEdges, postEdges);
|
|
|
|
// Assert
|
|
diff.AllVulnerableEdgesRemoved.Should().BeTrue();
|
|
diff.EdgesRemoved.Should().HaveCount(2);
|
|
diff.EdgesAdded.Should().BeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void Compute_SomeEdgesRemoved_SetsFlag()
|
|
{
|
|
// Arrange
|
|
var preEdges = ImmutableArray.Create("bb0->bb1", "bb1->bb2");
|
|
var postEdges = ImmutableArray.Create("bb0->bb1");
|
|
|
|
// Act
|
|
var diff = VulnerableEdgeDiff.Compute(preEdges, postEdges);
|
|
|
|
// Assert
|
|
diff.AllVulnerableEdgesRemoved.Should().BeFalse();
|
|
diff.SomeVulnerableEdgesRemoved.Should().BeTrue();
|
|
diff.EdgesRemoved.Should().Contain("bb1->bb2");
|
|
}
|
|
|
|
[Fact]
|
|
public void Compute_NoChange_NoEdgesRemovedOrAdded()
|
|
{
|
|
// Arrange
|
|
var edges = ImmutableArray.Create("bb0->bb1", "bb1->bb2");
|
|
|
|
// Act
|
|
var diff = VulnerableEdgeDiff.Compute(edges, edges);
|
|
|
|
// Assert
|
|
diff.NoChange.Should().BeTrue();
|
|
diff.EdgesRemoved.Should().BeEmpty();
|
|
diff.EdgesAdded.Should().BeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void Compute_EdgesAdded_TracksNewEdges()
|
|
{
|
|
// Arrange
|
|
var preEdges = ImmutableArray.Create("bb0->bb1");
|
|
var postEdges = ImmutableArray.Create("bb0->bb1", "bb1->bb3");
|
|
|
|
// Act
|
|
var diff = VulnerableEdgeDiff.Compute(preEdges, postEdges);
|
|
|
|
// Assert
|
|
diff.EdgesAdded.Should().Contain("bb1->bb3");
|
|
diff.AllVulnerableEdgesRemoved.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Empty_ReturnsEmptyDiff()
|
|
{
|
|
// Act
|
|
var empty = VulnerableEdgeDiff.Empty;
|
|
|
|
// Assert
|
|
empty.EdgesInPre.Should().BeEmpty();
|
|
empty.EdgesInPost.Should().BeEmpty();
|
|
empty.EdgesRemoved.Should().BeEmpty();
|
|
empty.EdgesAdded.Should().BeEmpty();
|
|
}
|
|
}
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class SinkReachabilityDiffTests
|
|
{
|
|
[Fact]
|
|
public void Compute_AllSinksUnreachable_SetsFlag()
|
|
{
|
|
// Arrange
|
|
var preSinks = ImmutableArray.Create("memcpy", "strcpy");
|
|
var postSinks = ImmutableArray<string>.Empty;
|
|
|
|
// Act
|
|
var diff = SinkReachabilityDiff.Compute(preSinks, postSinks);
|
|
|
|
// Assert
|
|
diff.AllSinksUnreachable.Should().BeTrue();
|
|
diff.SinksMadeUnreachable.Should().HaveCount(2);
|
|
diff.SinksStillReachable.Should().BeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void Compute_SomeSinksUnreachable_SetsFlag()
|
|
{
|
|
// Arrange
|
|
var preSinks = ImmutableArray.Create("memcpy", "strcpy");
|
|
var postSinks = ImmutableArray.Create("memcpy");
|
|
|
|
// Act
|
|
var diff = SinkReachabilityDiff.Compute(preSinks, postSinks);
|
|
|
|
// Assert
|
|
diff.AllSinksUnreachable.Should().BeFalse();
|
|
diff.SomeSinksUnreachable.Should().BeTrue();
|
|
diff.SinksMadeUnreachable.Should().Contain("strcpy");
|
|
diff.SinksStillReachable.Should().Contain("memcpy");
|
|
}
|
|
|
|
[Fact]
|
|
public void Empty_ReturnsEmptyDiff()
|
|
{
|
|
// Act
|
|
var empty = SinkReachabilityDiff.Empty;
|
|
|
|
// Assert
|
|
empty.SinksReachableInPre.Should().BeEmpty();
|
|
empty.SinksReachableInPost.Should().BeEmpty();
|
|
empty.SinksMadeUnreachable.Should().BeEmpty();
|
|
empty.SinksStillReachable.Should().BeEmpty();
|
|
}
|
|
}
|