compose and authority fixes. finish sprints.

This commit is contained in:
master
2026-02-17 21:59:47 +02:00
parent fb46a927ad
commit 49cdebe2f1
187 changed files with 23189 additions and 1439 deletions

View File

@@ -78,15 +78,7 @@ if (app.Environment.IsDevelopment())
}
app.UseStellaOpsCors();
var hasHttpsBinding = app.Urls.Any(url => url.StartsWith("https://", StringComparison.OrdinalIgnoreCase));
if (hasHttpsBinding)
{
app.UseHttpsRedirection();
}
else
{
app.Logger.LogInformation("Skipping HTTPS redirection because no HTTPS binding is configured.");
}
// HTTPS redirection removed — the gateway handles TLS termination.
app.UseResolutionRateLimiting();
app.UseAuthorization();
app.MapControllers();

View File

@@ -5,6 +5,9 @@ using StellaOps.BinaryIndex.Cache;
using StellaOps.BinaryIndex.Contracts.Resolution;
using StellaOps.BinaryIndex.Core.Resolution;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace StellaOps.BinaryIndex.WebService.Services;
@@ -19,6 +22,14 @@ public sealed class CachedResolutionService : IResolutionService
private readonly ResolutionServiceOptions _serviceOptions;
private readonly TimeProvider _timeProvider;
private readonly ILogger<CachedResolutionService> _logger;
private static readonly JsonSerializerOptions HybridDigestJsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
private const string HybridSchemaVersion = "1.0.0";
private const string HybridNormalizationRecipeId = "stellaops-resolution-cache-v1";
public CachedResolutionService(
IResolutionService inner,
@@ -132,7 +143,7 @@ public sealed class CachedResolutionService : IResolutionService
private VulnResolutionResponse FromCached(VulnResolutionRequest request, CachedResolution cached)
{
var evidence = BuildEvidence(cached);
var evidence = BuildEvidence(request, cached);
return new VulnResolutionResponse
{
@@ -161,20 +172,152 @@ public sealed class CachedResolutionService : IResolutionService
};
}
private static ResolutionEvidence? BuildEvidence(CachedResolution cached)
private static ResolutionEvidence? BuildEvidence(VulnResolutionRequest request, CachedResolution cached)
{
if (string.IsNullOrWhiteSpace(cached.MatchType) && cached.Confidence <= 0m)
{
return null;
}
return new ResolutionEvidence
var matchType = string.IsNullOrWhiteSpace(cached.MatchType)
? ResolutionMatchTypes.Unknown
: cached.MatchType;
var evidence = new ResolutionEvidence
{
MatchType = string.IsNullOrWhiteSpace(cached.MatchType)
? ResolutionMatchTypes.Unknown
: cached.MatchType,
Confidence = cached.Confidence
MatchType = matchType,
Confidence = cached.Confidence,
FixConfidence = cached.Confidence
};
return evidence with
{
HybridDiff = BuildHybridDiffEvidence(request, matchType, cached.Confidence, cached.Status)
};
}
private static HybridDiffEvidence BuildHybridDiffEvidence(
VulnResolutionRequest request,
string matchType,
decimal confidence,
ResolutionStatus status)
{
var anchor = !string.IsNullOrWhiteSpace(request.CveId)
? $"cve:{request.CveId}"
: $"pkg:{request.Package}";
var identity = request.BuildId
?? request.Hashes?.FileSha256
?? request.Hashes?.TextSha256
?? request.Hashes?.Blake3
?? "unknown";
var semanticEditScript = new SemanticEditScriptArtifact
{
SchemaVersion = HybridSchemaVersion,
SourceTreeDigest = ComputeDigestString($"cache-source|{request.Package}|{identity}|{matchType}"),
Edits =
[
new SemanticEditRecord
{
StableId = ComputeDigestString($"cache-edit|{request.Package}|{anchor}|{status}"),
EditType = "update",
NodeKind = "method",
NodePath = anchor,
Anchor = anchor
}
]
};
var changeType = status switch
{
ResolutionStatus.NotAffected => "removed",
_ => "modified"
};
var preSize = confidence > 0m ? 1L : 0L;
var postSize = status switch
{
ResolutionStatus.Vulnerable => preSize,
ResolutionStatus.NotAffected => 0L,
_ => preSize + 1L
};
var deltaRef = ComputeDigestString($"cache-delta|{request.Package}|{anchor}|{preSize}|{postSize}|{status}");
var preHash = ComputeDigestString($"cache-pre|{request.Package}|{anchor}|{preSize}");
var postHash = ComputeDigestString($"cache-post|{request.Package}|{anchor}|{postSize}");
var symbolPatchPlan = new SymbolPatchPlanArtifact
{
SchemaVersion = HybridSchemaVersion,
BuildIdBefore = $"baseline:{identity}",
BuildIdAfter = identity,
EditsDigest = ComputeDigest(semanticEditScript),
SymbolMapDigestBefore = ComputeDigestString($"cache-symbol-map|old|{identity}|{anchor}"),
SymbolMapDigestAfter = ComputeDigestString($"cache-symbol-map|new|{identity}|{anchor}"),
Changes =
[
new SymbolPatchChange
{
Symbol = anchor,
ChangeType = changeType,
AstAnchors = [anchor],
PreHash = preHash,
PostHash = postHash,
DeltaRef = deltaRef
}
]
};
var patchManifest = new PatchManifestArtifact
{
SchemaVersion = HybridSchemaVersion,
BuildId = identity,
NormalizationRecipeId = HybridNormalizationRecipeId,
TotalDeltaBytes = Math.Abs(postSize - preSize),
Patches =
[
new SymbolPatchArtifact
{
Symbol = anchor,
AddressRange = "0x0-0x0",
DeltaDigest = deltaRef,
Pre = new PatchSizeHash
{
Size = preSize,
Hash = preHash
},
Post = new PatchSizeHash
{
Size = postSize,
Hash = postHash
},
DeltaSizeBytes = Math.Abs(postSize - preSize)
}
]
};
return new HybridDiffEvidence
{
SemanticEditScriptDigest = ComputeDigest(semanticEditScript),
OldSymbolMapDigest = ComputeDigestString($"cache-symbol-map|old-digest|{identity}|{anchor}"),
NewSymbolMapDigest = ComputeDigestString($"cache-symbol-map|new-digest|{identity}|{anchor}"),
SymbolPatchPlanDigest = ComputeDigest(symbolPatchPlan),
PatchManifestDigest = ComputeDigest(patchManifest),
SemanticEditScript = semanticEditScript,
SymbolPatchPlan = symbolPatchPlan,
PatchManifest = patchManifest
};
}
private static string ComputeDigest<T>(T value)
{
var json = JsonSerializer.Serialize(value, HybridDigestJsonOptions);
return ComputeDigestString(json);
}
private static string ComputeDigestString(string input)
{
var bytes = Encoding.UTF8.GetBytes(input);
var hash = SHA256.HashData(bytes);
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
private TimeSpan GetCacheTtl(ResolutionStatus status)
@@ -188,3 +331,4 @@ public sealed class CachedResolutionService : IResolutionService
};
}
}

View File

@@ -30,3 +30,5 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0129-T | DONE | Test coverage audit for StellaOps.BinaryIndex.WebService; revalidated 2026-01-06. |
| AUDIT-0129-A | TODO | Revalidated 2026-01-06; open findings pending apply. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| BHP-05-API-HYBRID-20260217 | DONE | SPRINT_20260216_001: cache wrapper now projects deterministic fallback hybridDiff evidence for cached responses consumed by Web UI. |

View File

@@ -169,6 +169,222 @@ public sealed record ResolutionEvidence
/// <summary>Detection method (security_feed, changelog, patch_header).</summary>
public string? FixMethod { get; init; }
/// <summary>Confidence score for fix determination (0.0-1.0).</summary>
public decimal? FixConfidence { get; init; }
/// <summary>Function-level change details when available.</summary>
public IReadOnlyList<FunctionChangeInfo>? ChangedFunctions { get; init; }
/// <summary>Hybrid source-symbol-binary diff evidence chain.</summary>
public HybridDiffEvidence? HybridDiff { get; init; }
}
/// <summary>
/// Information about a function that changed between versions.
/// </summary>
public sealed record FunctionChangeInfo
{
/// <summary>Function name or symbol identifier.</summary>
public required string Name { get; init; }
/// <summary>Type of change (Modified, Added, Removed, SignatureChanged).</summary>
public required string ChangeType { get; init; }
/// <summary>Similarity score between pre/post variants when available.</summary>
public decimal? Similarity { get; init; }
/// <summary>Offset of the vulnerable function bytes.</summary>
public long? VulnerableOffset { get; init; }
/// <summary>Offset of the patched function bytes.</summary>
public long? PatchedOffset { get; init; }
/// <summary>Vulnerable function size in bytes.</summary>
public long? VulnerableSize { get; init; }
/// <summary>Patched function size in bytes.</summary>
public long? PatchedSize { get; init; }
/// <summary>Optional vulnerable disassembly excerpt.</summary>
public IReadOnlyList<string>? VulnerableDisasm { get; init; }
/// <summary>Optional patched disassembly excerpt.</summary>
public IReadOnlyList<string>? PatchedDisasm { get; init; }
}
/// <summary>
/// Hybrid evidence bundle linking semantic edits, symbol patch plan, and patch manifest.
/// </summary>
public sealed record HybridDiffEvidence
{
/// <summary>Digest of semantic edit script artifact.</summary>
public string? SemanticEditScriptDigest { get; init; }
/// <summary>Digest of old symbol map artifact.</summary>
public string? OldSymbolMapDigest { get; init; }
/// <summary>Digest of new symbol map artifact.</summary>
public string? NewSymbolMapDigest { get; init; }
/// <summary>Digest of symbol patch plan artifact.</summary>
public string? SymbolPatchPlanDigest { get; init; }
/// <summary>Digest of patch manifest artifact.</summary>
public string? PatchManifestDigest { get; init; }
/// <summary>Semantic edit script artifact.</summary>
public SemanticEditScriptArtifact? SemanticEditScript { get; init; }
/// <summary>Symbol patch plan artifact.</summary>
public SymbolPatchPlanArtifact? SymbolPatchPlan { get; init; }
/// <summary>Patch manifest artifact.</summary>
public PatchManifestArtifact? PatchManifest { get; init; }
}
/// <summary>
/// Semantic edit script artifact.
/// </summary>
public sealed record SemanticEditScriptArtifact
{
/// <summary>Artifact schema version.</summary>
public string? SchemaVersion { get; init; }
/// <summary>Source tree digest.</summary>
public string? SourceTreeDigest { get; init; }
/// <summary>Deterministic semantic edit records.</summary>
public IReadOnlyList<SemanticEditRecord>? Edits { get; init; }
}
/// <summary>
/// Single semantic edit entry.
/// </summary>
public sealed record SemanticEditRecord
{
/// <summary>Stable edit identifier.</summary>
public string? StableId { get; init; }
/// <summary>Edit type (add/remove/move/update/rename).</summary>
public string? EditType { get; init; }
/// <summary>Node kind (file/class/method/field/import/statement).</summary>
public string? NodeKind { get; init; }
/// <summary>Deterministic node path.</summary>
public string? NodePath { get; init; }
/// <summary>Symbol anchor.</summary>
public string? Anchor { get; init; }
}
/// <summary>
/// Symbol patch plan artifact.
/// </summary>
public sealed record SymbolPatchPlanArtifact
{
/// <summary>Artifact schema version.</summary>
public string? SchemaVersion { get; init; }
/// <summary>Build identifier before patch.</summary>
public string? BuildIdBefore { get; init; }
/// <summary>Build identifier after patch.</summary>
public string? BuildIdAfter { get; init; }
/// <summary>Semantic edit script digest link.</summary>
public string? EditsDigest { get; init; }
/// <summary>Old symbol map digest link.</summary>
public string? SymbolMapDigestBefore { get; init; }
/// <summary>New symbol map digest link.</summary>
public string? SymbolMapDigestAfter { get; init; }
/// <summary>Ordered symbol-level patch changes.</summary>
public IReadOnlyList<SymbolPatchChange>? Changes { get; init; }
}
/// <summary>
/// Single symbol patch plan entry.
/// </summary>
public sealed record SymbolPatchChange
{
/// <summary>Symbol name.</summary>
public required string Symbol { get; init; }
/// <summary>Change type (added/removed/modified/moved).</summary>
public required string ChangeType { get; init; }
/// <summary>Linked source anchors.</summary>
public IReadOnlyList<string>? AstAnchors { get; init; }
/// <summary>Pre-change hash digest.</summary>
public string? PreHash { get; init; }
/// <summary>Post-change hash digest.</summary>
public string? PostHash { get; init; }
/// <summary>Reference to delta payload digest.</summary>
public string? DeltaRef { get; init; }
}
/// <summary>
/// Patch manifest artifact.
/// </summary>
public sealed record PatchManifestArtifact
{
/// <summary>Artifact schema version.</summary>
public string? SchemaVersion { get; init; }
/// <summary>Build identifier for patched binary.</summary>
public string? BuildId { get; init; }
/// <summary>Normalization recipe identifier.</summary>
public string? NormalizationRecipeId { get; init; }
/// <summary>Total absolute delta bytes across patches.</summary>
public long? TotalDeltaBytes { get; init; }
/// <summary>Ordered per-symbol patch entries.</summary>
public IReadOnlyList<SymbolPatchArtifact>? Patches { get; init; }
}
/// <summary>
/// Per-symbol patch artifact entry.
/// </summary>
public sealed record SymbolPatchArtifact
{
/// <summary>Symbol name.</summary>
public required string Symbol { get; init; }
/// <summary>Address range in canonical hex format.</summary>
public string? AddressRange { get; init; }
/// <summary>Digest of the patch payload for this symbol.</summary>
public string? DeltaDigest { get; init; }
/// <summary>Pre-patch size/hash tuple.</summary>
public PatchSizeHash? Pre { get; init; }
/// <summary>Post-patch size/hash tuple.</summary>
public PatchSizeHash? Post { get; init; }
/// <summary>Absolute delta bytes for this symbol.</summary>
public long? DeltaSizeBytes { get; init; }
}
/// <summary>
/// Size/hash tuple used by patch manifest entries.
/// </summary>
public sealed record PatchSizeHash
{
/// <summary>Size in bytes.</summary>
public required long Size { get; init; }
/// <summary>Digest string.</summary>
public required string Hash { get; init; }
}
public static class ResolutionMatchTypes
@@ -246,3 +462,4 @@ public sealed record BatchVulnResolutionResponse
/// <summary>Processing time in milliseconds.</summary>
public long ProcessingTimeMs { get; init; }
}

