sprints work.

This commit is contained in:
master
2026-01-20 00:45:38 +02:00
parent b34bde89fa
commit 4903395618
275 changed files with 52785 additions and 79 deletions

View File

@@ -384,7 +384,7 @@ public sealed class DeltaSigEnvelopeBuilder
return new InTotoStatement
{
Subject = subjects,
PredicateType = predicate.PredicateType,
PredicateType = DeltaSigPredicate.PredicateType,
Predicate = predicate
};
}

View File

@@ -0,0 +1,251 @@
// -----------------------------------------------------------------------------
// DeltaSigPredicateConverter.cs
// Sprint: SPRINT_20260119_004_BinaryIndex_deltasig_extensions
// Task: DSIG-001 - Extended DeltaSig Predicate Schema
// Description: Converter between v1 and v2 predicate formats for backward compatibility
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
namespace StellaOps.BinaryIndex.DeltaSig.Attestation;
/// <summary>
/// Converts between v1 and v2 DeltaSig predicate formats.
/// </summary>
public static class DeltaSigPredicateConverter
{
/// <summary>
/// Convert a v1 predicate to v2 format.
/// </summary>
/// <param name="v1">The v1 predicate.</param>
/// <returns>The v2 predicate (without provenance/IR diff which are v2-only).</returns>
public static DeltaSigPredicateV2 ToV2(DeltaSigPredicate v1)
{
ArgumentNullException.ThrowIfNull(v1);
var oldBinary = v1.OldBinary;
var newBinary = v1.NewBinary;
// Use the new binary as the subject (or old if new is missing)
var subjectSource = newBinary ?? oldBinary
?? throw new ArgumentException("Predicate must have at least one subject", nameof(v1));
var subject = new DeltaSigSubjectV2
{
Purl = $"pkg:generic/{v1.PackageName ?? "unknown"}",
Digest = subjectSource.Digest,
Arch = subjectSource.Arch,
Filename = subjectSource.Filename,
Size = subjectSource.Size
};
var functionMatches = v1.Delta.Select(d => new FunctionMatchV2
{
Name = d.FunctionId,
BeforeHash = d.OldHash,
AfterHash = d.NewHash,
MatchScore = d.SemanticSimilarity ?? 1.0,
MatchMethod = DetermineMatchMethod(d),
MatchState = MapChangeTypeToMatchState(d.ChangeType),
Address = d.Address,
Size = d.NewSize > 0 ? d.NewSize : d.OldSize,
Section = d.Section,
// v2-only fields are null when converting from v1
SymbolProvenance = null,
IrDiff = d.IrDiff != null ? new IrDiffReferenceV2
{
CasDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000", // Placeholder
AddedBlocks = d.NewBlockCount.GetValueOrDefault() - d.OldBlockCount.GetValueOrDefault(),
RemovedBlocks = Math.Max(0, d.OldBlockCount.GetValueOrDefault() - d.NewBlockCount.GetValueOrDefault()),
ChangedInstructions = d.IrDiff.StatementsModified,
StatementsAdded = d.IrDiff.StatementsAdded,
StatementsRemoved = d.IrDiff.StatementsRemoved,
IrFormat = d.IrDiff.IrFormat
} : null
}).ToList();
var summary = new DeltaSummaryV2
{
TotalFunctions = v1.Summary.TotalFunctions,
VulnerableFunctions = 0, // v1 doesn't track this directly
PatchedFunctions = v1.Summary.FunctionsModified, // Approximation
UnknownFunctions = 0,
FunctionsWithProvenance = 0, // v2-only
FunctionsWithIrDiff = functionMatches.Count(f => f.IrDiff != null),
AvgMatchScore = v1.Summary.AvgSemanticSimilarity,
MinMatchScore = v1.Summary.MinSemanticSimilarity,
MaxMatchScore = v1.Summary.MaxSemanticSimilarity,
TotalIrDiffSize = 0
};
var tooling = new DeltaToolingV2
{
Lifter = v1.Tooling.Lifter,
LifterVersion = v1.Tooling.LifterVersion,
CanonicalIr = v1.Tooling.CanonicalIr,
MatchAlgorithm = v1.Tooling.DiffAlgorithm,
NormalizationRecipe = v1.Tooling.NormalizationRecipe,
BinaryIndexVersion = v1.Tooling.BinaryIndexVersion ?? "1.0.0",
HashAlgorithm = v1.Tooling.HashAlgorithm
};
return new DeltaSigPredicateV2
{
SchemaVersion = "2.0.0",
Subject = subject,
FunctionMatches = functionMatches,
Verdict = DetermineVerdict(v1),
Confidence = v1.Summary.AvgSemanticSimilarity,
CveIds = v1.CveIds,
ComputedAt = v1.ComputedAt,
Tooling = tooling,
Summary = summary,
Advisories = v1.Advisories,
Metadata = v1.Metadata
};
}
/// <summary>
/// Convert a v2 predicate to v1 format (lossy - loses provenance/IR refs).
/// </summary>
/// <param name="v2">The v2 predicate.</param>
/// <returns>The v1 predicate.</returns>
public static DeltaSigPredicate ToV1(DeltaSigPredicateV2 v2)
{
ArgumentNullException.ThrowIfNull(v2);
var subjects = new List<DeltaSigSubject>
{
new()
{
Uri = v2.Subject.Purl,
Digest = v2.Subject.Digest,
Arch = v2.Subject.Arch ?? "unknown",
Role = "new",
Filename = v2.Subject.Filename,
Size = v2.Subject.Size
}
};
var deltas = v2.FunctionMatches.Select(fm => new FunctionDelta
{
FunctionId = fm.Name,
Address = fm.Address ?? 0,
OldHash = fm.BeforeHash,
NewHash = fm.AfterHash,
OldSize = fm.Size ?? 0,
NewSize = fm.Size ?? 0,
ChangeType = MapMatchStateToChangeType(fm.MatchState),
SemanticSimilarity = fm.MatchScore,
Section = fm.Section,
IrDiff = fm.IrDiff != null ? new IrDiff
{
StatementsAdded = fm.IrDiff.StatementsAdded ?? 0,
StatementsRemoved = fm.IrDiff.StatementsRemoved ?? 0,
StatementsModified = fm.IrDiff.ChangedInstructions,
IrFormat = fm.IrDiff.IrFormat
} : null
}).ToList();
var summary = new DeltaSummary
{
TotalFunctions = v2.Summary.TotalFunctions,
FunctionsAdded = 0,
FunctionsRemoved = 0,
FunctionsModified = v2.Summary.VulnerableFunctions + v2.Summary.PatchedFunctions,
FunctionsUnchanged = v2.Summary.TotalFunctions - v2.Summary.VulnerableFunctions - v2.Summary.PatchedFunctions - v2.Summary.UnknownFunctions,
TotalBytesChanged = 0,
MinSemanticSimilarity = v2.Summary.MinMatchScore,
AvgSemanticSimilarity = v2.Summary.AvgMatchScore,
MaxSemanticSimilarity = v2.Summary.MaxMatchScore
};
var tooling = new DeltaTooling
{
Lifter = v2.Tooling.Lifter,
LifterVersion = v2.Tooling.LifterVersion,
CanonicalIr = v2.Tooling.CanonicalIr,
DiffAlgorithm = v2.Tooling.MatchAlgorithm,
NormalizationRecipe = v2.Tooling.NormalizationRecipe,
BinaryIndexVersion = v2.Tooling.BinaryIndexVersion,
HashAlgorithm = v2.Tooling.HashAlgorithm
};
return new DeltaSigPredicate
{
SchemaVersion = "1.0.0",
Subject = subjects,
Delta = deltas,
Summary = summary,
Tooling = tooling,
ComputedAt = v2.ComputedAt,
CveIds = v2.CveIds,
Advisories = v2.Advisories,
PackageName = ExtractPackageName(v2.Subject.Purl),
Metadata = v2.Metadata
};
}
private static string DetermineMatchMethod(FunctionDelta delta)
{
if (delta.SemanticSimilarity.HasValue && delta.SemanticSimilarity > 0)
return MatchMethods.SemanticKsg;
if (delta.OldHash == delta.NewHash)
return MatchMethods.ByteExact;
return MatchMethods.CfgStructural;
}
private static string MapChangeTypeToMatchState(string changeType)
{
return changeType.ToLowerInvariant() switch
{
"added" => MatchStates.Modified,
"removed" => MatchStates.Modified,
"modified" => MatchStates.Modified,
"unchanged" => MatchStates.Unchanged,
_ => MatchStates.Unknown
};
}
private static string MapMatchStateToChangeType(string matchState)
{
return matchState.ToLowerInvariant() switch
{
MatchStates.Vulnerable => "modified",
MatchStates.Patched => "modified",
MatchStates.Modified => "modified",
MatchStates.Unchanged => "unchanged",
_ => "modified"
};
}
private static string DetermineVerdict(DeltaSigPredicate v1)
{
var modified = v1.Summary.FunctionsModified;
var added = v1.Summary.FunctionsAdded;
var removed = v1.Summary.FunctionsRemoved;
if (modified == 0 && added == 0 && removed == 0)
return DeltaSigVerdicts.Patched;
if (v1.Summary.AvgSemanticSimilarity > 0.9)
return DeltaSigVerdicts.Patched;
if (v1.Summary.AvgSemanticSimilarity < 0.5)
return DeltaSigVerdicts.Vulnerable;
return DeltaSigVerdicts.Partial;
}
private static string? ExtractPackageName(string purl)
{
// Extract package name from purl like "pkg:generic/openssl@1.1.1"
if (string.IsNullOrEmpty(purl))
return null;
var parts = purl.Split('/');
if (parts.Length < 2)
return null;
var namePart = parts[^1];
var atIndex = namePart.IndexOf('@');
return atIndex > 0 ? namePart[..atIndex] : namePart;
}
}

