Files
git.stella-ops.org/src/BinaryIndex/__Libraries/StellaOps.BinaryIndex.DeltaSig/VexIntegration/DeltaSigVexBridge.cs
2026-01-20 00:45:38 +02:00

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";
}