sprints work.
This commit is contained in:
@@ -384,7 +384,7 @@ public sealed class DeltaSigEnvelopeBuilder
|
||||
return new InTotoStatement
|
||||
{
|
||||
Subject = subjects,
|
||||
PredicateType = predicate.PredicateType,
|
||||
PredicateType = DeltaSigPredicate.PredicateType,
|
||||
Predicate = predicate
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
Reference in New Issue
Block a user