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; /// /// Orchestrates four-tier backport detection and proof generation. /// Queries all evidence tiers and produces cryptographic ProofBlobs. /// public sealed class BackportProofService { private readonly ILogger _logger; private readonly IDistroAdvisoryRepository _advisoryRepo; private readonly ISourceArtifactRepository _sourceRepo; private readonly IPatchRepository _patchRepo; private readonly BinaryFingerprintFactory _fingerprintFactory; public BackportProofService( ILogger logger, IDistroAdvisoryRepository advisoryRepo, ISourceArtifactRepository sourceRepo, IPatchRepository patchRepo, BinaryFingerprintFactory fingerprintFactory) { _logger = logger; _advisoryRepo = advisoryRepo; _sourceRepo = sourceRepo; _patchRepo = patchRepo; _fingerprintFactory = fingerprintFactory; } /// /// Generate proof for a CVE + package combination using all available evidence. /// /// CVE identifier (e.g., CVE-2024-1234) /// Package URL (e.g., pkg:deb/debian/curl@7.64.0-4) /// Cancellation token /// ProofBlob with aggregated evidence, or null if no evidence found public async Task GenerateProofAsync( string cveId, string packagePurl, CancellationToken cancellationToken = default) { _logger.LogInformation("Generating proof for {CveId} in {Package}", cveId, packagePurl); var evidences = new List(); // 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() ); } // 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; } /// /// Generate proofs for multiple CVE + package combinations in batch. /// public async Task> 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 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> QueryChangelogsAsync( string cveId, string packagePurl, CancellationToken cancellationToken) { var evidences = new List(); 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> QueryPatchesAsync( string cveId, string packagePurl, CancellationToken cancellationToken) { var evidences = new List(); // 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> QueryBinaryFingerprintsAsync( string cveId, string binaryPath, CancellationToken cancellationToken) { var evidences = new List(); // 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 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 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 FindByCveAndPackageAsync(string cveId, string packagePurl, CancellationToken ct); } public interface ISourceArtifactRepository { Task> FindChangelogsByCveAsync(string cveId, string packagePurl, CancellationToken ct); } public interface IPatchRepository { Task> FindPatchHeadersByCveAsync(string cveId, CancellationToken ct); Task> FindPatchSignaturesByCveAsync(string cveId, CancellationToken ct); Task> 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 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 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; } }