174 lines
6.1 KiB
C#
174 lines
6.1 KiB
C#
namespace StellaOps.Scanner.ProofIntegration;
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
using StellaOps.Attestor.ProofChain.Generators;
|
|
using StellaOps.Attestor.ProofChain.Models;
|
|
using StellaOps.Attestor.ProofChain.Statements;
|
|
using StellaOps.Concelier.ProofService;
|
|
|
|
/// <summary>
|
|
/// Generates VEX verdicts with cryptographic proof references.
|
|
/// Integrates Scanner vulnerability detection with proof-driven backport detection.
|
|
/// </summary>
|
|
public sealed class ProofAwareVexGenerator
|
|
{
|
|
private readonly ILogger<ProofAwareVexGenerator> _logger;
|
|
private readonly BackportProofService _proofService;
|
|
private readonly TimeProvider _timeProvider;
|
|
|
|
public ProofAwareVexGenerator(
|
|
ILogger<ProofAwareVexGenerator> logger,
|
|
BackportProofService proofService,
|
|
TimeProvider? timeProvider = null)
|
|
{
|
|
_logger = logger;
|
|
_proofService = proofService;
|
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generate VEX verdict with proof for a vulnerability finding.
|
|
/// </summary>
|
|
/// <param name="finding">Vulnerability finding from scanner</param>
|
|
/// <param name="sbomEntryId">SBOM entry ID for the component</param>
|
|
/// <param name="policyVersion">Policy version used for decisioning</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>VEX verdict statement with embedded proof reference</returns>
|
|
public async Task<VexVerdictWithProof> GenerateVexWithProofAsync(
|
|
VulnerabilityFinding finding,
|
|
string sbomEntryId,
|
|
string policyVersion,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
_logger.LogInformation(
|
|
"Generating proof-carrying VEX verdict for {CveId} in {Package}",
|
|
finding.CveId, finding.PackagePurl);
|
|
|
|
// Step 1: Generate cryptographic proof using four-tier detection
|
|
var proof = await _proofService.GenerateProofAsync(
|
|
finding.CveId,
|
|
finding.PackagePurl,
|
|
cancellationToken);
|
|
|
|
if (proof == null)
|
|
{
|
|
_logger.LogWarning(
|
|
"No proof generated for {CveId} in {Package}, using fallback verdict",
|
|
finding.CveId, finding.PackagePurl);
|
|
|
|
// Fallback: Generate VEX without proof
|
|
return GenerateFallbackVex(finding, sbomEntryId, policyVersion);
|
|
}
|
|
|
|
_logger.LogInformation(
|
|
"Generated proof {ProofId} with confidence {Confidence:P0} for {CveId}",
|
|
proof.ProofId, proof.Confidence, finding.CveId);
|
|
|
|
// Step 2: Generate VEX verdict with proof reference
|
|
var reasoningId = GenerateReasoningId(finding, proof);
|
|
var (statement, proofPayload) = VexProofIntegrator.GenerateWithProofMetadata(
|
|
proof,
|
|
sbomEntryId,
|
|
policyVersion,
|
|
reasoningId);
|
|
|
|
return new VexVerdictWithProof
|
|
{
|
|
Statement = statement,
|
|
ProofPayload = proofPayload,
|
|
Proof = proof,
|
|
GeneratedAt = _timeProvider.GetUtcNow()
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generate VEX verdicts for multiple findings in batch.
|
|
/// </summary>
|
|
public async Task<IReadOnlyList<VexVerdictWithProof>> GenerateBatchVexWithProofAsync(
|
|
IEnumerable<VulnerabilityFinding> findings,
|
|
string policyVersion,
|
|
Func<VulnerabilityFinding, string> sbomEntryIdResolver,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var tasks = findings.Select(finding =>
|
|
{
|
|
var sbomEntryId = sbomEntryIdResolver(finding);
|
|
return GenerateVexWithProofAsync(finding, sbomEntryId, policyVersion, cancellationToken);
|
|
});
|
|
|
|
var results = await Task.WhenAll(tasks);
|
|
return results.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieve existing proof for a CVE + package combination.
|
|
/// Useful for audit replay and verification.
|
|
/// </summary>
|
|
public async Task<ProofBlob?> RetrieveProofAsync(
|
|
string cveId,
|
|
string packagePurl,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return await _proofService.GenerateProofAsync(cveId, packagePurl, cancellationToken);
|
|
}
|
|
|
|
private VexVerdictWithProof GenerateFallbackVex(
|
|
VulnerabilityFinding finding,
|
|
string sbomEntryId,
|
|
string policyVersion)
|
|
{
|
|
// Generate basic VEX without proof
|
|
// This is used when no evidence is available (e.g., newly disclosed CVE)
|
|
|
|
var unknownProof = BackportProofGenerator.Unknown(
|
|
finding.CveId,
|
|
finding.PackagePurl,
|
|
"no_evidence_available",
|
|
Array.Empty<ProofEvidence>());
|
|
|
|
var reasoningId = $"reasoning:{finding.CveId}:{finding.PackagePurl}";
|
|
var (statement, proofPayload) = VexProofIntegrator.GenerateWithProofMetadata(
|
|
unknownProof,
|
|
sbomEntryId,
|
|
policyVersion,
|
|
reasoningId);
|
|
|
|
return new VexVerdictWithProof
|
|
{
|
|
Statement = statement,
|
|
ProofPayload = proofPayload,
|
|
Proof = unknownProof,
|
|
GeneratedAt = _timeProvider.GetUtcNow()
|
|
};
|
|
}
|
|
|
|
private string GenerateReasoningId(VulnerabilityFinding finding, ProofBlob proof)
|
|
{
|
|
// Reasoning ID format: reasoning:{cve}:{method}:{snapshot}
|
|
return $"reasoning:{finding.CveId}:{proof.Method}:{proof.SnapshotId}";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Vulnerability finding from scanner.
|
|
/// </summary>
|
|
public sealed record VulnerabilityFinding
|
|
{
|
|
public required string CveId { get; init; }
|
|
public required string PackagePurl { get; init; }
|
|
public required string PackageName { get; init; }
|
|
public required string PackageVersion { get; init; }
|
|
public required string Severity { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// VEX verdict with associated proof.
|
|
/// </summary>
|
|
public sealed record VexVerdictWithProof
|
|
{
|
|
public required VexVerdictStatement Statement { get; init; }
|
|
public required VexVerdictProofPayload ProofPayload { get; init; }
|
|
public required ProofBlob Proof { get; init; }
|
|
public required DateTimeOffset GeneratedAt { get; init; }
|
|
}
|