save progress
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user