using System.Collections.Immutable; using Microsoft.Extensions.Logging; using StellaOps.BinaryIndex.Core.Models; using StellaOps.BinaryIndex.Core.Services; using StellaOps.BinaryIndex.FixIndex.Repositories; using StellaOps.BinaryIndex.Fingerprints.Matching; using StellaOps.BinaryIndex.Persistence.Repositories; namespace StellaOps.BinaryIndex.Persistence.Services; /// /// Implementation of binary vulnerability lookup service. /// public sealed class BinaryVulnerabilityService : IBinaryVulnerabilityService { private readonly IBinaryVulnAssertionRepository _assertionRepo; private readonly IFixIndexRepository? _fixIndexRepo; private readonly IFingerprintMatcher? _fingerprintMatcher; private readonly ILogger _logger; public BinaryVulnerabilityService( IBinaryVulnAssertionRepository assertionRepo, ILogger logger, IFixIndexRepository? fixIndexRepo = null, IFingerprintMatcher? fingerprintMatcher = null) { _assertionRepo = assertionRepo; _logger = logger; _fixIndexRepo = fixIndexRepo; _fingerprintMatcher = fingerprintMatcher; } public async Task> LookupByIdentityAsync( BinaryIdentity identity, LookupOptions? options = null, CancellationToken ct = default) { options ??= new LookupOptions(); var matches = new List(); // Check explicit assertions var assertions = await _assertionRepo.GetByBinaryKeyAsync(identity.BinaryKey, ct); foreach (var assertion in assertions.Where(a => a.Status == "affected")) { matches.Add(new BinaryVulnMatch { CveId = assertion.CveId, VulnerablePurl = "pkg:unknown", // Resolved from advisory Method = MapMethod(assertion.Method), Confidence = assertion.Confidence ?? 0.9m, Evidence = new MatchEvidence { BuildId = identity.BuildId } }); } _logger.LogDebug("Found {Count} vulnerability matches for {BinaryKey}", matches.Count, identity.BinaryKey); return matches.ToImmutableArray(); } public async Task>> LookupBatchAsync( IEnumerable identities, LookupOptions? options = null, CancellationToken ct = default) { var results = new Dictionary>(); foreach (var identity in identities) { var matches = await LookupByIdentityAsync(identity, options, ct); results[identity.BinaryKey] = matches; } return results.ToImmutableDictionary(); } public async Task GetFixStatusAsync( string distro, string release, string sourcePkg, string cveId, CancellationToken ct = default) { if (_fixIndexRepo is null) { _logger.LogWarning("Fix index repository not configured, cannot check fix status"); return null; } var entry = await _fixIndexRepo.GetFixStatusAsync(distro, release, sourcePkg, cveId, ct); if (entry is null) { _logger.LogDebug("No fix status found for {CveId} in {Distro}/{Release}/{Package}", cveId, distro, release, sourcePkg); return null; } return new FixStatusResult { State = entry.State, FixedVersion = entry.FixedVersion, Method = entry.Method, Confidence = entry.Confidence, EvidenceId = entry.EvidenceId }; } public async Task> GetFixStatusBatchAsync( string distro, string release, string sourcePkg, IEnumerable cveIds, CancellationToken ct = default) { var results = new Dictionary(); if (_fixIndexRepo is null) { _logger.LogWarning("Fix index repository not configured, cannot check fix status"); return results.ToImmutableDictionary(); } foreach (var cveId in cveIds) { var status = await GetFixStatusAsync(distro, release, sourcePkg, cveId, ct); if (status is not null) { results[cveId] = status; } } _logger.LogDebug("Found fix status for {Count} CVEs in {Distro}/{Release}/{Package}", results.Count, distro, release, sourcePkg); return results.ToImmutableDictionary(); } private static MatchMethod MapMethod(string method) => method switch { "buildid_catalog" => MatchMethod.BuildIdCatalog, "fingerprint_match" => MatchMethod.FingerprintMatch, _ => MatchMethod.RangeMatch }; public async Task> LookupByFingerprintAsync( byte[] fingerprint, FingerprintLookupOptions? options = null, CancellationToken ct = default) { if (_fingerprintMatcher is null) { _logger.LogWarning("Fingerprint matcher not configured, cannot perform fingerprint lookup"); return ImmutableArray.Empty; } options ??= new FingerprintLookupOptions(); var matches = new List(); var matchOptions = new MatchOptions { MinSimilarity = options.MinSimilarity, MaxCandidates = options.MaxCandidates, Architecture = options.Architecture }; var result = await _fingerprintMatcher.MatchAsync(fingerprint, matchOptions, ct).ConfigureAwait(false); if (result.IsMatch && result.MatchedFingerprint is not null) { var fp = result.MatchedFingerprint; matches.Add(new BinaryVulnMatch { CveId = fp.CveId, VulnerablePurl = fp.Purl ?? $"pkg:generic/{fp.Component}", Method = MatchMethod.FingerprintMatch, Confidence = result.Confidence, Evidence = new MatchEvidence { Similarity = result.Similarity, MatchedFunction = fp.FunctionName } }); } _logger.LogDebug("Fingerprint lookup found {Count} matches", matches.Count); return matches.ToImmutableArray(); } public async Task>> LookupByFingerprintBatchAsync( IEnumerable<(string Key, byte[] Fingerprint)> fingerprints, FingerprintLookupOptions? options = null, CancellationToken ct = default) { var results = new Dictionary>(); foreach (var (key, fingerprint) in fingerprints) { var matches = await LookupByFingerprintAsync(fingerprint, options, ct).ConfigureAwait(false); results[key] = matches; } return results.ToImmutableDictionary(); } }