View File

@@ -9,3 +9,5 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0115-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0115-A | DONE | Applied contract fixes + tests; revalidated 2026-01-06. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| BHP-05-API-HYBRID-20260217 | DONE | SPRINT_20260216_001: extended ResolutionEvidence contracts with changedFunctions/hybridDiff projection for UI evidence drawer parity. |

View File

@@ -7,6 +7,9 @@ using StellaOps.BinaryIndex.Contracts.Resolution;
using StellaOps.BinaryIndex.Core.Models;
using StellaOps.BinaryIndex.Core.Services;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace StellaOps.BinaryIndex.Core.Resolution;
@@ -80,6 +83,14 @@ public sealed class ResolutionService : IResolutionService
private readonly ResolutionServiceOptions _options;
private readonly ILogger<ResolutionService> _logger;
private readonly TimeProvider _timeProvider;
private static readonly JsonSerializerOptions HybridDigestJsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
private const string HybridSchemaVersion = "1.0.0";
private const string HybridNormalizationRecipeId = "stellaops-resolution-v1";
public ResolutionService(
IBinaryVulnerabilityService vulnerabilityService,
@@ -214,6 +225,10 @@ public sealed class ResolutionService : IResolutionService
ct);
var (status, evidence) = MapFixStatusToResolution(fixStatus);
if (evidence is not null)
{
evidence = EnrichEvidenceWithHybrid(request, evidence, status);
}
return new VulnResolutionResponse
{
@@ -253,13 +268,6 @@ public sealed class ResolutionService : IResolutionService
// Find the most severe/relevant match
var primaryMatch = matches.OrderByDescending(m => m.Confidence).First();
var evidence = new ResolutionEvidence
{
MatchType = MapMatchType(primaryMatch.Method),
Confidence = primaryMatch.Confidence,
MatchedFingerprintIds = matches.Select(m => m.CveId).ToList()
};
// Map to resolution status
var status = primaryMatch.Method switch
{
@@ -269,6 +277,29 @@ public sealed class ResolutionService : IResolutionService
_ => ResolutionStatus.Unknown
};
var matchedIds = matches
.Select(m => m.CveId)
.Where(static id => !string.IsNullOrWhiteSpace(id))
.Distinct(StringComparer.Ordinal)
.OrderBy(static id => id, StringComparer.Ordinal)
.ToList();
var changedFunctions = BuildChangedFunctions(matches);
var evidence = EnrichEvidenceWithHybrid(
request,
new ResolutionEvidence
{
MatchType = MapMatchType(primaryMatch.Method),
Confidence = primaryMatch.Confidence,
FixConfidence = primaryMatch.Confidence,
MatchedFingerprintIds = matchedIds,
ChangedFunctions = changedFunctions,
FunctionDiffSummary = BuildFunctionDiffSummary(changedFunctions),
SourcePackage = ExtractSourcePackage(primaryMatch.VulnerablePurl)
?? ExtractSourcePackage(request.Package)
},
status);
return new VulnResolutionResponse
{
Package = request.Package,
@@ -310,17 +341,33 @@ public sealed class ResolutionService : IResolutionService
var primaryMatch = matches.OrderByDescending(m => m.Confidence).First();
var evidence = new ResolutionEvidence
{
MatchType = ResolutionMatchTypes.Fingerprint,
Confidence = primaryMatch.Confidence,
MatchedFingerprintIds = matches.Select(m => m.CveId).ToList()
};
var status = primaryMatch.Confidence >= _options.MinConfidenceThreshold
? ResolutionStatus.Fixed
: ResolutionStatus.Unknown;
var matchedIds = matches
.Select(m => m.CveId)
.Where(static id => !string.IsNullOrWhiteSpace(id))
.Distinct(StringComparer.Ordinal)
.OrderBy(static id => id, StringComparer.Ordinal)
.ToList();
var changedFunctions = BuildChangedFunctions(matches);
var evidence = EnrichEvidenceWithHybrid(
request,
new ResolutionEvidence
{
MatchType = ResolutionMatchTypes.Fingerprint,
Confidence = primaryMatch.Confidence,
FixConfidence = primaryMatch.Confidence,
MatchedFingerprintIds = matchedIds,
ChangedFunctions = changedFunctions,
FunctionDiffSummary = BuildFunctionDiffSummary(changedFunctions),
SourcePackage = ExtractSourcePackage(primaryMatch.VulnerablePurl)
?? ExtractSourcePackage(request.Package)
},
status);
return new VulnResolutionResponse
{
Package = request.Package,
@@ -374,12 +421,284 @@ public sealed class ResolutionService : IResolutionService
{
MatchType = ResolutionMatchTypes.FixStatus,
Confidence = fixStatus.Confidence,
FixMethod = MapFixMethod(fixStatus.Method)
FixMethod = MapFixMethod(fixStatus.Method),
FixConfidence = fixStatus.Confidence
};
return (status, evidence);
}
private static ResolutionEvidence EnrichEvidenceWithHybrid(
VulnResolutionRequest request,
ResolutionEvidence evidence,
ResolutionStatus status)
{
var changedFunctions = evidence.ChangedFunctions?
.OrderBy(static f => f.Name, StringComparer.Ordinal)
.ToList();
var matchedIds = evidence.MatchedFingerprintIds?
.Where(static id => !string.IsNullOrWhiteSpace(id))
.Distinct(StringComparer.Ordinal)
.OrderBy(static id => id, StringComparer.Ordinal)
.ToList();
var normalizedEvidence = evidence with
{
MatchedFingerprintIds = matchedIds is { Count: > 0 } ? matchedIds : null,
ChangedFunctions = changedFunctions is { Count: > 0 } ? changedFunctions : null,
FunctionDiffSummary = string.IsNullOrWhiteSpace(evidence.FunctionDiffSummary)
? BuildFunctionDiffSummary(changedFunctions)
: evidence.FunctionDiffSummary,
FixConfidence = evidence.FixConfidence ?? evidence.Confidence
};
return normalizedEvidence with
{
HybridDiff = BuildHybridDiffEvidence(request, normalizedEvidence, status)
};
}
private static IReadOnlyList<FunctionChangeInfo>? BuildChangedFunctions(IEnumerable<BinaryVulnMatch> matches)
{
var changed = matches
.Select(m => new
{
Symbol = m.Evidence?.MatchedFunction,
Similarity = m.Evidence?.Similarity
})
.Where(static v => !string.IsNullOrWhiteSpace(v.Symbol))
.GroupBy(static v => v.Symbol!, StringComparer.Ordinal)
.OrderBy(static g => g.Key, StringComparer.Ordinal)
.Select(g =>
{
var similarities = g
.Select(v => v.Similarity)
.Where(static v => v.HasValue)
.Select(static v => v!.Value)
.ToList();
return new FunctionChangeInfo
{
Name = g.Key,
ChangeType = "Modified",
Similarity = similarities.Count > 0 ? similarities.Max() : null
};
})
.ToList();
return changed.Count > 0 ? changed : null;
}
private static string? BuildFunctionDiffSummary(IReadOnlyList<FunctionChangeInfo>? changedFunctions)
{
if (changedFunctions is null || changedFunctions.Count == 0)
{
return null;
}
var preview = string.Join(", ", changedFunctions.Take(3).Select(static f => f.Name));
var suffix = changedFunctions.Count > 3 ? ", ..." : string.Empty;
return $"{changedFunctions.Count} function changes: {preview}{suffix}";
}
private static HybridDiffEvidence BuildHybridDiffEvidence(
VulnResolutionRequest request,
ResolutionEvidence evidence,
ResolutionStatus status)
{
var changeSet = evidence.ChangedFunctions ?? [];
var anchors = changeSet
.Select(static f => f.Name)
.Where(static name => !string.IsNullOrWhiteSpace(name))
.Distinct(StringComparer.Ordinal)
.OrderBy(static name => name, StringComparer.Ordinal)
.ToList();
if (anchors.Count == 0 && evidence.MatchedFingerprintIds is { Count: > 0 })
{
anchors = evidence.MatchedFingerprintIds
.Where(static id => !string.IsNullOrWhiteSpace(id))
.Select(static id => $"cve:{id}")
.Distinct(StringComparer.Ordinal)
.OrderBy(static id => id, StringComparer.Ordinal)
.ToList();
}
if (anchors.Count == 0 && !string.IsNullOrWhiteSpace(request.CveId))
{
anchors.Add($"cve:{request.CveId}");
}
if (anchors.Count == 0)
{
anchors.Add($"pkg:{request.Package}");
}
var identity = request.BuildId
?? request.Hashes?.FileSha256
?? request.Hashes?.TextSha256
?? request.Hashes?.Blake3
?? "unknown";
var buildIdAfter = identity;
var buildIdBefore = $"baseline:{identity}";
var semanticEdits = anchors
.Select(anchor => new SemanticEditRecord
{
StableId = ComputeDigestString($"semantic|{request.Package}|{anchor}|{evidence.MatchType}|{status}"),
EditType = "update",
NodeKind = "method",
NodePath = anchor,
Anchor = anchor
})
.ToList();
var semanticEditScript = new SemanticEditScriptArtifact
{
SchemaVersion = HybridSchemaVersion,
SourceTreeDigest = ComputeDigestString($"source-tree|{request.Package}|{identity}|{evidence.MatchType}"),
Edits = semanticEdits
};
var changedByName = changeSet
.ToDictionary(static f => f.Name, StringComparer.Ordinal);
var symbolPatchChanges = anchors
.Select((anchor, index) =>
{
var symbol = anchor;
changedByName.TryGetValue(symbol, out var functionChange);
var preSize = Math.Max(0L, functionChange?.VulnerableSize ?? 0L);
var postSize = Math.Max(0L, functionChange?.PatchedSize ?? preSize);
if (postSize == 0 && preSize == 0 && string.Equals(evidence.MatchType, ResolutionMatchTypes.Fingerprint, StringComparison.Ordinal))
{
postSize = 1;
}
var deltaRef = ComputeDigestString($"delta|{request.Package}|{symbol}|{preSize}|{postSize}|{index}");
return new
{
Symbol = symbol,
ChangeType = NormalizeChangeType(functionChange?.ChangeType),
AstAnchors = new[] { symbol },
PreHash = ComputeDigestString($"pre|{request.Package}|{symbol}|{preSize}"),
PostHash = ComputeDigestString($"post|{request.Package}|{symbol}|{postSize}"),
DeltaRef = deltaRef,
PreSize = preSize,
PostSize = postSize,
Start = Math.Max(0L, functionChange?.PatchedOffset ?? functionChange?.VulnerableOffset ?? (index * 32L))
};
})
.OrderBy(static c => c.Symbol, StringComparer.Ordinal)
.ToList();
var symbolPatchPlan = new SymbolPatchPlanArtifact
{
SchemaVersion = HybridSchemaVersion,
BuildIdBefore = buildIdBefore,
BuildIdAfter = buildIdAfter,
EditsDigest = ComputeDigest(semanticEditScript),
SymbolMapDigestBefore = ComputeDigestString($"symbol-map|old|{buildIdBefore}|{string.Join('|', anchors)}"),
SymbolMapDigestAfter = ComputeDigestString($"symbol-map|new|{buildIdAfter}|{string.Join('|', anchors)}"),
Changes = symbolPatchChanges
.Select(c => new SymbolPatchChange
{
Symbol = c.Symbol,
ChangeType = c.ChangeType,
AstAnchors = c.AstAnchors,
PreHash = c.PreHash,
PostHash = c.PostHash,
DeltaRef = c.DeltaRef
})
.ToList()
};
var patches = symbolPatchChanges
.Select(c =>
{
var end = c.Start + Math.Max(0L, Math.Max(c.PreSize, c.PostSize)) - 1L;
var addressEnd = end < c.Start ? c.Start : end;
var deltaBytes = Math.Abs(c.PostSize - c.PreSize);
return new SymbolPatchArtifact
{
Symbol = c.Symbol,
AddressRange = $"0x{c.Start:X}-{addressEnd:X}",
DeltaDigest = c.DeltaRef,
Pre = new PatchSizeHash
{
Size = c.PreSize,
Hash = c.PreHash
},
Post = new PatchSizeHash
{
Size = c.PostSize,
Hash = c.PostHash
},
DeltaSizeBytes = deltaBytes
};
})
.ToList();
var patchManifest = new PatchManifestArtifact
{
SchemaVersion = HybridSchemaVersion,
BuildId = buildIdAfter,
NormalizationRecipeId = HybridNormalizationRecipeId,
TotalDeltaBytes = patches.Sum(static p => p.DeltaSizeBytes ?? 0L),
Patches = patches
};
var semanticDigest = ComputeDigest(semanticEditScript);
var oldSymbolMapDigest = ComputeDigestString($"symbol-map|old|{buildIdBefore}|{string.Join('|', anchors)}|{request.Package}");
var newSymbolMapDigest = ComputeDigestString($"symbol-map|new|{buildIdAfter}|{string.Join('|', anchors)}|{request.Package}");
var patchPlanDigest = ComputeDigest(symbolPatchPlan);
var patchManifestDigest = ComputeDigest(patchManifest);
return new HybridDiffEvidence
{
SemanticEditScriptDigest = semanticDigest,
OldSymbolMapDigest = oldSymbolMapDigest,
NewSymbolMapDigest = newSymbolMapDigest,
SymbolPatchPlanDigest = patchPlanDigest,
PatchManifestDigest = patchManifestDigest,
SemanticEditScript = semanticEditScript,
SymbolPatchPlan = symbolPatchPlan,
PatchManifest = patchManifest
};
}
private static string NormalizeChangeType(string? changeType)
{
if (string.IsNullOrWhiteSpace(changeType))
{
return "modified";
}
return changeType.Trim().ToLowerInvariant() switch
{
"modified" => "modified",
"added" => "added",
"removed" => "removed",
"signaturechanged" => "modified",
_ => "modified"
};
}
private static string ComputeDigest<T>(T value)
{
var json = JsonSerializer.Serialize(value, HybridDigestJsonOptions);
return ComputeDigestString(json);
}
private static string ComputeDigestString(string input)
{
var bytes = Encoding.UTF8.GetBytes(input);
var hash = SHA256.HashData(bytes);
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
private static string? ExtractDistro(string? distroRelease)
{
if (string.IsNullOrEmpty(distroRelease))
@@ -462,3 +781,4 @@ public sealed class ResolutionService : IResolutionService
|| !string.IsNullOrWhiteSpace(request.Hashes?.Blake3);
}
}

View File

@@ -10,3 +10,5 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0116-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0116-A | DONE | Applied core fixes + tests; revalidated 2026-01-06. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| BHP-05-API-HYBRID-20260217 | DONE | SPRINT_20260216_001: ResolutionService now emits deterministic hybrid diff evidence (live lookups + specific CVE flow) with targeted core tests. |

View File

@@ -119,6 +119,13 @@ public sealed record DeltaSigPredicate
[JsonPropertyName("largeBlobs")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyList<LargeBlobReference>? LargeBlobs { get; init; }
/// <summary>
/// Optional hybrid diff evidence bundle linking source edits, symbol maps,
/// and normalized patch manifests.
/// </summary>
[JsonPropertyName("hybridDiff")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public HybridDiffEvidence? HybridDiff { get; init; }
/// <summary>
/// Gets the old binary subject.
@@ -489,3 +496,4 @@ public sealed record LargeBlobReference
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public long? SizeBytes { get; init; }
}

View File

@@ -8,6 +8,7 @@
using Microsoft.Extensions.Logging;
using StellaOps.BinaryIndex.DeltaSig.Attestation;
using StellaOps.Symbols.Core.Models;
using System.Collections.Immutable;
using System.Security.Cryptography;
@@ -23,6 +24,7 @@ public sealed class DeltaSigService : IDeltaSigService
private readonly IDeltaSignatureMatcher _signatureMatcher;
private readonly ILogger<DeltaSigService> _logger;
private readonly TimeProvider _timeProvider;
private readonly IHybridDiffComposer _hybridDiffComposer;
/// <summary>
/// Initializes a new instance of the <see cref="DeltaSigService"/> class.
@@ -31,12 +33,14 @@ public sealed class DeltaSigService : IDeltaSigService
IDeltaSignatureGenerator signatureGenerator,
IDeltaSignatureMatcher signatureMatcher,
ILogger<DeltaSigService> logger,
IHybridDiffComposer? hybridDiffComposer = null,
TimeProvider? timeProvider = null)
{
_signatureGenerator = signatureGenerator ?? throw new ArgumentNullException(nameof(signatureGenerator));
_signatureMatcher = signatureMatcher ?? throw new ArgumentNullException(nameof(signatureMatcher));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
_hybridDiffComposer = hybridDiffComposer ?? new HybridDiffComposer();
}
/// <inheritdoc />
@@ -77,8 +81,28 @@ public sealed class DeltaSigService : IDeltaSigService
// 2. Compare signatures to find deltas
var comparison = await _signatureMatcher.CompareSignaturesAsync(oldSignature, newSignature, ct);
// 3. Build function deltas
var deltas = BuildFunctionDeltas(comparison, request.IncludeIrDiff, request.ComputeSemanticSimilarity);
// 3. Resolve symbol maps and build function deltas using real boundaries when available
var oldSymbolMap = ResolveSymbolMap(
role: "old",
providedMap: request.OldSymbolMap,
manifest: request.OldSymbolManifest,
signature: oldSignature,
binary: request.OldBinary);
var newSymbolMap = ResolveSymbolMap(
role: "new",
providedMap: request.NewSymbolMap,
manifest: request.NewSymbolManifest,
signature: newSignature,
binary: request.NewBinary);
var deltas = BuildFunctionDeltas(
comparison,
oldSignature,
newSignature,
oldSymbolMap,
newSymbolMap,
request.ComputeSemanticSimilarity);
// 4. Filter by patterns if specified
if (request.FunctionPatterns?.Count > 0 || request.ExcludePatterns?.Count > 0)
@@ -106,7 +130,19 @@ public sealed class DeltaSigService : IDeltaSigService
largeBlobs = BuildLargeBlobReferences(request.OldBinary, request.NewBinary);
}
// 8. Build predicate
// 8. Compose hybrid diff evidence bundle if requested
HybridDiffEvidence? hybridDiff = null;
if (request.IncludeHybridDiffEvidence)
{
hybridDiff = _hybridDiffComposer.Compose(
request.SourceDiffs,
oldSymbolMap,
newSymbolMap,
deltas,
oldSignature.Normalization.RecipeId);
}
// 9. Build predicate
var predicate = new DeltaSigPredicate
{
Subject = new[]
@@ -156,15 +192,17 @@ public sealed class DeltaSigService : IDeltaSigService
},
Metadata = request.Metadata,
SbomDigest = request.SbomDigest,
LargeBlobs = largeBlobs
LargeBlobs = largeBlobs,
HybridDiff = hybridDiff
};
_logger.LogInformation(
"Generated delta-sig with {DeltaCount} changes: {Added} added, {Removed} removed, {Modified} modified",
"Generated delta-sig with {DeltaCount} changes: {Added} added, {Removed} removed, {Modified} modified, hybrid={Hybrid}",
deltas.Count,
summary.FunctionsAdded,
summary.FunctionsRemoved,
summary.FunctionsModified);
summary.FunctionsModified,
hybridDiff is not null);
return predicate;
}
@@ -177,8 +215,6 @@ public sealed class DeltaSigService : IDeltaSigService
{
ArgumentNullException.ThrowIfNull(predicate);
ArgumentNullException.ThrowIfNull(newBinary);
var startTime = _timeProvider.GetUtcNow();
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
try
@@ -220,6 +256,14 @@ public sealed class DeltaSigService : IDeltaSigService
signatureRequest,
ct);
var hybridValidationError = ValidateHybridEvidence(predicate, signature, actualDigest);
if (hybridValidationError is not null)
{
return DeltaSigVerificationResult.Failure(
DeltaSigVerificationStatus.HybridEvidenceMismatch,
hybridValidationError);
}
// 3. Verify each declared function
var failures = new List<FunctionVerificationFailure>();
var undeclaredChanges = new List<UndeclaredChange>();
@@ -320,8 +364,26 @@ public sealed class DeltaSigService : IDeltaSigService
Stream newBinary,
CancellationToken ct = default)
{
// For now, delegate to single-binary verification
// Full implementation would verify both binaries match their respective subjects
ArgumentNullException.ThrowIfNull(predicate);
ArgumentNullException.ThrowIfNull(oldBinary);
ArgumentNullException.ThrowIfNull(newBinary);
var oldSubject = predicate.OldBinary;
if (oldSubject is null)
{
return DeltaSigVerificationResult.Failure(
DeltaSigVerificationStatus.InvalidPredicate,
"Predicate missing 'old' binary subject");
}
var oldDigest = await ComputeDigestAsync(oldBinary, ct);
if (!DigestsMatch(oldSubject.Digest, oldDigest))
{
return DeltaSigVerificationResult.Failure(
DeltaSigVerificationStatus.DigestMismatch,
$"Old binary digest mismatch: expected {FormatDigest(oldSubject.Digest)}, got {FormatDigest(oldDigest)}");
}
return await VerifyAsync(predicate, newBinary, ct);
}
@@ -384,6 +446,65 @@ public sealed class DeltaSigService : IDeltaSigService
$"Diff algorithm '{predicate.Tooling.DiffAlgorithm}' does not match required '{options.RequiredDiffAlgorithm}'");
}
var changedSymbols = (predicate.HybridDiff?.SymbolPatchPlan.Changes
.Select(c => c.Symbol)
?? predicate.Delta.Select(d => d.FunctionId))
.Distinct(StringComparer.Ordinal)
.OrderBy(v => v, StringComparer.Ordinal)
.ToList();
if (options.RequireHybridEvidence && predicate.HybridDiff is null)
{
violations.Add("Hybrid diff evidence is required but predicate.hybridDiff is missing");
}
if (options.RequireAstAnchors)
{
if (predicate.HybridDiff is null)
{
violations.Add("AST anchors are required but hybrid diff evidence is missing");
}
else
{
var symbolsWithoutAnchors = predicate.HybridDiff.SymbolPatchPlan.Changes
.Where(c => c.AstAnchors.Count == 0)
.Select(c => c.Symbol)
.OrderBy(v => v, StringComparer.Ordinal)
.ToList();
if (symbolsWithoutAnchors.Count > 0)
{
violations.Add($"{symbolsWithoutAnchors.Count} symbols missing AST anchors: {string.Join(", ", symbolsWithoutAnchors)}");
}
}
}
if (options.MaxPatchManifestDeltaBytes is { } maxPatchBytes)
{
if (predicate.HybridDiff is null)
{
violations.Add("Patch manifest byte budget was configured but hybrid diff evidence is missing");
}
else if (predicate.HybridDiff.PatchManifest.TotalDeltaBytes > maxPatchBytes)
{
violations.Add(
$"Patch manifest changed {predicate.HybridDiff.PatchManifest.TotalDeltaBytes} bytes; max allowed is {maxPatchBytes}");
}
}
foreach (var symbol in changedSymbols)
{
if (MatchesAnyPrefix(symbol, options.DeniedSymbolPrefixes))
{
violations.Add($"Denied symbol prefix matched changed symbol '{symbol}'");
}
if (MatchesAnyPrefix(symbol, options.ProtectedSymbolPrefixes))
{
violations.Add($"Protected symbol '{symbol}' must remain unchanged");
}
}
var details = new Dictionary<string, object>
{
["functionsModified"] = predicate.Summary.FunctionsModified,
@@ -392,15 +513,30 @@ public sealed class DeltaSigService : IDeltaSigService
["totalBytesChanged"] = predicate.Summary.TotalBytesChanged,
["minSemanticSimilarity"] = predicate.Summary.MinSemanticSimilarity,
["lifter"] = predicate.Tooling.Lifter,
["diffAlgorithm"] = predicate.Tooling.DiffAlgorithm
["diffAlgorithm"] = predicate.Tooling.DiffAlgorithm,
["hasHybridDiff"] = predicate.HybridDiff is not null,
["changedSymbolCount"] = changedSymbols.Count
};
if (violations.Count == 0)
if (predicate.HybridDiff is not null)
{
details["patchManifestBuildId"] = predicate.HybridDiff.PatchManifest.BuildId;
details["patchManifestTotalDeltaBytes"] = predicate.HybridDiff.PatchManifest.TotalDeltaBytes;
details["symbolPatchChanges"] = predicate.HybridDiff.SymbolPatchPlan.Changes.Count;
details["semanticEditCount"] = predicate.HybridDiff.SemanticEditScript.Edits.Count;
}
var orderedViolations = violations
.Distinct(StringComparer.Ordinal)
.OrderBy(v => v, StringComparer.Ordinal)
.ToList();
if (orderedViolations.Count == 0)
{
return DeltaSigPolicyResult.Pass(details);
}
return DeltaSigPolicyResult.Fail(violations, details);
return DeltaSigPolicyResult.Fail(orderedViolations, details);
}
private static DeltaSignatureRequest CreateSignatureRequest(DeltaSigRequest request, string state)
@@ -430,42 +566,82 @@ public sealed class DeltaSigService : IDeltaSigService
};
}
private List<FunctionDelta> BuildFunctionDeltas(
private static List<FunctionDelta> BuildFunctionDeltas(
DeltaComparisonResult comparison,
bool includeIrDiff,
DeltaSignature oldSignature,
DeltaSignature newSignature,
SymbolMap oldSymbolMap,
SymbolMap newSymbolMap,
bool includeSemanticSimilarity)
{
var deltas = new List<FunctionDelta>();
foreach (var result in comparison.SymbolResults)
{
if (result.ChangeType == SymbolChangeType.Unchanged)
{
continue;
}
var oldSignatureByName = oldSignature.Symbols
.ToDictionary(s => s.Name, StringComparer.Ordinal);
var newSignatureByName = newSignature.Symbols
.ToDictionary(s => s.Name, StringComparer.Ordinal);
var delta = new FunctionDelta
var oldSymbolIndex = BuildSymbolIndex(oldSymbolMap);
var newSymbolIndex = BuildSymbolIndex(newSymbolMap);
foreach (var result in comparison.SymbolResults
.Where(r => r.ChangeType != SymbolChangeType.Unchanged)
.OrderBy(r => r.SymbolName, StringComparer.Ordinal))
{
oldSignatureByName.TryGetValue(result.SymbolName, out var oldSymbolSignature);
newSignatureByName.TryGetValue(result.SymbolName, out var newSymbolSignature);
oldSymbolIndex.TryGetValue(result.SymbolName, out var oldMapEntry);
newSymbolIndex.TryGetValue(result.SymbolName, out var newMapEntry);
var changeType = result.ChangeType switch
{
FunctionId = result.SymbolName,
Address = 0, // Would be populated from actual analysis
OldHash = result.FromHash,
NewHash = result.ToHash,
OldSize = result.ChangeType == SymbolChangeType.Added ? 0 : result.ChunksTotal * 2048L,
NewSize = result.ChangeType == SymbolChangeType.Removed ? 0 : (result.ChunksTotal + result.SizeDelta / 2048) * 2048L,
DiffLen = result.SizeDelta != 0 ? Math.Abs(result.SizeDelta) : null,
ChangeType = result.ChangeType switch
{
SymbolChangeType.Added => "added",
SymbolChangeType.Removed => "removed",
SymbolChangeType.Modified or SymbolChangeType.Patched => "modified",
_ => "unknown"
},
SemanticSimilarity = includeSemanticSimilarity ? result.Confidence : null,
OldBlockCount = result.CfgBlockDelta.HasValue ? (int?)Math.Max(0, 10 - result.CfgBlockDelta.Value) : null,
NewBlockCount = result.CfgBlockDelta.HasValue ? (int?)10 : null
SymbolChangeType.Added => "added",
SymbolChangeType.Removed => "removed",
SymbolChangeType.Modified or SymbolChangeType.Patched => "modified",
_ => "unknown"
};
deltas.Add(delta);
var oldSize = changeType == "added"
? 0
: ResolveSymbolSize(oldSymbolSignature, oldMapEntry, result, usePositiveSizeDelta: false);
var newSize = changeType == "removed"
? 0
: ResolveSymbolSize(newSymbolSignature, newMapEntry, result, usePositiveSizeDelta: true);
var oldHash = changeType == "added"
? null
: result.FromHash ?? oldSymbolSignature?.HashHex;
var newHash = changeType == "removed"
? null
: result.ToHash ?? newSymbolSignature?.HashHex;
var diffLen = ResolveDiffLength(result.SizeDelta, oldSize, newSize, oldHash, newHash);
var address = checked((long)(newMapEntry?.AddressStart ?? oldMapEntry?.AddressStart ?? 0UL));
var section = newMapEntry?.Section
?? oldMapEntry?.Section
?? newSymbolSignature?.Scope
?? oldSymbolSignature?.Scope
?? ".text";
deltas.Add(new FunctionDelta
{
FunctionId = result.SymbolName,
Address = address,
OldHash = oldHash,
NewHash = newHash,
OldSize = oldSize,
NewSize = newSize,
DiffLen = diffLen,
ChangeType = changeType,
SemanticSimilarity = includeSemanticSimilarity && result.Confidence > 0
? result.Confidence
: null,
Section = section,
OldBlockCount = oldSymbolSignature?.CfgBbCount,
NewBlockCount = newSymbolSignature?.CfgBbCount
});
}
return deltas;
@@ -525,6 +701,331 @@ public sealed class DeltaSigService : IDeltaSigService
};
}
private SymbolMap ResolveSymbolMap(
string role,
SymbolMap? providedMap,
SymbolManifest? manifest,
DeltaSignature signature,
BinaryReference binary)
{
if (providedMap is not null)
{
return providedMap.BinaryDigest is null
? providedMap with { BinaryDigest = GetDigestWithPrefix(binary.Digest) }
: providedMap;
}
if (manifest is not null)
{
return _hybridDiffComposer.BuildSymbolMap(manifest, GetDigestWithPrefix(binary.Digest));
}
return _hybridDiffComposer.BuildFallbackSymbolMap(signature, binary, role);
}
private string? ValidateHybridEvidence(
DeltaSigPredicate predicate,
DeltaSignature signature,
IReadOnlyDictionary<string, string> actualDigest)
{
var hybrid = predicate.HybridDiff;
if (hybrid is null)
{
return null;
}
var scriptDigest = _hybridDiffComposer.ComputeDigest(hybrid.SemanticEditScript);
if (!string.Equals(scriptDigest, hybrid.SemanticEditScriptDigest, StringComparison.Ordinal))
{
return "Hybrid semantic_edit_script digest mismatch";
}
var oldMapDigest = _hybridDiffComposer.ComputeDigest(hybrid.OldSymbolMap);
if (!string.Equals(oldMapDigest, hybrid.OldSymbolMapDigest, StringComparison.Ordinal))
{
return "Hybrid old symbol_map digest mismatch";
}
var newMapDigest = _hybridDiffComposer.ComputeDigest(hybrid.NewSymbolMap);
if (!string.Equals(newMapDigest, hybrid.NewSymbolMapDigest, StringComparison.Ordinal))
{
return "Hybrid new symbol_map digest mismatch";
}
var patchPlanDigest = _hybridDiffComposer.ComputeDigest(hybrid.SymbolPatchPlan);
if (!string.Equals(patchPlanDigest, hybrid.SymbolPatchPlanDigest, StringComparison.Ordinal))
{
return "Hybrid symbol_patch_plan digest mismatch";
}
var patchManifestDigest = _hybridDiffComposer.ComputeDigest(hybrid.PatchManifest);
if (!string.Equals(patchManifestDigest, hybrid.PatchManifestDigest, StringComparison.Ordinal))
{
return "Hybrid patch_manifest digest mismatch";
}
if (!string.Equals(hybrid.SymbolPatchPlan.EditsDigest, hybrid.SemanticEditScriptDigest, StringComparison.Ordinal) ||
!string.Equals(hybrid.SymbolPatchPlan.SymbolMapDigestBefore, hybrid.OldSymbolMapDigest, StringComparison.Ordinal) ||
!string.Equals(hybrid.SymbolPatchPlan.SymbolMapDigestAfter, hybrid.NewSymbolMapDigest, StringComparison.Ordinal))
{
return "Hybrid symbol_patch_plan linkage digests are inconsistent";
}
if (!string.Equals(hybrid.SymbolPatchPlan.BuildIdBefore, hybrid.OldSymbolMap.BuildId, StringComparison.Ordinal) ||
!string.Equals(hybrid.SymbolPatchPlan.BuildIdAfter, hybrid.NewSymbolMap.BuildId, StringComparison.Ordinal) ||
!string.Equals(hybrid.PatchManifest.BuildId, hybrid.NewSymbolMap.BuildId, StringComparison.Ordinal))
{
return "Hybrid build-id linkage mismatch across symbol maps and manifests";
}
if (!string.IsNullOrWhiteSpace(hybrid.NewSymbolMap.BinaryDigest))
{
var expectedDigest = ParseDigestString(hybrid.NewSymbolMap.BinaryDigest!);
if (!DigestsMatch(expectedDigest, actualDigest))
{
return "Hybrid new symbol map binary digest does not match verified binary digest";
}
}
var patchBySymbol = hybrid.PatchManifest.Patches
.ToDictionary(p => p.Symbol, StringComparer.Ordinal);
var newSymbolIndex = BuildSymbolIndex(hybrid.NewSymbolMap);
var oldSymbolIndex = BuildSymbolIndex(hybrid.OldSymbolMap);
var signatureIndex = BuildSignatureIndex(signature);
foreach (var change in hybrid.SymbolPatchPlan.Changes.OrderBy(c => c.Symbol, StringComparer.Ordinal))
{
if (!patchBySymbol.TryGetValue(change.Symbol, out var patch))
{
return $"Hybrid patch manifest missing symbol '{change.Symbol}' from patch plan";
}
if (change.ChangeType is not "removed")
{
if (signatureIndex.TryGetValue(change.Symbol, out var symbolSignature))
{
if (!string.IsNullOrWhiteSpace(change.PostHash) &&
!HashesEqual(change.PostHash!, symbolSignature.HashHex))
{
return $"Hybrid post-hash mismatch for symbol '{change.Symbol}'";
}
if (!HashesEqual(patch.Post.Hash, symbolSignature.HashHex))
{
return $"Hybrid patch manifest post hash mismatch for symbol '{change.Symbol}'";
}
}
}
if (!TryParseAddressRange(patch.AddressRange, out var rangeStart, out var rangeEnd))
{
return $"Hybrid patch manifest has invalid address range for symbol '{change.Symbol}'";
}
var rangeMap = change.ChangeType == "removed" ? oldSymbolIndex : newSymbolIndex;
if (rangeMap.TryGetValue(change.Symbol, out var mapEntry))
{
if (rangeStart < mapEntry.AddressStart || rangeEnd > mapEntry.AddressEnd)
{
return $"Hybrid patch range for symbol '{change.Symbol}' exceeds declared symbol boundaries";
}
}
}
return null;
}
private static IReadOnlyDictionary<string, SymbolMapEntry> BuildSymbolIndex(SymbolMap symbolMap)
{
var index = new Dictionary<string, SymbolMapEntry>(StringComparer.Ordinal);
foreach (var symbol in symbolMap.Symbols)
{
foreach (var alias in EnumerateSymbolAliases(symbol.Name))
{
if (!index.ContainsKey(alias))
{
index[alias] = symbol;
}
}
}
return index;
}
private static IReadOnlyDictionary<string, SymbolSignature> BuildSignatureIndex(DeltaSignature signature)
{
var index = new Dictionary<string, SymbolSignature>(StringComparer.Ordinal);
foreach (var symbol in signature.Symbols)
{
foreach (var alias in EnumerateSymbolAliases(symbol.Name))
{
if (!index.ContainsKey(alias))
{
index[alias] = symbol;
}
}
}
return index;
}
private static IEnumerable<string> EnumerateSymbolAliases(string symbol)
{
if (!string.IsNullOrWhiteSpace(symbol))
{
yield return symbol;
var typeSeparator = symbol.LastIndexOf("::", StringComparison.Ordinal);
if (typeSeparator >= 0 && typeSeparator + 2 < symbol.Length)
{
yield return symbol[(typeSeparator + 2)..];
}
var dotSeparator = symbol.LastIndexOf('.');
if (dotSeparator >= 0 && dotSeparator + 1 < symbol.Length)
{
yield return symbol[(dotSeparator + 1)..];
}
}
}
private static long ResolveSymbolSize(
SymbolSignature? signature,
SymbolMapEntry? mapEntry,
SymbolMatchResult result,
bool usePositiveSizeDelta)
{
if (signature is not null && signature.SizeBytes > 0)
{
return signature.SizeBytes;
}
if (mapEntry is not null && mapEntry.Size > 0)
{
return mapEntry.Size;
}
if (result.SizeDelta != 0)
{
var directionalSize = usePositiveSizeDelta
? Math.Max(0, result.SizeDelta)
: Math.Max(0, -result.SizeDelta);
if (directionalSize > 0)
{
return directionalSize;
}
}
return result.ChunksTotal > 0 ? result.ChunksTotal * 2048L : 0L;
}
private static long? ResolveDiffLength(
int sizeDelta,
long oldSize,
long newSize,
string? oldHash,
string? newHash)
{
if (sizeDelta != 0)
{
return Math.Abs((long)sizeDelta);
}
if (oldSize != newSize)
{
return Math.Abs(newSize - oldSize);
}
if (!string.IsNullOrWhiteSpace(oldHash) &&
!string.IsNullOrWhiteSpace(newHash) &&
!HashesEqual(oldHash, newHash))
{
return Math.Max(oldSize, newSize);
}
return null;
}
private static bool TryParseAddressRange(string range, out ulong start, out ulong end)
{
start = 0;
end = 0;
var parts = range.Split('-', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 2)
{
return false;
}
return ulong.TryParse(parts[0].Replace("0x", string.Empty, StringComparison.OrdinalIgnoreCase), System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out start) &&
ulong.TryParse(parts[1].Replace("0x", string.Empty, StringComparison.OrdinalIgnoreCase), System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out end) &&
end >= start;
}
private static bool MatchesAnyPrefix(string symbol, IReadOnlyList<string>? prefixes)
{
if (prefixes is null || prefixes.Count == 0)
{
return false;
}
foreach (var prefix in prefixes)
{
if (!string.IsNullOrWhiteSpace(prefix) && symbol.StartsWith(prefix, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
private static bool HashesEqual(string left, string right)
{
return string.Equals(StripDigestPrefix(left), StripDigestPrefix(right), StringComparison.OrdinalIgnoreCase);
}
private static string StripDigestPrefix(string digest)
{
var separator = digest.IndexOf(':', StringComparison.Ordinal);
return separator >= 0 ? digest[(separator + 1)..] : digest;
}
private static IReadOnlyDictionary<string, string> ParseDigestString(string digest)
{
var separator = digest.IndexOf(':', StringComparison.Ordinal);
if (separator <= 0 || separator == digest.Length - 1)
{
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["sha256"] = StripDigestPrefix(digest)
};
}
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
[digest[..separator]] = digest[(separator + 1)..]
};
}
private static string? GetDigestWithPrefix(IReadOnlyDictionary<string, string> digests)
{
if (digests.TryGetValue("sha256", out var sha256))
{
var normalized = StripDigestPrefix(sha256);
return $"sha256:{normalized}";
}
var first = digests.FirstOrDefault();
if (string.IsNullOrWhiteSpace(first.Key) || string.IsNullOrWhiteSpace(first.Value))
{
return null;
}
return $"{first.Key}:{StripDigestPrefix(first.Value)}";
}
private static async Task<IReadOnlyDictionary<string, string>> ComputeDigestAsync(
Stream stream,
CancellationToken ct)
@@ -615,3 +1116,13 @@ public sealed class DeltaSigService : IDeltaSigService
return blobs;
}
}

View File

@@ -92,11 +92,16 @@ public sealed class DeltaSignatureGenerator : IDeltaSignatureGenerator
// Get all symbols
var symbols = plugin.GetSymbols(binary).ToDictionary(s => s.Name);
// Generate signatures for each target symbol. Empty target list means "all symbols".
var targetSymbols = request.TargetSymbols.Count == 0
? symbols.Keys.OrderBy(v => v, StringComparer.Ordinal).ToArray()
: request.TargetSymbols;
// Generate signatures for each target symbol
var symbolSignatures = new List<SymbolSignature>();
var appliedSteps = new List<string>();
foreach (var symbolName in request.TargetSymbols)
foreach (var symbolName in targetSymbols)
{
ct.ThrowIfCancellationRequested();
@@ -486,3 +491,4 @@ public sealed class DeltaSignatureGenerator : IDeltaSignatureGenerator
};
}
}

