Files
git.stella-ops.org/src/Concelier/__Libraries/StellaOps.Concelier.ProofService/BackportProofService.cs
StellaOps Bot 3f197814c5 save progress
2026-01-02 21:06:27 +02:00

332 lines
13 KiB
C#

namespace StellaOps.Concelier.ProofService;
using Microsoft.Extensions.Logging;
using StellaOps.Attestor.ProofChain.Generators;
using StellaOps.Attestor.ProofChain.Models;
using StellaOps.Concelier.SourceIntel;
using StellaOps.Feedser.BinaryAnalysis;
using StellaOps.Feedser.Core;
using System.Text.Json;
/// <summary>
/// Orchestrates four-tier backport detection and proof generation.
/// Queries all evidence tiers and produces cryptographic ProofBlobs.
/// </summary>
public sealed class BackportProofService
{
private readonly ILogger<BackportProofService> _logger;
private readonly IDistroAdvisoryRepository _advisoryRepo;
private readonly ISourceArtifactRepository _sourceRepo;
private readonly IPatchRepository _patchRepo;
private readonly BinaryFingerprintFactory _fingerprintFactory;
public BackportProofService(
ILogger<BackportProofService> logger,
IDistroAdvisoryRepository advisoryRepo,
ISourceArtifactRepository sourceRepo,
IPatchRepository patchRepo,
BinaryFingerprintFactory fingerprintFactory)
{
_logger = logger;
_advisoryRepo = advisoryRepo;
_sourceRepo = sourceRepo;
_patchRepo = patchRepo;
_fingerprintFactory = fingerprintFactory;
}
/// <summary>
/// Generate proof for a CVE + package combination using all available evidence.
/// </summary>
/// <param name="cveId">CVE identifier (e.g., CVE-2024-1234)</param>
/// <param name="packagePurl">Package URL (e.g., pkg:deb/debian/curl@7.64.0-4)</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>ProofBlob with aggregated evidence, or null if no evidence found</returns>
public async Task<ProofBlob?> GenerateProofAsync(
string cveId,
string packagePurl,
CancellationToken cancellationToken = default)
{
_logger.LogInformation("Generating proof for {CveId} in {Package}", cveId, packagePurl);
var evidences = new List<ProofEvidence>();
// Tier 1: Query distro advisories
var advisoryEvidence = await QueryDistroAdvisoriesAsync(cveId, packagePurl, cancellationToken);
if (advisoryEvidence != null)
{
evidences.Add(advisoryEvidence);
_logger.LogInformation("Found Tier 1 evidence (distro advisory) for {CveId}", cveId);
}
// Tier 2: Query changelog mentions
var changelogEvidences = await QueryChangelogsAsync(cveId, packagePurl, cancellationToken);
evidences.AddRange(changelogEvidences);
if (changelogEvidences.Count > 0)
{
_logger.LogInformation("Found {Count} Tier 2 evidence(s) (changelog) for {CveId}",
changelogEvidences.Count, cveId);
}
// Tier 3: Query patch headers and HunkSig
var patchEvidences = await QueryPatchesAsync(cveId, packagePurl, cancellationToken);
evidences.AddRange(patchEvidences);
if (patchEvidences.Count > 0)
{
_logger.LogInformation("Found {Count} Tier 3 evidence(s) (patches) for {CveId}",
patchEvidences.Count, cveId);
}
// Tier 4: Query binary fingerprints (if binary path available)
// Note: Binary fingerprinting requires actual binary, skipped if unavailable
var binaryPath = await ResolveBinaryPathAsync(packagePurl, cancellationToken);
if (binaryPath != null)
{
var binaryEvidences = await QueryBinaryFingerprintsAsync(cveId, binaryPath, cancellationToken);
evidences.AddRange(binaryEvidences);
if (binaryEvidences.Count > 0)
{
_logger.LogInformation("Found {Count} Tier 4 evidence(s) (binary) for {CveId}",
binaryEvidences.Count, cveId);
}
}
// If no evidence found, return unknown proof
if (evidences.Count == 0)
{
_logger.LogWarning("No evidence found for {CveId} in {Package}", cveId, packagePurl);
return BackportProofGenerator.Unknown(
cveId,
packagePurl,
"no_evidence_found",
Array.Empty<ProofEvidence>()
);
}
// Aggregate evidences into combined proof
var proof = BackportProofGenerator.CombineEvidence(cveId, packagePurl, evidences);
_logger.LogInformation(
"Generated proof {ProofId} for {CveId} with confidence {Confidence:P0} from {EvidenceCount} evidence(s)",
proof.ProofId, cveId, proof.Confidence, evidences.Count);
return proof;
}
/// <summary>
/// Generate proofs for multiple CVE + package combinations in batch.
/// </summary>
public async Task<IReadOnlyList<ProofBlob>> GenerateProofBatchAsync(
IEnumerable<(string CveId, string PackagePurl)> requests,
CancellationToken cancellationToken = default)
{
var tasks = requests.Select(req =>
GenerateProofAsync(req.CveId, req.PackagePurl, cancellationToken));
var results = await Task.WhenAll(tasks);
return results.Where(p => p != null).ToList()!;
}
private async Task<ProofEvidence?> QueryDistroAdvisoriesAsync(
string cveId,
string packagePurl,
CancellationToken cancellationToken)
{
var advisory = await _advisoryRepo.FindByCveAndPackageAsync(cveId, packagePurl, cancellationToken);
if (advisory == null) return null;
// Create evidence from advisory data
var advisoryData = SerializeToElement(advisory, out var advisoryBytes);
var dataHash = StellaOps.Canonical.Json.CanonJson.Sha256Prefixed(
StellaOps.Canonical.Json.CanonJson.CanonicalizeParsedJson(advisoryBytes));
return new ProofEvidence
{
EvidenceId = $"evidence:distro:{advisory.DistroName}:{advisory.AdvisoryId}",
Type = EvidenceType.DistroAdvisory,
Source = advisory.DistroName,
Timestamp = advisory.PublishedAt,
Data = advisoryData,
DataHash = dataHash
};
}
private async Task<List<ProofEvidence>> QueryChangelogsAsync(
string cveId,
string packagePurl,
CancellationToken cancellationToken)
{
var evidences = new List<ProofEvidence>();
var changelogs = await _sourceRepo.FindChangelogsByCveAsync(cveId, packagePurl, cancellationToken);
foreach (var changelog in changelogs)
{
var changelogData = SerializeToElement(changelog, out var changelogBytes);
var dataHash = StellaOps.Canonical.Json.CanonJson.Sha256Prefixed(
StellaOps.Canonical.Json.CanonJson.CanonicalizeParsedJson(changelogBytes));
evidences.Add(new ProofEvidence
{
EvidenceId = $"evidence:changelog:{changelog.Format}:{changelog.Version}",
Type = EvidenceType.ChangelogMention,
Source = changelog.Format,
Timestamp = changelog.Date,
Data = changelogData,
DataHash = dataHash
});
}
return evidences;
}
private async Task<List<ProofEvidence>> QueryPatchesAsync(
string cveId,
string packagePurl,
CancellationToken cancellationToken)
{
var evidences = new List<ProofEvidence>();
// Query patch headers
var patchHeaders = await _patchRepo.FindPatchHeadersByCveAsync(cveId, cancellationToken);
foreach (var header in patchHeaders)
{
var headerData = SerializeToElement(header, out var headerBytes);
var dataHash = StellaOps.Canonical.Json.CanonJson.Sha256Prefixed(
StellaOps.Canonical.Json.CanonJson.CanonicalizeParsedJson(headerBytes));
evidences.Add(new ProofEvidence
{
EvidenceId = $"evidence:patch_header:{header.PatchFilePath}",
Type = EvidenceType.PatchHeader,
Source = header.Origin ?? "unknown",
Timestamp = header.ParsedAt,
Data = headerData,
DataHash = dataHash
});
}
// Query HunkSig matches
var patchSigs = await _patchRepo.FindPatchSignaturesByCveAsync(cveId, cancellationToken);
foreach (var sig in patchSigs)
{
var sigData = SerializeToElement(sig, out var sigBytes);
var dataHash = StellaOps.Canonical.Json.CanonJson.Sha256Prefixed(
StellaOps.Canonical.Json.CanonJson.CanonicalizeParsedJson(sigBytes));
evidences.Add(new ProofEvidence
{
EvidenceId = $"evidence:hunksig:{sig.CommitSha}",
Type = EvidenceType.PatchHeader, // Reuse PatchHeader type
Source = sig.UpstreamRepo,
Timestamp = sig.ExtractedAt,
Data = sigData,
DataHash = dataHash
});
}
return evidences;
}
private async Task<List<ProofEvidence>> QueryBinaryFingerprintsAsync(
string cveId,
string binaryPath,
CancellationToken cancellationToken)
{
var evidences = new List<ProofEvidence>();
// Query known fingerprints for this CVE
var knownFingerprints = await _patchRepo.FindBinaryFingerprintsByCveAsync(cveId, cancellationToken);
if (knownFingerprints.Count == 0) return evidences;
// Match candidate binary against known fingerprints
var matchResult = await _fingerprintFactory.MatchBestAsync(binaryPath, knownFingerprints, cancellationToken);
if (matchResult?.IsMatch == true)
{
var fingerprintData = SerializeToElement(matchResult, out var fingerprintBytes);
var dataHash = StellaOps.Canonical.Json.CanonJson.Sha256Prefixed(
StellaOps.Canonical.Json.CanonJson.CanonicalizeParsedJson(fingerprintBytes));
evidences.Add(new ProofEvidence
{
EvidenceId = $"evidence:binary:{matchResult.Method}:{matchResult.MatchedFingerprintId}",
Type = EvidenceType.BinaryFingerprint,
Source = matchResult.Method.ToString(),
Timestamp = DateTimeOffset.UtcNow,
Data = fingerprintData,
DataHash = dataHash
});
}
return evidences;
}
private async Task<string?> ResolveBinaryPathAsync(string packagePurl, CancellationToken cancellationToken)
{
// Resolve PURL to actual binary path
// This would query package metadata or local package cache
// Simplified: return null if not available
await Task.CompletedTask;
return null;
}
private static JsonElement SerializeToElement<T>(T value, out byte[] jsonBytes)
{
jsonBytes = JsonSerializer.SerializeToUtf8Bytes(value);
using var document = JsonDocument.Parse(jsonBytes);
return document.RootElement.Clone();
}
}
// Repository interfaces (to be implemented by storage layer)
public interface IDistroAdvisoryRepository
{
Task<DistroAdvisoryDto?> FindByCveAndPackageAsync(string cveId, string packagePurl, CancellationToken ct);
}
public interface ISourceArtifactRepository
{
Task<IReadOnlyList<ChangelogDto>> FindChangelogsByCveAsync(string cveId, string packagePurl, CancellationToken ct);
}
public interface IPatchRepository
{
Task<IReadOnlyList<PatchHeaderDto>> FindPatchHeadersByCveAsync(string cveId, CancellationToken ct);
Task<IReadOnlyList<PatchSigDto>> FindPatchSignaturesByCveAsync(string cveId, CancellationToken ct);
Task<IReadOnlyList<StellaOps.Feedser.BinaryAnalysis.Models.BinaryFingerprint>> FindBinaryFingerprintsByCveAsync(string cveId, CancellationToken ct);
}
// DTOs for repository results
public sealed record DistroAdvisoryDto
{
public required string AdvisoryId { get; init; }
public required string DistroName { get; init; }
public required DateTimeOffset PublishedAt { get; init; }
public required string Status { get; init; }
}
public sealed record ChangelogDto
{
public required string Format { get; init; }
public required string Version { get; init; }
public required DateTimeOffset Date { get; init; }
public required IReadOnlyList<string> CveIds { get; init; }
}
public sealed record PatchHeaderDto
{
public required string PatchFilePath { get; init; }
public required string? Origin { get; init; }
public required DateTimeOffset ParsedAt { get; init; }
public required IReadOnlyList<string> CveIds { get; init; }
}
public sealed record PatchSigDto
{
public required string CommitSha { get; init; }
public required string UpstreamRepo { get; init; }
public required DateTimeOffset ExtractedAt { get; init; }
public required string HunkHash { get; init; }
}