release orchestrator v1 draft and build fixes

This commit is contained in:
master
2026-01-12 12:24:17 +02:00
parent f3de858c59
commit 9873f80830
1598 changed files with 240385 additions and 5944 deletions

View File

@@ -16,9 +16,7 @@
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
</PackageReference> <PackageReference Include="Moq" />
<PackageReference Include="FluentAssertions" />
</ItemGroup>

View File

@@ -0,0 +1,297 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under AGPL-3.0-or-later. See LICENSE in the project root.
using System.Collections.Immutable;
using FluentAssertions;
namespace StellaOps.BinaryIndex.DeltaSig.Tests;
/// <summary>
/// Tests for DeltaSignatureMatcher.CompareSignaturesAsync via SymbolChangeTracer.
/// Note: CompareSignaturesAsync only requires ISymbolChangeTracer, not disassembly services.
/// </summary>
[Trait("Category", "Unit")]
public sealed class ExtendedMatcherTests
{
private readonly SymbolChangeTracer _changeTracer;
public ExtendedMatcherTests()
{
_changeTracer = new SymbolChangeTracer();
}
// Helper to directly test the comparison logic that CompareSignaturesAsync uses
private DeltaComparisonResult CompareSignatures(DeltaSignature from, DeltaSignature to)
{
var symbolResults = _changeTracer.CompareAllSymbols(from, to);
var summary = new DeltaComparisonSummary
{
TotalSymbols = symbolResults.Count,
UnchangedSymbols = symbolResults.Count(r => r.ChangeType == SymbolChangeType.Unchanged),
AddedSymbols = symbolResults.Count(r => r.ChangeType == SymbolChangeType.Added),
RemovedSymbols = symbolResults.Count(r => r.ChangeType == SymbolChangeType.Removed),
ModifiedSymbols = symbolResults.Count(r => r.ChangeType == SymbolChangeType.Modified),
PatchedSymbols = symbolResults.Count(r => r.ChangeType == SymbolChangeType.Patched),
AverageConfidence = symbolResults.Count > 0
? symbolResults.Average(r => r.Confidence)
: 0.0,
TotalSizeDelta = symbolResults.Sum(r => r.SizeDelta)
};
return new DeltaComparisonResult
{
FromSignatureId = from.SignatureId,
ToSignatureId = to.SignatureId,
SymbolResults = [.. symbolResults],
Summary = summary
};
}
[Fact]
public void CompareSignaturesAsync_IdenticalSignatures_ReturnsAllUnchanged()
{
// Arrange
var symbols = new[]
{
CreateSymbol("func1", "sha256:abc", 100),
CreateSymbol("func2", "sha256:def", 200)
};
var fromSig = CreateSignature("from-1", symbols);
var toSig = CreateSignature("to-1", symbols);
// Act
var result = CompareSignatures(fromSig, toSig);
// Assert
result.FromSignatureId.Should().Be("from-1");
result.ToSignatureId.Should().Be("to-1");
result.Summary.TotalSymbols.Should().Be(2);
result.Summary.UnchangedSymbols.Should().Be(2);
result.Summary.AddedSymbols.Should().Be(0);
result.Summary.RemovedSymbols.Should().Be(0);
result.Summary.ModifiedSymbols.Should().Be(0);
result.Summary.AverageConfidence.Should().Be(1.0);
}
[Fact]
public void CompareSignaturesAsync_AddedSymbols_CountsCorrectly()
{
// Arrange
var fromSig = CreateSignature("from", [CreateSymbol("existing", "sha256:a", 100)]);
var toSig = CreateSignature("to",
[
CreateSymbol("existing", "sha256:a", 100),
CreateSymbol("new1", "sha256:b", 200),
CreateSymbol("new2", "sha256:c", 300)
]);
// Act
var result = CompareSignatures(fromSig, toSig);
// Assert
result.Summary.AddedSymbols.Should().Be(2);
result.Summary.UnchangedSymbols.Should().Be(1);
result.Summary.TotalSymbols.Should().Be(3);
}
[Fact]
public void CompareSignaturesAsync_RemovedSymbols_CountsCorrectly()
{
// Arrange
var fromSig = CreateSignature("from",
[
CreateSymbol("staying", "sha256:a", 100),
CreateSymbol("removed1", "sha256:b", 200),
CreateSymbol("removed2", "sha256:c", 300)
]);
var toSig = CreateSignature("to", [CreateSymbol("staying", "sha256:a", 100)]);
// Act
var result = CompareSignatures(fromSig, toSig);
// Assert
result.Summary.RemovedSymbols.Should().Be(2);
result.Summary.UnchangedSymbols.Should().Be(1);
result.Summary.TotalSymbols.Should().Be(3);
}
[Fact]
public void CompareSignaturesAsync_ModifiedSymbols_CountsCorrectly()
{
// Arrange
var fromSig = CreateSignature("from",
[
CreateSymbol("unchanged", "sha256:same", 100),
CreateSymbol("modified", "sha256:old", 200)
]);
var toSig = CreateSignature("to",
[
CreateSymbol("unchanged", "sha256:same", 100),
CreateSymbol("modified", "sha256:new", 220)
]);
// Act
var result = CompareSignatures(fromSig, toSig);
// Assert
result.Summary.ModifiedSymbols.Should().Be(1);
result.Summary.UnchangedSymbols.Should().Be(1);
}
[Fact]
public void CompareSignaturesAsync_CalculatesTotalSizeDelta()
{
// Arrange
var fromSig = CreateSignature("from",
[
CreateSymbol("func1", "sha256:a", 100),
CreateSymbol("func2", "sha256:b", 200)
]);
var toSig = CreateSignature("to",
[
CreateSymbol("func1", "sha256:c", 150), // +50
CreateSymbol("func2", "sha256:d", 180) // -20
]);
// Act
var result = CompareSignatures(fromSig, toSig);
// Assert - Total delta should be +50 - 20 = +30
result.Summary.TotalSizeDelta.Should().Be(30);
}
[Fact]
public void CompareSignaturesAsync_MixedChanges_SummaryIsComplete()
{
// Arrange
var fromSig = CreateSignature("from",
[
CreateSymbol("unchanged", "sha256:same", 100),
CreateSymbol("modified", "sha256:old", 200),
CreateSymbol("removed", "sha256:gone", 150)
]);
var toSig = CreateSignature("to",
[
CreateSymbol("unchanged", "sha256:same", 100),
CreateSymbol("modified", "sha256:new", 220),
CreateSymbol("added", "sha256:brand-new", 300)
]);
// Act
var result = CompareSignatures(fromSig, toSig);
// Assert
result.Summary.TotalSymbols.Should().Be(4);
result.Summary.UnchangedSymbols.Should().Be(1);
result.Summary.ModifiedSymbols.Should().Be(1);
result.Summary.AddedSymbols.Should().Be(1);
result.Summary.RemovedSymbols.Should().Be(1);
result.Summary.PatchedSymbols.Should().Be(0);
result.SymbolResults.Should().HaveCount(4);
}
[Fact]
public void CompareSignaturesAsync_PatchedSymbols_CountsCorrectly()
{
// Arrange - High chunk similarity (>= 85%) with CFG change -> 6/7 = 86%
var fromSig = CreateSignature("from",
[
CreateSymbolWithChunks("patched", "sha256:before", 350, 10,
[("sha256:1", 0, 50), ("sha256:2", 50, 50), ("sha256:3", 100, 50), ("sha256:4", 150, 50),
("sha256:5", 200, 50), ("sha256:6", 250, 50), ("sha256:7", 300, 50)])
]);
var toSig = CreateSignature("to",
[
CreateSymbolWithChunks("patched", "sha256:after", 360, 12,
[("sha256:1", 0, 50), ("sha256:2", 50, 50), ("sha256:3", 100, 50), ("sha256:4", 150, 50),
("sha256:5", 200, 50), ("sha256:6", 250, 50), ("sha256:new", 300, 60)])
]);
// Act
var result = CompareSignatures(fromSig, toSig);
// Assert
result.Summary.PatchedSymbols.Should().Be(1);
result.Summary.ModifiedSymbols.Should().Be(0); // Patched is separate from Modified
}
[Fact]
public void CompareSignaturesAsync_EmptySignatures_ReturnsEmptyResult()
{
// Arrange
var fromSig = CreateSignature("from", []);
var toSig = CreateSignature("to", []);
// Act
var result = CompareSignatures(fromSig, toSig);
// Assert
result.Summary.TotalSymbols.Should().Be(0);
result.SymbolResults.Should().BeEmpty();
}
[Fact]
public void CompareSignaturesAsync_SymbolResultsContainDetailedInfo()
{
// Arrange
var fromSig = CreateSignature("from", [CreateSymbol("func", "sha256:old", 100)]);
var toSig = CreateSignature("to", [CreateSymbol("func", "sha256:new", 120)]);
// Act
var result = CompareSignatures(fromSig, toSig);
// Assert
result.SymbolResults.Should().HaveCount(1);
var symbolResult = result.SymbolResults[0];
symbolResult.SymbolName.Should().Be("func");
symbolResult.FromHash.Should().Be("sha256:old");
symbolResult.ToHash.Should().Be("sha256:new");
symbolResult.SizeDelta.Should().Be(20);
symbolResult.ChangeExplanation.Should().NotBeNullOrEmpty();
}
// Helper methods
private static SymbolSignature CreateSymbol(string name, string hash, int size, int? cfgCount = null)
{
return new SymbolSignature
{
Name = name,
HashAlg = "sha256",
HashHex = hash,
SizeBytes = size,
CfgBbCount = cfgCount
};
}
private static SymbolSignature CreateSymbolWithChunks(
string name, string hash, int size, int? cfgCount,
(string hash, int offset, int size)[] chunks)
{
return new SymbolSignature
{
Name = name,
HashAlg = "sha256",
HashHex = hash,
SizeBytes = size,
CfgBbCount = cfgCount,
Chunks = chunks.Select(c => new ChunkHash(c.offset, c.size, c.hash)).ToImmutableArray()
};
}
private static DeltaSignature CreateSignature(string id, SymbolSignature[] symbols)
{
return new DeltaSignature
{
SignatureId = id,
Cve = "CVE-2026-0001",
Package = new PackageRef("testpkg", null),
Target = new TargetRef("x86_64", "gnu"),
Normalization = new NormalizationRef("test", "1.0", []),
SignatureState = "vulnerable",
Symbols = symbols.ToImmutableArray()
};
}
}