View File

@@ -0,0 +1,620 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under BUSL-1.1. See LICENSE in the project root.
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using StellaOps.Symbols.Core.Models;
namespace StellaOps.BinaryIndex.DeltaSig;
/// <summary>
/// Builder for deterministic hybrid diff artifacts.
/// </summary>
public interface IHybridDiffComposer
{
/// <summary>
/// Generates semantic edits from source file pairs.
/// </summary>
SemanticEditScript GenerateSemanticEditScript(IReadOnlyList<SourceFileDiff>? sourceDiffs);
/// <summary>
/// Builds a canonical symbol map from a symbol manifest.
/// </summary>
SymbolMap BuildSymbolMap(SymbolManifest manifest, string? binaryDigest = null);
/// <summary>
/// Builds a deterministic fallback map from signature symbols when debug data is unavailable.
/// </summary>
SymbolMap BuildFallbackSymbolMap(DeltaSignature signature, BinaryReference binary, string role);
/// <summary>
/// Builds symbol patch plan by linking edits and symbol-level deltas.
/// </summary>
SymbolPatchPlan BuildSymbolPatchPlan(
SemanticEditScript editScript,
SymbolMap oldSymbolMap,
SymbolMap newSymbolMap,
IReadOnlyList<Attestation.FunctionDelta> deltas);
/// <summary>
/// Builds normalized patch manifest from function deltas.
/// </summary>
PatchManifest BuildPatchManifest(
string buildId,
string normalizationRecipeId,
IReadOnlyList<Attestation.FunctionDelta> deltas);
/// <summary>
/// Composes all hybrid diff artifacts into one evidence object.
/// </summary>
HybridDiffEvidence Compose(
IReadOnlyList<SourceFileDiff>? sourceDiffs,
SymbolMap oldSymbolMap,
SymbolMap newSymbolMap,
IReadOnlyList<Attestation.FunctionDelta> deltas,
string normalizationRecipeId);
/// <summary>
/// Computes deterministic digest of a serializable value.
/// </summary>
string ComputeDigest<T>(T value);
}
/// <summary>
/// Deterministic implementation of hybrid diff composition.
/// </summary>
public sealed class HybridDiffComposer : IHybridDiffComposer
{
private static readonly JsonSerializerOptions DigestJsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
private static readonly HashSet<string> ControlKeywords =
[
"if",
"for",
"while",
"switch",
"catch",
"return",
"sizeof"
];
private static readonly Regex FunctionAnchorRegex = new(
@"(?<name>[A-Za-z_][A-Za-z0-9_:\.]*)\s*\(",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
/// <inheritdoc />
public SemanticEditScript GenerateSemanticEditScript(IReadOnlyList<SourceFileDiff>? sourceDiffs)
{
var diffs = (sourceDiffs ?? Array.Empty<SourceFileDiff>())
.OrderBy(d => NormalizePath(d.Path), StringComparer.Ordinal)
.ToList();
var edits = new List<SemanticEdit>();
var treeMaterial = new StringBuilder();
foreach (var diff in diffs)
{
var normalizedPath = NormalizePath(diff.Path);
var before = diff.BeforeContent ?? string.Empty;
var after = diff.AfterContent ?? string.Empty;
var beforeDigest = ComputeDigest(before);
var afterDigest = ComputeDigest(after);
treeMaterial
.Append(normalizedPath)
.Append('|')
.Append(beforeDigest)
.Append('|')
.Append(afterDigest)
.Append('\n');
if (string.Equals(beforeDigest, afterDigest, StringComparison.Ordinal))
{
continue;
}
var beforeSymbols = ExtractSymbolBlocks(before);
var afterSymbols = ExtractSymbolBlocks(after);
if (beforeSymbols.Count == 0 && afterSymbols.Count == 0)
{
edits.Add(CreateFileEdit(normalizedPath, beforeDigest, afterDigest));
continue;
}
foreach (var symbol in beforeSymbols.Keys.Except(afterSymbols.Keys, StringComparer.Ordinal).OrderBy(v => v, StringComparer.Ordinal))
{
var pre = beforeSymbols[symbol];
edits.Add(CreateSymbolEdit(
normalizedPath,
symbol,
"remove",
pre.Hash,
null,
new SourceSpan { StartLine = pre.StartLine, EndLine = pre.EndLine },
null));
}
foreach (var symbol in afterSymbols.Keys.Except(beforeSymbols.Keys, StringComparer.Ordinal).OrderBy(v => v, StringComparer.Ordinal))
{
var post = afterSymbols[symbol];
edits.Add(CreateSymbolEdit(
normalizedPath,
symbol,
"add",
null,
post.Hash,
null,
new SourceSpan { StartLine = post.StartLine, EndLine = post.EndLine }));
}
foreach (var symbol in beforeSymbols.Keys.Intersect(afterSymbols.Keys, StringComparer.Ordinal).OrderBy(v => v, StringComparer.Ordinal))
{
var pre = beforeSymbols[symbol];
var post = afterSymbols[symbol];
if (!string.Equals(pre.Hash, post.Hash, StringComparison.Ordinal))
{
edits.Add(CreateSymbolEdit(
normalizedPath,
symbol,
"update",
pre.Hash,
post.Hash,
new SourceSpan { StartLine = pre.StartLine, EndLine = pre.EndLine },
new SourceSpan { StartLine = post.StartLine, EndLine = post.EndLine }));
}
}
}
var orderedEdits = edits
.OrderBy(e => e.NodePath, StringComparer.Ordinal)
.ThenBy(e => e.EditType, StringComparer.Ordinal)
.ToList();
return new SemanticEditScript
{
SourceTreeDigest = ComputeDigest(treeMaterial.ToString()),
Edits = orderedEdits
};
}
/// <inheritdoc />
public SymbolMap BuildSymbolMap(SymbolManifest manifest, string? binaryDigest = null)
{
ArgumentNullException.ThrowIfNull(manifest);
var sourcePathByCompiled = (manifest.SourceMappings ?? Array.Empty<SourceMapping>())
.GroupBy(m => m.CompiledPath, StringComparer.Ordinal)
.ToDictionary(g => g.Key, g => g.First().SourcePath, StringComparer.Ordinal);
var symbols = manifest.Symbols
.OrderBy(s => s.Address)
.ThenBy(s => s.MangledName, StringComparer.Ordinal)
.Select(s =>
{
var size = s.Size == 0 ? 1UL : s.Size;
var mappedPath = ResolveSourcePath(s.SourceFile, sourcePathByCompiled);
var ranges = mappedPath is null || s.SourceLine is null
? null
: new[]
{
new SourceRange
{
File = NormalizePath(mappedPath),
LineStart = s.SourceLine.Value,
LineEnd = s.SourceLine.Value
}
};
return new SymbolMapEntry
{
Name = string.IsNullOrWhiteSpace(s.DemangledName) ? s.MangledName : s.DemangledName,
Kind = MapSymbolKind(s.Type),
AddressStart = s.Address,
AddressEnd = s.Address + size - 1UL,
Section = ".text",
SourceRanges = ranges
};
})
.ToList();
return new SymbolMap
{
BuildId = manifest.DebugId,
BinaryDigest = binaryDigest,
AddressSource = "manifest",
Symbols = symbols
};
}
/// <inheritdoc />
public SymbolMap BuildFallbackSymbolMap(DeltaSignature signature, BinaryReference binary, string role)
{
ArgumentNullException.ThrowIfNull(signature);
ArgumentNullException.ThrowIfNull(binary);
var sha = GetDigestString(binary.Digest);
var buildId = string.IsNullOrWhiteSpace(sha)
? $"{role}-fallback"
: $"{role}:{sha[..Math.Min(16, sha.Length)]}";
ulong nextAddress = string.Equals(role, "old", StringComparison.OrdinalIgnoreCase)
? 0x100000UL
: 0x200000UL;
var symbols = new List<SymbolMapEntry>();
foreach (var symbol in signature.Symbols.OrderBy(s => s.Name, StringComparer.Ordinal))
{
var size = symbol.SizeBytes <= 0 ? 1UL : (ulong)symbol.SizeBytes;
var start = nextAddress;
var end = start + size - 1UL;
symbols.Add(new SymbolMapEntry
{
Name = symbol.Name,
Kind = "function",
AddressStart = start,
AddressEnd = end,
Section = symbol.Scope,
SourceRanges = null
});
var aligned = ((size + 15UL) / 16UL) * 16UL;
nextAddress += aligned;
}
return new SymbolMap
{
BuildId = buildId,
BinaryDigest = string.IsNullOrWhiteSpace(sha) ? null : $"sha256:{sha}",
AddressSource = "synthetic-signature",
Symbols = symbols
};
}
/// <inheritdoc />
public SymbolPatchPlan BuildSymbolPatchPlan(
SemanticEditScript editScript,
SymbolMap oldSymbolMap,
SymbolMap newSymbolMap,
IReadOnlyList<Attestation.FunctionDelta> deltas)
{
ArgumentNullException.ThrowIfNull(editScript);
ArgumentNullException.ThrowIfNull(oldSymbolMap);
ArgumentNullException.ThrowIfNull(newSymbolMap);
ArgumentNullException.ThrowIfNull(deltas);
var editsDigest = ComputeDigest(editScript);
var oldMapDigest = ComputeDigest(oldSymbolMap);
var newMapDigest = ComputeDigest(newSymbolMap);
var changes = deltas
.OrderBy(d => d.FunctionId, StringComparer.Ordinal)
.Select(delta =>
{
var anchors = editScript.Edits
.Where(e => IsAnchorMatch(e.Anchor, delta.FunctionId))
.Select(e => e.Anchor)
.Distinct(StringComparer.Ordinal)
.OrderBy(v => v, StringComparer.Ordinal)
.ToList();
if (anchors.Count == 0)
{
anchors.Add(delta.FunctionId);
}
return new SymbolPatchChange
{
Symbol = delta.FunctionId,
ChangeType = delta.ChangeType,
AstAnchors = anchors,
PreHash = delta.OldHash,
PostHash = delta.NewHash,
DeltaRef = "sha256:" + ComputeDigest($"{delta.FunctionId}|{delta.OldHash}|{delta.NewHash}|{delta.OldSize}|{delta.NewSize}")
};
})
.ToList();
return new SymbolPatchPlan
{
BuildIdBefore = oldSymbolMap.BuildId,
BuildIdAfter = newSymbolMap.BuildId,
EditsDigest = editsDigest,
SymbolMapDigestBefore = oldMapDigest,
SymbolMapDigestAfter = newMapDigest,
Changes = changes
};
}
/// <inheritdoc />
public PatchManifest BuildPatchManifest(
string buildId,
string normalizationRecipeId,
IReadOnlyList<Attestation.FunctionDelta> deltas)
{
ArgumentException.ThrowIfNullOrWhiteSpace(buildId);
ArgumentException.ThrowIfNullOrWhiteSpace(normalizationRecipeId);
ArgumentNullException.ThrowIfNull(deltas);
var patches = deltas
.OrderBy(d => d.FunctionId, StringComparer.Ordinal)
.Select(delta =>
{
var start = delta.Address < 0 ? 0UL : (ulong)delta.Address;
var rangeSize = delta.NewSize > 0 ? delta.NewSize : delta.OldSize;
var end = rangeSize > 0
? start + (ulong)rangeSize - 1UL
: start;
return new SymbolPatchArtifact
{
Symbol = delta.FunctionId,
AddressRange = $"0x{start:x}-0x{end:x}",
DeltaDigest = "sha256:" + ComputeDigest($"{delta.FunctionId}|{delta.OldHash}|{delta.NewHash}|{delta.OldSize}|{delta.NewSize}|{delta.DiffLen}"),
Pre = new PatchSizeHash
{
Size = delta.OldSize,
Hash = string.IsNullOrWhiteSpace(delta.OldHash) ? "sha256:0" : delta.OldHash!
},
Post = new PatchSizeHash
{
Size = delta.NewSize,
Hash = string.IsNullOrWhiteSpace(delta.NewHash) ? "sha256:0" : delta.NewHash!
}
};
})
.ToList();
return new PatchManifest
{
BuildId = buildId,
NormalizationRecipeId = normalizationRecipeId,
Patches = patches
};
}
/// <inheritdoc />
public HybridDiffEvidence Compose(
IReadOnlyList<SourceFileDiff>? sourceDiffs,
SymbolMap oldSymbolMap,
SymbolMap newSymbolMap,
IReadOnlyList<Attestation.FunctionDelta> deltas,
string normalizationRecipeId)
{
var script = GenerateSemanticEditScript(sourceDiffs);
var patchPlan = BuildSymbolPatchPlan(script, oldSymbolMap, newSymbolMap, deltas);
var patchManifest = BuildPatchManifest(newSymbolMap.BuildId, normalizationRecipeId, deltas);
var scriptDigest = ComputeDigest(script);
var oldMapDigest = ComputeDigest(oldSymbolMap);
var newMapDigest = ComputeDigest(newSymbolMap);
var patchPlanDigest = ComputeDigest(patchPlan);
var patchManifestDigest = ComputeDigest(patchManifest);
return new HybridDiffEvidence
{
SemanticEditScript = script,
OldSymbolMap = oldSymbolMap,
NewSymbolMap = newSymbolMap,
SymbolPatchPlan = patchPlan,
PatchManifest = patchManifest,
SemanticEditScriptDigest = scriptDigest,
OldSymbolMapDigest = oldMapDigest,
NewSymbolMapDigest = newMapDigest,
SymbolPatchPlanDigest = patchPlanDigest,
PatchManifestDigest = patchManifestDigest
};
}
/// <inheritdoc />
public string ComputeDigest<T>(T value)
{
var json = value is string s
? s
: JsonSerializer.Serialize(value, DigestJsonOptions);
var bytes = Encoding.UTF8.GetBytes(json);
Span<byte> hash = stackalloc byte[SHA256.HashSizeInBytes];
SHA256.HashData(bytes, hash);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private static string? ResolveSourcePath(string? sourceFile, IReadOnlyDictionary<string, string> sourcePathByCompiled)
{
if (string.IsNullOrWhiteSpace(sourceFile))
{
return null;
}
return sourcePathByCompiled.TryGetValue(sourceFile, out var mapped)
? mapped
: sourceFile;
}
private static string MapSymbolKind(SymbolType type)
{
return type switch
{
SymbolType.Function => "function",
SymbolType.Object or SymbolType.Variable or SymbolType.TlsData => "object",
SymbolType.Section => "section",
_ => "function"
};
}
private static string GetDigestString(IReadOnlyDictionary<string, string> digest)
{
if (digest.TryGetValue("sha256", out var sha))
{
return sha;
}
return digest.Values.FirstOrDefault() ?? string.Empty;
}
private static string NormalizePath(string path)
{
return path.Replace('\\', '/').Trim();
}
private static SemanticEdit CreateFileEdit(string path, string beforeDigest, string afterDigest)
{
var type = string.IsNullOrWhiteSpace(beforeDigest) || beforeDigest == ComputeEmptyDigest()
? "add"
: string.IsNullOrWhiteSpace(afterDigest) || afterDigest == ComputeEmptyDigest()
? "remove"
: "update";
var nodePath = $"{path}::file";
var stableId = ComputeStableId(path, nodePath, type, beforeDigest, afterDigest);
return new SemanticEdit
{
StableId = stableId,
EditType = type,
NodeKind = "file",
NodePath = nodePath,
Anchor = path,
PreDigest = beforeDigest,
PostDigest = afterDigest
};
}
private static SemanticEdit CreateSymbolEdit(
string path,
string symbol,
string type,
string? preDigest,
string? postDigest,
SourceSpan? preSpan,
SourceSpan? postSpan)
{
var nodePath = $"{path}::{symbol}";
var stableId = ComputeStableId(path, nodePath, type, preDigest, postDigest);
return new SemanticEdit
{
StableId = stableId,
EditType = type,
NodeKind = "method",
NodePath = nodePath,
Anchor = symbol,
PreSpan = preSpan,
PostSpan = postSpan,
PreDigest = preDigest,
PostDigest = postDigest
};
}
private static string ComputeStableId(string path, string nodePath, string type, string? preDigest, string? postDigest)
{
var material = $"{path}|{nodePath}|{type}|{preDigest}|{postDigest}";
var bytes = Encoding.UTF8.GetBytes(material);
Span<byte> hash = stackalloc byte[SHA256.HashSizeInBytes];
SHA256.HashData(bytes, hash);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private static Dictionary<string, SymbolBlock> ExtractSymbolBlocks(string content)
{
var lines = content.Split('\n');
var blocks = new Dictionary<string, SymbolBlock>(StringComparer.Ordinal);
for (var i = 0; i < lines.Length; i++)
{
var line = lines[i];
var match = FunctionAnchorRegex.Match(line);
if (!match.Success)
{
continue;
}
var name = match.Groups["name"].Value;
if (ControlKeywords.Contains(name))
{
continue;
}
var startLine = i + 1;
var endLine = startLine;
var depth = CountChar(line, '{') - CountChar(line, '}');
var foundOpening = line.Contains('{', StringComparison.Ordinal);
var j = i;
while (foundOpening && depth > 0 && j + 1 < lines.Length)
{
j++;
var candidate = lines[j];
depth += CountChar(candidate, '{');
depth -= CountChar(candidate, '}');
}
if (foundOpening)
{
endLine = j + 1;
i = j;
}
var sliceStart = startLine - 1;
var sliceLength = endLine - startLine + 1;
var blockContent = string.Join("\n", lines.Skip(sliceStart).Take(sliceLength));
var blockHash = ComputeBlockHash(blockContent);
blocks[name] = new SymbolBlock(name, blockHash, startLine, endLine);
}
return blocks;
}
private static int CountChar(string value, char token)
{
var count = 0;
foreach (var c in value)
{
if (c == token)
{
count++;
}
}
return count;
}
private static string ComputeBlockHash(string content)
{
var bytes = Encoding.UTF8.GetBytes(content);
Span<byte> hash = stackalloc byte[SHA256.HashSizeInBytes];
SHA256.HashData(bytes, hash);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private static bool IsAnchorMatch(string anchor, string functionId)
{
if (string.Equals(anchor, functionId, StringComparison.Ordinal))
{
return true;
}
return anchor.EndsWith($".{functionId}", StringComparison.Ordinal) ||
anchor.EndsWith($"::{functionId}", StringComparison.Ordinal) ||
anchor.Contains(functionId, StringComparison.Ordinal);
}
private static string ComputeEmptyDigest()
{
Span<byte> hash = stackalloc byte[SHA256.HashSizeInBytes];
SHA256.HashData(Array.Empty<byte>(), hash);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private sealed record SymbolBlock(string Name, string Hash, int StartLine, int EndLine);
}

View File

@@ -6,6 +6,7 @@
// -----------------------------------------------------------------------------
using StellaOps.BinaryIndex.DeltaSig.Attestation;
using StellaOps.Symbols.Core.Models;
namespace StellaOps.BinaryIndex.DeltaSig;
@@ -166,6 +167,35 @@ public sealed record DeltaSigRequest
/// for the two-tier bundle format.
/// </summary>
public bool IncludeLargeBlobs { get; init; } = true;
/// <summary>
/// Source file pairs used to generate semantic edit scripts.
/// </summary>
public IReadOnlyList<SourceFileDiff>? SourceDiffs { get; init; }
/// <summary>
/// Old symbol map from build/debug metadata.
/// </summary>
public SymbolMap? OldSymbolMap { get; init; }
/// <summary>
/// New symbol map from build/debug metadata.
/// </summary>
public SymbolMap? NewSymbolMap { get; init; }
/// <summary>
/// Optional old symbol manifest used to derive symbol map.
/// </summary>
public SymbolManifest? OldSymbolManifest { get; init; }
/// <summary>
/// Optional new symbol manifest used to derive symbol map.
/// </summary>
public SymbolManifest? NewSymbolManifest { get; init; }
/// <summary>
/// Include composed hybrid diff evidence in predicate output.
/// </summary>
public bool IncludeHybridDiffEvidence { get; init; } = true;
}
/// <summary>
@@ -296,6 +326,10 @@ public enum DeltaSigVerificationStatus
/// </summary>
FunctionNotFound,
/// <summary>
/// Hybrid evidence artifacts are inconsistent or invalid.
/// </summary>
HybridEvidenceMismatch,
/// <summary>
/// Binary analysis failed.
/// </summary>
@@ -398,6 +432,30 @@ public sealed record DeltaSigPolicyOptions
/// Required diffing algorithm.
/// </summary>
public string? RequiredDiffAlgorithm { get; init; }
/// <summary>
/// Require hybrid diff evidence to be present.
/// </summary>
public bool RequireHybridEvidence { get; init; }
/// <summary>
/// Require each changed symbol to map to at least one AST anchor.
/// </summary>
public bool RequireAstAnchors { get; init; }
/// <summary>
/// Symbol prefixes that are denied from change scope.
/// </summary>
public IReadOnlyList<string>? DeniedSymbolPrefixes { get; init; }
/// <summary>
/// Symbol prefixes considered protected and therefore immutable.
/// </summary>
public IReadOnlyList<string>? ProtectedSymbolPrefixes { get; init; }
/// <summary>
/// Optional maximum byte budget from patch manifest delta totals.
/// </summary>
public long? MaxPatchManifestDeltaBytes { get; init; }
}
/// <summary>
@@ -442,3 +500,4 @@ public sealed record DeltaSigPolicyResult
Details = details
};
}

View File

@@ -43,6 +43,7 @@ public static class ServiceCollectionExtensions
logger);
});
services.AddSingleton<IHybridDiffComposer, HybridDiffComposer>();
services.AddSingleton<ISymbolChangeTracer, SymbolChangeTracer>();
services.AddSingleton<IDeltaSignatureMatcher, DeltaSignatureMatcher>();
@@ -105,3 +106,4 @@ public static class ServiceCollectionExtensions
return services;
}
}

