Files
git.stella-ops.org/src/BinaryIndex/__Libraries/StellaOps.BinaryIndex.Persistence/Services/BinaryVulnerabilityService.cs

202 lines
7.1 KiB
C#

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;
/// <summary>
/// Implementation of binary vulnerability lookup service.
/// </summary>
public sealed class BinaryVulnerabilityService : IBinaryVulnerabilityService
{
private readonly IBinaryVulnAssertionRepository _assertionRepo;
private readonly IFixIndexRepository? _fixIndexRepo;
private readonly IFingerprintMatcher? _fingerprintMatcher;
private readonly ILogger<BinaryVulnerabilityService> _logger;
public BinaryVulnerabilityService(
IBinaryVulnAssertionRepository assertionRepo,
ILogger<BinaryVulnerabilityService> logger,
IFixIndexRepository? fixIndexRepo = null,
IFingerprintMatcher? fingerprintMatcher = null)
{
_assertionRepo = assertionRepo;
_logger = logger;
_fixIndexRepo = fixIndexRepo;
_fingerprintMatcher = fingerprintMatcher;
}
public async Task<ImmutableArray<BinaryVulnMatch>> LookupByIdentityAsync(
BinaryIdentity identity,
LookupOptions? options = null,
CancellationToken ct = default)
{
options ??= new LookupOptions();
var matches = new List<BinaryVulnMatch>();
// 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<ImmutableDictionary<string, ImmutableArray<BinaryVulnMatch>>> LookupBatchAsync(
IEnumerable<BinaryIdentity> identities,
LookupOptions? options = null,
CancellationToken ct = default)
{
var results = new Dictionary<string, ImmutableArray<BinaryVulnMatch>>();
foreach (var identity in identities)
{
var matches = await LookupByIdentityAsync(identity, options, ct);
results[identity.BinaryKey] = matches;
}
return results.ToImmutableDictionary();
}
public async Task<FixStatusResult?> 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<ImmutableDictionary<string, FixStatusResult>> GetFixStatusBatchAsync(
string distro,
string release,
string sourcePkg,
IEnumerable<string> cveIds,
CancellationToken ct = default)
{
var results = new Dictionary<string, FixStatusResult>();
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<ImmutableArray<BinaryVulnMatch>> 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<BinaryVulnMatch>.Empty;
}
options ??= new FingerprintLookupOptions();
var matches = new List<BinaryVulnMatch>();
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<ImmutableDictionary<string, ImmutableArray<BinaryVulnMatch>>> LookupByFingerprintBatchAsync(
IEnumerable<(string Key, byte[] Fingerprint)> fingerprints,
FingerprintLookupOptions? options = null,
CancellationToken ct = default)
{
var results = new Dictionary<string, ImmutableArray<BinaryVulnMatch>>();
foreach (var (key, fingerprint) in fingerprints)
{
var matches = await LookupByFingerprintAsync(fingerprint, options, ct).ConfigureAwait(false);
results[key] = matches;
}
return results.ToImmutableDictionary();
}
}