save progress
This commit is contained in:
@@ -125,4 +125,21 @@ internal sealed class NullBinaryVulnerabilityService : IBinaryVulnerabilityServi
|
||||
{
|
||||
return Task.FromResult(System.Collections.Immutable.ImmutableDictionary<string, System.Collections.Immutable.ImmutableArray<BinaryVulnMatch>>.Empty);
|
||||
}
|
||||
|
||||
public Task<System.Collections.Immutable.ImmutableArray<BinaryVulnMatch>> LookupByDeltaSignatureAsync(
|
||||
Stream binaryStream,
|
||||
DeltaSigLookupOptions? options = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return Task.FromResult(System.Collections.Immutable.ImmutableArray<BinaryVulnMatch>.Empty);
|
||||
}
|
||||
|
||||
public Task<System.Collections.Immutable.ImmutableArray<BinaryVulnMatch>> LookupBySymbolHashAsync(
|
||||
string symbolHash,
|
||||
string symbolName,
|
||||
DeltaSigLookupOptions? options = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return Task.FromResult(System.Collections.Immutable.ImmutableArray<BinaryVulnMatch>.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_20260102_001_BE - Binary Delta Signatures
|
||||
// Task: DS-040 - Scanner integration (match service)
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.BinaryIndex.Core.Services;
|
||||
using StellaOps.BinaryIndex.DeltaSig;
|
||||
using StellaOps.BinaryIndex.Disassembly;
|
||||
using StellaOps.BinaryIndex.Normalization;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Processing;
|
||||
|
||||
/// <summary>
|
||||
/// Analyzer that performs binary-level delta signature matching to detect
|
||||
/// backported security patches that version-based scanning would miss.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This analyzer extracts function hashes from binaries and compares them
|
||||
/// against known vulnerable/patched signatures. When a "patched" signature
|
||||
/// matches, it indicates the security fix is present even if the version
|
||||
/// string suggests vulnerability.
|
||||
/// </remarks>
|
||||
public sealed class DeltaSigAnalyzer
|
||||
{
|
||||
private readonly IBinaryVulnerabilityService _binaryVulnService;
|
||||
private readonly IDisassemblyService _disassemblyService;
|
||||
private readonly INormalizationPipeline _normalizationPipeline;
|
||||
private readonly ILogger<DeltaSigAnalyzer> _logger;
|
||||
|
||||
public DeltaSigAnalyzer(
|
||||
IBinaryVulnerabilityService binaryVulnService,
|
||||
IDisassemblyService disassemblyService,
|
||||
INormalizationPipeline normalizationPipeline,
|
||||
ILogger<DeltaSigAnalyzer> logger)
|
||||
{
|
||||
_binaryVulnService = binaryVulnService ?? throw new ArgumentNullException(nameof(binaryVulnService));
|
||||
_disassemblyService = disassemblyService ?? throw new ArgumentNullException(nameof(disassemblyService));
|
||||
_normalizationPipeline = normalizationPipeline ?? throw new ArgumentNullException(nameof(normalizationPipeline));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unique identifier for this analyzer.
|
||||
/// </summary>
|
||||
public string AnalyzerId => "delta-signature";
|
||||
|
||||
/// <summary>
|
||||
/// Analyzer priority. Higher values run later. Run after package analyzers
|
||||
/// but before final verdict computation.
|
||||
/// </summary>
|
||||
public int Priority => 150;
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes a binary for delta signature matches.
|
||||
/// </summary>
|
||||
/// <param name="context">Analysis context with binary data and scan metadata.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Analysis result with vulnerability findings.</returns>
|
||||
public async Task<DeltaSigAnalysisResult> AnalyzeAsync(
|
||||
DeltaSigContext context,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
var findings = new List<DeltaSigFinding>();
|
||||
var processedSymbols = new List<string>();
|
||||
var errors = new List<string>();
|
||||
|
||||
_logger.LogDebug(
|
||||
"Starting delta signature analysis for {FilePath} in scan {ScanId}",
|
||||
context.FilePath,
|
||||
context.ScanId);
|
||||
|
||||
try
|
||||
{
|
||||
// Load and analyze the binary
|
||||
var (binaryInfo, plugin) = _disassemblyService.LoadBinary(context.BinaryStream);
|
||||
if (binaryInfo is null)
|
||||
{
|
||||
_logger.LogDebug("Could not load binary from {FilePath}", context.FilePath);
|
||||
return DeltaSigAnalysisResult.Empty(context.ScanId, context.FilePath);
|
||||
}
|
||||
|
||||
// Get symbols to analyze
|
||||
var symbols = plugin.GetSymbols(binaryInfo).ToList();
|
||||
if (symbols.Count == 0)
|
||||
{
|
||||
_logger.LogDebug("No symbols found in {FilePath}", context.FilePath);
|
||||
return DeltaSigAnalysisResult.Empty(context.ScanId, context.FilePath);
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"Found {SymbolCount} symbols in {FilePath}, analyzing {TargetCount} targets",
|
||||
symbols.Count,
|
||||
context.FilePath,
|
||||
context.TargetSymbols?.Count ?? symbols.Count);
|
||||
|
||||
// Filter to target symbols if specified
|
||||
var targetSymbols = context.TargetSymbols is { Count: > 0 }
|
||||
? symbols.Where(s => context.TargetSymbols.Contains(s.Name)).ToList()
|
||||
: symbols;
|
||||
|
||||
// Analyze each symbol
|
||||
foreach (var symbol in targetSymbols)
|
||||
{
|
||||
try
|
||||
{
|
||||
var finding = await AnalyzeSymbolAsync(
|
||||
binaryInfo,
|
||||
plugin,
|
||||
symbol,
|
||||
context,
|
||||
ct).ConfigureAwait(false);
|
||||
|
||||
if (finding is not null)
|
||||
{
|
||||
findings.Add(finding);
|
||||
}
|
||||
|
||||
processedSymbols.Add(symbol.Name);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Error analyzing symbol {Symbol} in {FilePath}", symbol.Name, context.FilePath);
|
||||
errors.Add($"{symbol.Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Delta signature analysis complete for {FilePath}: {FindingCount} findings, {SymbolCount} symbols processed",
|
||||
context.FilePath,
|
||||
findings.Count,
|
||||
processedSymbols.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during delta signature analysis for {FilePath}", context.FilePath);
|
||||
errors.Add($"Analysis error: {ex.Message}");
|
||||
}
|
||||
|
||||
return new DeltaSigAnalysisResult
|
||||
{
|
||||
ScanId = context.ScanId,
|
||||
FilePath = context.FilePath,
|
||||
AnalyzerId = AnalyzerId,
|
||||
Findings = findings.ToImmutableArray(),
|
||||
ProcessedSymbols = processedSymbols.ToImmutableArray(),
|
||||
Errors = errors.ToImmutableArray()
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<DeltaSigFinding?> AnalyzeSymbolAsync(
|
||||
BinaryInfo binaryInfo,
|
||||
IDisassemblyPlugin plugin,
|
||||
SymbolInfo symbol,
|
||||
DeltaSigContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Disassemble the symbol
|
||||
var instructions = plugin.DisassembleSymbol(binaryInfo, symbol).ToList();
|
||||
if (instructions.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Normalize and hash
|
||||
var normalizedFunction = _normalizationPipeline.Normalize(instructions, binaryInfo.Architecture);
|
||||
|
||||
// Extract all normalized bytes
|
||||
var normalizedBytes = normalizedFunction.Instructions
|
||||
.SelectMany(i => i.NormalizedBytes)
|
||||
.ToArray();
|
||||
|
||||
if (normalizedBytes.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var hash = ComputeSymbolHash(normalizedBytes);
|
||||
|
||||
// Look up against signature database
|
||||
var lookupOptions = new DeltaSigLookupOptions
|
||||
{
|
||||
Architecture = MapArchitectureToString(binaryInfo.Architecture),
|
||||
CveFilter = context.CveFilter,
|
||||
IncludePatched = true,
|
||||
IncludeVulnerable = true,
|
||||
MinConfidence = context.MinConfidence
|
||||
};
|
||||
|
||||
var matches = await _binaryVulnService.LookupBySymbolHashAsync(
|
||||
hash,
|
||||
symbol.Name,
|
||||
lookupOptions,
|
||||
ct).ConfigureAwait(false);
|
||||
|
||||
if (matches.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Aggregate matches into a finding
|
||||
var vulnerableMatches = matches.Where(m => m.Evidence?.SignatureState == "vulnerable").ToList();
|
||||
var patchedMatches = matches.Where(m => m.Evidence?.SignatureState == "patched").ToList();
|
||||
|
||||
// Determine the state: if we have a patched match, the fix is present
|
||||
var isPatched = patchedMatches.Count > 0;
|
||||
var cves = matches.Select(m => m.CveId).Distinct().ToImmutableArray();
|
||||
|
||||
return new DeltaSigFinding
|
||||
{
|
||||
ScanId = context.ScanId,
|
||||
FilePath = context.FilePath,
|
||||
SymbolName = symbol.Name,
|
||||
SymbolHash = hash,
|
||||
CveIds = cves,
|
||||
IsPatched = isPatched,
|
||||
HasVulnerableMatch = vulnerableMatches.Count > 0,
|
||||
HasPatchedMatch = patchedMatches.Count > 0,
|
||||
Confidence = matches.Max(m => m.Confidence),
|
||||
MatchDetails = matches.Select(m => new DeltaSigMatchDetail
|
||||
{
|
||||
CveId = m.CveId,
|
||||
SignatureState = m.Evidence?.SignatureState ?? "unknown",
|
||||
Confidence = m.Confidence
|
||||
}).ToImmutableArray()
|
||||
};
|
||||
}
|
||||
|
||||
private static string ComputeSymbolHash(byte[] normalizedBytes)
|
||||
{
|
||||
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
||||
var hashBytes = sha256.ComputeHash(normalizedBytes);
|
||||
return Convert.ToHexString(hashBytes).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string MapArchitectureToString(CpuArchitecture arch) => arch switch
|
||||
{
|
||||
CpuArchitecture.X86_64 => "x86_64",
|
||||
CpuArchitecture.ARM64 => "aarch64",
|
||||
CpuArchitecture.X86 => "x86",
|
||||
CpuArchitecture.ARM32 => "arm",
|
||||
_ => arch.ToString().ToLowerInvariant()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Context for delta signature analysis.
|
||||
/// </summary>
|
||||
public sealed class DeltaSigContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique scan identifier.
|
||||
/// </summary>
|
||||
public required Guid ScanId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to the binary being analyzed.
|
||||
/// </summary>
|
||||
public required string FilePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Stream containing the binary data.
|
||||
/// </summary>
|
||||
public required Stream BinaryStream { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Specific symbols to analyze. If null/empty, analyzes all symbols.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? TargetSymbols { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Filter to specific CVEs. If null/empty, checks all signatures.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? CveFilter { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Minimum confidence threshold for matches. Default 1.0 (exact match).
|
||||
/// </summary>
|
||||
public decimal MinConfidence { get; init; } = 1.0m;
|
||||
|
||||
/// <summary>
|
||||
/// Detected distribution (e.g., debian, ubuntu, rhel).
|
||||
/// </summary>
|
||||
public string? DetectedDistro { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Detected release (e.g., bookworm, jammy).
|
||||
/// </summary>
|
||||
public string? DetectedRelease { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of delta signature analysis.
|
||||
/// </summary>
|
||||
public sealed record DeltaSigAnalysisResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Scan identifier.
|
||||
/// </summary>
|
||||
public required Guid ScanId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to the analyzed binary.
|
||||
/// </summary>
|
||||
public required string FilePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Analyzer identifier.
|
||||
/// </summary>
|
||||
public required string AnalyzerId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Delta signature findings.
|
||||
/// </summary>
|
||||
public required ImmutableArray<DeltaSigFinding> Findings { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Names of symbols that were processed.
|
||||
/// </summary>
|
||||
public ImmutableArray<string> ProcessedSymbols { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Errors encountered during analysis.
|
||||
/// </summary>
|
||||
public ImmutableArray<string> Errors { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Creates an empty result.
|
||||
/// </summary>
|
||||
public static DeltaSigAnalysisResult Empty(Guid scanId, string filePath) => new()
|
||||
{
|
||||
ScanId = scanId,
|
||||
FilePath = filePath,
|
||||
AnalyzerId = "delta-signature",
|
||||
Findings = [],
|
||||
ProcessedSymbols = [],
|
||||
Errors = []
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A delta signature finding for a symbol.
|
||||
/// </summary>
|
||||
public sealed record DeltaSigFinding
|
||||
{
|
||||
/// <summary>
|
||||
/// Scan identifier.
|
||||
/// </summary>
|
||||
public Guid ScanId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to the binary containing the symbol.
|
||||
/// </summary>
|
||||
public required string FilePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the matched symbol/function.
|
||||
/// </summary>
|
||||
public required string SymbolName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// SHA-256 hash of the normalized symbol.
|
||||
/// </summary>
|
||||
public required string SymbolHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CVE IDs associated with this symbol.
|
||||
/// </summary>
|
||||
public required ImmutableArray<string> CveIds { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether a patched signature was matched, indicating the fix is present.
|
||||
/// </summary>
|
||||
public required bool IsPatched { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether a vulnerable signature was matched.
|
||||
/// </summary>
|
||||
public required bool HasVulnerableMatch { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether a patched signature was matched.
|
||||
/// </summary>
|
||||
public required bool HasPatchedMatch { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Confidence score of the best match.
|
||||
/// </summary>
|
||||
public required decimal Confidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Detailed match information.
|
||||
/// </summary>
|
||||
public required ImmutableArray<DeltaSigMatchDetail> MatchDetails { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the finding type for categorization.
|
||||
/// </summary>
|
||||
public string FindingType => "delta-signature";
|
||||
|
||||
/// <summary>
|
||||
/// Gets a summary of the finding.
|
||||
/// </summary>
|
||||
public string GetSummary()
|
||||
{
|
||||
var cveList = string.Join(", ", CveIds.Take(3));
|
||||
if (CveIds.Length > 3)
|
||||
cveList += $" (+{CveIds.Length - 3} more)";
|
||||
|
||||
var status = IsPatched ? "PATCHED" : "VULNERABLE";
|
||||
return $"{status}: {cveList} in {SymbolName} (confidence {Confidence:P0})";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detailed match information for a delta signature match.
|
||||
/// </summary>
|
||||
public sealed record DeltaSigMatchDetail
|
||||
{
|
||||
/// <summary>
|
||||
/// CVE identifier.
|
||||
/// </summary>
|
||||
public required string CveId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Signature state: "vulnerable" or "patched".
|
||||
/// </summary>
|
||||
public required string SignatureState { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Match confidence.
|
||||
/// </summary>
|
||||
public required decimal Confidence { get; init; }
|
||||
}
|
||||
@@ -34,6 +34,10 @@
|
||||
<ProjectReference Include="../../Unknowns/__Libraries/StellaOps.Unknowns.Core/StellaOps.Unknowns.Core.csproj" />
|
||||
<ProjectReference Include="../../BinaryIndex/__Libraries/StellaOps.BinaryIndex.Core/StellaOps.BinaryIndex.Core.csproj" />
|
||||
<ProjectReference Include="../../BinaryIndex/__Libraries/StellaOps.BinaryIndex.Persistence/StellaOps.BinaryIndex.Persistence.csproj" />
|
||||
<ProjectReference Include="../../BinaryIndex/__Libraries/StellaOps.BinaryIndex.DeltaSig/StellaOps.BinaryIndex.DeltaSig.csproj" />
|
||||
<ProjectReference Include="../../BinaryIndex/__Libraries/StellaOps.BinaryIndex.Disassembly/StellaOps.BinaryIndex.Disassembly.csproj" />
|
||||
<ProjectReference Include="../../BinaryIndex/__Libraries/StellaOps.BinaryIndex.Disassembly.Abstractions/StellaOps.BinaryIndex.Disassembly.Abstractions.csproj" />
|
||||
<ProjectReference Include="../../BinaryIndex/__Libraries/StellaOps.BinaryIndex.Normalization/StellaOps.BinaryIndex.Normalization.csproj" />
|
||||
<ProjectReference Include="../../Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/StellaOps.Attestor.Core.csproj" />
|
||||
<ProjectReference Include="../../Signals/StellaOps.Signals/StellaOps.Signals.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user