save progress

This commit is contained in:
StellaOps Bot
2026-01-03 11:02:24 +02:00
parent ca578801fd
commit 83c37243e0
446 changed files with 22798 additions and 4031 deletions

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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. |

View File

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

View File

@@ -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);