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>
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_20260102_001_BE
|
||||
// Task: DS-041 - VEX evidence emission for backport detection
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using StellaOps.Scanner.Evidence.Models;
|
||||
|
||||
namespace StellaOps.Scanner.Evidence;
|
||||
|
||||
/// <summary>
|
||||
/// Emits VEX candidates based on delta signature analysis results.
|
||||
/// When a binary is confirmed to have patched code via signature matching,
|
||||
/// this emitter generates a not_affected VEX candidate with full evidence trail.
|
||||
/// </summary>
|
||||
public sealed class DeltaSigVexEmitter
|
||||
{
|
||||
private readonly DeltaSigVexEmitterOptions _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeltaSigVexEmitter"/> class.
|
||||
/// </summary>
|
||||
public DeltaSigVexEmitter(
|
||||
DeltaSigVexEmitterOptions? options = null,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_options = options ?? DeltaSigVexEmitterOptions.Default;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates delta signature evidence and emits VEX candidates for patched binaries.
|
||||
/// </summary>
|
||||
public DeltaSigVexEmissionResult EmitCandidates(DeltaSigVexEmissionContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
var candidates = new List<DeltaSigVexCandidate>();
|
||||
|
||||
foreach (var evidence in context.EvidenceItems)
|
||||
{
|
||||
// Only emit candidates for confirmed patched binaries
|
||||
if (evidence.Result != DeltaSigResult.Patched)
|
||||
continue;
|
||||
|
||||
// Must meet minimum confidence threshold
|
||||
if (evidence.Confidence < _options.MinConfidence)
|
||||
continue;
|
||||
|
||||
var candidate = CreateVexCandidate(evidence, context);
|
||||
candidates.Add(candidate);
|
||||
|
||||
if (candidates.Count >= _options.MaxCandidatesPerBatch)
|
||||
break;
|
||||
}
|
||||
|
||||
return new DeltaSigVexEmissionResult(
|
||||
ImageDigest: context.ImageDigest,
|
||||
CandidatesEmitted: candidates.Count,
|
||||
Candidates: [.. candidates],
|
||||
GeneratedAt: _timeProvider.GetUtcNow());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a VEX candidate from delta signature evidence.
|
||||
/// </summary>
|
||||
private DeltaSigVexCandidate CreateVexCandidate(
|
||||
DeltaSignatureEvidence evidence,
|
||||
DeltaSigVexEmissionContext context)
|
||||
{
|
||||
var candidateId = GenerateCandidateId(evidence, context);
|
||||
|
||||
// Build evidence links
|
||||
var evidenceLinks = new List<DeltaSigEvidenceLink>
|
||||
{
|
||||
new(
|
||||
Type: "delta_signature_analysis",
|
||||
Uri: $"deltasig://{context.ImageDigest}/{evidence.BinaryId}",
|
||||
Description: evidence.Summary)
|
||||
};
|
||||
|
||||
// Add symbol-level evidence links
|
||||
foreach (var match in evidence.SymbolMatches)
|
||||
{
|
||||
if (match.State == SignatureState.Patched)
|
||||
{
|
||||
var hashPreview = match.HashHex.Length > 16
|
||||
? $"{match.HashHex[..16]}..."
|
||||
: match.HashHex;
|
||||
|
||||
evidenceLinks.Add(new DeltaSigEvidenceLink(
|
||||
Type: "patched_symbol",
|
||||
Uri: $"symbol://{match.SymbolName}",
|
||||
Description: match.ExactMatch
|
||||
? $"Exact hash match: {hashPreview}"
|
||||
: $"Partial match: {match.ChunksMatched}/{match.ChunksTotal} chunks"));
|
||||
}
|
||||
}
|
||||
|
||||
// Add attestation link if available
|
||||
if (evidence.AttestationUri is not null)
|
||||
{
|
||||
evidenceLinks.Add(new DeltaSigEvidenceLink(
|
||||
Type: "dsse_attestation",
|
||||
Uri: evidence.AttestationUri,
|
||||
Description: "Signed attestation envelope"));
|
||||
}
|
||||
|
||||
return new DeltaSigVexCandidate(
|
||||
CandidateId: candidateId,
|
||||
CveIds: evidence.CveIds,
|
||||
PackagePurl: evidence.PackagePurl,
|
||||
BinaryId: evidence.BinaryId,
|
||||
SuggestedStatus: DeltaSigVexStatus.NotAffected,
|
||||
Justification: DeltaSigVexJustification.VulnerableCodeNotPresent,
|
||||
Rationale: GenerateRationale(evidence),
|
||||
Confidence: evidence.Confidence,
|
||||
EvidenceLinks: [.. evidenceLinks],
|
||||
DeltaSignatureEvidence: evidence,
|
||||
ImageDigest: context.ImageDigest,
|
||||
GeneratedAt: _timeProvider.GetUtcNow(),
|
||||
ExpiresAt: _timeProvider.GetUtcNow().Add(_options.CandidateTtl),
|
||||
RequiresReview: evidence.Confidence < _options.AutoApprovalThreshold);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a human-readable rationale for the VEX determination.
|
||||
/// </summary>
|
||||
private static string GenerateRationale(DeltaSignatureEvidence evidence)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("Binary delta signature analysis confirms the security fix is applied. ");
|
||||
|
||||
var patchedCount = evidence.SymbolMatches.Count(m => m.State == SignatureState.Patched);
|
||||
var exactCount = evidence.SymbolMatches.Count(m => m.State == SignatureState.Patched && m.ExactMatch);
|
||||
|
||||
if (exactCount > 0)
|
||||
{
|
||||
sb.Append($"{exactCount} symbol(s) matched patched signatures exactly. ");
|
||||
}
|
||||
|
||||
if (patchedCount > exactCount)
|
||||
{
|
||||
sb.Append($"{patchedCount - exactCount} symbol(s) matched via chunk analysis. ");
|
||||
}
|
||||
|
||||
sb.Append($"Confidence: {evidence.Confidence:P0}. ");
|
||||
sb.Append($"Recipe: {evidence.NormalizationRecipe.RecipeId} v{evidence.NormalizationRecipe.Version}.");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a deterministic candidate ID.
|
||||
/// </summary>
|
||||
private string GenerateCandidateId(
|
||||
DeltaSignatureEvidence evidence,
|
||||
DeltaSigVexEmissionContext context)
|
||||
{
|
||||
var input = $"{context.ImageDigest}:{evidence.BinaryId}:{string.Join(",", evidence.CveIds)}:{evidence.GeneratedAt.Ticks}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return $"vexds-{Convert.ToHexString(hash).ToLowerInvariant()[..16]}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for delta signature VEX emission.
|
||||
/// </summary>
|
||||
public sealed record DeltaSigVexEmitterOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Minimum confidence required to emit a VEX candidate.
|
||||
/// </summary>
|
||||
public decimal MinConfidence { get; init; } = 0.75m;
|
||||
|
||||
/// <summary>
|
||||
/// Confidence threshold for auto-approval (no human review required).
|
||||
/// </summary>
|
||||
public decimal AutoApprovalThreshold { get; init; } = 0.95m;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum candidates per emission batch.
|
||||
/// </summary>
|
||||
public int MaxCandidatesPerBatch { get; init; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Time-to-live for candidates before they expire.
|
||||
/// </summary>
|
||||
public TimeSpan CandidateTtl { get; init; } = TimeSpan.FromDays(30);
|
||||
|
||||
/// <summary>
|
||||
/// Default options.
|
||||
/// </summary>
|
||||
public static DeltaSigVexEmitterOptions Default { get; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Context for delta signature VEX emission.
|
||||
/// </summary>
|
||||
public sealed record DeltaSigVexEmissionContext(
|
||||
string ImageDigest,
|
||||
IReadOnlyList<DeltaSignatureEvidence> EvidenceItems);
|
||||
|
||||
/// <summary>
|
||||
/// Result of delta signature VEX emission.
|
||||
/// </summary>
|
||||
public sealed record DeltaSigVexEmissionResult(
|
||||
string ImageDigest,
|
||||
int CandidatesEmitted,
|
||||
ImmutableArray<DeltaSigVexCandidate> Candidates,
|
||||
DateTimeOffset GeneratedAt);
|
||||
|
||||
/// <summary>
|
||||
/// A VEX candidate generated from delta signature analysis.
|
||||
/// </summary>
|
||||
public sealed record DeltaSigVexCandidate(
|
||||
string CandidateId,
|
||||
ImmutableArray<string> CveIds,
|
||||
string PackagePurl,
|
||||
string BinaryId,
|
||||
DeltaSigVexStatus SuggestedStatus,
|
||||
DeltaSigVexJustification Justification,
|
||||
string Rationale,
|
||||
decimal Confidence,
|
||||
ImmutableArray<DeltaSigEvidenceLink> EvidenceLinks,
|
||||
DeltaSignatureEvidence DeltaSignatureEvidence,
|
||||
string ImageDigest,
|
||||
DateTimeOffset GeneratedAt,
|
||||
DateTimeOffset ExpiresAt,
|
||||
bool RequiresReview);
|
||||
|
||||
/// <summary>
|
||||
/// VEX status for delta signature candidates.
|
||||
/// </summary>
|
||||
public enum DeltaSigVexStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Not affected - patched code is present.
|
||||
/// </summary>
|
||||
NotAffected,
|
||||
|
||||
/// <summary>
|
||||
/// Affected - vulnerable code is present.
|
||||
/// </summary>
|
||||
Affected,
|
||||
|
||||
/// <summary>
|
||||
/// Under investigation - analysis was inconclusive.
|
||||
/// </summary>
|
||||
UnderInvestigation
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VEX justification for delta signature candidates.
|
||||
/// </summary>
|
||||
public enum DeltaSigVexJustification
|
||||
{
|
||||
/// <summary>
|
||||
/// Vulnerable code is not present (patched binary).
|
||||
/// </summary>
|
||||
VulnerableCodeNotPresent,
|
||||
|
||||
/// <summary>
|
||||
/// Component was rebuilt with fix.
|
||||
/// </summary>
|
||||
ComponentRebuiltWithFix
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evidence link for delta signature VEX candidates.
|
||||
/// </summary>
|
||||
public sealed record DeltaSigEvidenceLink(
|
||||
string Type,
|
||||
string Uri,
|
||||
string? Description = null);
|
||||
@@ -0,0 +1,303 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_20260102_001_BE
|
||||
// Task: DS-041 - VEX evidence emission for backport detection
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.Evidence.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Evidence from binary delta signature analysis.
|
||||
/// Provides cryptographic proof of backport status by comparing
|
||||
/// normalized binary code against known patched/vulnerable signatures.
|
||||
/// </summary>
|
||||
public sealed record DeltaSignatureEvidence
|
||||
{
|
||||
/// <summary>
|
||||
/// Schema version for this evidence type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("schema")]
|
||||
public string Schema { get; init; } = "stellaops.evidence.deltasig.v1";
|
||||
|
||||
/// <summary>
|
||||
/// Overall result: patched, vulnerable, inconclusive.
|
||||
/// </summary>
|
||||
[JsonPropertyName("result")]
|
||||
public required DeltaSigResult Result { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CVE identifier(s) that were evaluated.
|
||||
/// </summary>
|
||||
[JsonPropertyName("cveIds")]
|
||||
public required ImmutableArray<string> CveIds { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Package reference (PURL) being analyzed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("packagePurl")]
|
||||
public required string PackagePurl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Binary identifier (path or Build-ID).
|
||||
/// </summary>
|
||||
[JsonPropertyName("binaryId")]
|
||||
public required string BinaryId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Target architecture (x86_64, aarch64, etc.).
|
||||
/// </summary>
|
||||
[JsonPropertyName("architecture")]
|
||||
public required string Architecture { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Symbol-level match results.
|
||||
/// </summary>
|
||||
[JsonPropertyName("symbolMatches")]
|
||||
public required ImmutableArray<SymbolMatchEvidence> SymbolMatches { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Confidence score [0, 1] of the overall determination.
|
||||
/// </summary>
|
||||
[JsonPropertyName("confidence")]
|
||||
public required decimal Confidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalization recipe used for comparison.
|
||||
/// </summary>
|
||||
[JsonPropertyName("normalizationRecipe")]
|
||||
public required NormalizationRecipeRef NormalizationRecipe { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When this evidence was generated.
|
||||
/// </summary>
|
||||
[JsonPropertyName("generatedAt")]
|
||||
public required DateTimeOffset GeneratedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable summary of the determination.
|
||||
/// </summary>
|
||||
[JsonPropertyName("summary")]
|
||||
public required string Summary { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional DSSE envelope URI containing the signed attestation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("attestationUri")]
|
||||
public string? AttestationUri { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates evidence for a patched binary.
|
||||
/// </summary>
|
||||
public static DeltaSignatureEvidence CreatePatched(
|
||||
ImmutableArray<string> cveIds,
|
||||
string packagePurl,
|
||||
string binaryId,
|
||||
string architecture,
|
||||
ImmutableArray<SymbolMatchEvidence> symbolMatches,
|
||||
decimal confidence,
|
||||
NormalizationRecipeRef recipe,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
var matchSummary = symbolMatches.IsDefaultOrEmpty
|
||||
? "No symbols analyzed"
|
||||
: $"{symbolMatches.Count(m => m.State == SignatureState.Patched)} patched, " +
|
||||
$"{symbolMatches.Count(m => m.State == SignatureState.Vulnerable)} vulnerable";
|
||||
|
||||
return new DeltaSignatureEvidence
|
||||
{
|
||||
Result = DeltaSigResult.Patched,
|
||||
CveIds = cveIds,
|
||||
PackagePurl = packagePurl,
|
||||
BinaryId = binaryId,
|
||||
Architecture = architecture,
|
||||
SymbolMatches = symbolMatches,
|
||||
Confidence = confidence,
|
||||
NormalizationRecipe = recipe,
|
||||
GeneratedAt = (timeProvider ?? TimeProvider.System).GetUtcNow(),
|
||||
Summary = $"Binary confirmed PATCHED with {confidence:P0} confidence. {matchSummary}."
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates evidence for a vulnerable binary.
|
||||
/// </summary>
|
||||
public static DeltaSignatureEvidence CreateVulnerable(
|
||||
ImmutableArray<string> cveIds,
|
||||
string packagePurl,
|
||||
string binaryId,
|
||||
string architecture,
|
||||
ImmutableArray<SymbolMatchEvidence> symbolMatches,
|
||||
decimal confidence,
|
||||
NormalizationRecipeRef recipe,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
var matchSummary = symbolMatches.IsDefaultOrEmpty
|
||||
? "No symbols analyzed"
|
||||
: $"{symbolMatches.Count(m => m.State == SignatureState.Vulnerable)} vulnerable symbols matched";
|
||||
|
||||
return new DeltaSignatureEvidence
|
||||
{
|
||||
Result = DeltaSigResult.Vulnerable,
|
||||
CveIds = cveIds,
|
||||
PackagePurl = packagePurl,
|
||||
BinaryId = binaryId,
|
||||
Architecture = architecture,
|
||||
SymbolMatches = symbolMatches,
|
||||
Confidence = confidence,
|
||||
NormalizationRecipe = recipe,
|
||||
GeneratedAt = (timeProvider ?? TimeProvider.System).GetUtcNow(),
|
||||
Summary = $"Binary confirmed VULNERABLE with {confidence:P0} confidence. {matchSummary}."
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates evidence for an inconclusive analysis.
|
||||
/// </summary>
|
||||
public static DeltaSignatureEvidence CreateInconclusive(
|
||||
ImmutableArray<string> cveIds,
|
||||
string packagePurl,
|
||||
string binaryId,
|
||||
string architecture,
|
||||
string reason,
|
||||
ImmutableArray<SymbolMatchEvidence> symbolMatches,
|
||||
NormalizationRecipeRef recipe,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
return new DeltaSignatureEvidence
|
||||
{
|
||||
Result = DeltaSigResult.Inconclusive,
|
||||
CveIds = cveIds,
|
||||
PackagePurl = packagePurl,
|
||||
BinaryId = binaryId,
|
||||
Architecture = architecture,
|
||||
SymbolMatches = symbolMatches,
|
||||
Confidence = 0m,
|
||||
NormalizationRecipe = recipe,
|
||||
GeneratedAt = (timeProvider ?? TimeProvider.System).GetUtcNow(),
|
||||
Summary = $"Analysis INCONCLUSIVE: {reason}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of delta signature analysis.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<DeltaSigResult>))]
|
||||
public enum DeltaSigResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Binary contains patched code - security fix is applied.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("patched")]
|
||||
Patched,
|
||||
|
||||
/// <summary>
|
||||
/// Binary contains vulnerable code - security fix is NOT applied.
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("vulnerable")]
|
||||
Vulnerable,
|
||||
|
||||
/// <summary>
|
||||
/// Unable to determine patch status (symbols not found, LTO obfuscation, etc.).
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("inconclusive")]
|
||||
Inconclusive
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evidence for a single symbol match.
|
||||
/// </summary>
|
||||
public sealed record SymbolMatchEvidence
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol/function name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("symbolName")]
|
||||
public required string SymbolName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalized hash of the symbol (SHA-256).
|
||||
/// </summary>
|
||||
[JsonPropertyName("hashHex")]
|
||||
public required string HashHex { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Matched signature state (patched or vulnerable).
|
||||
/// </summary>
|
||||
[JsonPropertyName("state")]
|
||||
public required SignatureState State { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Match confidence [0, 1].
|
||||
/// </summary>
|
||||
[JsonPropertyName("confidence")]
|
||||
public required decimal Confidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this was an exact full-hash match.
|
||||
/// </summary>
|
||||
[JsonPropertyName("exactMatch")]
|
||||
public required bool ExactMatch { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of chunks matched (for partial matching).
|
||||
/// </summary>
|
||||
[JsonPropertyName("chunksMatched")]
|
||||
public int? ChunksMatched { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total chunks in signature.
|
||||
/// </summary>
|
||||
[JsonPropertyName("chunksTotal")]
|
||||
public int? ChunksTotal { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reference to the matched signature (PURL of the signature source package).
|
||||
/// </summary>
|
||||
[JsonPropertyName("signatureSourcePurl")]
|
||||
public string? SignatureSourcePurl { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// State of a signature: vulnerable or patched.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<SignatureState>))]
|
||||
public enum SignatureState
|
||||
{
|
||||
/// <summary>
|
||||
/// Signature represents vulnerable code (before fix).
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("vulnerable")]
|
||||
Vulnerable,
|
||||
|
||||
/// <summary>
|
||||
/// Signature represents patched code (after fix).
|
||||
/// </summary>
|
||||
[JsonStringEnumMemberName("patched")]
|
||||
Patched
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference to the normalization recipe used.
|
||||
/// </summary>
|
||||
public sealed record NormalizationRecipeRef
|
||||
{
|
||||
/// <summary>
|
||||
/// Recipe identifier (e.g., "stellaops.normalize.x64.v1").
|
||||
/// </summary>
|
||||
[JsonPropertyName("recipeId")]
|
||||
public required string RecipeId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Recipe version.
|
||||
/// </summary>
|
||||
[JsonPropertyName("version")]
|
||||
public required string Version { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalization steps applied.
|
||||
/// </summary>
|
||||
[JsonPropertyName("steps")]
|
||||
public ImmutableArray<string> Steps { get; init; } = [];
|
||||
}
|
||||
@@ -39,6 +39,12 @@ public sealed record EvidenceBundle
|
||||
/// Shows which comparator was used and why a package is considered fixed/vulnerable.
|
||||
/// </summary>
|
||||
public VersionComparisonEvidence? VersionComparison { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Delta signature evidence for binary-level backport detection.
|
||||
/// Cryptographic proof that patched code is present regardless of version string.
|
||||
/// </summary>
|
||||
public DeltaSignatureEvidence? DeltaSignature { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,429 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_20260102_001_BE
|
||||
// Task: DS-041 - VEX evidence emission for backport detection
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.Evidence.Models;
|
||||
|
||||
namespace StellaOps.Scanner.Evidence.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class DeltaSigVexEmitterTests
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider = new();
|
||||
private readonly DeltaSigVexEmitter _emitter;
|
||||
|
||||
public DeltaSigVexEmitterTests()
|
||||
{
|
||||
_timeProvider.SetUtcNow(new DateTimeOffset(2026, 1, 3, 12, 0, 0, TimeSpan.Zero));
|
||||
_emitter = new DeltaSigVexEmitter(timeProvider: _timeProvider);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitCandidates_PatchedBinary_EmitsCandidate()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = CreatePatchedEvidence(
|
||||
cveId: "CVE-2025-12345",
|
||||
confidence: 0.95m);
|
||||
|
||||
var context = CreateContext([evidence]);
|
||||
|
||||
// Act
|
||||
var result = _emitter.EmitCandidates(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, result.CandidatesEmitted);
|
||||
Assert.Single(result.Candidates);
|
||||
|
||||
var candidate = result.Candidates[0];
|
||||
Assert.Equal(DeltaSigVexStatus.NotAffected, candidate.SuggestedStatus);
|
||||
Assert.Equal(DeltaSigVexJustification.VulnerableCodeNotPresent, candidate.Justification);
|
||||
Assert.Contains("CVE-2025-12345", candidate.CveIds);
|
||||
Assert.Equal(0.95m, candidate.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitCandidates_VulnerableBinary_DoesNotEmitCandidate()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = CreateVulnerableEvidence("CVE-2025-12345");
|
||||
var context = CreateContext([evidence]);
|
||||
|
||||
// Act
|
||||
var result = _emitter.EmitCandidates(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, result.CandidatesEmitted);
|
||||
Assert.Empty(result.Candidates);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitCandidates_InconclusiveAnalysis_DoesNotEmitCandidate()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = CreateInconclusiveEvidence("CVE-2025-12345");
|
||||
var context = CreateContext([evidence]);
|
||||
|
||||
// Act
|
||||
var result = _emitter.EmitCandidates(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, result.CandidatesEmitted);
|
||||
Assert.Empty(result.Candidates);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitCandidates_LowConfidence_DoesNotEmitCandidate()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = CreatePatchedEvidence(
|
||||
cveId: "CVE-2025-12345",
|
||||
confidence: 0.50m); // Below default threshold of 0.75
|
||||
|
||||
var context = CreateContext([evidence]);
|
||||
|
||||
// Act
|
||||
var result = _emitter.EmitCandidates(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, result.CandidatesEmitted);
|
||||
Assert.Empty(result.Candidates);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitCandidates_CustomMinConfidence_RespectsThreshold()
|
||||
{
|
||||
// Arrange
|
||||
var options = new DeltaSigVexEmitterOptions { MinConfidence = 0.40m };
|
||||
var emitter = new DeltaSigVexEmitter(options, _timeProvider);
|
||||
|
||||
var evidence = CreatePatchedEvidence(
|
||||
cveId: "CVE-2025-12345",
|
||||
confidence: 0.50m);
|
||||
|
||||
var context = CreateContext([evidence]);
|
||||
|
||||
// Act
|
||||
var result = emitter.EmitCandidates(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, result.CandidatesEmitted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitCandidates_HighConfidence_DoesNotRequireReview()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = CreatePatchedEvidence(
|
||||
cveId: "CVE-2025-12345",
|
||||
confidence: 0.99m); // Above auto-approval threshold of 0.95
|
||||
|
||||
var context = CreateContext([evidence]);
|
||||
|
||||
// Act
|
||||
var result = _emitter.EmitCandidates(context);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result.Candidates);
|
||||
Assert.False(result.Candidates[0].RequiresReview);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitCandidates_MediumConfidence_RequiresReview()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = CreatePatchedEvidence(
|
||||
cveId: "CVE-2025-12345",
|
||||
confidence: 0.85m); // Between min and auto-approval thresholds
|
||||
|
||||
var context = CreateContext([evidence]);
|
||||
|
||||
// Act
|
||||
var result = _emitter.EmitCandidates(context);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result.Candidates);
|
||||
Assert.True(result.Candidates[0].RequiresReview);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitCandidates_MultiplePatchedBinaries_EmitsMultipleCandidates()
|
||||
{
|
||||
// Arrange
|
||||
var evidence1 = CreatePatchedEvidence("CVE-2025-0001", confidence: 0.90m);
|
||||
var evidence2 = CreatePatchedEvidence("CVE-2025-0002", confidence: 0.85m);
|
||||
var evidence3 = CreateVulnerableEvidence("CVE-2025-0003"); // Should be skipped
|
||||
|
||||
var context = CreateContext([evidence1, evidence2, evidence3]);
|
||||
|
||||
// Act
|
||||
var result = _emitter.EmitCandidates(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.CandidatesEmitted);
|
||||
Assert.All(result.Candidates, c => Assert.Equal(DeltaSigVexStatus.NotAffected, c.SuggestedStatus));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitCandidates_RespectsMaxCandidatesLimit()
|
||||
{
|
||||
// Arrange
|
||||
var options = new DeltaSigVexEmitterOptions { MaxCandidatesPerBatch = 2 };
|
||||
var emitter = new DeltaSigVexEmitter(options, _timeProvider);
|
||||
|
||||
var evidenceList = Enumerable.Range(1, 10)
|
||||
.Select(i => CreatePatchedEvidence($"CVE-2025-{i:D4}", confidence: 0.90m))
|
||||
.ToList();
|
||||
|
||||
var context = CreateContext(evidenceList);
|
||||
|
||||
// Act
|
||||
var result = emitter.EmitCandidates(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.CandidatesEmitted);
|
||||
Assert.Equal(2, result.Candidates.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitCandidates_CandidateContainsSymbolMatchEvidence()
|
||||
{
|
||||
// Arrange
|
||||
var symbolMatches = ImmutableArray.Create(
|
||||
new SymbolMatchEvidence
|
||||
{
|
||||
SymbolName = "vulnerable_function",
|
||||
HashHex = "abc123def456",
|
||||
State = SignatureState.Patched,
|
||||
Confidence = 1.0m,
|
||||
ExactMatch = true
|
||||
},
|
||||
new SymbolMatchEvidence
|
||||
{
|
||||
SymbolName = "another_function",
|
||||
HashHex = "789xyz012",
|
||||
State = SignatureState.Patched,
|
||||
Confidence = 0.85m,
|
||||
ExactMatch = false,
|
||||
ChunksMatched = 17,
|
||||
ChunksTotal = 20
|
||||
});
|
||||
|
||||
var evidence = DeltaSignatureEvidence.CreatePatched(
|
||||
cveIds: ["CVE-2025-12345"],
|
||||
packagePurl: "pkg:rpm/openssl@1.0.1e-30.el6_6?arch=x86_64",
|
||||
binaryId: "build-id:abc123",
|
||||
architecture: "x86_64",
|
||||
symbolMatches: symbolMatches,
|
||||
confidence: 0.92m,
|
||||
recipe: new NormalizationRecipeRef
|
||||
{
|
||||
RecipeId = "stellaops.normalize.x64.v1",
|
||||
Version = "1.0.0"
|
||||
},
|
||||
timeProvider: _timeProvider);
|
||||
|
||||
var context = CreateContext([evidence]);
|
||||
|
||||
// Act
|
||||
var result = _emitter.EmitCandidates(context);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result.Candidates);
|
||||
var candidate = result.Candidates[0];
|
||||
|
||||
// Should have evidence links for patched symbols
|
||||
Assert.Contains(candidate.EvidenceLinks, e => e.Type == "patched_symbol");
|
||||
Assert.Equal(2, candidate.EvidenceLinks.Count(e => e.Type == "patched_symbol"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitCandidates_CandidateHasCorrectExpiration()
|
||||
{
|
||||
// Arrange
|
||||
var options = new DeltaSigVexEmitterOptions { CandidateTtl = TimeSpan.FromDays(14) };
|
||||
var emitter = new DeltaSigVexEmitter(options, _timeProvider);
|
||||
|
||||
var evidence = CreatePatchedEvidence("CVE-2025-12345", confidence: 0.90m);
|
||||
var context = CreateContext([evidence]);
|
||||
|
||||
// Act
|
||||
var result = emitter.EmitCandidates(context);
|
||||
|
||||
// Assert
|
||||
var candidate = result.Candidates[0];
|
||||
var expectedExpiry = _timeProvider.GetUtcNow().AddDays(14);
|
||||
Assert.Equal(expectedExpiry, candidate.ExpiresAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitCandidates_GeneratesUniqueCandidateIds()
|
||||
{
|
||||
// Arrange
|
||||
var evidence1 = CreatePatchedEvidence("CVE-2025-0001", confidence: 0.90m);
|
||||
var evidence2 = CreatePatchedEvidence("CVE-2025-0002", confidence: 0.85m);
|
||||
|
||||
var context = CreateContext([evidence1, evidence2]);
|
||||
|
||||
// Act
|
||||
var result = _emitter.EmitCandidates(context);
|
||||
|
||||
// Assert
|
||||
var ids = result.Candidates.Select(c => c.CandidateId).ToList();
|
||||
Assert.Equal(2, ids.Distinct().Count());
|
||||
Assert.All(ids, id => Assert.StartsWith("vexds-", id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitCandidates_RationaleIncludesMatchDetails()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = CreatePatchedEvidence("CVE-2025-12345", confidence: 0.95m);
|
||||
var context = CreateContext([evidence]);
|
||||
|
||||
// Act
|
||||
var result = _emitter.EmitCandidates(context);
|
||||
|
||||
// Assert
|
||||
var candidate = result.Candidates[0];
|
||||
Assert.Contains("delta signature analysis", candidate.Rationale.ToLowerInvariant());
|
||||
Assert.Contains("95", candidate.Rationale); // Confidence percentage
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitCandidates_IncludesAttestationLinkWhenPresent()
|
||||
{
|
||||
// Arrange
|
||||
var symbolMatches = ImmutableArray.Create(
|
||||
new SymbolMatchEvidence
|
||||
{
|
||||
SymbolName = "vuln_func",
|
||||
HashHex = "abc123",
|
||||
State = SignatureState.Patched,
|
||||
Confidence = 1.0m,
|
||||
ExactMatch = true
|
||||
});
|
||||
|
||||
var evidence = DeltaSignatureEvidence.CreatePatched(
|
||||
cveIds: ["CVE-2025-12345"],
|
||||
packagePurl: "pkg:rpm/test@1.0.0",
|
||||
binaryId: "build-id:xyz",
|
||||
architecture: "x86_64",
|
||||
symbolMatches: symbolMatches,
|
||||
confidence: 0.95m,
|
||||
recipe: new NormalizationRecipeRef
|
||||
{
|
||||
RecipeId = "test.recipe",
|
||||
Version = "1.0.0"
|
||||
},
|
||||
timeProvider: _timeProvider) with
|
||||
{
|
||||
AttestationUri = "dsse://rekor.example.com/entries/abc123"
|
||||
};
|
||||
|
||||
var context = CreateContext([evidence]);
|
||||
|
||||
// Act
|
||||
var result = _emitter.EmitCandidates(context);
|
||||
|
||||
// Assert
|
||||
var candidate = result.Candidates[0];
|
||||
Assert.Contains(candidate.EvidenceLinks, e => e.Type == "dsse_attestation");
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
private DeltaSigVexEmissionContext CreateContext(IReadOnlyList<DeltaSignatureEvidence> evidence)
|
||||
{
|
||||
return new DeltaSigVexEmissionContext(
|
||||
ImageDigest: "sha256:abc123def456",
|
||||
EvidenceItems: evidence);
|
||||
}
|
||||
|
||||
private DeltaSignatureEvidence CreatePatchedEvidence(string cveId, decimal confidence)
|
||||
{
|
||||
var symbolMatches = ImmutableArray.Create(
|
||||
new SymbolMatchEvidence
|
||||
{
|
||||
SymbolName = "vulnerable_function",
|
||||
HashHex = "abc123def456",
|
||||
State = SignatureState.Patched,
|
||||
Confidence = confidence,
|
||||
ExactMatch = confidence >= 0.90m
|
||||
});
|
||||
|
||||
return DeltaSignatureEvidence.CreatePatched(
|
||||
cveIds: [cveId],
|
||||
packagePurl: "pkg:rpm/openssl@1.0.1e-30.el6_6?arch=x86_64",
|
||||
binaryId: "build-id:abc123",
|
||||
architecture: "x86_64",
|
||||
symbolMatches: symbolMatches,
|
||||
confidence: confidence,
|
||||
recipe: new NormalizationRecipeRef
|
||||
{
|
||||
RecipeId = "stellaops.normalize.x64.v1",
|
||||
Version = "1.0.0",
|
||||
Steps = ["zero_addresses", "canonicalize_nops", "normalize_plt"]
|
||||
},
|
||||
timeProvider: _timeProvider);
|
||||
}
|
||||
|
||||
private DeltaSignatureEvidence CreateVulnerableEvidence(string cveId)
|
||||
{
|
||||
var symbolMatches = ImmutableArray.Create(
|
||||
new SymbolMatchEvidence
|
||||
{
|
||||
SymbolName = "vulnerable_function",
|
||||
HashHex = "vuln_hash_123",
|
||||
State = SignatureState.Vulnerable,
|
||||
Confidence = 0.95m,
|
||||
ExactMatch = true
|
||||
});
|
||||
|
||||
return DeltaSignatureEvidence.CreateVulnerable(
|
||||
cveIds: [cveId],
|
||||
packagePurl: "pkg:rpm/openssl@1.0.1e-30.el6_6?arch=x86_64",
|
||||
binaryId: "build-id:abc123",
|
||||
architecture: "x86_64",
|
||||
symbolMatches: symbolMatches,
|
||||
confidence: 0.95m,
|
||||
recipe: new NormalizationRecipeRef
|
||||
{
|
||||
RecipeId = "stellaops.normalize.x64.v1",
|
||||
Version = "1.0.0"
|
||||
},
|
||||
timeProvider: _timeProvider);
|
||||
}
|
||||
|
||||
private DeltaSignatureEvidence CreateInconclusiveEvidence(string cveId)
|
||||
{
|
||||
return DeltaSignatureEvidence.CreateInconclusive(
|
||||
cveIds: [cveId],
|
||||
packagePurl: "pkg:rpm/openssl@1.0.1e-30.el6_6?arch=x86_64",
|
||||
binaryId: "build-id:abc123",
|
||||
architecture: "x86_64",
|
||||
reason: "Target symbols not found in binary",
|
||||
symbolMatches: [],
|
||||
recipe: new NormalizationRecipeRef
|
||||
{
|
||||
RecipeId = "stellaops.normalize.x64.v1",
|
||||
Version = "1.0.0"
|
||||
},
|
||||
timeProvider: _timeProvider);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fake TimeProvider for deterministic testing.
|
||||
/// </summary>
|
||||
internal sealed class FakeTimeProvider : TimeProvider
|
||||
{
|
||||
private DateTimeOffset _utcNow = DateTimeOffset.UtcNow;
|
||||
|
||||
public void SetUtcNow(DateTimeOffset value) => _utcNow = value;
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _utcNow;
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_20260102_001_BE
|
||||
// Task: DS-041 - VEX evidence emission for backport detection
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Scanner.Evidence.Models;
|
||||
|
||||
namespace StellaOps.Scanner.Evidence.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class DeltaSignatureEvidenceTests
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider = new();
|
||||
|
||||
public DeltaSignatureEvidenceTests()
|
||||
{
|
||||
_timeProvider.SetUtcNow(new DateTimeOffset(2026, 1, 3, 12, 0, 0, TimeSpan.Zero));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreatePatched_ReturnsCorrectResult()
|
||||
{
|
||||
// Arrange
|
||||
var symbolMatches = CreateSymbolMatches(SignatureState.Patched);
|
||||
|
||||
// Act
|
||||
var evidence = DeltaSignatureEvidence.CreatePatched(
|
||||
cveIds: ["CVE-2025-12345"],
|
||||
packagePurl: "pkg:rpm/openssl@1.0.1e",
|
||||
binaryId: "build-id:abc123",
|
||||
architecture: "x86_64",
|
||||
symbolMatches: symbolMatches,
|
||||
confidence: 0.95m,
|
||||
recipe: CreateRecipe(),
|
||||
timeProvider: _timeProvider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(DeltaSigResult.Patched, evidence.Result);
|
||||
Assert.Contains("PATCHED", evidence.Summary);
|
||||
Assert.Contains("95", evidence.Summary);
|
||||
Assert.Equal(0.95m, evidence.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateVulnerable_ReturnsCorrectResult()
|
||||
{
|
||||
// Arrange
|
||||
var symbolMatches = CreateSymbolMatches(SignatureState.Vulnerable);
|
||||
|
||||
// Act
|
||||
var evidence = DeltaSignatureEvidence.CreateVulnerable(
|
||||
cveIds: ["CVE-2025-12345"],
|
||||
packagePurl: "pkg:rpm/openssl@1.0.1e",
|
||||
binaryId: "build-id:abc123",
|
||||
architecture: "x86_64",
|
||||
symbolMatches: symbolMatches,
|
||||
confidence: 0.90m,
|
||||
recipe: CreateRecipe(),
|
||||
timeProvider: _timeProvider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(DeltaSigResult.Vulnerable, evidence.Result);
|
||||
Assert.Contains("VULNERABLE", evidence.Summary);
|
||||
Assert.Contains("90", evidence.Summary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateInconclusive_ReturnsCorrectResult()
|
||||
{
|
||||
// Arrange & Act
|
||||
var evidence = DeltaSignatureEvidence.CreateInconclusive(
|
||||
cveIds: ["CVE-2025-12345"],
|
||||
packagePurl: "pkg:rpm/openssl@1.0.1e",
|
||||
binaryId: "build-id:abc123",
|
||||
architecture: "x86_64",
|
||||
reason: "Symbol not found",
|
||||
symbolMatches: [],
|
||||
recipe: CreateRecipe(),
|
||||
timeProvider: _timeProvider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(DeltaSigResult.Inconclusive, evidence.Result);
|
||||
Assert.Contains("INCONCLUSIVE", evidence.Summary);
|
||||
Assert.Contains("Symbol not found", evidence.Summary);
|
||||
Assert.Equal(0m, evidence.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evidence_SerializesToJson()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = DeltaSignatureEvidence.CreatePatched(
|
||||
cveIds: ["CVE-2025-12345", "CVE-2025-67890"],
|
||||
packagePurl: "pkg:rpm/openssl@1.0.1e",
|
||||
binaryId: "build-id:abc123",
|
||||
architecture: "x86_64",
|
||||
symbolMatches: CreateSymbolMatches(SignatureState.Patched),
|
||||
confidence: 0.95m,
|
||||
recipe: CreateRecipe(),
|
||||
timeProvider: _timeProvider);
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(evidence);
|
||||
var deserialized = JsonSerializer.Deserialize<DeltaSignatureEvidence>(json);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(deserialized);
|
||||
Assert.Equal(evidence.Result, deserialized.Result);
|
||||
Assert.Equal(evidence.CveIds, deserialized.CveIds);
|
||||
Assert.Equal(evidence.PackagePurl, deserialized.PackagePurl);
|
||||
Assert.Equal(evidence.Confidence, deserialized.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeltaSigResult_SerializesAsString()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = DeltaSignatureEvidence.CreatePatched(
|
||||
cveIds: ["CVE-2025-12345"],
|
||||
packagePurl: "pkg:rpm/test@1.0.0",
|
||||
binaryId: "test",
|
||||
architecture: "x86_64",
|
||||
symbolMatches: [],
|
||||
confidence: 0.95m,
|
||||
recipe: CreateRecipe(),
|
||||
timeProvider: _timeProvider);
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(evidence);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("\"patched\"", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SignatureState_SerializesAsString()
|
||||
{
|
||||
// Arrange
|
||||
var match = new SymbolMatchEvidence
|
||||
{
|
||||
SymbolName = "test_func",
|
||||
HashHex = "abc123",
|
||||
State = SignatureState.Patched,
|
||||
Confidence = 1.0m,
|
||||
ExactMatch = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(match);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("\"patched\"", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SymbolMatchEvidence_SerializesChunkInfo()
|
||||
{
|
||||
// Arrange
|
||||
var match = new SymbolMatchEvidence
|
||||
{
|
||||
SymbolName = "partial_match_func",
|
||||
HashHex = "xyz789",
|
||||
State = SignatureState.Patched,
|
||||
Confidence = 0.85m,
|
||||
ExactMatch = false,
|
||||
ChunksMatched = 17,
|
||||
ChunksTotal = 20
|
||||
};
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(match);
|
||||
var deserialized = JsonSerializer.Deserialize<SymbolMatchEvidence>(json);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(deserialized);
|
||||
Assert.Equal(17, deserialized.ChunksMatched);
|
||||
Assert.Equal(20, deserialized.ChunksTotal);
|
||||
Assert.False(deserialized.ExactMatch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizationRecipeRef_SerializesSteps()
|
||||
{
|
||||
// Arrange
|
||||
var recipe = new NormalizationRecipeRef
|
||||
{
|
||||
RecipeId = "stellaops.normalize.x64.v1",
|
||||
Version = "1.0.0",
|
||||
Steps = ["zero_addresses", "canonicalize_nops", "normalize_plt_got"]
|
||||
};
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(recipe);
|
||||
var deserialized = JsonSerializer.Deserialize<NormalizationRecipeRef>(json);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(deserialized);
|
||||
Assert.Equal(3, deserialized.Steps.Length);
|
||||
Assert.Contains("zero_addresses", deserialized.Steps);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evidence_WithAttestationUri_SerializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var evidence = DeltaSignatureEvidence.CreatePatched(
|
||||
cveIds: ["CVE-2025-12345"],
|
||||
packagePurl: "pkg:rpm/test@1.0.0",
|
||||
binaryId: "test",
|
||||
architecture: "x86_64",
|
||||
symbolMatches: [],
|
||||
confidence: 0.95m,
|
||||
recipe: CreateRecipe(),
|
||||
timeProvider: _timeProvider) with
|
||||
{
|
||||
AttestationUri = "dsse://rekor.sigstore.dev/entries/12345"
|
||||
};
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(evidence);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("attestationUri", json);
|
||||
Assert.Contains("rekor.sigstore.dev", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evidence_SchemaVersionIsSet()
|
||||
{
|
||||
// Arrange & Act
|
||||
var evidence = DeltaSignatureEvidence.CreatePatched(
|
||||
cveIds: ["CVE-2025-12345"],
|
||||
packagePurl: "pkg:rpm/test@1.0.0",
|
||||
binaryId: "test",
|
||||
architecture: "x86_64",
|
||||
symbolMatches: [],
|
||||
confidence: 0.95m,
|
||||
recipe: CreateRecipe(),
|
||||
timeProvider: _timeProvider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("stellaops.evidence.deltasig.v1", evidence.Schema);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evidence_GeneratedAtIsSet()
|
||||
{
|
||||
// Arrange
|
||||
var expectedTime = new DateTimeOffset(2026, 1, 3, 12, 0, 0, TimeSpan.Zero);
|
||||
_timeProvider.SetUtcNow(expectedTime);
|
||||
|
||||
// Act
|
||||
var evidence = DeltaSignatureEvidence.CreatePatched(
|
||||
cveIds: ["CVE-2025-12345"],
|
||||
packagePurl: "pkg:rpm/test@1.0.0",
|
||||
binaryId: "test",
|
||||
architecture: "x86_64",
|
||||
symbolMatches: [],
|
||||
confidence: 0.95m,
|
||||
recipe: CreateRecipe(),
|
||||
timeProvider: _timeProvider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedTime, evidence.GeneratedAt);
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
private static ImmutableArray<SymbolMatchEvidence> CreateSymbolMatches(SignatureState state)
|
||||
{
|
||||
return
|
||||
[
|
||||
new SymbolMatchEvidence
|
||||
{
|
||||
SymbolName = "vulnerable_function",
|
||||
HashHex = "abc123def456",
|
||||
State = state,
|
||||
Confidence = 0.95m,
|
||||
ExactMatch = true
|
||||
},
|
||||
new SymbolMatchEvidence
|
||||
{
|
||||
SymbolName = "another_function",
|
||||
HashHex = "789xyz012",
|
||||
State = state,
|
||||
Confidence = 0.85m,
|
||||
ExactMatch = false,
|
||||
ChunksMatched = 17,
|
||||
ChunksTotal = 20
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private static NormalizationRecipeRef CreateRecipe()
|
||||
{
|
||||
return new NormalizationRecipeRef
|
||||
{
|
||||
RecipeId = "stellaops.normalize.x64.v1",
|
||||
Version = "1.0.0",
|
||||
Steps = ["zero_addresses", "canonicalize_nops"]
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user