278 lines
9.0 KiB
C#
278 lines
9.0 KiB
C#
// 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);
|