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>

View File

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

View File

@@ -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; } = [];
}

View File

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

View File

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

View File

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