release orchestrator v1 draft and build fixes
This commit is contained in:
@@ -320,7 +320,7 @@ public sealed class GoldenSetController : ControllerBase
|
||||
};
|
||||
|
||||
// Validate
|
||||
var validationResult = _validator.Validate(definition);
|
||||
var validationResult = await _validator.ValidateAsync(definition, null, ct);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
return BadRequest(CreateProblem(
|
||||
|
||||
@@ -17,18 +17,33 @@ public sealed class DeltaSignatureMatcher : IDeltaSignatureMatcher
|
||||
{
|
||||
private readonly DisassemblyService _disassemblyService;
|
||||
private readonly NormalizationService _normalizationService;
|
||||
private readonly ISymbolChangeTracer _changeTracer;
|
||||
private readonly ILogger<DeltaSignatureMatcher> _logger;
|
||||
|
||||
public DeltaSignatureMatcher(
|
||||
DisassemblyService disassemblyService,
|
||||
NormalizationService normalizationService,
|
||||
ISymbolChangeTracer changeTracer,
|
||||
ILogger<DeltaSignatureMatcher> logger)
|
||||
{
|
||||
_disassemblyService = disassemblyService;
|
||||
_normalizationService = normalizationService;
|
||||
_changeTracer = changeTracer;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Legacy constructor for backward compatibility.
|
||||
/// Creates an internal SymbolChangeTracer instance.
|
||||
/// </summary>
|
||||
public DeltaSignatureMatcher(
|
||||
DisassemblyService disassemblyService,
|
||||
NormalizationService normalizationService,
|
||||
ILogger<DeltaSignatureMatcher> logger)
|
||||
: this(disassemblyService, normalizationService, new SymbolChangeTracer(), logger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<MatchResult>> MatchAsync(
|
||||
Stream binaryStream,
|
||||
@@ -329,6 +344,58 @@ public sealed class DeltaSignatureMatcher : IDeltaSignatureMatcher
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DeltaComparisonResult> CompareSignaturesAsync(
|
||||
DeltaSignature fromSignature,
|
||||
DeltaSignature toSignature,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(fromSignature);
|
||||
ArgumentNullException.ThrowIfNull(toSignature);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Comparing signatures: {From} ({FromSymbols} symbols) -> {To} ({ToSymbols} symbols)",
|
||||
fromSignature.SignatureId,
|
||||
fromSignature.Symbols.Length,
|
||||
toSignature.SignatureId,
|
||||
toSignature.Symbols.Length);
|
||||
|
||||
var symbolResults = _changeTracer.CompareAllSymbols(fromSignature, toSignature);
|
||||
|
||||
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)
|
||||
};
|
||||
|
||||
_logger.LogInformation(
|
||||
"Comparison complete: {Total} symbols, {Unchanged} unchanged, {Added} added, {Removed} removed, {Modified} modified, {Patched} patched",
|
||||
summary.TotalSymbols,
|
||||
summary.UnchangedSymbols,
|
||||
summary.AddedSymbols,
|
||||
summary.RemovedSymbols,
|
||||
summary.ModifiedSymbols,
|
||||
summary.PatchedSymbols);
|
||||
|
||||
var result = new DeltaComparisonResult
|
||||
{
|
||||
FromSignatureId = fromSignature.SignatureId,
|
||||
ToSignatureId = toSignature.SignatureId,
|
||||
SymbolResults = [.. symbolResults],
|
||||
Summary = summary
|
||||
};
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
private static byte[] GetNormalizedBytes(NormalizedFunction normalized)
|
||||
{
|
||||
var totalSize = normalized.Instructions.Sum(i => i.NormalizedBytes.Length);
|
||||
|
||||
@@ -35,4 +35,16 @@ public interface IDeltaSignatureMatcher
|
||||
string symbolHash,
|
||||
string symbolName,
|
||||
IEnumerable<DeltaSignature> signatures);
|
||||
|
||||
/// <summary>
|
||||
/// Compare two delta signatures and return detailed change information.
|
||||
/// </summary>
|
||||
/// <param name="fromSignature">The "before" signature.</param>
|
||||
/// <param name="toSignature">The "after" signature.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Detailed comparison result with symbol-level changes.</returns>
|
||||
Task<DeltaComparisonResult> CompareSignaturesAsync(
|
||||
DeltaSignature fromSignature,
|
||||
DeltaSignature toSignature,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) StellaOps. All rights reserved.
|
||||
// Licensed under AGPL-3.0-or-later. See LICENSE in the project root.
|
||||
|
||||
namespace StellaOps.BinaryIndex.DeltaSig;
|
||||
|
||||
/// <summary>
|
||||
/// Service for detailed symbol comparison between binary versions.
|
||||
/// </summary>
|
||||
public interface ISymbolChangeTracer
|
||||
{
|
||||
/// <summary>
|
||||
/// Compare two symbol signatures and compute detailed change metrics.
|
||||
/// </summary>
|
||||
/// <param name="fromSymbol">Symbol from the "before" version (null if added).</param>
|
||||
/// <param name="toSymbol">Symbol from the "after" version (null if removed).</param>
|
||||
/// <returns>Detailed symbol match result with change tracking.</returns>
|
||||
SymbolMatchResult CompareSymbols(
|
||||
SymbolSignature? fromSymbol,
|
||||
SymbolSignature? toSymbol);
|
||||
|
||||
/// <summary>
|
||||
/// Compare all symbols between two delta signatures.
|
||||
/// </summary>
|
||||
/// <param name="fromSignature">The "before" delta signature.</param>
|
||||
/// <param name="toSignature">The "after" delta signature.</param>
|
||||
/// <returns>List of symbol comparison results.</returns>
|
||||
IReadOnlyList<SymbolMatchResult> CompareAllSymbols(
|
||||
DeltaSignature fromSignature,
|
||||
DeltaSignature toSignature);
|
||||
}
|
||||
@@ -72,6 +72,11 @@ public sealed record DeltaSignatureRequest
|
||||
/// </summary>
|
||||
public sealed record DeltaSignature
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for this signature.
|
||||
/// </summary>
|
||||
public string SignatureId { get; init; } = Guid.NewGuid().ToString("N");
|
||||
|
||||
/// <summary>
|
||||
/// Schema identifier for this signature format.
|
||||
/// </summary>
|
||||
@@ -278,6 +283,79 @@ public sealed record SymbolMatchResult
|
||||
/// Match confidence (0.0 - 1.0).
|
||||
/// </summary>
|
||||
public double Confidence { get; init; }
|
||||
|
||||
// ====== CHANGE TRACKING FIELDS ======
|
||||
|
||||
/// <summary>
|
||||
/// Type of change detected.
|
||||
/// </summary>
|
||||
public SymbolChangeType ChangeType { get; init; } = SymbolChangeType.Unchanged;
|
||||
|
||||
/// <summary>
|
||||
/// Size delta in bytes (positive = larger, negative = smaller).
|
||||
/// </summary>
|
||||
public int SizeDelta { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CFG basic block count delta (if available).
|
||||
/// </summary>
|
||||
public int? CfgBlockDelta { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Indices of chunks that matched (for partial match analysis).
|
||||
/// </summary>
|
||||
public ImmutableArray<int> MatchedChunkIndices { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable explanation of the change.
|
||||
/// </summary>
|
||||
public string? ChangeExplanation { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash of the "from" version (before change).
|
||||
/// </summary>
|
||||
public string? FromHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash of the "to" version (after change).
|
||||
/// </summary>
|
||||
public string? ToHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Method used for matching (CFGHash, InstructionHash, SemanticHash, ChunkHash).
|
||||
/// </summary>
|
||||
public string? MatchMethod { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type of symbol change detected.
|
||||
/// </summary>
|
||||
public enum SymbolChangeType
|
||||
{
|
||||
/// <summary>
|
||||
/// No change detected.
|
||||
/// </summary>
|
||||
Unchanged,
|
||||
|
||||
/// <summary>
|
||||
/// Symbol was added (not present in "from" version).
|
||||
/// </summary>
|
||||
Added,
|
||||
|
||||
/// <summary>
|
||||
/// Symbol was removed (not present in "to" version).
|
||||
/// </summary>
|
||||
Removed,
|
||||
|
||||
/// <summary>
|
||||
/// Symbol was modified (hash changed).
|
||||
/// </summary>
|
||||
Modified,
|
||||
|
||||
/// <summary>
|
||||
/// Symbol was patched (security fix applied, verified).
|
||||
/// </summary>
|
||||
Patched
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -310,3 +388,75 @@ public sealed record AuthoringResult
|
||||
/// </summary>
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of comparing two delta signatures.
|
||||
/// </summary>
|
||||
public sealed record DeltaComparisonResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifier for the "from" signature.
|
||||
/// </summary>
|
||||
public required string FromSignatureId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Identifier for the "to" signature.
|
||||
/// </summary>
|
||||
public required string ToSignatureId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Individual symbol comparison results.
|
||||
/// </summary>
|
||||
public ImmutableArray<SymbolMatchResult> SymbolResults { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Summary of the comparison.
|
||||
/// </summary>
|
||||
public required DeltaComparisonSummary Summary { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Summary of a delta comparison between two signatures.
|
||||
/// </summary>
|
||||
public sealed record DeltaComparisonSummary
|
||||
{
|
||||
/// <summary>
|
||||
/// Total number of symbols compared.
|
||||
/// </summary>
|
||||
public int TotalSymbols { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of unchanged symbols.
|
||||
/// </summary>
|
||||
public int UnchangedSymbols { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of added symbols.
|
||||
/// </summary>
|
||||
public int AddedSymbols { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of removed symbols.
|
||||
/// </summary>
|
||||
public int RemovedSymbols { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of modified symbols.
|
||||
/// </summary>
|
||||
public int ModifiedSymbols { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of patched symbols (security fixes).
|
||||
/// </summary>
|
||||
public int PatchedSymbols { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Average confidence across all symbol comparisons.
|
||||
/// </summary>
|
||||
public double AverageConfidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total size delta in bytes.
|
||||
/// </summary>
|
||||
public int TotalSizeDelta { get; init; }
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ public static class ServiceCollectionExtensions
|
||||
logger);
|
||||
});
|
||||
|
||||
services.AddSingleton<ISymbolChangeTracer, SymbolChangeTracer>();
|
||||
services.AddSingleton<IDeltaSignatureMatcher, DeltaSignatureMatcher>();
|
||||
|
||||
return services;
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
// Copyright (c) StellaOps. All rights reserved.
|
||||
// Licensed under AGPL-3.0-or-later. See LICENSE in the project root.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
|
||||
namespace StellaOps.BinaryIndex.DeltaSig;
|
||||
|
||||
/// <summary>
|
||||
/// Service for detailed symbol comparison between binary versions.
|
||||
/// Determines change type, similarity, and generates explanations.
|
||||
/// </summary>
|
||||
public sealed class SymbolChangeTracer : ISymbolChangeTracer
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public SymbolMatchResult CompareSymbols(
|
||||
SymbolSignature? fromSymbol,
|
||||
SymbolSignature? toSymbol)
|
||||
{
|
||||
// Case 1: Symbol added
|
||||
if (fromSymbol is null && toSymbol is not null)
|
||||
{
|
||||
return new SymbolMatchResult
|
||||
{
|
||||
SymbolName = toSymbol.Name,
|
||||
ExactMatch = false,
|
||||
Confidence = 1.0,
|
||||
ChangeType = SymbolChangeType.Added,
|
||||
SizeDelta = toSymbol.SizeBytes,
|
||||
ToHash = toSymbol.HashHex,
|
||||
ChangeExplanation = "Symbol added in new version"
|
||||
};
|
||||
}
|
||||
|
||||
// Case 2: Symbol removed
|
||||
if (fromSymbol is not null && toSymbol is null)
|
||||
{
|
||||
return new SymbolMatchResult
|
||||
{
|
||||
SymbolName = fromSymbol.Name,
|
||||
ExactMatch = false,
|
||||
Confidence = 1.0,
|
||||
ChangeType = SymbolChangeType.Removed,
|
||||
SizeDelta = -fromSymbol.SizeBytes,
|
||||
FromHash = fromSymbol.HashHex,
|
||||
ChangeExplanation = "Symbol removed in new version"
|
||||
};
|
||||
}
|
||||
|
||||
// Case 3: Both exist - compare
|
||||
if (fromSymbol is not null && toSymbol is not null)
|
||||
{
|
||||
return CompareExistingSymbols(fromSymbol, toSymbol);
|
||||
}
|
||||
|
||||
// Case 4: Both null (shouldn't happen)
|
||||
throw new ArgumentException("Both symbols cannot be null");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<SymbolMatchResult> CompareAllSymbols(
|
||||
DeltaSignature fromSignature,
|
||||
DeltaSignature toSignature)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(fromSignature);
|
||||
ArgumentNullException.ThrowIfNull(toSignature);
|
||||
|
||||
var fromSymbols = fromSignature.Symbols
|
||||
.ToDictionary(s => s.Name, StringComparer.Ordinal);
|
||||
var toSymbols = toSignature.Symbols
|
||||
.ToDictionary(s => s.Name, StringComparer.Ordinal);
|
||||
|
||||
var allNames = fromSymbols.Keys
|
||||
.Union(toSymbols.Keys, StringComparer.Ordinal)
|
||||
.OrderBy(n => n, StringComparer.Ordinal);
|
||||
|
||||
var results = new List<SymbolMatchResult>();
|
||||
|
||||
foreach (var name in allNames)
|
||||
{
|
||||
fromSymbols.TryGetValue(name, out var fromSymbol);
|
||||
toSymbols.TryGetValue(name, out var toSymbol);
|
||||
|
||||
var result = CompareSymbols(fromSymbol, toSymbol);
|
||||
results.Add(result);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static SymbolMatchResult CompareExistingSymbols(
|
||||
SymbolSignature from,
|
||||
SymbolSignature to)
|
||||
{
|
||||
var exactMatch = string.Equals(from.HashHex, to.HashHex, StringComparison.OrdinalIgnoreCase);
|
||||
var sizeDelta = to.SizeBytes - from.SizeBytes;
|
||||
var cfgDelta = (from.CfgBbCount.HasValue && to.CfgBbCount.HasValue)
|
||||
? to.CfgBbCount.Value - from.CfgBbCount.Value
|
||||
: (int?)null;
|
||||
|
||||
if (exactMatch)
|
||||
{
|
||||
return new SymbolMatchResult
|
||||
{
|
||||
SymbolName = from.Name,
|
||||
ExactMatch = true,
|
||||
Confidence = 1.0,
|
||||
ChangeType = SymbolChangeType.Unchanged,
|
||||
SizeDelta = 0,
|
||||
FromHash = from.HashHex,
|
||||
ToHash = to.HashHex,
|
||||
MatchMethod = "ExactHash",
|
||||
ChangeExplanation = "No change detected"
|
||||
};
|
||||
}
|
||||
|
||||
// Compute chunk matches
|
||||
var fromChunks = from.Chunks ?? [];
|
||||
var toChunks = to.Chunks ?? [];
|
||||
var (chunksMatched, matchedIndices) = CompareChunks(fromChunks, toChunks);
|
||||
var chunkSimilarity = fromChunks.Length > 0
|
||||
? (double)chunksMatched / fromChunks.Length
|
||||
: 0.0;
|
||||
|
||||
// Determine change type and confidence
|
||||
var (changeType, confidence, explanation, method) = DetermineChange(
|
||||
from, to, chunkSimilarity, cfgDelta);
|
||||
|
||||
return new SymbolMatchResult
|
||||
{
|
||||
SymbolName = from.Name,
|
||||
ExactMatch = false,
|
||||
ChunksMatched = chunksMatched,
|
||||
ChunksTotal = Math.Max(fromChunks.Length, toChunks.Length),
|
||||
Confidence = confidence,
|
||||
ChangeType = changeType,
|
||||
SizeDelta = sizeDelta,
|
||||
CfgBlockDelta = cfgDelta,
|
||||
MatchedChunkIndices = matchedIndices,
|
||||
FromHash = from.HashHex,
|
||||
ToHash = to.HashHex,
|
||||
MatchMethod = method,
|
||||
ChangeExplanation = explanation
|
||||
};
|
||||
}
|
||||
|
||||
private static (int matched, ImmutableArray<int> indices) CompareChunks(
|
||||
ImmutableArray<ChunkHash> fromChunks,
|
||||
ImmutableArray<ChunkHash> toChunks)
|
||||
{
|
||||
if (fromChunks.Length == 0 || toChunks.Length == 0)
|
||||
{
|
||||
return (0, []);
|
||||
}
|
||||
|
||||
var toChunkSet = toChunks
|
||||
.Select(c => c.HashHex)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var matchedIndices = new List<int>();
|
||||
var matched = 0;
|
||||
|
||||
for (var i = 0; i < fromChunks.Length; i++)
|
||||
{
|
||||
if (toChunkSet.Contains(fromChunks[i].HashHex))
|
||||
{
|
||||
matched++;
|
||||
matchedIndices.Add(i);
|
||||
}
|
||||
}
|
||||
|
||||
return (matched, matchedIndices.ToImmutableArray());
|
||||
}
|
||||
|
||||
private static (SymbolChangeType type, double confidence, string explanation, string method)
|
||||
DetermineChange(
|
||||
SymbolSignature from,
|
||||
SymbolSignature to,
|
||||
double chunkSimilarity,
|
||||
int? cfgDelta)
|
||||
{
|
||||
// High chunk similarity with CFG change = likely patch
|
||||
if (chunkSimilarity >= 0.85 && cfgDelta.HasValue && Math.Abs(cfgDelta.Value) <= 5)
|
||||
{
|
||||
return (
|
||||
SymbolChangeType.Patched,
|
||||
Math.Min(0.95, chunkSimilarity),
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"Function patched: {0} basic blocks changed",
|
||||
Math.Abs(cfgDelta.Value)),
|
||||
"CFGHash+ChunkMatch"
|
||||
);
|
||||
}
|
||||
|
||||
// High chunk similarity = minor modification
|
||||
if (chunkSimilarity >= 0.7)
|
||||
{
|
||||
return (
|
||||
SymbolChangeType.Modified,
|
||||
chunkSimilarity,
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"Function modified: {0:P0} of code changed",
|
||||
1 - chunkSimilarity),
|
||||
"ChunkMatch"
|
||||
);
|
||||
}
|
||||
|
||||
// Semantic match check (if available)
|
||||
if (!string.IsNullOrEmpty(from.SemanticHashHex) &&
|
||||
!string.IsNullOrEmpty(to.SemanticHashHex))
|
||||
{
|
||||
var semanticMatch = string.Equals(
|
||||
from.SemanticHashHex, to.SemanticHashHex,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (semanticMatch)
|
||||
{
|
||||
return (
|
||||
SymbolChangeType.Modified,
|
||||
0.80,
|
||||
"Function semantically equivalent (compiler variation)",
|
||||
"SemanticHash"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Low similarity = significant modification
|
||||
return (
|
||||
SymbolChangeType.Modified,
|
||||
Math.Max(0.4, chunkSimilarity),
|
||||
"Function significantly modified",
|
||||
"ChunkMatch"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.BinaryIndex.Disassembly;
|
||||
using StellaOps.Determinism;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Ghidra;
|
||||
|
||||
@@ -29,6 +30,7 @@ public sealed class GhidraDisassemblyPlugin : IDisassemblyPlugin, IDisposable
|
||||
private readonly GhidraOptions _options;
|
||||
private readonly ILogger<GhidraDisassemblyPlugin> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
private bool _disposed;
|
||||
|
||||
private static readonly DisassemblyCapabilities s_capabilities = new()
|
||||
@@ -74,16 +76,19 @@ public sealed class GhidraDisassemblyPlugin : IDisassemblyPlugin, IDisposable
|
||||
/// <param name="options">Ghidra options.</param>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
/// <param name="timeProvider">Time provider for timestamps.</param>
|
||||
/// <param name="guidProvider">GUID provider for deterministic IDs.</param>
|
||||
public GhidraDisassemblyPlugin(
|
||||
IGhidraService ghidraService,
|
||||
IOptions<GhidraOptions> options,
|
||||
ILogger<GhidraDisassemblyPlugin> logger,
|
||||
TimeProvider timeProvider)
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_ghidraService = ghidraService ?? throw new ArgumentNullException(nameof(ghidraService));
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -297,7 +302,7 @@ public sealed class GhidraDisassemblyPlugin : IDisassemblyPlugin, IDisposable
|
||||
// Write bytes to temp file
|
||||
var tempPath = Path.Combine(
|
||||
_options.WorkDir,
|
||||
$"disasm_{_timeProvider.GetUtcNow():yyyyMMddHHmmssfff}_{Guid.NewGuid():N}.bin");
|
||||
$"disasm_{_timeProvider.GetUtcNow():yyyyMMddHHmmssfff}_{_guidProvider.NewGuid():N}.bin");
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Determinism;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Ghidra;
|
||||
|
||||
@@ -18,6 +19,8 @@ public sealed class GhidraHeadlessManager : IAsyncDisposable
|
||||
{
|
||||
private readonly GhidraOptions _options;
|
||||
private readonly ILogger<GhidraHeadlessManager> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
private readonly SemaphoreSlim _semaphore;
|
||||
private bool _disposed;
|
||||
|
||||
@@ -26,12 +29,18 @@ public sealed class GhidraHeadlessManager : IAsyncDisposable
|
||||
/// </summary>
|
||||
/// <param name="options">Ghidra configuration options.</param>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
/// <param name="timeProvider">Time provider for deterministic time.</param>
|
||||
/// <param name="guidProvider">GUID provider for deterministic IDs.</param>
|
||||
public GhidraHeadlessManager(
|
||||
IOptions<GhidraOptions> options,
|
||||
ILogger<GhidraHeadlessManager> logger)
|
||||
ILogger<GhidraHeadlessManager> logger,
|
||||
TimeProvider? timeProvider = null,
|
||||
IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
_semaphore = new SemaphoreSlim(_options.MaxConcurrentInstances, _options.MaxConcurrentInstances);
|
||||
|
||||
EnsureWorkDirectoryExists();
|
||||
@@ -180,7 +189,7 @@ public sealed class GhidraHeadlessManager : IAsyncDisposable
|
||||
{
|
||||
var projectDir = Path.Combine(
|
||||
_options.WorkDir,
|
||||
$"project_{DateTime.UtcNow:yyyyMMddHHmmssfff}_{Guid.NewGuid():N}");
|
||||
$"project_{_timeProvider.GetUtcNow():yyyyMMddHHmmssfff}_{_guidProvider.NewGuid():N}");
|
||||
|
||||
Directory.CreateDirectory(projectDir);
|
||||
_logger.LogDebug("Created temp project directory: {Path}", projectDir);
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Determinism;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Ghidra;
|
||||
|
||||
@@ -25,6 +26,7 @@ public sealed class GhidraService : IGhidraService, IAsyncDisposable
|
||||
private readonly GhidraOptions _options;
|
||||
private readonly ILogger<GhidraService> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new GhidraService.
|
||||
@@ -33,16 +35,19 @@ public sealed class GhidraService : IGhidraService, IAsyncDisposable
|
||||
/// <param name="options">Ghidra options.</param>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
/// <param name="timeProvider">Time provider for timestamps.</param>
|
||||
/// <param name="guidProvider">GUID provider for deterministic IDs.</param>
|
||||
public GhidraService(
|
||||
GhidraHeadlessManager headlessManager,
|
||||
IOptions<GhidraOptions> options,
|
||||
ILogger<GhidraService> logger,
|
||||
TimeProvider timeProvider)
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_headlessManager = headlessManager;
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
_timeProvider = timeProvider;
|
||||
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -56,7 +61,7 @@ public sealed class GhidraService : IGhidraService, IAsyncDisposable
|
||||
// Write stream to temp file
|
||||
var tempPath = Path.Combine(
|
||||
_options.WorkDir,
|
||||
$"binary_{_timeProvider.GetUtcNow():yyyyMMddHHmmssfff}_{Guid.NewGuid():N}.bin");
|
||||
$"binary_{_timeProvider.GetUtcNow():yyyyMMddHHmmssfff}_{_guidProvider.NewGuid():N}.bin");
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Determinism;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Ghidra;
|
||||
|
||||
@@ -28,6 +29,7 @@ public sealed class GhidriffBridge : IGhidriffBridge
|
||||
private readonly GhidraOptions _ghidraOptions;
|
||||
private readonly ILogger<GhidriffBridge> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new GhidriffBridge.
|
||||
@@ -36,16 +38,19 @@ public sealed class GhidriffBridge : IGhidriffBridge
|
||||
/// <param name="ghidraOptions">Ghidra options for path configuration.</param>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
/// <param name="timeProvider">Time provider.</param>
|
||||
/// <param name="guidProvider">GUID provider for deterministic IDs.</param>
|
||||
public GhidriffBridge(
|
||||
IOptions<GhidriffOptions> options,
|
||||
IOptions<GhidraOptions> ghidraOptions,
|
||||
ILogger<GhidriffBridge> logger,
|
||||
TimeProvider timeProvider)
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_options = options.Value;
|
||||
_ghidraOptions = ghidraOptions.Value;
|
||||
_logger = logger;
|
||||
_timeProvider = timeProvider;
|
||||
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
|
||||
EnsureWorkDirectoryExists();
|
||||
}
|
||||
@@ -212,7 +217,7 @@ public sealed class GhidriffBridge : IGhidriffBridge
|
||||
{
|
||||
var outputDir = Path.Combine(
|
||||
_options.WorkDir,
|
||||
$"diff_{_timeProvider.GetUtcNow():yyyyMMddHHmmssfff}_{Guid.NewGuid():N}");
|
||||
$"diff_{_timeProvider.GetUtcNow():yyyyMMddHHmmssfff}_{_guidProvider.NewGuid():N}");
|
||||
|
||||
Directory.CreateDirectory(outputDir);
|
||||
return outputDir;
|
||||
@@ -523,7 +528,7 @@ public sealed class GhidriffBridge : IGhidriffBridge
|
||||
{
|
||||
var path = Path.Combine(
|
||||
_options.WorkDir,
|
||||
$"{prefix}_{_timeProvider.GetUtcNow():yyyyMMddHHmmssfff}_{Guid.NewGuid():N}.bin");
|
||||
$"{prefix}_{_timeProvider.GetUtcNow():yyyyMMddHHmmssfff}_{_guidProvider.NewGuid():N}.bin");
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Determinism;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Ghidra;
|
||||
|
||||
@@ -25,6 +26,7 @@ public sealed class VersionTrackingService : IVersionTrackingService
|
||||
private readonly GhidraOptions _options;
|
||||
private readonly ILogger<VersionTrackingService> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new VersionTrackingService.
|
||||
@@ -33,16 +35,19 @@ public sealed class VersionTrackingService : IVersionTrackingService
|
||||
/// <param name="options">Ghidra options.</param>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
/// <param name="timeProvider">Time provider.</param>
|
||||
/// <param name="guidProvider">GUID provider for deterministic IDs.</param>
|
||||
public VersionTrackingService(
|
||||
GhidraHeadlessManager headlessManager,
|
||||
IOptions<GhidraOptions> options,
|
||||
ILogger<VersionTrackingService> logger,
|
||||
TimeProvider timeProvider)
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_headlessManager = headlessManager;
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
_timeProvider = timeProvider;
|
||||
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -343,7 +348,7 @@ public sealed class VersionTrackingService : IVersionTrackingService
|
||||
{
|
||||
var path = Path.Combine(
|
||||
_options.WorkDir,
|
||||
$"{prefix}_{_timeProvider.GetUtcNow():yyyyMMddHHmmssfff}_{Guid.NewGuid():N}.bin");
|
||||
$"{prefix}_{_timeProvider.GetUtcNow():yyyyMMddHHmmssfff}_{_guidProvider.NewGuid():N}.bin");
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.BinaryIndex.Disassembly.Abstractions\StellaOps.BinaryIndex.Disassembly.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.BinaryIndex.Contracts\StellaOps.BinaryIndex.Contracts.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Text.Json;
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.BinaryIndex.DeltaSig;
|
||||
using StellaOps.Determinism;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Persistence.Repositories;
|
||||
|
||||
@@ -17,6 +18,8 @@ public sealed class DeltaSignatureRepository : IDeltaSignatureRepository
|
||||
{
|
||||
private readonly BinaryIndexDbContext _dbContext;
|
||||
private readonly ILogger<DeltaSignatureRepository> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
private static readonly JsonSerializerOptions s_jsonOptions = new()
|
||||
{
|
||||
@@ -26,10 +29,14 @@ public sealed class DeltaSignatureRepository : IDeltaSignatureRepository
|
||||
|
||||
public DeltaSignatureRepository(
|
||||
BinaryIndexDbContext dbContext,
|
||||
ILogger<DeltaSignatureRepository> logger)
|
||||
ILogger<DeltaSignatureRepository> logger,
|
||||
TimeProvider? timeProvider = null,
|
||||
IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
_logger = logger;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -59,8 +66,8 @@ public sealed class DeltaSignatureRepository : IDeltaSignatureRepository
|
||||
RETURNING id, created_at, updated_at
|
||||
""";
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var id = entity.Id != Guid.Empty ? entity.Id : Guid.NewGuid();
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var id = entity.Id != Guid.Empty ? entity.Id : _guidProvider.NewGuid();
|
||||
|
||||
var command = new CommandDefinition(
|
||||
sql,
|
||||
@@ -362,7 +369,7 @@ public sealed class DeltaSignatureRepository : IDeltaSignatureRepository
|
||||
RETURNING updated_at
|
||||
""";
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
var command = new CommandDefinition(
|
||||
sql,
|
||||
|
||||
@@ -3,6 +3,8 @@ using Dapper;
|
||||
using StellaOps.BinaryIndex.Fingerprints;
|
||||
using StellaOps.BinaryIndex.Fingerprints.Models;
|
||||
using System.Text.Json;
|
||||
using IGuidProvider = StellaOps.Determinism.IGuidProvider;
|
||||
using SystemGuidProvider = StellaOps.Determinism.SystemGuidProvider;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Persistence.Repositories;
|
||||
|
||||
@@ -12,11 +14,13 @@ namespace StellaOps.BinaryIndex.Persistence.Repositories;
|
||||
public sealed class FingerprintRepository : IFingerprintRepository
|
||||
{
|
||||
private readonly BinaryIndexDbContext _dbContext;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public FingerprintRepository(BinaryIndexDbContext dbContext)
|
||||
public FingerprintRepository(BinaryIndexDbContext dbContext, IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
}
|
||||
|
||||
public async Task<VulnFingerprint> CreateAsync(VulnFingerprint fingerprint, CancellationToken ct = default)
|
||||
@@ -42,7 +46,7 @@ public sealed class FingerprintRepository : IFingerprintRepository
|
||||
sql,
|
||||
new
|
||||
{
|
||||
Id = fingerprint.Id != Guid.Empty ? fingerprint.Id : Guid.NewGuid(),
|
||||
Id = fingerprint.Id != Guid.Empty ? fingerprint.Id : _guidProvider.NewGuid(),
|
||||
fingerprint.CveId,
|
||||
fingerprint.Component,
|
||||
fingerprint.Purl,
|
||||
@@ -256,10 +260,12 @@ public sealed class FingerprintRepository : IFingerprintRepository
|
||||
public sealed class FingerprintMatchRepository : IFingerprintMatchRepository
|
||||
{
|
||||
private readonly BinaryIndexDbContext _dbContext;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
public FingerprintMatchRepository(BinaryIndexDbContext dbContext)
|
||||
public FingerprintMatchRepository(BinaryIndexDbContext dbContext, IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
}
|
||||
|
||||
public async Task<FingerprintMatch> CreateAsync(FingerprintMatch match, CancellationToken ct = default)
|
||||
@@ -284,7 +290,7 @@ public sealed class FingerprintMatchRepository : IFingerprintMatchRepository
|
||||
sql,
|
||||
new
|
||||
{
|
||||
Id = match.Id != Guid.Empty ? match.Id : Guid.NewGuid(),
|
||||
Id = match.Id != Guid.Empty ? match.Id : _guidProvider.NewGuid(),
|
||||
match.ScanId,
|
||||
MatchType = match.Type.ToString().ToLowerInvariant(),
|
||||
match.BinaryKey,
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<ProjectReference Include="..\StellaOps.BinaryIndex.FixIndex\StellaOps.BinaryIndex.FixIndex.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.BinaryIndex.Fingerprints\StellaOps.BinaryIndex.Fingerprints.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Infrastructure.Postgres\StellaOps.Infrastructure.Postgres.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user