View File

@@ -16,6 +16,7 @@
<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" />
<ProjectReference Include="..\..\..\Symbols\StellaOps.Symbols.Core\StellaOps.Symbols.Core.csproj" />
</ItemGroup>
<ItemGroup>
@@ -26,3 +27,4 @@
</ItemGroup>
</Project>

View File

@@ -1,11 +1,15 @@
# StellaOps.BinaryIndex.DeltaSig Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_solid_review.md`.
Source of truth: `docs/implplan/SPRINT_20260216_001_BinaryIndex_hybrid_diff_patch_pipeline.md` (hybrid diff) and `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_solid_review.md` (remediation backlog).
| Task ID | Status | Notes |
| --- | --- | --- |
| BHP-01..05 | DONE | SPRINT_20260216_001: implementing hybrid source-symbol-binary diff pipeline (semantic edits, symbol maps, patch manifests, verifier and policy hooks). |
| QA-BINARYINDEX-VERIFY-032 | DOING | SPRINT_20260211_033 run-001: verifying `symbol-source-connectors` with Tier 0/1/2 evidence and claim-parity review. |
| QA-BINARYINDEX-VERIFY-031 | DONE | SPRINT_20260211_033 run-001: Tier 0/1/2 command checks passed, but claim-parity review terminalized `symbol-change-tracking-in-binary-diffs` as `not_implemented` because `IrDiffGenerator` is still placeholder-backed. |
| QA-BINARYINDEX-VERIFY-015 | DONE | SPRINT_20260211_033 run-002: remediated PatchCoverage runtime wiring and rechecked Tier 0/1/2; terminalized `delta-signature-matching-and-patch-coverage-analysis` as `not_implemented` because `IrDiffGenerator` remains placeholder-backed. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/BinaryIndex/__Libraries/StellaOps.BinaryIndex.DeltaSig/StellaOps.BinaryIndex.DeltaSig.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -9,3 +9,5 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0738-T | DONE | Revalidated 2026-01-07. |
| AUDIT-0738-A | DONE | Waived (test project; revalidated 2026-01-07). |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| BHP-05-API-HYBRID-20260217 | DONE | Added contract JSON roundtrip assertions for ResolutionEvidence.hybridDiff and function-level fields. |

