save progress
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace StellaOps.BinaryIndex.VexBridge;
|
||||
@@ -44,6 +45,52 @@ public static class BinaryMatchEvidenceSchema
|
||||
public const string HashExact = "hash_exact";
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> s_validMatchTypes = new(StringComparer.Ordinal)
|
||||
{
|
||||
MatchTypes.BuildId,
|
||||
MatchTypes.Fingerprint,
|
||||
MatchTypes.HashExact
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Validates an evidence payload against the expected schema.
|
||||
/// </summary>
|
||||
public static bool ValidateEvidence(JsonObject evidence, out string? error)
|
||||
{
|
||||
error = null;
|
||||
if (evidence is null)
|
||||
{
|
||||
error = "Evidence payload is null.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryGetString(evidence, Fields.Type, out var type) || type != EvidenceType)
|
||||
{
|
||||
error = $"Evidence type must be '{EvidenceType}'.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryGetString(evidence, Fields.SchemaVersion, out var version) || version != SchemaVersion)
|
||||
{
|
||||
error = $"Schema version must be '{SchemaVersion}'.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryGetString(evidence, Fields.MatchType, out var matchType))
|
||||
{
|
||||
error = "Match type is missing or invalid.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!s_validMatchTypes.Contains(matchType))
|
||||
{
|
||||
error = "Match type is missing or invalid.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an evidence JSON object from the provided parameters.
|
||||
/// </summary>
|
||||
@@ -119,4 +166,19 @@ public static class BinaryMatchEvidenceSchema
|
||||
|
||||
return evidence;
|
||||
}
|
||||
|
||||
private static bool TryGetString(
|
||||
JsonObject evidence,
|
||||
string field,
|
||||
[NotNullWhen(true)] out string? value)
|
||||
{
|
||||
value = null;
|
||||
if (!evidence.TryGetPropertyValue(field, out var node))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = node?.GetValue<string>();
|
||||
return !string.IsNullOrWhiteSpace(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// IDsseSigningAdapter.cs
|
||||
// Sprint: SPRINT_1227_0001_0001_LB_binary_vex_generator
|
||||
// Task: T5 — DSSE signing integration
|
||||
// Task: T5 - DSSE signing integration
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
namespace StellaOps.BinaryIndex.VexBridge;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Description>Bridges binary fingerprint matching to VEX observation generation for StellaOps.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -7,4 +7,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0127-M | DONE | Maintainability audit for StellaOps.BinaryIndex.VexBridge. |
|
||||
| AUDIT-0127-T | DONE | Test coverage audit for StellaOps.BinaryIndex.VexBridge. |
|
||||
| AUDIT-0127-A | TODO | Pending approval for changes. |
|
||||
| AUDIT-0127-A | DONE | Applied TimeProvider, link control, DSSE metadata, schema validation, algorithm propagation, deterministic tests. |
|
||||
|
||||
@@ -51,4 +51,14 @@ public sealed class VexBridgeOptions
|
||||
/// Default: StellaOps BinaryIndex namespace.
|
||||
/// </summary>
|
||||
public Guid ObservationIdNamespace { get; set; } = new("d9e0a5f3-7b2c-4e8d-9a1f-6c3b5d8e2f0a");
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include external reference links (e.g., NVD) in linksets.
|
||||
/// </summary>
|
||||
public bool IncludeExternalLinks { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Base URL for CVE references when external links are enabled.
|
||||
/// </summary>
|
||||
public string NvdCveBaseUrl { get; set; } = "https://nvd.nist.gov/vuln/detail/";
|
||||
}
|
||||
|
||||
@@ -22,15 +22,18 @@ public sealed class VexEvidenceGenerator : IVexEvidenceGenerator
|
||||
private readonly ILogger<VexEvidenceGenerator> _logger;
|
||||
private readonly VexBridgeOptions _options;
|
||||
private readonly IDsseSigningAdapter? _dsseSigner;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public VexEvidenceGenerator(
|
||||
ILogger<VexEvidenceGenerator> logger,
|
||||
IOptions<VexBridgeOptions> options,
|
||||
IDsseSigningAdapter? dsseSigner = null)
|
||||
IDsseSigningAdapter? dsseSigner = null,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_dsseSigner = dsseSigner;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -47,17 +50,7 @@ public sealed class VexEvidenceGenerator : IVexEvidenceGenerator
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
// Check confidence threshold
|
||||
var effectiveConfidence = fixStatus?.Confidence ?? match.Confidence;
|
||||
if (effectiveConfidence < _options.MinConfidenceThreshold)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Skipping observation for {CveId}: confidence {Confidence} below threshold {Threshold}",
|
||||
match.CveId, effectiveConfidence, _options.MinConfidenceThreshold);
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Match confidence {effectiveConfidence} is below minimum threshold {_options.MinConfidenceThreshold}");
|
||||
}
|
||||
EnsureAboveThreshold(match, fixStatus);
|
||||
|
||||
var observation = await CreateObservationAsync(match, identity, fixStatus, context, ct);
|
||||
return observation;
|
||||
@@ -87,6 +80,14 @@ public sealed class VexEvidenceGenerator : IVexEvidenceGenerator
|
||||
|
||||
try
|
||||
{
|
||||
if (IsBelowThreshold(item.Match, item.FixStatus))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Skipping observation for {CveId}: confidence below threshold {Threshold}",
|
||||
item.Match.CveId, _options.MinConfidenceThreshold);
|
||||
continue;
|
||||
}
|
||||
|
||||
var observation = await GenerateFromBinaryMatchAsync(
|
||||
item.Match,
|
||||
item.Identity,
|
||||
@@ -98,7 +99,6 @@ public sealed class VexEvidenceGenerator : IVexEvidenceGenerator
|
||||
}
|
||||
catch (InvalidOperationException ex) when (ex.Message.Contains("below minimum threshold"))
|
||||
{
|
||||
// Skip items below threshold, continue with batch
|
||||
_logger.LogDebug("Skipping batch item: {Message}", ex.Message);
|
||||
}
|
||||
}
|
||||
@@ -133,7 +133,7 @@ public sealed class VexEvidenceGenerator : IVexEvidenceGenerator
|
||||
context.ProductKey,
|
||||
context.ScanId);
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
// Map fix status to VEX status and justification
|
||||
var (vexStatus, justification) = MapToVexStatus(fixStatus);
|
||||
@@ -145,7 +145,7 @@ public sealed class VexEvidenceGenerator : IVexEvidenceGenerator
|
||||
var upstream = await CreateUpstreamAsync(observationId, evidence, now, context.SignWithDsse, ct);
|
||||
|
||||
// Create statement
|
||||
var statement = CreateStatement(match, context, vexStatus, justification, fixStatus);
|
||||
var statement = CreateStatement(match, context, vexStatus, justification, fixStatus, now);
|
||||
|
||||
// Create content
|
||||
var content = CreateContent(evidence);
|
||||
@@ -217,7 +217,9 @@ public sealed class VexEvidenceGenerator : IVexEvidenceGenerator
|
||||
buildId: identity.BuildId,
|
||||
fileSha256: identity.FileSha256,
|
||||
textSha256: identity.TextSha256,
|
||||
fingerprintAlgorithm: matchType == BinaryMatchEvidenceSchema.MatchTypes.Fingerprint ? "combined" : null,
|
||||
fingerprintAlgorithm: matchType == BinaryMatchEvidenceSchema.MatchTypes.Fingerprint
|
||||
? match.Evidence?.FingerprintAlgorithm
|
||||
: null,
|
||||
similarity: match.Evidence?.Similarity ?? match.Confidence,
|
||||
distroRelease: context.DistroRelease,
|
||||
sourcePackage: ExtractSourcePackage(match.VulnerablePurl),
|
||||
@@ -243,6 +245,7 @@ public sealed class VexEvidenceGenerator : IVexEvidenceGenerator
|
||||
var contentHash = ComputeSha256(evidenceJson);
|
||||
|
||||
VexObservationSignature signature;
|
||||
var metadata = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
||||
|
||||
// Sign with DSSE if requested and signer is available
|
||||
if (signWithDsse && _dsseSigner is { IsAvailable: true })
|
||||
@@ -263,6 +266,8 @@ public sealed class VexEvidenceGenerator : IVexEvidenceGenerator
|
||||
format: "dsse",
|
||||
keyId: _dsseSigner.SigningKeyId,
|
||||
signature: envelopeBase64);
|
||||
metadata["dsse_status"] = "signed";
|
||||
metadata["dsse_envelope_hash"] = envelopeHash;
|
||||
|
||||
_logger.LogDebug(
|
||||
"DSSE signature generated for observation {ObservationId} with key {KeyId}",
|
||||
@@ -279,6 +284,8 @@ public sealed class VexEvidenceGenerator : IVexEvidenceGenerator
|
||||
format: null,
|
||||
keyId: null,
|
||||
signature: null);
|
||||
metadata["dsse_status"] = "failed";
|
||||
metadata["dsse_error"] = ex.GetType().Name;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -288,6 +295,7 @@ public sealed class VexEvidenceGenerator : IVexEvidenceGenerator
|
||||
_logger.LogDebug(
|
||||
"DSSE signing requested but no signer configured for observation {ObservationId}",
|
||||
observationId);
|
||||
metadata["dsse_status"] = "unavailable";
|
||||
}
|
||||
|
||||
signature = new VexObservationSignature(
|
||||
@@ -304,7 +312,7 @@ public sealed class VexEvidenceGenerator : IVexEvidenceGenerator
|
||||
receivedAt: now,
|
||||
contentHash: contentHash,
|
||||
signature: signature,
|
||||
metadata: ImmutableDictionary<string, string>.Empty
|
||||
metadata: metadata.ToImmutable()
|
||||
.Add("source", "binary_fingerprint_analysis"));
|
||||
}
|
||||
|
||||
@@ -313,7 +321,8 @@ public sealed class VexEvidenceGenerator : IVexEvidenceGenerator
|
||||
VexGenerationContext context,
|
||||
VexClaimStatus status,
|
||||
VexJustification? justification,
|
||||
FixStatusResult? fixStatus)
|
||||
FixStatusResult? fixStatus,
|
||||
DateTimeOffset now)
|
||||
{
|
||||
var detail = BuildStatementDetail(match, fixStatus);
|
||||
|
||||
@@ -321,7 +330,7 @@ public sealed class VexEvidenceGenerator : IVexEvidenceGenerator
|
||||
vulnerabilityId: match.CveId,
|
||||
productKey: context.ProductKey,
|
||||
status: status,
|
||||
lastObserved: DateTimeOffset.UtcNow,
|
||||
lastObserved: now,
|
||||
locator: null,
|
||||
justification: justification,
|
||||
introducedVersion: null,
|
||||
@@ -365,16 +374,24 @@ public sealed class VexEvidenceGenerator : IVexEvidenceGenerator
|
||||
raw: evidence);
|
||||
}
|
||||
|
||||
private static VexObservationLinkset CreateLinkset(
|
||||
private VexObservationLinkset CreateLinkset(
|
||||
BinaryVulnMatch match,
|
||||
BinaryIdentity identity)
|
||||
{
|
||||
var refs = new List<VexObservationReference>
|
||||
{
|
||||
new(type: "vulnerability", url: $"https://nvd.nist.gov/vuln/detail/{match.CveId}"),
|
||||
new(type: "package", url: match.VulnerablePurl)
|
||||
};
|
||||
|
||||
if (_options.IncludeExternalLinks)
|
||||
{
|
||||
var baseUrl = string.IsNullOrWhiteSpace(_options.NvdCveBaseUrl)
|
||||
? "https://nvd.nist.gov/vuln/detail/"
|
||||
: _options.NvdCveBaseUrl;
|
||||
var separator = baseUrl.EndsWith("/", StringComparison.Ordinal) ? string.Empty : "/";
|
||||
refs.Insert(0, new(type: "vulnerability", url: $"{baseUrl}{separator}{match.CveId}"));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(identity.BuildId))
|
||||
{
|
||||
refs.Add(new(type: "build_id", url: $"urn:build-id:{identity.BuildId}"));
|
||||
@@ -389,19 +406,39 @@ public sealed class VexEvidenceGenerator : IVexEvidenceGenerator
|
||||
|
||||
private static string? ExtractSourcePackage(string purl)
|
||||
{
|
||||
// Simple extraction from PURL: pkg:deb/debian/openssl@3.0.7 → openssl
|
||||
// Simple extraction from PURL: pkg:deb/debian/openssl@3.0.7 -> openssl
|
||||
if (string.IsNullOrEmpty(purl))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var parts = purl.Split('/');
|
||||
if (parts.Length >= 3)
|
||||
var trimmed = purl;
|
||||
if (trimmed.StartsWith("pkg:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var nameVersion = parts[^1];
|
||||
var atIndex = nameVersion.IndexOf('@');
|
||||
return atIndex > 0 ? nameVersion[..atIndex] : nameVersion;
|
||||
trimmed = trimmed[4..];
|
||||
}
|
||||
|
||||
var qualifierIndex = trimmed.IndexOf('?');
|
||||
if (qualifierIndex >= 0)
|
||||
{
|
||||
trimmed = trimmed[..qualifierIndex];
|
||||
}
|
||||
|
||||
var subpathIndex = trimmed.IndexOf('#');
|
||||
if (subpathIndex >= 0)
|
||||
{
|
||||
trimmed = trimmed[..subpathIndex];
|
||||
}
|
||||
|
||||
var segments = trimmed.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (segments.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var nameVersion = segments[^1];
|
||||
var atIndex = nameVersion.IndexOf('@');
|
||||
return atIndex > 0 ? nameVersion[..atIndex] : nameVersion;
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -411,6 +448,23 @@ public sealed class VexEvidenceGenerator : IVexEvidenceGenerator
|
||||
return null;
|
||||
}
|
||||
|
||||
private void EnsureAboveThreshold(BinaryVulnMatch match, FixStatusResult? fixStatus)
|
||||
{
|
||||
var effectiveConfidence = fixStatus?.Confidence ?? match.Confidence;
|
||||
if (effectiveConfidence < _options.MinConfidenceThreshold)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Skipping observation for {CveId}: confidence {Confidence} below threshold {Threshold}",
|
||||
match.CveId, effectiveConfidence, _options.MinConfidenceThreshold);
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Match confidence {effectiveConfidence} is below minimum threshold {_options.MinConfidenceThreshold}");
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsBelowThreshold(BinaryVulnMatch match, FixStatusResult? fixStatus)
|
||||
=> (fixStatus?.Confidence ?? match.Confidence) < _options.MinConfidenceThreshold;
|
||||
|
||||
private static string ComputeSha256(string content)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(content);
|
||||
|
||||
Reference in New Issue
Block a user