save progress

This commit is contained in:
StellaOps Bot
2026-01-03 11:02:24 +02:00
parent ca578801fd
commit 83c37243e0
446 changed files with 22798 additions and 4031 deletions

View File

@@ -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; }

View File

@@ -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();
}

View File

@@ -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>();

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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. |