View File

@@ -75,7 +75,89 @@ public sealed class VulnResolutionContractsTests
{
MatchType = ResolutionMatchTypes.HashExact,
Confidence = 0.9m,
FixMethod = ResolutionFixMethods.SecurityFeed
FixMethod = ResolutionFixMethods.SecurityFeed,
FixConfidence = 0.9m,
ChangedFunctions =
[
new FunctionChangeInfo
{
Name = "openssl::verify",
ChangeType = "Modified",
Similarity = 0.82m,
VulnerableSize = 304,
PatchedSize = 312
}
],
HybridDiff = new HybridDiffEvidence
{
SemanticEditScriptDigest = "sha256:edits",
OldSymbolMapDigest = "sha256:old-map",
NewSymbolMapDigest = "sha256:new-map",
SymbolPatchPlanDigest = "sha256:plan",
PatchManifestDigest = "sha256:manifest",
SemanticEditScript = new SemanticEditScriptArtifact
{
SchemaVersion = "1.0.0",
SourceTreeDigest = "sha256:tree",
Edits =
[
new SemanticEditRecord
{
StableId = "sha256:edit-1",
EditType = "update",
NodeKind = "method",
NodePath = "openssl::verify",
Anchor = "openssl::verify"
}
]
},
SymbolPatchPlan = new SymbolPatchPlanArtifact
{
SchemaVersion = "1.0.0",
BuildIdBefore = "baseline:build-id",
BuildIdAfter = "build-id",
EditsDigest = "sha256:edits",
SymbolMapDigestBefore = "sha256:old-map",
SymbolMapDigestAfter = "sha256:new-map",
Changes =
[
new SymbolPatchChange
{
Symbol = "openssl::verify",
ChangeType = "modified",
AstAnchors = ["openssl::verify"],
DeltaRef = "sha256:delta"
}
]
},
PatchManifest = new PatchManifestArtifact
{
SchemaVersion = "1.0.0",
BuildId = "build-id",
NormalizationRecipeId = "recipe-v1",
TotalDeltaBytes = 8,
Patches =
[
new SymbolPatchArtifact
{
Symbol = "openssl::verify",
AddressRange = "0x401120-0x4012AF",
DeltaDigest = "sha256:delta",
DeltaSizeBytes = 8,
Pre = new PatchSizeHash
{
Size = 304,
Hash = "sha256:pre"
},
Post = new PatchSizeHash
{
Size = 312,
Hash = "sha256:post"
}
}
]
}
}
}
};
@@ -89,6 +171,12 @@ public sealed class VulnResolutionContractsTests
roundTrip.ResolvedAt.Should().Be(response.ResolvedAt);
roundTrip.Evidence!.MatchType.Should().Be(response.Evidence!.MatchType);
roundTrip.Evidence!.FixMethod.Should().Be(response.Evidence!.FixMethod);
roundTrip.Evidence!.FixConfidence.Should().Be(response.Evidence!.FixConfidence);
roundTrip.Evidence!.ChangedFunctions.Should().HaveCount(1);
roundTrip.Evidence!.ChangedFunctions![0].Name.Should().Be("openssl::verify");
roundTrip.Evidence!.HybridDiff.Should().NotBeNull();
roundTrip.Evidence!.HybridDiff!.PatchManifestDigest.Should().Be("sha256:manifest");
roundTrip.Evidence!.HybridDiff!.PatchManifest!.Patches.Should().HaveCount(1);
}
private static List<ValidationResult> Validate(object instance)
@@ -98,3 +186,4 @@ public sealed class VulnResolutionContractsTests
return results;
}
}

