332 lines
13 KiB
C#
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; }
|
|
}
|