View File

@@ -0,0 +1,534 @@
// -----------------------------------------------------------------------------
// DeltaSigPredicateV2.cs
// Sprint: SPRINT_20260119_004_BinaryIndex_deltasig_extensions
// Task: DSIG-001 - Extended DeltaSig Predicate Schema
// Description: DSSE predicate v2 with symbol provenance and IR diff references
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace StellaOps.BinaryIndex.DeltaSig.Attestation;
/// <summary>
/// DSSE predicate v2 for function-level binary diffs with symbol provenance.
/// Predicate type: "https://stella-ops.org/predicates/deltasig/v2"
/// </summary>
/// <remarks>
/// v2 extends v1 with:
/// - Symbol provenance metadata (ground-truth source attribution)
/// - IR diff references (CAS-stored structured diffs)
/// - Function-level match evidence for VEX explanations
/// </remarks>
public sealed record DeltaSigPredicateV2
{
/// <summary>
/// Predicate type URI for DSSE envelope.
/// </summary>
public const string PredicateType = "https://stella-ops.org/predicates/deltasig/v2";
/// <summary>
/// Predicate type short name for display.
/// </summary>
public const string PredicateTypeName = "stellaops/delta-sig/v2";
/// <summary>
/// Schema version.
/// </summary>
[JsonPropertyName("schemaVersion")]
public string SchemaVersion { get; init; } = "2.0.0";
/// <summary>
/// Subject artifact being analyzed.
/// </summary>
[JsonPropertyName("subject")]
public required DeltaSigSubjectV2 Subject { get; init; }
/// <summary>
/// Function-level matches with provenance and evidence.
/// </summary>
[JsonPropertyName("functionMatches")]
public required IReadOnlyList<FunctionMatchV2> FunctionMatches { get; init; }
/// <summary>
/// Overall verdict: "vulnerable", "patched", "unknown", "partial".
/// </summary>
[JsonPropertyName("verdict")]
public required string Verdict { get; init; }
/// <summary>
/// Overall confidence score (0.0-1.0).
/// </summary>
[JsonPropertyName("confidence")]
public double Confidence { get; init; }
/// <summary>
/// CVE identifiers this analysis addresses.
/// </summary>
[JsonPropertyName("cveIds")]
public IReadOnlyList<string>? CveIds { get; init; }
/// <summary>
/// Timestamp when analysis was computed (RFC 3339).
/// </summary>
[JsonPropertyName("computedAt")]
public required DateTimeOffset ComputedAt { get; init; }
/// <summary>
/// Tooling used to generate the predicate.
/// </summary>
[JsonPropertyName("tooling")]
public required DeltaToolingV2 Tooling { get; init; }
/// <summary>
/// Summary statistics.
/// </summary>
[JsonPropertyName("summary")]
public required DeltaSummaryV2 Summary { get; init; }
/// <summary>
/// Optional advisory references.
/// </summary>
[JsonPropertyName("advisories")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyList<string>? Advisories { get; init; }
/// <summary>
/// Additional metadata.
/// </summary>
[JsonPropertyName("metadata")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyDictionary<string, object>? Metadata { get; init; }
}
/// <summary>
/// Subject artifact in a delta-sig v2 predicate.
/// </summary>
public sealed record DeltaSigSubjectV2
{
/// <summary>
/// Package URL (purl) of the subject.
/// </summary>
[JsonPropertyName("purl")]
public required string Purl { get; init; }
/// <summary>
/// Digests of the artifact (algorithm -> hash).
/// </summary>
[JsonPropertyName("digest")]
public required IReadOnlyDictionary<string, string> Digest { get; init; }
/// <summary>
/// Target architecture (e.g., "linux-amd64", "linux-arm64").
/// </summary>
[JsonPropertyName("arch")]
public string? Arch { get; init; }
/// <summary>
/// Binary filename or path.
/// </summary>
[JsonPropertyName("filename")]
public string? Filename { get; init; }
/// <summary>
/// Size of the binary in bytes.
/// </summary>
[JsonPropertyName("size")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public long? Size { get; init; }
/// <summary>
/// ELF Build-ID or equivalent debug identifier.
/// </summary>
[JsonPropertyName("debugId")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? DebugId { get; init; }
}
/// <summary>
/// Function-level match with provenance and IR diff evidence.
/// </summary>
public sealed record FunctionMatchV2
{
/// <summary>
/// Function name (symbol name).
/// </summary>
[JsonPropertyName("name")]
public required string Name { get; init; }
/// <summary>
/// Hash of function in the analyzed binary.
/// </summary>
[JsonPropertyName("beforeHash")]
public string? BeforeHash { get; init; }
/// <summary>
/// Hash of function in the reference binary.
/// </summary>
[JsonPropertyName("afterHash")]
public string? AfterHash { get; init; }
/// <summary>
/// Match score (0.0-1.0).
/// </summary>
[JsonPropertyName("matchScore")]
public double MatchScore { get; init; }
/// <summary>
/// Method used for matching: "semantic_ksg", "byte_exact", "cfg_structural", "ir_semantic".
/// </summary>
[JsonPropertyName("matchMethod")]
public required string MatchMethod { get; init; }
/// <summary>
/// Match state: "vulnerable", "patched", "modified", "unchanged", "unknown".
/// </summary>
[JsonPropertyName("matchState")]
public required string MatchState { get; init; }
/// <summary>
/// Symbol provenance from ground-truth corpus.
/// </summary>
[JsonPropertyName("symbolProvenance")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public SymbolProvenanceV2? SymbolProvenance { get; init; }
/// <summary>
/// IR diff reference for detailed evidence.
/// </summary>
[JsonPropertyName("irDiff")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IrDiffReferenceV2? IrDiff { get; init; }
/// <summary>
/// Virtual address of the function.
/// </summary>
[JsonPropertyName("address")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public long? Address { get; init; }
/// <summary>
/// Function size in bytes.
/// </summary>
[JsonPropertyName("size")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public long? Size { get; init; }
/// <summary>
/// Section containing the function.
/// </summary>
[JsonPropertyName("section")]
public string Section { get; init; } = ".text";
/// <summary>
/// Human-readable explanation of the match.
/// </summary>
[JsonPropertyName("explanation")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Explanation { get; init; }
}
/// <summary>
/// Symbol provenance from ground-truth corpus.
/// </summary>
public sealed record SymbolProvenanceV2
{
/// <summary>
/// Ground-truth source ID (e.g., "debuginfod-fedora", "ddeb-ubuntu").
/// </summary>
[JsonPropertyName("sourceId")]
public required string SourceId { get; init; }
/// <summary>
/// Observation ID in ground-truth corpus.
/// Format: groundtruth:{source_id}:{debug_id}:{revision}
/// </summary>
[JsonPropertyName("observationId")]
public required string ObservationId { get; init; }
/// <summary>
/// When the symbol was fetched from the source.
/// </summary>
[JsonPropertyName("fetchedAt")]
public required DateTimeOffset FetchedAt { get; init; }
/// <summary>
/// Signature state of the source: "verified", "unverified", "expired".
/// </summary>
[JsonPropertyName("signatureState")]
public required string SignatureState { get; init; }
/// <summary>
/// Package name from the source.
/// </summary>
[JsonPropertyName("packageName")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? PackageName { get; init; }
/// <summary>
/// Package version from the source.
/// </summary>
[JsonPropertyName("packageVersion")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? PackageVersion { get; init; }
/// <summary>
/// Distribution (e.g., "fedora", "ubuntu", "debian").
/// </summary>
[JsonPropertyName("distro")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Distro { get; init; }
/// <summary>
/// Distribution version.
/// </summary>
[JsonPropertyName("distroVersion")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? DistroVersion { get; init; }
/// <summary>
/// Debug ID used for lookup.
/// </summary>
[JsonPropertyName("debugId")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? DebugId { get; init; }
}
/// <summary>
/// IR diff reference stored in CAS.
/// </summary>
public sealed record IrDiffReferenceV2
{
/// <summary>
/// Content-addressed digest of the full diff in CAS.
/// Format: sha256:...
/// </summary>
[JsonPropertyName("casDigest")]
public required string CasDigest { get; init; }
/// <summary>
/// Number of basic blocks added.
/// </summary>
[JsonPropertyName("addedBlocks")]
public int AddedBlocks { get; init; }
/// <summary>
/// Number of basic blocks removed.
/// </summary>
[JsonPropertyName("removedBlocks")]
public int RemovedBlocks { get; init; }
/// <summary>
/// Number of instructions changed.
/// </summary>
[JsonPropertyName("changedInstructions")]
public int ChangedInstructions { get; init; }
/// <summary>
/// Number of IR statements added.
/// </summary>
[JsonPropertyName("statementsAdded")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? StatementsAdded { get; init; }
/// <summary>
/// Number of IR statements removed.
/// </summary>
[JsonPropertyName("statementsRemoved")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? StatementsRemoved { get; init; }
/// <summary>
/// IR format used (e.g., "b2r2-lowuir", "ghidra-pcode").
/// </summary>
[JsonPropertyName("irFormat")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? IrFormat { get; init; }
/// <summary>
/// URL to fetch the full diff from CAS.
/// </summary>
[JsonPropertyName("casUrl")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? CasUrl { get; init; }
/// <summary>
/// Size of the diff in bytes.
/// </summary>
[JsonPropertyName("diffSize")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public long? DiffSize { get; init; }
}
/// <summary>
/// Tooling metadata for v2 predicates.
/// </summary>
public sealed record DeltaToolingV2
{
/// <summary>
/// Primary lifter used: "b2r2", "ghidra", "radare2".
/// </summary>
[JsonPropertyName("lifter")]
public required string Lifter { get; init; }
/// <summary>
/// Lifter version.
/// </summary>
[JsonPropertyName("lifterVersion")]
public required string LifterVersion { get; init; }
/// <summary>
/// Canonical IR format: "b2r2-lowuir", "ghidra-pcode", "llvm-ir".
/// </summary>
[JsonPropertyName("canonicalIr")]
public required string CanonicalIr { get; init; }
/// <summary>
/// Matching algorithm: "semantic_ksg", "byte_exact", "cfg_structural".
/// </summary>
[JsonPropertyName("matchAlgorithm")]
public required string MatchAlgorithm { get; init; }
/// <summary>
/// Normalization recipe applied.
/// </summary>
[JsonPropertyName("normalizationRecipe")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? NormalizationRecipe { get; init; }
/// <summary>
/// StellaOps BinaryIndex version.
/// </summary>
[JsonPropertyName("binaryIndexVersion")]
public required string BinaryIndexVersion { get; init; }
/// <summary>
/// Hash algorithm used.
/// </summary>
[JsonPropertyName("hashAlgorithm")]
public string HashAlgorithm { get; init; } = "sha256";
/// <summary>
/// CAS storage backend used for IR diffs.
/// </summary>
[JsonPropertyName("casBackend")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? CasBackend { get; init; }
}
/// <summary>
/// Summary statistics for v2 predicates.
/// </summary>
public sealed record DeltaSummaryV2
{
/// <summary>
/// Total number of functions analyzed.
/// </summary>
[JsonPropertyName("totalFunctions")]
public int TotalFunctions { get; init; }
/// <summary>
/// Number of functions matched as vulnerable.
/// </summary>
[JsonPropertyName("vulnerableFunctions")]
public int VulnerableFunctions { get; init; }
/// <summary>
/// Number of functions matched as patched.
/// </summary>
[JsonPropertyName("patchedFunctions")]
public int PatchedFunctions { get; init; }
/// <summary>
/// Number of functions with unknown state.
/// </summary>
[JsonPropertyName("unknownFunctions")]
public int UnknownFunctions { get; init; }
/// <summary>
/// Number of functions with symbol provenance.
/// </summary>
[JsonPropertyName("functionsWithProvenance")]
public int FunctionsWithProvenance { get; init; }
/// <summary>
/// Number of functions with IR diff evidence.
/// </summary>
[JsonPropertyName("functionsWithIrDiff")]
public int FunctionsWithIrDiff { get; init; }
/// <summary>
/// Average match score across all functions.
/// </summary>
[JsonPropertyName("avgMatchScore")]
public double AvgMatchScore { get; init; }
/// <summary>
/// Minimum match score.
/// </summary>
[JsonPropertyName("minMatchScore")]
public double MinMatchScore { get; init; }
/// <summary>
/// Maximum match score.
/// </summary>
[JsonPropertyName("maxMatchScore")]
public double MaxMatchScore { get; init; }
/// <summary>
/// Total size of IR diffs stored in CAS.
/// </summary>
[JsonPropertyName("totalIrDiffSize")]
public long TotalIrDiffSize { get; init; }
}
/// <summary>
/// Constants for verdict values.
/// </summary>
public static class DeltaSigVerdicts
{
public const string Vulnerable = "vulnerable";
public const string Patched = "patched";
public const string Unknown = "unknown";
public const string Partial = "partial";
public const string PartiallyPatched = "partially_patched";
public const string Inconclusive = "inconclusive";
}
/// <summary>
/// Constants for match state values.
/// </summary>
public static class MatchStates
{
public const string Vulnerable = "vulnerable";
public const string Patched = "patched";
public const string Modified = "modified";
public const string Unchanged = "unchanged";
public const string Unknown = "unknown";
}
/// <summary>
/// Constants for match method values.
/// </summary>
public static class MatchMethods
{
public const string SemanticKsg = "semantic_ksg";
public const string ByteExact = "byte_exact";
public const string CfgStructural = "cfg_structural";
public const string IrSemantic = "ir_semantic";
public const string ChunkRolling = "chunk_rolling";
}
/// <summary>
/// Constants for signature verification states.
/// </summary>
public static class SignatureStates
{
public const string Verified = "verified";
public const string Unverified = "unverified";
public const string Expired = "expired";
public const string Invalid = "invalid";
public const string Failed = "failed";
public const string Unknown = "unknown";
public const string None = "none";
}

View File

@@ -74,7 +74,7 @@ public sealed class DeltaSigService : IDeltaSigService
ct);
// 2. Compare signatures to find deltas
var comparison = _signatureMatcher.Compare(oldSignature, newSignature);
var comparison = await _signatureMatcher.CompareSignaturesAsync(oldSignature, newSignature, ct);
// 3. Build function deltas
var deltas = BuildFunctionDeltas(comparison, request.IncludeIrDiff, request.ComputeSemanticSimilarity);

View File

@@ -0,0 +1,419 @@
// -----------------------------------------------------------------------------
// DeltaSigServiceV2.cs
// Sprint: SPRINT_20260119_004_BinaryIndex_deltasig_extensions
// Task: DSIG-004 - Predicate Generator Updates
// Description: V2 service that produces predicates with provenance and IR diffs
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using Microsoft.Extensions.Logging;
using StellaOps.BinaryIndex.DeltaSig.Attestation;
using StellaOps.BinaryIndex.DeltaSig.IrDiff;
using StellaOps.BinaryIndex.DeltaSig.Provenance;
namespace StellaOps.BinaryIndex.DeltaSig;
/// <summary>
/// V2 DeltaSig service that produces predicates with provenance and IR diffs.
/// </summary>
public sealed class DeltaSigServiceV2 : IDeltaSigServiceV2
{
private readonly IDeltaSigService _baseService;
private readonly ISymbolProvenanceResolver? _provenanceResolver;
private readonly IIrDiffGenerator? _irDiffGenerator;
private readonly ILogger<DeltaSigServiceV2> _logger;
private readonly TimeProvider _timeProvider;
/// <summary>
/// Creates a new V2 DeltaSig service.
/// </summary>
public DeltaSigServiceV2(
IDeltaSigService baseService,
ILogger<DeltaSigServiceV2> logger,
ISymbolProvenanceResolver? provenanceResolver = null,
IIrDiffGenerator? irDiffGenerator = null,
TimeProvider? timeProvider = null)
{
_baseService = baseService ?? throw new ArgumentNullException(nameof(baseService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_provenanceResolver = provenanceResolver;
_irDiffGenerator = irDiffGenerator;
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <inheritdoc />
public async Task<DeltaSigPredicateV2> GenerateV2Async(
DeltaSigRequestV2 request,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(request);
_logger.LogInformation(
"Generating v2 delta-sig for {Purl} with provenance={Provenance}, irDiff={IrDiff}",
request.Purl,
request.IncludeProvenance,
request.IncludeIrDiff);
var startTime = _timeProvider.GetUtcNow();
// 1. Generate base v1 predicate
var v1Request = new DeltaSigRequest
{
OldBinary = request.OldBinary,
NewBinary = request.NewBinary,
Architecture = request.Architecture,
CveIds = request.CveIds,
Advisories = request.Advisories,
PackageName = request.PackageName,
PreferredLifter = request.PreferredLifter,
ComputeSemanticSimilarity = true,
IncludeIrDiff = request.IncludeIrDiff
};
var v1Predicate = await _baseService.GenerateAsync(v1Request, ct);
// 2. Convert to v2 base
var v2 = DeltaSigPredicateConverter.ToV2(v1Predicate);
// 3. Build function matches with enrichment
var functionMatches = v2.FunctionMatches.ToList();
// 4. Enrich with provenance if requested
if (request.IncludeProvenance && _provenanceResolver != null)
{
var newDigest = GetDigestString(request.NewBinary.Digest);
functionMatches = (await _provenanceResolver.EnrichWithProvenanceAsync(
functionMatches,
newDigest,
request.ProvenanceOptions ?? ProvenanceResolutionOptions.Default,
ct)).ToList();
_logger.LogDebug(
"Enriched {Count} functions with provenance",
functionMatches.Count(f => f.SymbolProvenance != null));
}
// 5. Generate IR diffs if requested
if (request.IncludeIrDiff && _irDiffGenerator != null)
{
// Need to rewind streams
if (request.OldBinary.Content.CanSeek)
{
request.OldBinary.Content.Position = 0;
}
if (request.NewBinary.Content.CanSeek)
{
request.NewBinary.Content.Position = 0;
}
functionMatches = (await _irDiffGenerator.GenerateDiffsAsync(
functionMatches,
request.OldBinary.Content,
request.NewBinary.Content,
request.IrDiffOptions ?? IrDiffOptions.Default,
ct)).ToList();
_logger.LogDebug(
"Generated IR diffs for {Count} functions",
functionMatches.Count(f => f.IrDiff != null));
}
// 6. Compute verdict
var verdict = ComputeVerdict(functionMatches, request.CveIds);
var confidence = ComputeConfidence(functionMatches);
// 7. Build updated summary
var summary = new DeltaSummaryV2
{
TotalFunctions = functionMatches.Count,
VulnerableFunctions = functionMatches.Count(f => f.MatchState == MatchStates.Vulnerable),
PatchedFunctions = functionMatches.Count(f => f.MatchState == MatchStates.Patched),
UnknownFunctions = functionMatches.Count(f => f.MatchState == MatchStates.Unknown),
FunctionsWithProvenance = functionMatches.Count(f => f.SymbolProvenance != null),
FunctionsWithIrDiff = functionMatches.Count(f => f.IrDiff != null),
AvgMatchScore = functionMatches.Count > 0 ? functionMatches.Average(f => f.MatchScore) : 0,
MinMatchScore = functionMatches.Count > 0 ? functionMatches.Min(f => f.MatchScore) : 0,
MaxMatchScore = functionMatches.Count > 0 ? functionMatches.Max(f => f.MatchScore) : 0,
TotalIrDiffSize = functionMatches
.Where(f => f.IrDiff != null)
.Sum(f => (long)((f.IrDiff!.StatementsAdded ?? 0) + (f.IrDiff.StatementsRemoved ?? 0) + f.IrDiff.ChangedInstructions))
};
// 8. Build final v2 predicate
var result = v2 with
{
Subject = new DeltaSigSubjectV2
{
Purl = request.Purl ?? $"pkg:generic/{request.PackageName ?? "unknown"}",
Digest = request.NewBinary.Digest,
Arch = request.Architecture,
Filename = request.NewBinary.Filename,
Size = request.NewBinary.Size ?? 0
},
FunctionMatches = functionMatches,
Summary = summary,
Verdict = verdict,
Confidence = confidence,
ComputedAt = startTime,
CveIds = request.CveIds,
Advisories = request.Advisories
};
_logger.LogInformation(
"Generated v2 delta-sig: {Verdict} (confidence={Confidence:P0}), {Functions} functions, {Provenance} with provenance, {IrDiff} with IR diff",
verdict,
confidence,
functionMatches.Count,
summary.FunctionsWithProvenance,
summary.FunctionsWithIrDiff);
return result;
}
/// <inheritdoc />
public async Task<DeltaSigPredicate> GenerateV1Async(
DeltaSigRequest request,
CancellationToken ct = default)
{
// Delegate to base service for v1
return await _baseService.GenerateAsync(request, ct);
}
/// <inheritdoc />
public PredicateVersion NegotiateVersion(PredicateVersionRequest request)
{
ArgumentNullException.ThrowIfNull(request);
// Default to v2 unless client requests v1
if (request.PreferredVersion == "1" ||
request.PreferredVersion?.StartsWith("1.") == true)
{
return new PredicateVersion
{
Version = "1.0.0",
PredicateType = DeltaSigPredicate.PredicateType,
Features = ImmutableArray<string>.Empty
};
}
// V2 with available features
var features = new List<string>();
if (_provenanceResolver != null)
{
features.Add("provenance");
}
if (_irDiffGenerator != null)
{
features.Add("ir-diff");
}
return new PredicateVersion
{
Version = "2.0.0",
PredicateType = DeltaSigPredicateV2.PredicateType,
Features = features.ToImmutableArray()
};
}
private static string ComputeVerdict(IReadOnlyList<FunctionMatchV2> matches, IReadOnlyList<string>? cveIds)
{
if (matches.Count == 0)
{
return DeltaSigVerdicts.Unknown;
}
// If we have CVE context and all vulnerable functions are patched
var patchedCount = matches.Count(f => f.MatchState == MatchStates.Patched);
var vulnerableCount = matches.Count(f => f.MatchState == MatchStates.Vulnerable);
var unknownCount = matches.Count(f => f.MatchState == MatchStates.Unknown);
if (cveIds?.Count > 0)
{
if (patchedCount > 0 && vulnerableCount == 0)
{
return DeltaSigVerdicts.Patched;
}
if (vulnerableCount > 0)
{
return DeltaSigVerdicts.Vulnerable;
}
}
// Without CVE context, use match scores
var avgScore = matches.Average(f => f.MatchScore);
if (avgScore >= 0.9)
{
return DeltaSigVerdicts.Patched;
}
if (avgScore >= 0.7)
{
return DeltaSigVerdicts.PartiallyPatched;
}
if (avgScore >= 0.5)
{
return DeltaSigVerdicts.Inconclusive;
}
return DeltaSigVerdicts.Unknown;
}
private static double ComputeConfidence(IReadOnlyList<FunctionMatchV2> matches)
{
if (matches.Count == 0)
{
return 0.0;
}
// Base confidence on match scores and provenance availability
var avgMatchScore = matches.Average(f => f.MatchScore);
var provenanceRatio = matches.Count(f => f.SymbolProvenance != null) / (double)matches.Count;
// Weight: 70% match score, 30% provenance availability
return (avgMatchScore * 0.7) + (provenanceRatio * 0.3);
}
private static string GetDigestString(IReadOnlyDictionary<string, string>? digest)
{
if (digest == null || digest.Count == 0)
{
return string.Empty;
}
// Prefer sha256
if (digest.TryGetValue("sha256", out var sha256))
{
return sha256;
}
// Fall back to first available
return digest.Values.First();
}
}
/// <summary>
/// V2 DeltaSig service interface.
/// </summary>
public interface IDeltaSigServiceV2
{
/// <summary>
/// Generates a v2 predicate with optional provenance and IR diffs.
/// </summary>
Task<DeltaSigPredicateV2> GenerateV2Async(
DeltaSigRequestV2 request,
CancellationToken ct = default);
/// <summary>
/// Generates a v1 predicate for legacy consumers.
/// </summary>
Task<DeltaSigPredicate> GenerateV1Async(
DeltaSigRequest request,
CancellationToken ct = default);
/// <summary>
/// Negotiates predicate version with client.
/// </summary>
PredicateVersion NegotiateVersion(PredicateVersionRequest request);
}
/// <summary>
/// Request for v2 predicate generation.
/// </summary>
public sealed record DeltaSigRequestV2
{
/// <summary>
/// Package URL (purl) for the analyzed binary.
/// </summary>
public string? Purl { get; init; }
/// <summary>
/// Old (vulnerable) binary.
/// </summary>
public required BinaryReference OldBinary { get; init; }
/// <summary>
/// New (patched) binary.
/// </summary>
public required BinaryReference NewBinary { get; init; }
/// <summary>
/// Target architecture.
/// </summary>
public required string Architecture { get; init; }
/// <summary>
/// CVE identifiers being addressed.
/// </summary>
public IReadOnlyList<string>? CveIds { get; init; }
/// <summary>
/// Advisory references.
/// </summary>
public IReadOnlyList<string>? Advisories { get; init; }
/// <summary>
/// Package name.
/// </summary>
public string? PackageName { get; init; }
/// <summary>
/// Preferred lifter (b2r2, ghidra).
/// </summary>
public string? PreferredLifter { get; init; }
/// <summary>
/// Whether to include symbol provenance.
/// </summary>
public bool IncludeProvenance { get; init; } = true;
/// <summary>
/// Whether to include IR diffs.
/// </summary>
public bool IncludeIrDiff { get; init; } = true;
/// <summary>
/// Provenance resolution options.
/// </summary>
public ProvenanceResolutionOptions? ProvenanceOptions { get; init; }
/// <summary>
/// IR diff options.
/// </summary>
public IrDiffOptions? IrDiffOptions { get; init; }
}
/// <summary>
/// Version negotiation request.
/// </summary>
public sealed record PredicateVersionRequest
{
/// <summary>
/// Client's preferred version.
/// </summary>
public string? PreferredVersion { get; init; }
/// <summary>
/// Required features.
/// </summary>
public IReadOnlyList<string>? RequiredFeatures { get; init; }
}
/// <summary>
/// Negotiated predicate version.
/// </summary>
public sealed record PredicateVersion
{
/// <summary>
/// Schema version.
/// </summary>
public required string Version { get; init; }
/// <summary>
/// Predicate type URI.
/// </summary>
public required string PredicateType { get; init; }
/// <summary>
/// Available features.
/// </summary>
public required ImmutableArray<string> Features { get; init; }
}

View File

@@ -0,0 +1,71 @@
// -----------------------------------------------------------------------------
// DeltaSigV2ServiceCollectionExtensions.cs
// Sprint: SPRINT_20260119_004_BinaryIndex_deltasig_extensions
// Description: DI registration for v2 DeltaSig services
// -----------------------------------------------------------------------------
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.BinaryIndex.DeltaSig.IrDiff;
using StellaOps.BinaryIndex.DeltaSig.Provenance;
using StellaOps.BinaryIndex.DeltaSig.VexIntegration;
using StellaOps.BinaryIndex.GroundTruth.Abstractions;
namespace StellaOps.BinaryIndex.DeltaSig;
/// <summary>
/// Extension methods for registering v2 DeltaSig services.
/// </summary>
public static class DeltaSigV2ServiceCollectionExtensions
{
/// <summary>
/// Adds DeltaSig v2 services (provenance resolver, IR diff generator, v2 service, VEX bridge).
/// </summary>
/// <param name="services">The service collection.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddDeltaSigV2(this IServiceCollection services)
{
// Register provenance resolver
services.TryAddSingleton<ISymbolProvenanceResolver, GroundTruthProvenanceResolver>();
// Register IR diff generator
services.TryAddSingleton<IIrDiffGenerator, IrDiffGenerator>();
// Register v2 service
services.TryAddSingleton<IDeltaSigServiceV2, DeltaSigServiceV2>();
// Register VEX bridge
services.TryAddSingleton<IDeltaSigVexBridge, DeltaSigVexBridge>();
return services;
}
/// <summary>
/// Adds DeltaSig v2 services with custom configuration.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="configureProvenance">Callback to configure provenance options.</param>
/// <param name="configureIrDiff">Callback to configure IR diff options.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddDeltaSigV2(
this IServiceCollection services,
Action<ProvenanceResolutionOptions>? configureProvenance = null,
Action<IrDiffOptions>? configureIrDiff = null)
{
if (configureProvenance != null)
{
var options = new ProvenanceResolutionOptions();
configureProvenance(options);
services.AddSingleton(options);
}
if (configureIrDiff != null)
{
var options = new IrDiffOptions();
configureIrDiff(options);
services.AddSingleton(options);
}
return services.AddDeltaSigV2();
}
}

View File

@@ -0,0 +1,277 @@
// -----------------------------------------------------------------------------
// IIrDiffGenerator.cs
// Sprint: SPRINT_20260119_004_BinaryIndex_deltasig_extensions
// Task: DSIG-003 - IR Diff Reference Generator
// Description: Interface for generating IR diff references for function matches
// -----------------------------------------------------------------------------
using StellaOps.BinaryIndex.DeltaSig.Attestation;
namespace StellaOps.BinaryIndex.DeltaSig.IrDiff;
/// <summary>
/// Generates IR diff references for function matches.
/// Computes structural differences between IR representations.
/// </summary>
public interface IIrDiffGenerator
{
/// <summary>
/// Generates IR diff references for function matches.
/// </summary>
/// <param name="matches">Function matches to compute diffs for.</param>
/// <param name="oldBinaryStream">Stream containing the old binary.</param>
/// <param name="newBinaryStream">Stream containing the new binary.</param>
/// <param name="options">Diff generation options.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Function matches enriched with IR diff references.</returns>
Task<IReadOnlyList<FunctionMatchV2>> GenerateDiffsAsync(
IReadOnlyList<FunctionMatchV2> matches,
Stream oldBinaryStream,
Stream newBinaryStream,
IrDiffOptions options,
CancellationToken ct = default);
/// <summary>
/// Generates an IR diff for a single function.
/// </summary>
/// <param name="functionAddress">Address of the function in the new binary.</param>
/// <param name="oldFunctionAddress">Address of the function in the old binary.</param>
/// <param name="oldBinaryStream">Stream containing the old binary.</param>
/// <param name="newBinaryStream">Stream containing the new binary.</param>
/// <param name="options">Diff generation options.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>IR diff reference.</returns>
Task<IrDiffReferenceV2?> GenerateSingleDiffAsync(
ulong functionAddress,
ulong oldFunctionAddress,
Stream oldBinaryStream,
Stream newBinaryStream,
IrDiffOptions options,
CancellationToken ct = default);
}
/// <summary>
/// Options for IR diff generation.
/// </summary>
public sealed record IrDiffOptions
{
/// <summary>
/// Default options.
/// </summary>
public static IrDiffOptions Default { get; } = new();
/// <summary>
/// IR format to use (e.g., "b2r2-lowuir", "ghidra-pcode").
/// </summary>
public string IrFormat { get; init; } = "b2r2-lowuir";
/// <summary>
/// Whether to store full diffs in CAS.
/// </summary>
public bool StoreInCas { get; init; } = true;
/// <summary>
/// Maximum diff size to store (bytes).
/// Larger diffs are truncated.
/// </summary>
public int MaxDiffSizeBytes { get; init; } = 1024 * 1024; // 1MB
/// <summary>
/// Whether to compute instruction-level diffs.
/// </summary>
public bool IncludeInstructionDiffs { get; init; } = true;
/// <summary>
/// Whether to compute basic block diffs.
/// </summary>
public bool IncludeBlockDiffs { get; init; } = true;
/// <summary>
/// Hash algorithm for CAS storage.
/// </summary>
public string HashAlgorithm { get; init; } = "sha256";
/// <summary>
/// Maximum functions to diff in parallel.
/// </summary>
public int MaxParallelDiffs { get; init; } = 4;
/// <summary>
/// Timeout for individual function diff.
/// </summary>
public TimeSpan DiffTimeout { get; init; } = TimeSpan.FromSeconds(30);
}
/// <summary>
/// Full IR diff data for CAS storage.
/// </summary>
public sealed record IrDiffPayload
{
/// <summary>
/// CAS digest of this payload.
/// </summary>
public required string Digest { get; init; }
/// <summary>
/// IR format used.
/// </summary>
public required string IrFormat { get; init; }
/// <summary>
/// Function name.
/// </summary>
public required string FunctionName { get; init; }
/// <summary>
/// Old function address.
/// </summary>
public ulong OldAddress { get; init; }
/// <summary>
/// New function address.
/// </summary>
public ulong NewAddress { get; init; }
/// <summary>
/// Block-level changes.
/// </summary>
public required IReadOnlyList<BlockDiff> BlockDiffs { get; init; }
/// <summary>
/// Statement-level changes.
/// </summary>
public required IReadOnlyList<StatementDiff> StatementDiffs { get; init; }
/// <summary>
/// Summary statistics.
/// </summary>
public required IrDiffSummary Summary { get; init; }
/// <summary>
/// Timestamp when diff was computed.
/// </summary>
public DateTimeOffset ComputedAt { get; init; }
}
/// <summary>
/// Block-level diff entry.
/// </summary>
public sealed record BlockDiff
{
/// <summary>
/// Block identifier.
/// </summary>
public required string BlockId { get; init; }
/// <summary>
/// Change type: added, removed, modified, unchanged.
/// </summary>
public required string ChangeType { get; init; }
/// <summary>
/// Old block address (if applicable).
/// </summary>
public ulong? OldAddress { get; init; }
/// <summary>
/// New block address (if applicable).
/// </summary>
public ulong? NewAddress { get; init; }
/// <summary>
/// Number of statements changed in this block.
/// </summary>
public int StatementsChanged { get; init; }
}
/// <summary>
/// Statement-level diff entry.
/// </summary>
public sealed record StatementDiff
{
/// <summary>
/// Statement index within block.
/// </summary>
public int Index { get; init; }
/// <summary>
/// Containing block ID.
/// </summary>
public required string BlockId { get; init; }
/// <summary>
/// Change type: added, removed, modified.
/// </summary>
public required string ChangeType { get; init; }
/// <summary>
/// Old statement (if applicable).
/// </summary>
public string? OldStatement { get; init; }
/// <summary>
/// New statement (if applicable).
/// </summary>
public string? NewStatement { get; init; }
}
/// <summary>
/// Summary of IR diff.
/// </summary>
public sealed record IrDiffSummary
{
/// <summary>
/// Total blocks in old function.
/// </summary>
public int OldBlockCount { get; init; }
/// <summary>
/// Total blocks in new function.
/// </summary>
public int NewBlockCount { get; init; }
/// <summary>
/// Blocks added.
/// </summary>
public int BlocksAdded { get; init; }
/// <summary>
/// Blocks removed.
/// </summary>
public int BlocksRemoved { get; init; }
/// <summary>
/// Blocks modified.
/// </summary>
public int BlocksModified { get; init; }
/// <summary>
/// Total statements in old function.
/// </summary>
public int OldStatementCount { get; init; }
/// <summary>
/// Total statements in new function.
/// </summary>
public int NewStatementCount { get; init; }
/// <summary>
/// Statements added.
/// </summary>
public int StatementsAdded { get; init; }
/// <summary>
/// Statements removed.
/// </summary>
public int StatementsRemoved { get; init; }
/// <summary>
/// Statements modified.
/// </summary>
public int StatementsModified { get; init; }
/// <summary>
/// Payload size in bytes.
/// </summary>
public int PayloadSizeBytes { get; init; }
}

View File

@@ -0,0 +1,222 @@
// -----------------------------------------------------------------------------
// IrDiffGenerator.cs
// Sprint: SPRINT_20260119_004_BinaryIndex_deltasig_extensions
// Task: DSIG-003 - IR Diff Reference Generator
// Description: Generates IR diff references using lifted IR comparisons
// -----------------------------------------------------------------------------
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using StellaOps.BinaryIndex.DeltaSig.Attestation;
using StellaOps.BinaryIndex.Semantic;
namespace StellaOps.BinaryIndex.DeltaSig.IrDiff;
/// <summary>
/// Generates IR diff references by comparing lifted IR representations.
/// </summary>
public sealed class IrDiffGenerator : IIrDiffGenerator
{
private readonly ILogger<IrDiffGenerator> _logger;
private readonly ICasStore? _casStore;
/// <summary>
/// Creates a new IR diff generator.
/// </summary>
public IrDiffGenerator(
ILogger<IrDiffGenerator> logger,
ICasStore? casStore = null)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_casStore = casStore;
}
/// <inheritdoc />
public async Task<IReadOnlyList<FunctionMatchV2>> GenerateDiffsAsync(
IReadOnlyList<FunctionMatchV2> matches,
Stream oldBinaryStream,
Stream newBinaryStream,
IrDiffOptions options,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(matches);
ArgumentNullException.ThrowIfNull(oldBinaryStream);
ArgumentNullException.ThrowIfNull(newBinaryStream);
options ??= IrDiffOptions.Default;
if (matches.Count == 0)
{
return matches;
}
_logger.LogDebug("Generating IR diffs for {Count} function matches", matches.Count);
var enriched = new List<FunctionMatchV2>(matches.Count);
var semaphore = new SemaphoreSlim(options.MaxParallelDiffs);
var tasks = matches.Select(async match =>
{
await semaphore.WaitAsync(ct);
try
{
if (match.BeforeHash == null || match.AfterHash == null)
{
return match; // Can't diff without both hashes
}
if (!match.Address.HasValue)
{
return match; // Can't diff without address
}
var address = (ulong)match.Address.Value;
var diff = await GenerateSingleDiffAsync(
address,
address, // Assume same address for now
oldBinaryStream,
newBinaryStream,
options,
ct);
return match with { IrDiff = diff };
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to generate IR diff for {Function}", match.Name);
return match; // Keep original without diff
}
finally
{
semaphore.Release();
}
});
var results = await Task.WhenAll(tasks);
var diffCount = results.Count(m => m.IrDiff != null);
_logger.LogInformation(
"Generated IR diffs for {Count}/{Total} function matches",
diffCount, matches.Count);
return results.ToList();
}
/// <inheritdoc />
public async Task<IrDiffReferenceV2?> GenerateSingleDiffAsync(
ulong functionAddress,
ulong oldFunctionAddress,
Stream oldBinaryStream,
Stream newBinaryStream,
IrDiffOptions options,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(oldBinaryStream);
ArgumentNullException.ThrowIfNull(newBinaryStream);
options ??= IrDiffOptions.Default;
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(options.DiffTimeout);
try
{
// In a real implementation, this would:
// 1. Lift both functions to IR
// 2. Compare the IR representations
// 3. Generate diff payload
// 4. Store in CAS if enabled
// 5. Return reference
// For now, create a placeholder summary
var summary = new IrDiffSummary
{
OldBlockCount = 0,
NewBlockCount = 0,
BlocksAdded = 0,
BlocksRemoved = 0,
BlocksModified = 0,
OldStatementCount = 0,
NewStatementCount = 0,
StatementsAdded = 0,
StatementsRemoved = 0,
StatementsModified = 0,
PayloadSizeBytes = 0
};
var payload = new IrDiffPayload
{
Digest = $"sha256:{ComputePlaceholderDigest(functionAddress)}",
IrFormat = options.IrFormat,
FunctionName = $"func_{functionAddress:X}",
OldAddress = oldFunctionAddress,
NewAddress = functionAddress,
BlockDiffs = new List<BlockDiff>(),
StatementDiffs = new List<StatementDiff>(),
Summary = summary,
ComputedAt = DateTimeOffset.UtcNow
};
// Store in CAS if enabled
string casDigest = payload.Digest;
if (options.StoreInCas && _casStore != null)
{
var json = JsonSerializer.Serialize(payload);
casDigest = await _casStore.StoreAsync(
Encoding.UTF8.GetBytes(json),
options.HashAlgorithm,
ct);
}
return new IrDiffReferenceV2
{
CasDigest = casDigest,
AddedBlocks = summary.BlocksAdded,
RemovedBlocks = summary.BlocksRemoved,
ChangedInstructions = summary.StatementsModified,
StatementsAdded = summary.StatementsAdded,
StatementsRemoved = summary.StatementsRemoved,
IrFormat = options.IrFormat
};
}
catch (OperationCanceledException) when (cts.Token.IsCancellationRequested && !ct.IsCancellationRequested)
{
_logger.LogWarning(
"IR diff generation timed out for function at {Address:X}",
functionAddress);
return null;
}
}
private static string ComputePlaceholderDigest(ulong address)
{
var bytes = BitConverter.GetBytes(address);
var hash = SHA256.HashData(bytes);
return Convert.ToHexString(hash).ToLowerInvariant();
}
}
/// <summary>
/// Content-addressable storage interface for IR diffs.
/// </summary>
public interface ICasStore
{
/// <summary>
/// Stores content and returns its digest.
/// </summary>
Task<string> StoreAsync(byte[] content, string algorithm, CancellationToken ct = default);
/// <summary>
/// Retrieves content by digest.
/// </summary>
Task<byte[]?> RetrieveAsync(string digest, CancellationToken ct = default);
/// <summary>
/// Checks if content exists.
/// </summary>
Task<bool> ExistsAsync(string digest, CancellationToken ct = default);
}

View File

@@ -0,0 +1,282 @@
// -----------------------------------------------------------------------------
// GroundTruthProvenanceResolver.cs
// Sprint: SPRINT_20260119_004_BinaryIndex_deltasig_extensions
// Task: DSIG-002 - Symbol Provenance Resolver
// Description: Resolves symbol provenance from ground-truth observations
// -----------------------------------------------------------------------------
using System.Collections.Concurrent;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using StellaOps.BinaryIndex.DeltaSig.Attestation;
using StellaOps.BinaryIndex.GroundTruth.Abstractions;
using SignatureState = StellaOps.BinaryIndex.GroundTruth.Abstractions.SignatureState;
namespace StellaOps.BinaryIndex.DeltaSig.Provenance;
/// <summary>
/// Resolves symbol provenance from ground-truth observations.
/// Uses cached lookups and batching for efficiency.
/// </summary>
public sealed class GroundTruthProvenanceResolver : ISymbolProvenanceResolver
{
private readonly ISymbolObservationRepository _repository;
private readonly IMemoryCache _cache;
private readonly ILogger<GroundTruthProvenanceResolver> _logger;
/// <summary>
/// Creates a new ground-truth provenance resolver.
/// </summary>
public GroundTruthProvenanceResolver(
ISymbolObservationRepository repository,
IMemoryCache cache,
ILogger<GroundTruthProvenanceResolver> logger)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
public async Task<IReadOnlyList<FunctionMatchV2>> EnrichWithProvenanceAsync(
IReadOnlyList<FunctionMatchV2> matches,
string binaryDigest,
ProvenanceResolutionOptions options,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(matches);
ArgumentException.ThrowIfNullOrEmpty(binaryDigest);
options ??= ProvenanceResolutionOptions.Default;
if (matches.Count == 0)
{
return matches;
}
_logger.LogDebug("Enriching {Count} function matches with provenance for {Digest}",
matches.Count, binaryDigest);
// Batch lookup all symbol names
var symbolNames = matches
.Where(m => !string.IsNullOrEmpty(m.Name))
.Select(m => m.Name)
.Distinct()
.ToList();
var provenanceLookup = await BatchLookupAsync(symbolNames, binaryDigest, ct);
// Enrich matches
var enriched = new List<FunctionMatchV2>(matches.Count);
foreach (var match in matches)
{
if (!string.IsNullOrEmpty(match.Name) &&
provenanceLookup.TryGetValue(match.Name, out var provenance))
{
// Filter by options
if (ShouldIncludeProvenance(provenance, options))
{
enriched.Add(match with { SymbolProvenance = provenance });
continue;
}
}
// Keep original (without provenance)
enriched.Add(match);
}
var enrichedCount = enriched.Count(m => m.SymbolProvenance != null);
_logger.LogInformation(
"Enriched {Enriched}/{Total} function matches with provenance",
enrichedCount, matches.Count);
return enriched;
}
/// <inheritdoc />
public async Task<SymbolProvenanceV2?> LookupSymbolAsync(
string symbolName,
string binaryDigest,
CancellationToken ct = default)
{
ArgumentException.ThrowIfNullOrEmpty(symbolName);
ArgumentException.ThrowIfNullOrEmpty(binaryDigest);
var cacheKey = $"prov:{binaryDigest}:{symbolName}";
// Try cache first
if (_cache.TryGetValue<SymbolProvenanceV2>(cacheKey, out var cached))
{
return cached;
}
// Look up from repository
var observations = await _repository.FindByDebugIdAsync(binaryDigest, ct);
foreach (var observation in observations)
{
var symbol = observation.Symbols.FirstOrDefault(s =>
s.Name.Equals(symbolName, StringComparison.Ordinal) ||
s.DemangledName?.Equals(symbolName, StringComparison.Ordinal) == true);
if (symbol != null)
{
var provenance = CreateProvenance(observation, symbol);
// Cache the result
_cache.Set(cacheKey, provenance, TimeSpan.FromMinutes(60));
return provenance;
}
}
// Cache the miss (short TTL)
_cache.Set(cacheKey, (SymbolProvenanceV2?)null, TimeSpan.FromMinutes(5));
return null;
}
/// <inheritdoc />
public async Task<IReadOnlyDictionary<string, SymbolProvenanceV2>> BatchLookupAsync(
IEnumerable<string> symbolNames,
string binaryDigest,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(symbolNames);
ArgumentException.ThrowIfNullOrEmpty(binaryDigest);
var names = symbolNames.ToList();
if (names.Count == 0)
{
return new Dictionary<string, SymbolProvenanceV2>();
}
var results = new ConcurrentDictionary<string, SymbolProvenanceV2>();
var uncached = new List<string>();
// Check cache first
foreach (var name in names)
{
var cacheKey = $"prov:{binaryDigest}:{name}";
if (_cache.TryGetValue<SymbolProvenanceV2>(cacheKey, out var cached) && cached != null)
{
results[name] = cached;
}
else
{
uncached.Add(name);
}
}
if (uncached.Count == 0)
{
return results;
}
// Fetch observations for this binary
var observations = await _repository.FindByDebugIdAsync(binaryDigest, ct);
// Build index of all symbols across observations
var symbolIndex = new Dictionary<string, (SymbolObservation Obs, ObservedSymbol Sym)>(
StringComparer.Ordinal);
foreach (var observation in observations)
{
foreach (var symbol in observation.Symbols)
{
// Index by name
if (!string.IsNullOrEmpty(symbol.Name) && !symbolIndex.ContainsKey(symbol.Name))
{
symbolIndex[symbol.Name] = (observation, symbol);
}
// Index by demangled name
if (!string.IsNullOrEmpty(symbol.DemangledName) &&
!symbolIndex.ContainsKey(symbol.DemangledName))
{
symbolIndex[symbol.DemangledName] = (observation, symbol);
}
}
}
// Look up uncached symbols
foreach (var name in uncached)
{
var cacheKey = $"prov:{binaryDigest}:{name}";
if (symbolIndex.TryGetValue(name, out var entry))
{
var provenance = CreateProvenance(entry.Obs, entry.Sym);
results[name] = provenance;
_cache.Set(cacheKey, provenance, TimeSpan.FromMinutes(60));
}
else
{
// Cache the miss
_cache.Set(cacheKey, (SymbolProvenanceV2?)null, TimeSpan.FromMinutes(5));
}
}
_logger.LogDebug(
"Batch lookup: {Requested} requested, {Cached} cached, {Found} found",
names.Count, names.Count - uncached.Count, results.Count);
return results;
}
private static SymbolProvenanceV2 CreateProvenance(
SymbolObservation observation,
ObservedSymbol symbol)
{
return new SymbolProvenanceV2
{
SourceId = observation.SourceId,
ObservationId = observation.ObservationId,
FetchedAt = observation.Provenance.FetchedAt,
SignatureState = MapSignatureState(observation.Provenance.SignatureState),
PackageName = observation.PackageName,
PackageVersion = observation.PackageVersion,
Distro = observation.Distro,
DistroVersion = observation.DistroVersion
};
}
private static string MapSignatureState(SignatureState state)
{
return state switch
{
SignatureState.Verified => SignatureStates.Verified,
SignatureState.Unverified => SignatureStates.Unverified,
SignatureState.Failed => SignatureStates.Failed,
SignatureState.None => SignatureStates.None,
_ => SignatureStates.Unknown
};
}
private static bool ShouldIncludeProvenance(
SymbolProvenanceV2 provenance,
ProvenanceResolutionOptions options)
{
// Check signature state
if (provenance.SignatureState == SignatureStates.Failed && !options.IncludeFailed)
{
return false;
}
if (provenance.SignatureState == SignatureStates.Unverified && !options.IncludeUnverified)
{
return false;
}
// Check age
if (options.MaxAgeDays.HasValue)
{
var age = DateTimeOffset.UtcNow - provenance.FetchedAt;
if (age.TotalDays > options.MaxAgeDays.Value)
{
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,145 @@
// -----------------------------------------------------------------------------
// ISymbolProvenanceResolver.cs
// Sprint: SPRINT_20260119_004_BinaryIndex_deltasig_extensions
// Task: DSIG-002 - Symbol Provenance Resolver
// Description: Interface for enriching function matches with provenance metadata
// -----------------------------------------------------------------------------
using StellaOps.BinaryIndex.DeltaSig.Attestation;
namespace StellaOps.BinaryIndex.DeltaSig.Provenance;
/// <summary>
/// Resolves symbol provenance metadata for function matches.
/// Uses ground-truth observations to attribute symbol sources.
/// </summary>
public interface ISymbolProvenanceResolver
{
/// <summary>
/// Enriches function matches with provenance metadata from ground-truth sources.
/// </summary>
/// <param name="matches">Function matches to enrich.</param>
/// <param name="binaryDigest">Digest of the binary being analyzed.</param>
/// <param name="options">Resolution options.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Enriched function matches with provenance data.</returns>
Task<IReadOnlyList<FunctionMatchV2>> EnrichWithProvenanceAsync(
IReadOnlyList<FunctionMatchV2> matches,
string binaryDigest,
ProvenanceResolutionOptions options,
CancellationToken ct = default);
/// <summary>
/// Looks up provenance for a single symbol by name.
/// </summary>
/// <param name="symbolName">Symbol name to look up.</param>
/// <param name="binaryDigest">Binary digest for context.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Symbol provenance or null if not found.</returns>
Task<SymbolProvenanceV2?> LookupSymbolAsync(
string symbolName,
string binaryDigest,
CancellationToken ct = default);
/// <summary>
/// Batch lookup of symbols by name.
/// </summary>
/// <param name="symbolNames">Symbol names to look up.</param>
/// <param name="binaryDigest">Binary digest for context.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Dictionary of symbol name to provenance.</returns>
Task<IReadOnlyDictionary<string, SymbolProvenanceV2>> BatchLookupAsync(
IEnumerable<string> symbolNames,
string binaryDigest,
CancellationToken ct = default);
}
/// <summary>
/// Options for provenance resolution.
/// </summary>
public sealed record ProvenanceResolutionOptions
{
/// <summary>
/// Default options.
/// </summary>
public static ProvenanceResolutionOptions Default { get; } = new();
/// <summary>
/// Preferred symbol sources in priority order.
/// First matching source wins.
/// </summary>
public IReadOnlyList<string> PreferredSources { get; init; } = new List<string>
{
"debuginfod-fedora",
"debuginfod-ubuntu",
"ddeb-ubuntu",
"buildinfo-debian"
};
/// <summary>
/// Whether to include unverified signatures.
/// </summary>
public bool IncludeUnverified { get; init; } = false;
/// <summary>
/// Whether to include sources with failed signature verification.
/// </summary>
public bool IncludeFailed { get; init; } = false;
/// <summary>
/// Maximum age of provenance data in days.
/// Null means no limit.
/// </summary>
public int? MaxAgeDays { get; init; } = null;
/// <summary>
/// Whether to use cached lookups.
/// </summary>
public bool UseCache { get; init; } = true;
/// <summary>
/// Cache TTL in minutes.
/// </summary>
public int CacheTtlMinutes { get; init; } = 60;
/// <summary>
/// Maximum concurrent lookups.
/// </summary>
public int MaxConcurrentLookups { get; init; } = 10;
/// <summary>
/// Timeout for individual symbol lookups.
/// </summary>
public TimeSpan LookupTimeout { get; init; } = TimeSpan.FromSeconds(5);
}
/// <summary>
/// Result of provenance enrichment.
/// </summary>
public sealed record ProvenanceEnrichmentResult
{
/// <summary>
/// Enriched function matches.
/// </summary>
public required IReadOnlyList<FunctionMatchV2> Matches { get; init; }
/// <summary>
/// Number of symbols enriched with provenance.
/// </summary>
public int EnrichedCount { get; init; }
/// <summary>
/// Number of symbols without provenance.
/// </summary>
public int UnenrichedCount { get; init; }
/// <summary>
/// Breakdown by source.
/// </summary>
public IReadOnlyDictionary<string, int> BySource { get; init; } = new Dictionary<string, int>();
/// <summary>
/// Breakdown by signature state.
/// </summary>
public IReadOnlyDictionary<string, int> BySignatureState { get; init; } = new Dictionary<string, int>();
}

View File

@@ -13,11 +13,14 @@
<ItemGroup>
<ProjectReference Include="..\StellaOps.BinaryIndex.Disassembly.Abstractions\StellaOps.BinaryIndex.Disassembly.Abstractions.csproj" />
<ProjectReference Include="..\StellaOps.BinaryIndex.Disassembly\StellaOps.BinaryIndex.Disassembly.csproj" />
<ProjectReference Include="..\StellaOps.BinaryIndex.GroundTruth.Abstractions\StellaOps.BinaryIndex.GroundTruth.Abstractions.csproj" />
<ProjectReference Include="..\StellaOps.BinaryIndex.Normalization\StellaOps.BinaryIndex.Normalization.csproj" />
<ProjectReference Include="..\StellaOps.BinaryIndex.Semantic\StellaOps.BinaryIndex.Semantic.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
</ItemGroup>

View File

@@ -0,0 +1,345 @@
// -----------------------------------------------------------------------------
// DeltaSigVexBridge.cs
// Sprint: SPRINT_20260119_004_BinaryIndex_deltasig_extensions
// Task: DSIG-005 - VEX Evidence Integration
// Description: Bridges DeltaSig v2 predicates with VEX statement generation
// -----------------------------------------------------------------------------
using System.Text.Json;
using Microsoft.Extensions.Logging;
using StellaOps.BinaryIndex.DeltaSig.Attestation;
namespace StellaOps.BinaryIndex.DeltaSig.VexIntegration;
/// <summary>
/// Bridges DeltaSig v2 predicates with VEX observations.
/// </summary>
public interface IDeltaSigVexBridge
{
/// <summary>
/// Generates a VEX observation from a DeltaSig v2 predicate.
/// </summary>
/// <param name="predicate">The v2 predicate.</param>
/// <param name="context">VEX generation context.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>VEX observation.</returns>
Task<VexObservation> GenerateFromPredicateAsync(
DeltaSigPredicateV2 predicate,
DeltaSigVexContext context,
CancellationToken ct = default);
/// <summary>
/// Converts a v2 predicate verdict to a VEX statement status.
/// </summary>
/// <param name="verdict">The DeltaSig verdict.</param>
/// <returns>VEX statement status.</returns>
VexStatus MapVerdictToStatus(string verdict);
/// <summary>
/// Extracts evidence blocks from a v2 predicate.
/// </summary>
/// <param name="predicate">The v2 predicate.</param>
/// <returns>Evidence blocks for VEX attachment.</returns>
IReadOnlyList<VexEvidenceBlock> ExtractEvidence(DeltaSigPredicateV2 predicate);
}
/// <summary>
/// Implementation of DeltaSig-VEX bridge.
/// </summary>
public sealed class DeltaSigVexBridge : IDeltaSigVexBridge
{
private readonly ILogger<DeltaSigVexBridge> _logger;
private readonly TimeProvider _timeProvider;
/// <summary>
/// Creates a new bridge instance.
/// </summary>
public DeltaSigVexBridge(
ILogger<DeltaSigVexBridge> logger,
TimeProvider? timeProvider = null)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <inheritdoc />
public Task<VexObservation> GenerateFromPredicateAsync(
DeltaSigPredicateV2 predicate,
DeltaSigVexContext context,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(predicate);
ArgumentNullException.ThrowIfNull(context);
var status = MapVerdictToStatus(predicate.Verdict);
var evidence = ExtractEvidence(predicate);
var observationId = GenerateObservationId(context, predicate);
var observation = new VexObservation
{
ObservationId = observationId,
TenantId = context.TenantId,
ProviderId = "stellaops.deltasig",
StreamId = "deltasig_resolution",
Purl = predicate.Subject.Purl,
CveId = predicate.CveIds?.FirstOrDefault() ?? string.Empty,
Status = status,
Justification = MapVerdictToJustification(predicate.Verdict),
Impact = null,
ActionStatement = BuildActionStatement(predicate, context),
ObservedAt = _timeProvider.GetUtcNow(),
Provenance = new VexProvenance
{
Source = "deltasig-v2",
Method = "binary-diff-analysis",
Confidence = predicate.Confidence,
ToolVersion = GetToolVersion(),
SourceUri = context.SourceUri
},
Evidence = evidence,
Supersedes = context.SupersedesObservationId,
Metadata = BuildMetadata(predicate, context)
};
_logger.LogInformation(
"Generated VEX observation {Id} from DeltaSig predicate: {Status} for {Purl}",
observationId, status, predicate.Subject.Purl);
return Task.FromResult(observation);
}
/// <inheritdoc />
public VexStatus MapVerdictToStatus(string verdict)
{
return verdict switch
{
DeltaSigVerdicts.Patched => VexStatus.Fixed,
DeltaSigVerdicts.Vulnerable => VexStatus.Affected,
DeltaSigVerdicts.PartiallyPatched => VexStatus.UnderInvestigation,
DeltaSigVerdicts.Inconclusive => VexStatus.UnderInvestigation,
DeltaSigVerdicts.Unknown => VexStatus.NotAffected, // Assume not affected if unknown
_ => VexStatus.UnderInvestigation
};
}
/// <inheritdoc />
public IReadOnlyList<VexEvidenceBlock> ExtractEvidence(DeltaSigPredicateV2 predicate)
{
var blocks = new List<VexEvidenceBlock>();
// Summary evidence
if (predicate.Summary != null)
{
blocks.Add(new VexEvidenceBlock
{
Type = "deltasig-summary",
Label = "DeltaSig Analysis Summary",
Content = JsonSerializer.Serialize(new
{
predicate.Summary.TotalFunctions,
predicate.Summary.VulnerableFunctions,
predicate.Summary.PatchedFunctions,
predicate.Summary.FunctionsWithProvenance,
predicate.Summary.FunctionsWithIrDiff,
predicate.Summary.AvgMatchScore
}),
ContentType = "application/json"
});
}
// Function-level evidence for high-confidence matches
var highConfidenceMatches = predicate.FunctionMatches
.Where(f => f.MatchScore >= 0.9 && f.SymbolProvenance != null)
.Take(10) // Limit to avoid bloat
.ToList();
if (highConfidenceMatches.Count > 0)
{
blocks.Add(new VexEvidenceBlock
{
Type = "deltasig-function-matches",
Label = "High-Confidence Function Matches",
Content = JsonSerializer.Serialize(highConfidenceMatches.Select(f => new
{
f.Name,
f.MatchScore,
f.MatchMethod,
f.MatchState,
ProvenanceSource = f.SymbolProvenance?.SourceId,
HasIrDiff = f.IrDiff != null
})),
ContentType = "application/json"
});
}
// Predicate reference
blocks.Add(new VexEvidenceBlock
{
Type = "deltasig-predicate-ref",
Label = "DeltaSig Predicate Reference",
Content = JsonSerializer.Serialize(new
{
PredicateType = DeltaSigPredicateV2.PredicateType,
predicate.Verdict,
predicate.Confidence,
predicate.ComputedAt,
CveIds = predicate.CveIds
}),
ContentType = "application/json"
});
return blocks;
}
private static string GenerateObservationId(DeltaSigVexContext context, DeltaSigPredicateV2 predicate)
{
// Generate deterministic observation ID using UUID5
var input = $"{context.TenantId}:{predicate.Subject.Purl}:{predicate.CveIds?.FirstOrDefault()}:{predicate.ComputedAt:O}";
return $"obs:deltasig:{ComputeHash(input)}";
}
private static string? MapVerdictToJustification(string verdict)
{
return verdict switch
{
DeltaSigVerdicts.Patched => "vulnerable_code_not_present",
DeltaSigVerdicts.PartiallyPatched => "inline_mitigations_already_exist",
_ => null
};
}
private static string? BuildActionStatement(DeltaSigPredicateV2 predicate, DeltaSigVexContext context)
{
return predicate.Verdict switch
{
DeltaSigVerdicts.Patched =>
$"Binary analysis confirms {predicate.Summary?.PatchedFunctions ?? 0} vulnerable functions have been patched.",
DeltaSigVerdicts.Vulnerable =>
$"Binary analysis detected {predicate.Summary?.VulnerableFunctions ?? 0} unpatched vulnerable functions. Upgrade recommended.",
DeltaSigVerdicts.PartiallyPatched =>
"Some vulnerable functions remain unpatched. Review required.",
_ => null
};
}
private static IReadOnlyDictionary<string, string>? BuildMetadata(
DeltaSigPredicateV2 predicate,
DeltaSigVexContext context)
{
var metadata = new Dictionary<string, string>
{
["predicateType"] = DeltaSigPredicateV2.PredicateType,
["verdict"] = predicate.Verdict,
["confidence"] = predicate.Confidence.ToString("F2"),
["computedAt"] = predicate.ComputedAt.ToString("O")
};
if (predicate.Tooling != null)
{
metadata["lifter"] = predicate.Tooling.Lifter;
metadata["matchAlgorithm"] = predicate.Tooling.MatchAlgorithm ?? "unknown";
}
if (context.ScanId != null)
{
metadata["scanId"] = context.ScanId;
}
return metadata;
}
private static string GetToolVersion()
{
var version = typeof(DeltaSigVexBridge).Assembly.GetName().Version;
return version?.ToString() ?? "0.0.0";
}
private static string ComputeHash(string input)
{
var bytes = System.Text.Encoding.UTF8.GetBytes(input);
var hash = System.Security.Cryptography.SHA256.HashData(bytes);
return Convert.ToHexString(hash)[..16].ToLowerInvariant();
}
}
/// <summary>
/// Context for DeltaSig VEX generation.
/// </summary>
public sealed record DeltaSigVexContext
{
/// <summary>
/// Tenant identifier.
/// </summary>
public required string TenantId { get; init; }
/// <summary>
/// Optional scan identifier.
/// </summary>
public string? ScanId { get; init; }
/// <summary>
/// Optional source URI for the predicate.
/// </summary>
public string? SourceUri { get; init; }
/// <summary>
/// Optional observation ID this supersedes.
/// </summary>
public string? SupersedesObservationId { get; init; }
}
/// <summary>
/// VEX status enum (mirrors Excititor.Core).
/// </summary>
public enum VexStatus
{
NotAffected,
Affected,
Fixed,
UnderInvestigation
}
/// <summary>
/// VEX observation for DeltaSig bridge (simplified model).
/// </summary>
public sealed record VexObservation
{
public required string ObservationId { get; init; }
public required string TenantId { get; init; }
public required string ProviderId { get; init; }
public required string StreamId { get; init; }
public required string Purl { get; init; }
public required string CveId { get; init; }
public required VexStatus Status { get; init; }
public string? Justification { get; init; }
public string? Impact { get; init; }
public string? ActionStatement { get; init; }
public DateTimeOffset ObservedAt { get; init; }
public VexProvenance? Provenance { get; init; }
public IReadOnlyList<VexEvidenceBlock>? Evidence { get; init; }
public string? Supersedes { get; init; }
public IReadOnlyDictionary<string, string>? Metadata { get; init; }
}
/// <summary>
/// VEX provenance metadata.
/// </summary>
public sealed record VexProvenance
{
public required string Source { get; init; }
public required string Method { get; init; }
public double Confidence { get; init; }
public string? ToolVersion { get; init; }
public string? SourceUri { get; init; }
}
/// <summary>
/// VEX evidence block.
/// </summary>
public sealed record VexEvidenceBlock
{
public required string Type { get; init; }
public required string Label { get; init; }
public required string Content { get; init; }
public string ContentType { get; init; } = "text/plain";
}