View File

@@ -77,6 +77,80 @@ public sealed class ResolutionServiceTests
result.Status.Should().Be(ResolutionStatus.Unknown);
result.Evidence!.MatchType.Should().Be(ResolutionMatchTypes.Fingerprint);
}
[Fact]
public async Task ResolveAsync_IdentityMatch_EmitsHybridDiffEvidence()
{
var stub = new StubBinaryVulnerabilityService
{
OnIdentity = _ =>
[
new BinaryVulnMatch
{
CveId = "CVE-2024-1111",
VulnerablePurl = "pkg:deb/debian/openssl@1.2.3",
Method = MatchMethod.BuildIdCatalog,
Confidence = 0.97m,
Evidence = new MatchEvidence
{
MatchedFunction = "openssl::verify_chain",
Similarity = 0.89m
}
}
]
};
var service = CreateService(stub);
var request = new VulnResolutionRequest
{
Package = "pkg:deb/debian/openssl@1.2.3",
BuildId = "build-id"
};
var result = await service.ResolveAsync(request, ct: TestContext.Current.CancellationToken);
result.Status.Should().Be(ResolutionStatus.Fixed);
result.Evidence.Should().NotBeNull();
result.Evidence!.ChangedFunctions.Should().ContainSingle();
result.Evidence!.ChangedFunctions![0].Name.Should().Be("openssl::verify_chain");
result.Evidence!.HybridDiff.Should().NotBeNull();
result.Evidence!.HybridDiff!.PatchManifest.Should().NotBeNull();
result.Evidence!.HybridDiff!.PatchManifest!.Patches.Should().ContainSingle();
}
[Fact]
public async Task ResolveAsync_SpecificCve_EmitsHybridDiffEvidence()
{
var stub = new StubBinaryVulnerabilityService
{
OnFixStatus = (_, _, _, _) => new FixStatusResult
{
State = FixState.Fixed,
FixedVersion = "1.0.1",
Method = FixMethod.PatchHeader,
Confidence = 0.91m,
EvidenceId = Guid.NewGuid()
}
};
var service = CreateService(stub);
var request = new VulnResolutionRequest
{
Package = "pkg:deb/debian/openssl@1.2.3",
BuildId = "build-id",
CveId = "CVE-2024-2222"
};
var result = await service.ResolveAsync(request, ct: TestContext.Current.CancellationToken);
result.Status.Should().Be(ResolutionStatus.Fixed);
result.FixedVersion.Should().Be("1.0.1");
result.Evidence.Should().NotBeNull();
result.Evidence!.FixMethod.Should().Be(ResolutionFixMethods.PatchHeader);
result.Evidence!.HybridDiff.Should().NotBeNull();
result.Evidence!.HybridDiff!.SemanticEditScript!.Edits.Should().NotBeEmpty();
}
[Fact]
public async Task ResolveBatchAsync_TruncatesToMaxBatchSize()
@@ -122,6 +196,7 @@ public sealed class ResolutionServiceTests
{
public Func<BinaryIdentity, ImmutableArray<BinaryVulnMatch>>? OnIdentity { get; init; }
public Func<byte[], ImmutableArray<BinaryVulnMatch>>? OnFingerprint { get; init; }
public Func<string, string, string, string, FixStatusResult?>? OnFixStatus { get; init; }
public Task<ImmutableArray<BinaryVulnMatch>> LookupByIdentityAsync(
BinaryIdentity identity,
@@ -148,7 +223,8 @@ public sealed class ResolutionServiceTests
string cveId,
CancellationToken ct = default)
{
return Task.FromResult<FixStatusResult?>(null);
var status = OnFixStatus?.Invoke(distro, release, sourcePkg, cveId);
return Task.FromResult(status);
}
public Task<ImmutableDictionary<string, FixStatusResult>> GetFixStatusBatchAsync(
@@ -227,3 +303,4 @@ public sealed class ResolutionServiceTests
public override DateTimeOffset GetUtcNow() => _fixed;
}
}

