346 lines
12 KiB
C#
346 lines
12 KiB
C#
// -----------------------------------------------------------------------------
|
|
// 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";
|
|
}
|