save progress

This commit is contained in:
StellaOps Bot
2026-01-03 00:47:24 +02:00
parent 3f197814c5
commit ca578801fd
319 changed files with 32478 additions and 2202 deletions

View File

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

View File

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

View File

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