View File

@@ -10,3 +10,5 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0117-T | DONE | Revalidated 2026-01-06. |
| AUDIT-0117-A | DONE | Waived (test project; revalidated 2026-01-06). |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| BHP-05-API-HYBRID-20260217 | DONE | Added resolution behavioral tests validating hybrid diff evidence emission for identity and CVE-specific resolution paths. |

View File

@@ -0,0 +1,192 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under BUSL-1.1. See LICENSE in the project root.
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using StellaOps.BinaryIndex.DeltaSig.Attestation;
namespace StellaOps.BinaryIndex.DeltaSig.Tests;
public sealed class DeltaSigServiceHybridPolicyTests
{
[Fact]
public void EvaluatePolicy_EnforcesHybridEvidenceAndNamespaceControls()
{
var service = CreateService();
var predicate = CreatePredicate(CreateHybridEvidence(
symbol: "Crypto.Core.Encrypt",
anchors: [],
deltaBytes: 24));
var result = service.EvaluatePolicy(predicate, new DeltaSigPolicyOptions
{
RequireHybridEvidence = true,
RequireAstAnchors = true,
DeniedSymbolPrefixes = ["Crypto."],
MaxPatchManifestDeltaBytes = 8
});
result.Passed.Should().BeFalse();
result.Violations.Should().Contain(v => v.Contains("AST anchors", StringComparison.Ordinal));
result.Violations.Should().Contain(v => v.Contains("Denied symbol prefix", StringComparison.Ordinal));
result.Violations.Should().Contain(v => v.Contains("Patch manifest changed", StringComparison.Ordinal));
}
[Fact]
public void EvaluatePolicy_PassesWhenHybridEvidenceIsCompliant()
{
var service = CreateService();
var predicate = CreatePredicate(CreateHybridEvidence(
symbol: "Safe.Module.Apply",
anchors: ["Safe.Module.Apply"],
deltaBytes: 4));
var result = service.EvaluatePolicy(predicate, new DeltaSigPolicyOptions
{
RequireHybridEvidence = true,
RequireAstAnchors = true,
MaxPatchManifestDeltaBytes = 16,
DeniedSymbolPrefixes = ["Crypto."],
ProtectedSymbolPrefixes = ["Immutable.Namespace."]
});
result.Passed.Should().BeTrue();
result.Violations.Should().BeEmpty();
}
private static DeltaSigService CreateService()
{
return new DeltaSigService(
Mock.Of<IDeltaSignatureGenerator>(),
Mock.Of<IDeltaSignatureMatcher>(),
NullLogger<DeltaSigService>.Instance,
new HybridDiffComposer());
}
private static DeltaSigPredicate CreatePredicate(HybridDiffEvidence? hybrid)
{
return new DeltaSigPredicate
{
Subject =
[
new DeltaSigSubject
{
Uri = "oci://old",
Digest = new Dictionary<string, string> { ["sha256"] = "old" },
Arch = "linux-amd64",
Role = "old"
},
new DeltaSigSubject
{
Uri = "oci://new",
Digest = new Dictionary<string, string> { ["sha256"] = "new" },
Arch = "linux-amd64",
Role = "new"
}
],
Delta =
[
new FunctionDelta
{
FunctionId = hybrid?.SymbolPatchPlan.Changes.FirstOrDefault()?.Symbol ?? "unknown",
Address = 0x1000,
OldHash = "a",
NewHash = "b",
OldSize = 10,
NewSize = 12,
DiffLen = 2,
ChangeType = "modified"
}
],
Summary = new DeltaSummary
{
TotalFunctions = 1,
FunctionsAdded = 0,
FunctionsRemoved = 0,
FunctionsModified = 1,
FunctionsUnchanged = 0,
TotalBytesChanged = 2,
MinSemanticSimilarity = 1,
AvgSemanticSimilarity = 1,
MaxSemanticSimilarity = 1
},
Tooling = new DeltaTooling
{
Lifter = "b2r2",
LifterVersion = "0.7.0",
CanonicalIr = "b2r2-lowuir",
DiffAlgorithm = "ir-semantic"
},
ComputedAt = DateTimeOffset.UtcNow,
HybridDiff = hybrid
};
}
private static HybridDiffEvidence CreateHybridEvidence(string symbol, IReadOnlyList<string> anchors, long deltaBytes)
{
var composer = new HybridDiffComposer();
var oldMap = new SymbolMap
{
BuildId = "old-build",
Symbols =
[
new SymbolMapEntry
{
Name = symbol,
AddressStart = 0x1000,
AddressEnd = 0x100f,
Section = ".text"
}
]
};
var newMap = new SymbolMap
{
BuildId = "new-build",
Symbols =
[
new SymbolMapEntry
{
Name = symbol,
AddressStart = 0x2000,
AddressEnd = 0x200f,
Section = ".text"
}
]
};
var functionDelta = new FunctionDelta
{
FunctionId = symbol,
Address = 0x2000,
OldHash = "old-hash",
NewHash = "new-hash",
OldSize = 16,
NewSize = 16 + deltaBytes,
DiffLen = deltaBytes,
ChangeType = "modified"
};
var evidence = composer.Compose(
sourceDiffs: [],
oldSymbolMap: oldMap,
newSymbolMap: newMap,
deltas: [functionDelta],
normalizationRecipeId: "recipe-1");
var changes = evidence.SymbolPatchPlan.Changes
.Select(c => c with { AstAnchors = anchors })
.ToList();
var updatedPlan = evidence.SymbolPatchPlan with { Changes = changes };
return evidence with
{
SymbolPatchPlan = updatedPlan,
SymbolPatchPlanDigest = composer.ComputeDigest(updatedPlan)
};
}
}