View File

@@ -20,6 +20,8 @@
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Moq" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,273 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under AGPL-3.0-or-later. See LICENSE in the project root.
using System.Collections.Immutable;
using FluentAssertions;
namespace StellaOps.BinaryIndex.DeltaSig.Tests;
/// <summary>
/// Tests for SymbolChangeTracer.
/// </summary>
[Trait("Category", "Unit")]
public sealed class SymbolChangeTracerTests
{
private readonly SymbolChangeTracer _tracer;
public SymbolChangeTracerTests()
{
_tracer = new SymbolChangeTracer();
}
[Fact]
public void CompareSymbols_AddedSymbol_ReturnsAddedChangeType()
{
// Arrange
var toSymbol = CreateSymbol("new_function", "sha256:abc123", 512);
// Act
var result = _tracer.CompareSymbols(null, toSymbol);
// Assert
result.ChangeType.Should().Be(SymbolChangeType.Added);
result.SymbolName.Should().Be("new_function");
result.SizeDelta.Should().Be(512);
result.ToHash.Should().Be("sha256:abc123");
result.FromHash.Should().BeNull();
result.Confidence.Should().Be(1.0);
result.ChangeExplanation.Should().Contain("added");
}
[Fact]
public void CompareSymbols_RemovedSymbol_ReturnsRemovedChangeType()
{
// Arrange
var fromSymbol = CreateSymbol("old_function", "sha256:def456", 256);
// Act
var result = _tracer.CompareSymbols(fromSymbol, null);
// Assert
result.ChangeType.Should().Be(SymbolChangeType.Removed);
result.SymbolName.Should().Be("old_function");
result.SizeDelta.Should().Be(-256);
result.FromHash.Should().Be("sha256:def456");
result.ToHash.Should().BeNull();
result.Confidence.Should().Be(1.0);
result.ChangeExplanation.Should().Contain("removed");
}
[Fact]
public void CompareSymbols_UnchangedSymbol_ReturnsUnchangedChangeType()
{
// Arrange
var fromSymbol = CreateSymbol("stable_function", "sha256:same", 128);
var toSymbol = CreateSymbol("stable_function", "sha256:same", 128);
// Act
var result = _tracer.CompareSymbols(fromSymbol, toSymbol);
// Assert
result.ChangeType.Should().Be(SymbolChangeType.Unchanged);
result.ExactMatch.Should().BeTrue();
result.SizeDelta.Should().Be(0);
result.Confidence.Should().Be(1.0);
result.MatchMethod.Should().Be("ExactHash");
}
[Fact]
public void CompareSymbols_ModifiedSymbol_ReturnsModifiedChangeType()
{
// Arrange
var fromSymbol = CreateSymbol("func", "sha256:before", 100);
var toSymbol = CreateSymbol("func", "sha256:after", 120);
// Act
var result = _tracer.CompareSymbols(fromSymbol, toSymbol);
// Assert
result.ChangeType.Should().Be(SymbolChangeType.Modified);
result.ExactMatch.Should().BeFalse();
result.SizeDelta.Should().Be(20);
}
[Fact]
public void CompareSymbols_HighChunkSimilarityWithCfgChange_ReturnsPatchedChangeType()
{
// Arrange - >= 85% chunk similarity (6/7 = 86%) with small CFG change indicates patch
var fromSymbol = CreateSymbolWithChunks("patched_func", "sha256:before", 350, 10,
[("sha256:chunk1", 0, 50), ("sha256:chunk2", 50, 50), ("sha256:chunk3", 100, 50),
("sha256:chunk4", 150, 50), ("sha256:chunk5", 200, 50), ("sha256:chunk6", 250, 50), ("sha256:chunk7", 300, 50)]);
var toSymbol = CreateSymbolWithChunks("patched_func", "sha256:after", 360, 12,
[("sha256:chunk1", 0, 50), ("sha256:chunk2", 50, 50), ("sha256:chunk3", 100, 50),
("sha256:chunk4", 150, 50), ("sha256:chunk5", 200, 50), ("sha256:chunk6", 250, 50), ("sha256:newchunk", 300, 60)]);
// Act
var result = _tracer.CompareSymbols(fromSymbol, toSymbol);
// Assert
result.ChangeType.Should().Be(SymbolChangeType.Patched);
result.CfgBlockDelta.Should().Be(2);
result.ChangeExplanation.Should().Contain("patched");
}
[Fact]
public void CompareSymbols_HighChunkSimilarity_ReturnsModifiedWithHighConfidence()
{
// Arrange - 75% chunk similarity
var fromSymbol = CreateSymbolWithChunks("func", "sha256:before", 200, null,
[("sha256:chunk1", 0, 50), ("sha256:chunk2", 50, 50), ("sha256:chunk3", 100, 50), ("sha256:chunk4", 150, 50)]);
var toSymbol = CreateSymbolWithChunks("func", "sha256:after", 200, null,
[("sha256:chunk1", 0, 50), ("sha256:chunk2", 50, 50), ("sha256:chunk3", 100, 50), ("sha256:new", 150, 50)]);
// Act
var result = _tracer.CompareSymbols(fromSymbol, toSymbol);
// Assert
result.ChangeType.Should().Be(SymbolChangeType.Modified);
result.ChunksMatched.Should().Be(3);
result.ChunksTotal.Should().Be(4);
result.Confidence.Should().BeGreaterThanOrEqualTo(0.7);
}
[Fact]
public void CompareSymbols_SemanticMatch_ReturnsModifiedWithSemanticMethod()
{
// Arrange - Different hash but same semantic fingerprint
var fromSymbol = CreateSymbolWithSemantic("func", "sha256:before", 200, "sha256:semantic1");
var toSymbol = CreateSymbolWithSemantic("func", "sha256:after", 200, "sha256:semantic1");
// Act
var result = _tracer.CompareSymbols(fromSymbol, toSymbol);
// Assert
result.ChangeType.Should().Be(SymbolChangeType.Modified);
result.MatchMethod.Should().Be("SemanticHash");
result.Confidence.Should().BeGreaterThanOrEqualTo(0.8);
result.ChangeExplanation.Should().Contain("semantically equivalent");
}
[Fact]
public void CompareSymbols_BothNull_ThrowsArgumentException()
{
// Act & Assert
var action = () => _tracer.CompareSymbols(null, null);
action.Should().Throw<ArgumentException>();
}
[Fact]
public void CompareAllSymbols_MixedChanges_ReturnsCorrectResults()
{
// Arrange
var fromSignature = CreateSignature("sig-from",
[
CreateSymbol("unchanged", "sha256:same", 100),
CreateSymbol("modified", "sha256:old", 200),
CreateSymbol("removed", "sha256:gone", 150)
]);
var toSignature = CreateSignature("sig-to",
[
CreateSymbol("unchanged", "sha256:same", 100),
CreateSymbol("modified", "sha256:new", 220),
CreateSymbol("added", "sha256:new", 300)
]);
// Act
var results = _tracer.CompareAllSymbols(fromSignature, toSignature);
// Assert
results.Should().HaveCount(4);
results.Should().ContainSingle(r => r.ChangeType == SymbolChangeType.Unchanged);
results.Should().ContainSingle(r => r.ChangeType == SymbolChangeType.Modified);
results.Should().ContainSingle(r => r.ChangeType == SymbolChangeType.Added);
results.Should().ContainSingle(r => r.ChangeType == SymbolChangeType.Removed);
}
[Fact]
public void CompareAllSymbols_ResultsAreSortedByName()
{
// Arrange
var fromSignature = CreateSignature("from", [CreateSymbol("z_func", "sha256:a", 100)]);
var toSignature = CreateSignature("to", [CreateSymbol("a_func", "sha256:b", 100)]);
// Act
var results = _tracer.CompareAllSymbols(fromSignature, toSignature);
// Assert
results.Should().HaveCount(2);
results[0].SymbolName.Should().Be("a_func");
results[1].SymbolName.Should().Be("z_func");
}
[Fact]
public void CompareSymbols_RecordsMatchedChunkIndices()
{
// Arrange
var fromSymbol = CreateSymbolWithChunks("func", "sha256:before", 200, null,
[("sha256:a", 0, 50), ("sha256:b", 50, 50), ("sha256:c", 100, 50), ("sha256:d", 150, 50)]);
var toSymbol = CreateSymbolWithChunks("func", "sha256:after", 200, null,
[("sha256:a", 0, 50), ("sha256:x", 50, 50), ("sha256:c", 100, 50), ("sha256:y", 150, 50)]);
// Act
var result = _tracer.CompareSymbols(fromSymbol, toSymbol);
// Assert
result.MatchedChunkIndices.Should().BeEquivalentTo([0, 2]);
}
// Helper methods
private static SymbolSignature CreateSymbol(string name, string hash, int size, int? cfgCount = null)
{
return new SymbolSignature
{
Name = name,
HashAlg = "sha256",
HashHex = hash,
SizeBytes = size,
CfgBbCount = cfgCount
};
}
private static SymbolSignature CreateSymbolWithChunks(
string name, string hash, int size, int? cfgCount,
(string hash, int offset, int size)[] chunks)
{
return new SymbolSignature
{
Name = name,
HashAlg = "sha256",
HashHex = hash,
SizeBytes = size,
CfgBbCount = cfgCount,
Chunks = chunks.Select(c => new ChunkHash(c.offset, c.size, c.hash)).ToImmutableArray()
};
}
private static SymbolSignature CreateSymbolWithSemantic(string name, string hash, int size, string semanticHash)
{
return new SymbolSignature
{
Name = name,
HashAlg = "sha256",
HashHex = hash,
SizeBytes = size,
SemanticHashHex = semanticHash
};
}
private static DeltaSignature CreateSignature(string id, SymbolSignature[] symbols)
{
return new DeltaSignature
{
SignatureId = id,
Cve = "CVE-2026-0001",
Package = new PackageRef("testpkg", null),
Target = new TargetRef("x86_64", "gnu"),
Normalization = new NormalizationRef("test", "1.0", []),
SignatureState = "vulnerable",
Symbols = symbols.ToImmutableArray()
};
}
}