Files
git.stella-ops.org/src/Excititor/StellaOps.Excititor.WebService/Services/VexEvidenceChunkService.cs
2026-02-01 21:37:40 +02:00

132 lines
4.5 KiB
C#

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<VexEvidenceChunkResult> QueryAsync(VexEvidenceChunkRequest request, CancellationToken cancellationToken);
}
internal sealed record VexEvidenceChunkRequest(
string Tenant,
string VulnerabilityId,
string ProductKey,
ImmutableHashSet<string> ProviderIds,
ImmutableHashSet<VexClaimStatus> Statuses,
DateTimeOffset? Since,
int Limit);
internal sealed record VexEvidenceChunkResult(
IReadOnlyList<VexEvidenceChunkResponse> 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<VexEvidenceChunkResult> 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<string> providers)
=> providers.Count == 0 || providers.Contains(claim.ProviderId, StringComparer.OrdinalIgnoreCase);
private static bool MatchesStatus(VexClaim claim, ImmutableHashSet<VexClaimStatus> 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);
}
}