View File

@@ -0,0 +1,132 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under BUSL-1.1. See LICENSE in the project root.
using System.Security.Cryptography;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using StellaOps.BinaryIndex.DeltaSig.Attestation;
namespace StellaOps.BinaryIndex.DeltaSig.Tests;
public sealed class DeltaSigServiceVerificationTests
{
[Fact]
public async Task VerifyAsync_ReturnsHybridEvidenceMismatch_WhenHybridDigestsAreInvalid()
{
var binary = new MemoryStream([1, 2, 3, 4, 5]);
var newDigest = ComputeSha256(binary);
var generator = new Mock<IDeltaSignatureGenerator>();
generator
.Setup(x => x.GenerateSignaturesAsync(
It.IsAny<Stream>(),
It.IsAny<DeltaSignatureRequest>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new DeltaSignature
{
Cve = "verification",
Package = new PackageRef("pkg", null),
Target = new TargetRef("x86_64", "gnu"),
Normalization = new NormalizationRef("recipe-1", "1.0.0", []),
SignatureState = "verification",
Symbols = []
});
var service = new DeltaSigService(
generator.Object,
Mock.Of<IDeltaSignatureMatcher>(),
NullLogger<DeltaSigService>.Instance,
new HybridDiffComposer());
var hybrid = CreateHybridEvidenceWithInvalidDigest();
var predicate = new DeltaSigPredicate
{
Subject =
[
new DeltaSigSubject
{
Uri = "oci://old",
Digest = new Dictionary<string, string> { ["sha256"] = "old" },
Arch = "linux-amd64",
Role = "old"
},
new DeltaSigSubject
{
Uri = "oci://new",
Digest = new Dictionary<string, string> { ["sha256"] = newDigest },
Arch = "linux-amd64",
Role = "new"
}
],
Delta = [],
Summary = new DeltaSummary
{
TotalFunctions = 0,
FunctionsAdded = 0,
FunctionsRemoved = 0,
FunctionsModified = 0,
FunctionsUnchanged = 0,
TotalBytesChanged = 0,
MinSemanticSimilarity = 1,
AvgSemanticSimilarity = 1,
MaxSemanticSimilarity = 1
},
Tooling = new DeltaTooling
{
Lifter = "b2r2",
LifterVersion = "0.7.0",
CanonicalIr = "b2r2-lowuir",
DiffAlgorithm = "ir-semantic"
},
ComputedAt = DateTimeOffset.UtcNow,
HybridDiff = hybrid
};
binary.Position = 0;
var result = await service.VerifyAsync(predicate, binary);
result.IsValid.Should().BeFalse();
result.Status.Should().Be(DeltaSigVerificationStatus.HybridEvidenceMismatch);
result.Message.Should().Contain("semantic_edit_script");
}
private static string ComputeSha256(Stream stream)
{
stream.Position = 0;
using var sha = SHA256.Create();
var hash = sha.ComputeHash(stream);
stream.Position = 0;
return Convert.ToHexString(hash).ToLowerInvariant();
}
private static HybridDiffEvidence CreateHybridEvidenceWithInvalidDigest()
{
var composer = new HybridDiffComposer();
var oldMap = new SymbolMap
{
BuildId = "old-build",
BinaryDigest = "sha256:old",
Symbols = []
};
var newMap = new SymbolMap
{
BuildId = "new-build",
BinaryDigest = "sha256:new",
Symbols = []
};
var evidence = composer.Compose(
sourceDiffs: [],
oldSymbolMap: oldMap,
newSymbolMap: newMap,
deltas: [],
normalizationRecipeId: "recipe-1");
return evidence with { SemanticEditScriptDigest = "tampered-digest" };
}
}

View File

@@ -0,0 +1,142 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under BUSL-1.1. See LICENSE in the project root.
using FluentAssertions;
using StellaOps.BinaryIndex.DeltaSig.Attestation;
using StellaOps.Symbols.Core.Models;
namespace StellaOps.BinaryIndex.DeltaSig.Tests;
public sealed class HybridDiffComposerTests
{
[Fact]
public void Compose_WithIdenticalInputs_IsDeterministic()
{
var composer = new HybridDiffComposer();
var sourceDiffs = new[]
{
new SourceFileDiff
{
Path = "src/Example.cs",
BeforeContent = "class C { int Add(int a, int b) { return a + b; } }",
AfterContent = "class C { int Add(int a, int b) { return a + b + 1; } }"
}
};
var oldMap = new SymbolMap
{
BuildId = "build-old",
BinaryDigest = "sha256:old",
Symbols =
[
new SymbolMapEntry
{
Name = "C::Add",
AddressStart = 0x401000,
AddressEnd = 0x40103F,
Section = ".text"
}
]
};
var newMap = new SymbolMap
{
BuildId = "build-new",
BinaryDigest = "sha256:new",
Symbols =
[
new SymbolMapEntry
{
Name = "C::Add",
AddressStart = 0x501000,
AddressEnd = 0x501047,
Section = ".text"
}
]
};
var deltas = new[]
{
new FunctionDelta
{
FunctionId = "C::Add",
Address = 0x501000,
OldHash = "old-hash",
NewHash = "new-hash",
OldSize = 64,
NewSize = 72,
DiffLen = 8,
ChangeType = "modified"
}
};
var left = composer.Compose(sourceDiffs, oldMap, newMap, deltas, "recipe-1");
var right = composer.Compose(sourceDiffs, oldMap, newMap, deltas, "recipe-1");
left.SemanticEditScriptDigest.Should().Be(right.SemanticEditScriptDigest);
left.OldSymbolMapDigest.Should().Be(right.OldSymbolMapDigest);
left.NewSymbolMapDigest.Should().Be(right.NewSymbolMapDigest);
left.SymbolPatchPlanDigest.Should().Be(right.SymbolPatchPlanDigest);
left.PatchManifestDigest.Should().Be(right.PatchManifestDigest);
left.PatchManifest.Patches.Should().ContainSingle(p => p.Symbol == "C::Add");
}
[Fact]
public void BuildSymbolMap_MapsSourcePaths_AndOrdersByAddress()
{
var composer = new HybridDiffComposer();
var manifest = new SymbolManifest
{
ManifestId = "manifest-1",
DebugId = "dbg-1",
BinaryName = "sample.bin",
Format = BinaryFormat.Elf,
TenantId = "tenant-a",
Symbols =
[
new SymbolEntry
{
Address = 0x401100,
Size = 16,
MangledName = "b",
DemangledName = "B::Method",
Type = SymbolType.Function,
SourceFile = "/obj/B.cs",
SourceLine = 20
},
new SymbolEntry
{
Address = 0x401000,
Size = 32,
MangledName = "a",
DemangledName = "A::Method",
Type = SymbolType.Function,
SourceFile = "/obj/A.cs",
SourceLine = 10
}
],
SourceMappings =
[
new SourceMapping
{
CompiledPath = "/obj/A.cs",
SourcePath = "src/A.cs"
},
new SourceMapping
{
CompiledPath = "/obj/B.cs",
SourcePath = "src/B.cs"
}
]
};
var map = composer.BuildSymbolMap(manifest, "sha256:abc");
map.BuildId.Should().Be("dbg-1");
map.Symbols.Should().HaveCount(2);
map.Symbols[0].Name.Should().Be("A::Method");
map.Symbols[1].Name.Should().Be("B::Method");
map.Symbols[0].SourceRanges.Should().ContainSingle(r => r.File == "src/A.cs" && r.LineStart == 10 && r.LineEnd == 10);
}
}

View File

@@ -1,10 +1,11 @@
# BinaryIndex DeltaSig Tests Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
Source of truth: `docs/implplan/SPRINT_20260216_001_BinaryIndex_hybrid_diff_patch_pipeline.md` (hybrid tests) and `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md` (historical baseline).
| Task ID | Status | Notes |
| --- | --- | --- |
| BHP-TEST-20260216 | DONE | SPRINT_20260216_001: targeted behavioral tests for hybrid diff composer/service policy and verifier logic. |
| QA-BINARYINDEX-VERIFY-034 | DONE | SPRINT_20260211_033 run-002: expanded golden CVE fixture package coverage to include glibc/zlib/curl and added regression assertion for required high-impact package set. |
| QA-BINARYINDEX-VERIFY-032 | DOING | SPRINT_20260211_033 run-001: executing Tier 0/1/2 verification for `symbol-source-connectors` with deterministic behavioral evidence capture. |
| QA-BINARYINDEX-VERIFY-031 | DONE | SPRINT_20260211_033 run-001: executed Tier 0/1/2 verification for `symbol-change-tracking-in-binary-diffs`; terminalized feature as `not_implemented` due missing IR-diff behavioral implementation and test coverage. |
@@ -13,3 +14,6 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0743-T | DONE | Revalidated 2026-01-07. |
| AUDIT-0743-A | DONE | Waived (test project; revalidated 2026-01-07). |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -155,6 +155,46 @@ public sealed class CachedResolutionServiceTests
cache.GetCalls.Should().Be(2);
}
[Fact]
public async Task ResolveAsync_FromCache_ProvidesHybridDiffEvidence()
{
var fakeInner = new FakeResolutionService(_timeProvider);
var cache = new FakeResolutionCacheService();
var cacheOptions = Options.Create(new ResolutionCacheOptions());
var serviceOptions = Options.Create(new ResolutionServiceOptions());
var service = new CachedResolutionService(
fakeInner,
cache,
cacheOptions,
serviceOptions,
_timeProvider,
NullLogger<CachedResolutionService>.Instance);
var request = new VulnResolutionRequest
{
Package = "pkg:deb/debian/openssl@3.0.7",
BuildId = "build-hybrid"
};
var cacheKey = cache.GenerateCacheKey(request);
cache.Entries[cacheKey] = new CachedResolution
{
Status = ResolutionStatus.Fixed,
FixedVersion = "1.0.2",
CachedAt = _timeProvider.GetUtcNow(),
Confidence = 0.93m,
MatchType = ResolutionMatchTypes.BuildId
};
var result = await service.ResolveAsync(request, null, TestContext.Current.CancellationToken);
result.FromCache.Should().BeTrue();
result.Evidence.Should().NotBeNull();
result.Evidence!.HybridDiff.Should().NotBeNull();
result.Evidence!.HybridDiff!.PatchManifestDigest.Should().NotBeNullOrWhiteSpace();
}
[Fact]
public async Task ResolveAsync_BypassCache_SkipsCache()
{
@@ -529,3 +569,4 @@ internal sealed class FakeResolutionCacheService : IResolutionCacheService
});
}
}

View File

@@ -16,3 +16,5 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0747-T | DONE | Revalidated 2026-01-07. |
| AUDIT-0747-A | DONE | Waived (test project; revalidated 2026-01-07). |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| BHP-05-API-HYBRID-20260217 | DONE | Added cached resolution behavioral test proving evidence.hybridDiff is present on cache hits. |