save progress
This commit is contained in:
@@ -97,7 +97,7 @@ public sealed record FingerprintClaimEvidence
|
||||
public required IReadOnlyList<string> ChangedFunctions { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Similarity scores for modified functions (function name → score).
|
||||
/// Similarity scores for modified functions (function name -> score).
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, decimal>? FunctionSimilarities { get; init; }
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace StellaOps.BinaryIndex.Builders;
|
||||
|
||||
/// <summary>
|
||||
/// Provides GUIDs for deterministic testing.
|
||||
/// </summary>
|
||||
public interface IGuidProvider
|
||||
{
|
||||
Guid NewGuid();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default GUID provider using <see cref="Guid.NewGuid"/>.
|
||||
/// </summary>
|
||||
public sealed class GuidProvider : IGuidProvider
|
||||
{
|
||||
public Guid NewGuid() => Guid.NewGuid();
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Builders;
|
||||
@@ -31,10 +30,13 @@ public sealed class PatchDiffEngine : IPatchDiffEngine
|
||||
vulnerable.Count, patched.Count);
|
||||
|
||||
var changes = new List<FunctionChange>();
|
||||
var weights = GetEffectiveWeights(options.Weights);
|
||||
|
||||
// Index by name for quick lookup
|
||||
var vulnerableByName = vulnerable.ToDictionary(f => f.Name, f => f);
|
||||
var patchedByName = patched.ToDictionary(f => f.Name, f => f);
|
||||
var patchedByNormalizedName = options.FuzzyNameMatching
|
||||
? BuildNormalizedNameIndex(patched)
|
||||
: null;
|
||||
|
||||
// Track processed functions to find additions
|
||||
var processedPatched = new HashSet<string>();
|
||||
@@ -46,7 +48,7 @@ public sealed class PatchDiffEngine : IPatchDiffEngine
|
||||
{
|
||||
processedPatched.Add(vulnFunc.Name);
|
||||
|
||||
var similarity = ComputeSimilarity(vulnFunc, patchedFunc);
|
||||
var similarity = ComputeSimilarity(vulnFunc, patchedFunc, weights);
|
||||
|
||||
if (similarity >= 1.0m)
|
||||
{
|
||||
@@ -86,17 +88,34 @@ public sealed class PatchDiffEngine : IPatchDiffEngine
|
||||
}
|
||||
else
|
||||
{
|
||||
if (options.FuzzyNameMatching &&
|
||||
TryGetFuzzyMatch(vulnFunc.Name, patchedByNormalizedName, processedPatched, out var fuzzyMatch))
|
||||
{
|
||||
processedPatched.Add(fuzzyMatch.Name);
|
||||
var similarity = ComputeSimilarity(vulnFunc, fuzzyMatch, weights);
|
||||
changes.Add(new FunctionChange
|
||||
{
|
||||
FunctionName = vulnFunc.Name,
|
||||
Type = similarity >= options.SimilarityThreshold ? ChangeType.Modified : ChangeType.SignatureChanged,
|
||||
VulnerableFingerprint = vulnFunc,
|
||||
PatchedFingerprint = fuzzyMatch,
|
||||
SimilarityScore = similarity,
|
||||
DifferingHashes = GetDifferingHashes(vulnFunc, fuzzyMatch)
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not found by name - check if renamed
|
||||
if (options.DetectRenames)
|
||||
{
|
||||
var bestMatch = FindBestMatch(vulnFunc, patched, processedPatched, options.RenameThreshold);
|
||||
var bestMatch = FindBestMatch(vulnFunc, patched, processedPatched, options.RenameThreshold, weights);
|
||||
if (bestMatch != null)
|
||||
{
|
||||
processedPatched.Add(bestMatch.Name);
|
||||
var similarity = ComputeSimilarity(vulnFunc, bestMatch);
|
||||
var similarity = ComputeSimilarity(vulnFunc, bestMatch, weights);
|
||||
changes.Add(new FunctionChange
|
||||
{
|
||||
FunctionName = $"{vulnFunc.Name} → {bestMatch.Name}",
|
||||
FunctionName = $"{vulnFunc.Name} -> {bestMatch.Name}",
|
||||
Type = ChangeType.Modified,
|
||||
VulnerableFingerprint = vulnFunc,
|
||||
PatchedFingerprint = bestMatch,
|
||||
@@ -156,32 +175,31 @@ public sealed class PatchDiffEngine : IPatchDiffEngine
|
||||
ArgumentNullException.ThrowIfNull(a);
|
||||
ArgumentNullException.ThrowIfNull(b);
|
||||
|
||||
return ComputeSimilarity(a, b, HashWeights.Default);
|
||||
}
|
||||
|
||||
private static decimal ComputeSimilarity(FunctionFingerprint a, FunctionFingerprint b, HashWeights weights)
|
||||
{
|
||||
// Compute weighted similarity based on hash matches
|
||||
decimal totalWeight = 0m;
|
||||
decimal matchedWeight = 0m;
|
||||
|
||||
// Basic block hash (weight: 0.5)
|
||||
const decimal bbWeight = 0.5m;
|
||||
totalWeight += bbWeight;
|
||||
totalWeight += weights.BasicBlockWeight;
|
||||
if (HashesEqual(a.BasicBlockHash, b.BasicBlockHash))
|
||||
{
|
||||
matchedWeight += bbWeight;
|
||||
matchedWeight += weights.BasicBlockWeight;
|
||||
}
|
||||
|
||||
// CFG hash (weight: 0.3)
|
||||
const decimal cfgWeight = 0.3m;
|
||||
totalWeight += cfgWeight;
|
||||
totalWeight += weights.CfgWeight;
|
||||
if (HashesEqual(a.CfgHash, b.CfgHash))
|
||||
{
|
||||
matchedWeight += cfgWeight;
|
||||
matchedWeight += weights.CfgWeight;
|
||||
}
|
||||
|
||||
// String refs hash (weight: 0.2)
|
||||
const decimal strWeight = 0.2m;
|
||||
totalWeight += strWeight;
|
||||
totalWeight += weights.StringRefsWeight;
|
||||
if (HashesEqual(a.StringRefsHash, b.StringRefsHash))
|
||||
{
|
||||
matchedWeight += strWeight;
|
||||
matchedWeight += weights.StringRefsWeight;
|
||||
}
|
||||
|
||||
// Size similarity bonus (if sizes are within 10%, add small bonus)
|
||||
@@ -207,7 +225,8 @@ public sealed class PatchDiffEngine : IPatchDiffEngine
|
||||
ArgumentNullException.ThrowIfNull(vulnerable);
|
||||
ArgumentNullException.ThrowIfNull(patched);
|
||||
|
||||
var mappings = new Dictionary<string, string>();
|
||||
var mappings = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
var patchedByNormalizedName = BuildNormalizedNameIndex(patched);
|
||||
var usedPatched = new HashSet<string>();
|
||||
|
||||
// First pass: exact name matches
|
||||
@@ -218,6 +237,13 @@ public sealed class PatchDiffEngine : IPatchDiffEngine
|
||||
{
|
||||
mappings[vulnFunc.Name] = match.Name;
|
||||
usedPatched.Add(match.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryGetFuzzyMatch(vulnFunc.Name, patchedByNormalizedName, usedPatched, out var fuzzyMatch))
|
||||
{
|
||||
mappings[vulnFunc.Name] = fuzzyMatch.Name;
|
||||
usedPatched.Add(fuzzyMatch.Name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +253,7 @@ public sealed class PatchDiffEngine : IPatchDiffEngine
|
||||
|
||||
foreach (var vulnFunc in unmatchedVulnerable)
|
||||
{
|
||||
var bestMatch = FindBestMatch(vulnFunc, unmatchedPatched, usedPatched, threshold);
|
||||
var bestMatch = FindBestMatch(vulnFunc, unmatchedPatched, usedPatched, threshold, HashWeights.Default);
|
||||
if (bestMatch != null)
|
||||
{
|
||||
mappings[vulnFunc.Name] = bestMatch.Name;
|
||||
@@ -242,7 +268,8 @@ public sealed class PatchDiffEngine : IPatchDiffEngine
|
||||
FunctionFingerprint target,
|
||||
IReadOnlyList<FunctionFingerprint> candidates,
|
||||
HashSet<string> excludeNames,
|
||||
decimal threshold)
|
||||
decimal threshold,
|
||||
HashWeights weights)
|
||||
{
|
||||
FunctionFingerprint? bestMatch = null;
|
||||
var bestScore = threshold - 0.001m; // Must exceed threshold
|
||||
@@ -252,7 +279,7 @@ public sealed class PatchDiffEngine : IPatchDiffEngine
|
||||
if (excludeNames.Contains(candidate.Name))
|
||||
continue;
|
||||
|
||||
var score = ComputeSimilarity(target, candidate);
|
||||
var score = ComputeSimilarity(target, candidate, weights);
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
@@ -263,6 +290,88 @@ public sealed class PatchDiffEngine : IPatchDiffEngine
|
||||
return bestMatch;
|
||||
}
|
||||
|
||||
private HashWeights GetEffectiveWeights(HashWeights weights)
|
||||
{
|
||||
if (!weights.IsValid)
|
||||
{
|
||||
_logger.LogWarning("Invalid diff weights supplied; using defaults.");
|
||||
return HashWeights.Default;
|
||||
}
|
||||
|
||||
return weights;
|
||||
}
|
||||
|
||||
private static Dictionary<string, List<FunctionFingerprint>> BuildNormalizedNameIndex(
|
||||
IReadOnlyList<FunctionFingerprint> fingerprints)
|
||||
{
|
||||
var index = new Dictionary<string, List<FunctionFingerprint>>(StringComparer.Ordinal);
|
||||
foreach (var fingerprint in fingerprints)
|
||||
{
|
||||
var key = NormalizeName(fingerprint.Name);
|
||||
if (!index.TryGetValue(key, out var bucket))
|
||||
{
|
||||
bucket = new List<FunctionFingerprint>();
|
||||
index[key] = bucket;
|
||||
}
|
||||
|
||||
bucket.Add(fingerprint);
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
private static bool TryGetFuzzyMatch(
|
||||
string name,
|
||||
Dictionary<string, List<FunctionFingerprint>>? index,
|
||||
HashSet<string> usedNames,
|
||||
out FunctionFingerprint match)
|
||||
{
|
||||
match = null!;
|
||||
if (index is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalized = NormalizeName(name);
|
||||
if (!index.TryGetValue(normalized, out var candidates))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
if (usedNames.Contains(candidate.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
match = candidate;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string NormalizeName(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var buffer = new char[name.Length];
|
||||
var index = 0;
|
||||
foreach (var ch in name)
|
||||
{
|
||||
if (char.IsLetterOrDigit(ch))
|
||||
{
|
||||
buffer[index++] = char.ToLowerInvariant(ch);
|
||||
}
|
||||
}
|
||||
|
||||
return new string(buffer, 0, index);
|
||||
}
|
||||
|
||||
private IReadOnlyList<string> GetDifferingHashes(FunctionFingerprint a, FunctionFingerprint b)
|
||||
{
|
||||
var differing = new List<string>();
|
||||
|
||||
@@ -130,6 +130,8 @@ public sealed class ReproducibleBuildJob : IReproducibleBuildJob
|
||||
private readonly IPatchDiffEngine _diffEngine;
|
||||
private readonly IFingerprintClaimRepository _claimRepository;
|
||||
private readonly IAdvisoryFeedMonitor _advisoryMonitor;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ReproducibleBuildJob"/>.
|
||||
@@ -141,7 +143,9 @@ public sealed class ReproducibleBuildJob : IReproducibleBuildJob
|
||||
IFunctionFingerprintExtractor fingerprintExtractor,
|
||||
IPatchDiffEngine diffEngine,
|
||||
IFingerprintClaimRepository claimRepository,
|
||||
IAdvisoryFeedMonitor advisoryMonitor)
|
||||
IAdvisoryFeedMonitor advisoryMonitor,
|
||||
TimeProvider? timeProvider = null,
|
||||
IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
@@ -150,6 +154,8 @@ public sealed class ReproducibleBuildJob : IReproducibleBuildJob
|
||||
_diffEngine = diffEngine ?? throw new ArgumentNullException(nameof(diffEngine));
|
||||
_claimRepository = claimRepository ?? throw new ArgumentNullException(nameof(claimRepository));
|
||||
_advisoryMonitor = advisoryMonitor ?? throw new ArgumentNullException(nameof(advisoryMonitor));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_guidProvider = guidProvider ?? new GuidProvider();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -308,9 +314,17 @@ public sealed class ReproducibleBuildJob : IReproducibleBuildJob
|
||||
{
|
||||
var claims = new List<FingerprintClaim>();
|
||||
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
// Create "fixed" claims for patched binaries
|
||||
foreach (var binary in patchedBuild.Binaries ?? [])
|
||||
{
|
||||
if (!TryGetFingerprintId(binary.BuildId, out var fingerprintId))
|
||||
{
|
||||
_logger.LogWarning("Skipping patched claim for {CveId}: build id '{BuildId}' is not a GUID.", cve.CveId, binary.BuildId);
|
||||
continue;
|
||||
}
|
||||
|
||||
var changedFunctions = diff.Changes
|
||||
.Where(c => c.Type is ChangeType.Modified or ChangeType.Added)
|
||||
.Select(c => c.FunctionName)
|
||||
@@ -318,8 +332,8 @@ public sealed class ReproducibleBuildJob : IReproducibleBuildJob
|
||||
|
||||
var claim = new FingerprintClaim
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
FingerprintId = Guid.Parse(binary.BuildId), // Assuming BuildId is GUID-like
|
||||
Id = _guidProvider.NewGuid(),
|
||||
FingerprintId = fingerprintId,
|
||||
CveId = cve.CveId,
|
||||
Verdict = ClaimVerdict.Fixed,
|
||||
Evidence = new FingerprintClaimEvidence
|
||||
@@ -332,7 +346,7 @@ public sealed class ReproducibleBuildJob : IReproducibleBuildJob
|
||||
VulnerableBuildRef = vulnerableBuild.BuildLogRef,
|
||||
PatchedBuildRef = patchedBuild.BuildLogRef
|
||||
},
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
CreatedAt = now
|
||||
};
|
||||
|
||||
claims.Add(claim);
|
||||
@@ -341,10 +355,16 @@ public sealed class ReproducibleBuildJob : IReproducibleBuildJob
|
||||
// Create "vulnerable" claims for vulnerable binaries
|
||||
foreach (var binary in vulnerableBuild.Binaries ?? [])
|
||||
{
|
||||
if (!TryGetFingerprintId(binary.BuildId, out var fingerprintId))
|
||||
{
|
||||
_logger.LogWarning("Skipping vulnerable claim for {CveId}: build id '{BuildId}' is not a GUID.", cve.CveId, binary.BuildId);
|
||||
continue;
|
||||
}
|
||||
|
||||
var claim = new FingerprintClaim
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
FingerprintId = Guid.Parse(binary.BuildId),
|
||||
Id = _guidProvider.NewGuid(),
|
||||
FingerprintId = fingerprintId,
|
||||
CveId = cve.CveId,
|
||||
Verdict = ClaimVerdict.Vulnerable,
|
||||
Evidence = new FingerprintClaimEvidence
|
||||
@@ -356,16 +376,54 @@ public sealed class ReproducibleBuildJob : IReproducibleBuildJob
|
||||
.ToList(),
|
||||
VulnerableBuildRef = vulnerableBuild.BuildLogRef
|
||||
},
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
CreatedAt = now
|
||||
};
|
||||
|
||||
claims.Add(claim);
|
||||
}
|
||||
|
||||
if (claims.Count == 0)
|
||||
{
|
||||
_logger.LogWarning("No fingerprint claims created for CVE {CveId}; no valid build IDs were available.", cve.CveId);
|
||||
return;
|
||||
}
|
||||
|
||||
await _claimRepository.CreateClaimsBatchAsync(claims, ct);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Created {Count} fingerprint claims for CVE {CveId}",
|
||||
claims.Count, cve.CveId);
|
||||
}
|
||||
|
||||
private static bool TryGetFingerprintId(string buildId, out Guid fingerprintId)
|
||||
{
|
||||
if (Guid.TryParse(buildId, out fingerprintId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (buildId.Length == 32 && IsHex(buildId))
|
||||
{
|
||||
return Guid.TryParseExact(buildId, "N", out fingerprintId);
|
||||
}
|
||||
|
||||
fingerprintId = Guid.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsHex(string value)
|
||||
{
|
||||
foreach (var ch in value)
|
||||
{
|
||||
var isHex = ch is >= '0' and <= '9'
|
||||
or >= 'a' and <= 'f'
|
||||
or >= 'A' and <= 'F';
|
||||
if (!isHex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,16 @@ public static class ServiceCollectionExtensions
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
|
||||
// Configuration - register options with defaults (configuration binding happens via host)
|
||||
services.Configure<BuilderServiceOptions>(options => { });
|
||||
services.Configure<FunctionExtractionOptions>(options => { });
|
||||
// Configuration - bind options from configuration
|
||||
services.AddOptions<BuilderServiceOptions>()
|
||||
.Bind(configuration.GetSection(BuilderServiceOptions.SectionName));
|
||||
services.AddOptions<FunctionExtractionOptions>()
|
||||
.Bind(configuration.GetSection(FunctionExtractionOptions.SectionName));
|
||||
|
||||
// Core services
|
||||
services.TryAddSingleton<IPatchDiffEngine, PatchDiffEngine>();
|
||||
services.TryAddSingleton<IGuidProvider, GuidProvider>();
|
||||
services.TryAddSingleton(TimeProvider.System);
|
||||
|
||||
// Builders will be added as they are implemented
|
||||
// services.TryAddSingleton<IReproducibleBuilder, AlpineBuilder>();
|
||||
@@ -56,6 +60,8 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
services.Configure(configureOptions);
|
||||
services.TryAddSingleton<IPatchDiffEngine, PatchDiffEngine>();
|
||||
services.TryAddSingleton<IGuidProvider, GuidProvider>();
|
||||
services.TryAddSingleton(TimeProvider.System);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Description>Reproducible distro builders and function-level fingerprinting for StellaOps BinaryIndex.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -7,4 +7,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0112-M | DONE | Maintainability audit for StellaOps.BinaryIndex.Builders. |
|
||||
| AUDIT-0112-T | DONE | Test coverage audit for StellaOps.BinaryIndex.Builders. |
|
||||
| AUDIT-0112-A | TODO | Pending approval for changes. |
|
||||
| AUDIT-0112-A | DONE | Applied audit fixes + tests. |
|
||||
|
||||
Reference in New Issue
Block a user