using StellaOps.Excititor.Core; using StellaOps.Excititor.Core.Storage; using StellaOps.Excititor.WebService.Contracts; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace StellaOps.Excititor.WebService.Services; internal interface IVexEvidenceChunkService { Task QueryAsync(VexEvidenceChunkRequest request, CancellationToken cancellationToken); } internal sealed record VexEvidenceChunkRequest( string Tenant, string VulnerabilityId, string ProductKey, ImmutableHashSet ProviderIds, ImmutableHashSet Statuses, DateTimeOffset? Since, int Limit); internal sealed record VexEvidenceChunkResult( IReadOnlyList Chunks, bool Truncated, int TotalCount, DateTimeOffset GeneratedAtUtc); internal sealed class VexEvidenceChunkService : IVexEvidenceChunkService { private readonly IVexClaimStore _claimStore; private readonly TimeProvider _timeProvider; public VexEvidenceChunkService(IVexClaimStore claimStore, TimeProvider timeProvider) { _claimStore = claimStore ?? throw new ArgumentNullException(nameof(claimStore)); _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); } public async Task QueryAsync(VexEvidenceChunkRequest request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); var claims = await _claimStore .FindAsync(request.VulnerabilityId, request.ProductKey, request.Since, cancellationToken) .ConfigureAwait(false); var filtered = claims .Where(claim => MatchesProvider(claim, request.ProviderIds)) .Where(claim => MatchesStatus(claim, request.Statuses)) .OrderByDescending(claim => claim.LastSeen) .ToList(); var total = filtered.Count; if (filtered.Count > request.Limit) { filtered = filtered.Take(request.Limit).ToList(); } var chunks = filtered .Select(MapChunk) .ToList(); return new VexEvidenceChunkResult( chunks, total > request.Limit, total, _timeProvider.GetUtcNow()); } private static bool MatchesProvider(VexClaim claim, ImmutableHashSet providers) => providers.Count == 0 || providers.Contains(claim.ProviderId, StringComparer.OrdinalIgnoreCase); private static bool MatchesStatus(VexClaim claim, ImmutableHashSet statuses) => statuses.Count == 0 || statuses.Contains(claim.Status); private static VexEvidenceChunkResponse MapChunk(VexClaim claim) { var observationId = string.Create(CultureInfo.InvariantCulture, $"{claim.ProviderId}:{claim.Document.Digest}"); var linksetId = string.Create(CultureInfo.InvariantCulture, $"{claim.VulnerabilityId}:{claim.Product.Key}"); var scope = new VexEvidenceChunkScope( claim.Product.Key, claim.Product.Name, claim.Product.Version, claim.Product.Purl, claim.Product.Cpe, claim.Product.ComponentIdentifiers); var document = new VexEvidenceChunkDocument( claim.Document.Digest, claim.Document.Format.ToString().ToLowerInvariant(), claim.Document.SourceUri.ToString(), claim.Document.Revision); var signature = claim.Document.Signature is null ? null : new VexEvidenceChunkSignature( claim.Document.Signature.Type, claim.Document.Signature.Subject, claim.Document.Signature.Issuer, claim.Document.Signature.KeyId, claim.Document.Signature.VerifiedAt, claim.Document.Signature.TransparencyLogReference); var scopeScore = claim.Confidence?.Score ?? claim.Signals?.Severity?.Score; return new VexEvidenceChunkResponse( observationId, linksetId, claim.VulnerabilityId, claim.Product.Key, claim.ProviderId, claim.Status.ToString(), claim.Justification?.ToString(), claim.Detail, scopeScore, claim.FirstSeen, claim.LastSeen, scope, document, signature, claim.AdditionalMetadata); } }