using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using StellaOps.Excititor.Core; namespace StellaOps.Excititor.WebService.Services; internal interface IVexObservationProjectionService { Task QueryAsync( VexObservationProjectionRequest request, CancellationToken cancellationToken); } internal sealed record VexObservationProjectionRequest( string Tenant, string VulnerabilityId, string ProductKey, ImmutableHashSet ProviderIds, ImmutableHashSet Statuses, DateTimeOffset? Since, int Limit); internal sealed record VexObservationProjectionResult( IReadOnlyList Statements, bool Truncated, int TotalCount, DateTimeOffset GeneratedAtUtc); internal sealed record VexObservationStatementProjection( string ObservationId, string ProviderId, VexClaimStatus Status, VexJustification? Justification, string? Detail, DateTimeOffset FirstSeen, DateTimeOffset LastSeen, VexProductScope Scope, IReadOnlyList Anchors, VexClaimDocument Document, VexSignatureMetadata? Signature); internal sealed record VexProductScope( string Key, string? Name, string? Version, string? Purl, string? Cpe, IReadOnlyList ComponentIdentifiers); internal sealed class VexObservationProjectionService : IVexObservationProjectionService { private static readonly string[] AnchorKeys = { "json_pointer", "jsonPointer", "statement_locator", "locator", "paragraph", "section", "path" }; private readonly IVexClaimStore _claimStore; private readonly TimeProvider _timeProvider; public VexObservationProjectionService(IVexClaimStore claimStore, TimeProvider? timeProvider = null) { _claimStore = claimStore ?? throw new ArgumentNullException(nameof(claimStore)); _timeProvider = timeProvider ?? TimeProvider.System; } public async Task QueryAsync( VexObservationProjectionRequest request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); cancellationToken.ThrowIfCancellationRequested(); 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) .ThenBy(claim => claim.ProviderId, StringComparer.Ordinal) .ToList(); var total = filtered.Count; var page = filtered.Take(request.Limit).ToList(); var statements = page .Select(claim => MapClaim(claim)) .ToList(); return new VexObservationProjectionResult( statements, 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 VexObservationStatementProjection MapClaim(VexClaim claim) { var observationId = string.Create(CultureInfo.InvariantCulture, $"{claim.ProviderId}:{claim.Document.Digest}"); var anchors = ExtractAnchors(claim.AdditionalMetadata); var scope = new VexProductScope( claim.Product.Key, claim.Product.Name, claim.Product.Version, claim.Product.Purl, claim.Product.Cpe, claim.Product.ComponentIdentifiers); return new VexObservationStatementProjection( observationId, claim.ProviderId, claim.Status, claim.Justification, claim.Detail, claim.FirstSeen, claim.LastSeen, scope, anchors, claim.Document, claim.Document.Signature); } private static IReadOnlyList ExtractAnchors(ImmutableSortedDictionary metadata) { if (metadata.Count == 0) { return Array.Empty(); } var anchors = new List(); foreach (var key in AnchorKeys) { if (metadata.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)) { anchors.Add(value.Trim()); } } return anchors.Count == 0 ? Array.Empty() : anchors; } }