Add unit tests and implementations for MongoDB index models and OpenAPI metadata
- Implemented `MongoIndexModelTests` to verify index models for various stores. - Created `OpenApiMetadataFactory` with methods to generate OpenAPI metadata. - Added tests for `OpenApiMetadataFactory` to ensure expected defaults and URL overrides. - Introduced `ObserverSurfaceSecrets` and `WebhookSurfaceSecrets` for managing secrets. - Developed `RuntimeSurfaceFsClient` and `WebhookSurfaceFsClient` for manifest retrieval. - Added dependency injection tests for `SurfaceEnvironmentRegistration` in both Observer and Webhook contexts. - Implemented tests for secret resolution in `ObserverSurfaceSecretsTests` and `WebhookSurfaceSecretsTests`. - Created `EnsureLinkNotMergeCollectionsMigrationTests` to validate MongoDB migration logic. - Added project files for MongoDB tests and NuGet package mirroring.
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Excititor.WebService.Contracts;
|
||||
|
||||
public sealed record VexEvidenceChunkResponse(
|
||||
[property: JsonPropertyName("observationId")] string ObservationId,
|
||||
[property: JsonPropertyName("linksetId")] string LinksetId,
|
||||
[property: JsonPropertyName("vulnerabilityId")] string VulnerabilityId,
|
||||
[property: JsonPropertyName("productKey")] string ProductKey,
|
||||
[property: JsonPropertyName("providerId")] string ProviderId,
|
||||
[property: JsonPropertyName("status")] string Status,
|
||||
[property: JsonPropertyName("justification")] string? Justification,
|
||||
[property: JsonPropertyName("detail")] string? Detail,
|
||||
[property: JsonPropertyName("scopeScore")] double? ScopeScore,
|
||||
[property: JsonPropertyName("firstSeen")] DateTimeOffset FirstSeen,
|
||||
[property: JsonPropertyName("lastSeen")] DateTimeOffset LastSeen,
|
||||
[property: JsonPropertyName("scope")] VexEvidenceChunkScope Scope,
|
||||
[property: JsonPropertyName("document")] VexEvidenceChunkDocument Document,
|
||||
[property: JsonPropertyName("signature")] VexEvidenceChunkSignature? Signature,
|
||||
[property: JsonPropertyName("metadata")] IReadOnlyDictionary<string, string> Metadata);
|
||||
|
||||
public sealed record VexEvidenceChunkScope(
|
||||
[property: JsonPropertyName("key")] string Key,
|
||||
[property: JsonPropertyName("name")] string? Name,
|
||||
[property: JsonPropertyName("version")] string? Version,
|
||||
[property: JsonPropertyName("purl")] string? Purl,
|
||||
[property: JsonPropertyName("cpe")] string? Cpe,
|
||||
[property: JsonPropertyName("componentIdentifiers")] IReadOnlyList<string> ComponentIdentifiers);
|
||||
|
||||
public sealed record VexEvidenceChunkDocument(
|
||||
[property: JsonPropertyName("digest")] string Digest,
|
||||
[property: JsonPropertyName("format")] string Format,
|
||||
[property: JsonPropertyName("sourceUri")] string SourceUri,
|
||||
[property: JsonPropertyName("revision")] string? Revision);
|
||||
|
||||
public sealed record VexEvidenceChunkSignature(
|
||||
[property: JsonPropertyName("type")] string Type,
|
||||
[property: JsonPropertyName("subject")] string? Subject,
|
||||
[property: JsonPropertyName("issuer")] string? Issuer,
|
||||
[property: JsonPropertyName("keyId")] string? KeyId,
|
||||
[property: JsonPropertyName("verifiedAt")] DateTimeOffset? VerifiedAt,
|
||||
[property: JsonPropertyName("transparencyRef")] string? TransparencyRef);
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -51,11 +52,12 @@ services.AddOptions<ExcititorObservabilityOptions>()
|
||||
services.AddScoped<ExcititorHealthService>();
|
||||
services.AddExcititorAocGuards();
|
||||
services.AddVexExportEngine();
|
||||
services.AddVexExportCacheServices();
|
||||
services.AddVexExportCacheServices();
|
||||
services.AddVexAttestation();
|
||||
services.Configure<VexAttestationClientOptions>(configuration.GetSection("Excititor:Attestation:Client"));
|
||||
services.Configure<VexAttestationVerificationOptions>(configuration.GetSection("Excititor:Attestation:Verification"));
|
||||
services.AddVexPolicy();
|
||||
services.AddVexPolicy();
|
||||
services.AddSingleton<IVexEvidenceChunkService, VexEvidenceChunkService>();
|
||||
services.AddRedHatCsafConnector();
|
||||
services.Configure<MirrorDistributionOptions>(configuration.GetSection(MirrorDistributionOptions.SectionName));
|
||||
services.AddSingleton<MirrorRateLimiter>();
|
||||
@@ -515,6 +517,69 @@ app.MapGet("/v1/vex/observations/{vulnerabilityId}/{productKey}", async (
|
||||
return Results.Json(response);
|
||||
});
|
||||
|
||||
app.MapGet("/v1/vex/evidence/chunks", async (
|
||||
HttpContext context,
|
||||
[FromServices] IVexEvidenceChunkService chunkService,
|
||||
[FromServices] IOptions<VexMongoStorageOptions> storageOptions,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.read");
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: false, out var tenant, out var tenantError))
|
||||
{
|
||||
return tenantError;
|
||||
}
|
||||
|
||||
var vulnerabilityId = context.Request.Query["vulnerabilityId"].FirstOrDefault();
|
||||
var productKey = context.Request.Query["productKey"].FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(vulnerabilityId) || string.IsNullOrWhiteSpace(productKey))
|
||||
{
|
||||
return ValidationProblem("vulnerabilityId and productKey are required.");
|
||||
}
|
||||
|
||||
var providerFilter = BuildStringFilterSet(context.Request.Query["providerId"]);
|
||||
var statusFilter = BuildStatusFilter(context.Request.Query["status"]);
|
||||
var since = ParseSinceTimestamp(context.Request.Query["since"]);
|
||||
var limit = ResolveLimit(context.Request.Query["limit"], defaultValue: 200, min: 1, max: 500);
|
||||
|
||||
var request = new VexEvidenceChunkRequest(
|
||||
tenant,
|
||||
vulnerabilityId.Trim(),
|
||||
productKey.Trim(),
|
||||
providerFilter,
|
||||
statusFilter,
|
||||
since,
|
||||
limit);
|
||||
|
||||
VexEvidenceChunkResult result;
|
||||
try
|
||||
{
|
||||
result = await chunkService.QueryAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return Results.StatusCode(StatusCodes.Status499ClientClosedRequest);
|
||||
}
|
||||
|
||||
context.Response.Headers["X-Total-Count"] = result.TotalCount.ToString(CultureInfo.InvariantCulture);
|
||||
context.Response.Headers["X-Truncated"] = result.Truncated ? "true" : "false";
|
||||
context.Response.ContentType = "application/x-ndjson";
|
||||
|
||||
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
||||
foreach (var chunk in result.Chunks)
|
||||
{
|
||||
var line = JsonSerializer.Serialize(chunk, options);
|
||||
await context.Response.WriteAsync(line, cancellationToken).ConfigureAwait(false);
|
||||
await context.Response.WriteAsync("\n", cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return Results.Empty;
|
||||
});
|
||||
|
||||
app.MapPost("/aoc/verify", async (
|
||||
HttpContext context,
|
||||
VexAocVerifyRequest? request,
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
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;
|
||||
using StellaOps.Excititor.Storage.Mongo;
|
||||
using StellaOps.Excititor.WebService.Contracts;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user