- Created SignerEndpointsTests to validate the SignDsse and VerifyReferrers endpoints. - Implemented StubBearerAuthenticationDefaults and StubBearerAuthenticationHandler for token-based authentication. - Developed ConcelierExporterClient for managing Trivy DB settings and export operations. - Added TrivyDbSettingsPageComponent for UI interactions with Trivy DB settings, including form handling and export triggering. - Implemented styles and HTML structure for Trivy DB settings page. - Created NotifySmokeCheck tool for validating Redis event streams and Notify deliveries.
288 lines
10 KiB
C#
288 lines
10 KiB
C#
using System.Collections.Immutable;
|
|
using System.Globalization;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using StellaOps.Excititor.WebService.Services;
|
|
|
|
namespace StellaOps.Excititor.WebService.Endpoints;
|
|
|
|
internal static class IngestEndpoints
|
|
{
|
|
private const string AdminScope = "vex.admin";
|
|
|
|
public static void MapIngestEndpoints(IEndpointRouteBuilder app)
|
|
{
|
|
var group = app.MapGroup("/excititor");
|
|
|
|
group.MapPost("/init", HandleInitAsync);
|
|
group.MapPost("/ingest/run", HandleRunAsync);
|
|
group.MapPost("/ingest/resume", HandleResumeAsync);
|
|
group.MapPost("/reconcile", HandleReconcileAsync);
|
|
}
|
|
|
|
internal static async Task<IResult> HandleInitAsync(
|
|
HttpContext httpContext,
|
|
ExcititorInitRequest request,
|
|
IVexIngestOrchestrator orchestrator,
|
|
TimeProvider timeProvider,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var scopeResult = ScopeAuthorization.RequireScope(httpContext, AdminScope);
|
|
if (scopeResult is not null)
|
|
{
|
|
return scopeResult;
|
|
}
|
|
|
|
var providerIds = NormalizeProviders(request.Providers);
|
|
_ = timeProvider;
|
|
var options = new IngestInitOptions(providerIds, request.Resume ?? false);
|
|
|
|
var summary = await orchestrator.InitializeAsync(options, cancellationToken).ConfigureAwait(false);
|
|
var message = $"Initialized {summary.ProviderCount} provider(s); {summary.SuccessCount} succeeded, {summary.FailureCount} failed.";
|
|
|
|
return TypedResults.Ok<object>(new
|
|
{
|
|
message,
|
|
runId = summary.RunId,
|
|
startedAt = summary.StartedAt,
|
|
completedAt = summary.CompletedAt,
|
|
providers = summary.Providers.Select(static provider => new
|
|
{
|
|
providerId = provider.ProviderId,
|
|
displayName = provider.DisplayName,
|
|
status = provider.Status,
|
|
durationMs = provider.Duration.TotalMilliseconds,
|
|
error = provider.Error
|
|
})
|
|
});
|
|
}
|
|
|
|
internal static async Task<IResult> HandleRunAsync(
|
|
HttpContext httpContext,
|
|
ExcititorIngestRunRequest request,
|
|
IVexIngestOrchestrator orchestrator,
|
|
TimeProvider timeProvider,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var scopeResult = ScopeAuthorization.RequireScope(httpContext, AdminScope);
|
|
if (scopeResult is not null)
|
|
{
|
|
return scopeResult;
|
|
}
|
|
|
|
if (!TryParseDateTimeOffset(request.Since, out var since, out var sinceError))
|
|
{
|
|
return TypedResults.BadRequest<object>(new { message = sinceError });
|
|
}
|
|
|
|
if (!TryParseTimeSpan(request.Window, out var window, out var windowError))
|
|
{
|
|
return TypedResults.BadRequest<object>(new { message = windowError });
|
|
}
|
|
|
|
_ = timeProvider;
|
|
var providerIds = NormalizeProviders(request.Providers);
|
|
var options = new IngestRunOptions(
|
|
providerIds,
|
|
since,
|
|
window,
|
|
request.Force ?? false);
|
|
|
|
var summary = await orchestrator.RunAsync(options, cancellationToken).ConfigureAwait(false);
|
|
var message = $"Ingest run completed for {summary.ProviderCount} provider(s); {summary.SuccessCount} succeeded, {summary.FailureCount} failed.";
|
|
|
|
return TypedResults.Ok<object>(new
|
|
{
|
|
message,
|
|
runId = summary.RunId,
|
|
startedAt = summary.StartedAt,
|
|
completedAt = summary.CompletedAt,
|
|
durationMs = summary.Duration.TotalMilliseconds,
|
|
providers = summary.Providers.Select(static provider => new
|
|
{
|
|
providerId = provider.ProviderId,
|
|
status = provider.Status,
|
|
documents = provider.Documents,
|
|
claims = provider.Claims,
|
|
startedAt = provider.StartedAt,
|
|
completedAt = provider.CompletedAt,
|
|
durationMs = provider.Duration.TotalMilliseconds,
|
|
lastDigest = provider.LastDigest,
|
|
lastUpdated = provider.LastUpdated,
|
|
checkpoint = provider.Checkpoint,
|
|
error = provider.Error
|
|
})
|
|
});
|
|
}
|
|
|
|
internal static async Task<IResult> HandleResumeAsync(
|
|
HttpContext httpContext,
|
|
ExcititorIngestResumeRequest request,
|
|
IVexIngestOrchestrator orchestrator,
|
|
TimeProvider timeProvider,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var scopeResult = ScopeAuthorization.RequireScope(httpContext, AdminScope);
|
|
if (scopeResult is not null)
|
|
{
|
|
return scopeResult;
|
|
}
|
|
|
|
_ = timeProvider;
|
|
var providerIds = NormalizeProviders(request.Providers);
|
|
var options = new IngestResumeOptions(providerIds, request.Checkpoint);
|
|
|
|
var summary = await orchestrator.ResumeAsync(options, cancellationToken).ConfigureAwait(false);
|
|
var message = $"Resume run completed for {summary.ProviderCount} provider(s); {summary.SuccessCount} succeeded, {summary.FailureCount} failed.";
|
|
|
|
return TypedResults.Ok<object>(new
|
|
{
|
|
message,
|
|
runId = summary.RunId,
|
|
startedAt = summary.StartedAt,
|
|
completedAt = summary.CompletedAt,
|
|
durationMs = summary.Duration.TotalMilliseconds,
|
|
providers = summary.Providers.Select(static provider => new
|
|
{
|
|
providerId = provider.ProviderId,
|
|
status = provider.Status,
|
|
documents = provider.Documents,
|
|
claims = provider.Claims,
|
|
startedAt = provider.StartedAt,
|
|
completedAt = provider.CompletedAt,
|
|
durationMs = provider.Duration.TotalMilliseconds,
|
|
since = provider.Since,
|
|
checkpoint = provider.Checkpoint,
|
|
error = provider.Error
|
|
})
|
|
});
|
|
}
|
|
|
|
internal static async Task<IResult> HandleReconcileAsync(
|
|
HttpContext httpContext,
|
|
ExcititorReconcileRequest request,
|
|
IVexIngestOrchestrator orchestrator,
|
|
TimeProvider timeProvider,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var scopeResult = ScopeAuthorization.RequireScope(httpContext, AdminScope);
|
|
if (scopeResult is not null)
|
|
{
|
|
return scopeResult;
|
|
}
|
|
|
|
if (!TryParseTimeSpan(request.MaxAge, out var maxAge, out var error))
|
|
{
|
|
return TypedResults.BadRequest<object>(new { message = error });
|
|
}
|
|
|
|
_ = timeProvider;
|
|
var providerIds = NormalizeProviders(request.Providers);
|
|
var options = new ReconcileOptions(providerIds, maxAge);
|
|
|
|
var summary = await orchestrator.ReconcileAsync(options, cancellationToken).ConfigureAwait(false);
|
|
var message = $"Reconcile completed for {summary.ProviderCount} provider(s); {summary.ReconciledCount} reconciled, {summary.SkippedCount} skipped, {summary.FailureCount} failed.";
|
|
|
|
return TypedResults.Ok<object>(new
|
|
{
|
|
message,
|
|
runId = summary.RunId,
|
|
startedAt = summary.StartedAt,
|
|
completedAt = summary.CompletedAt,
|
|
durationMs = summary.Duration.TotalMilliseconds,
|
|
providers = summary.Providers.Select(static provider => new
|
|
{
|
|
providerId = provider.ProviderId,
|
|
status = provider.Status,
|
|
action = provider.Action,
|
|
lastUpdated = provider.LastUpdated,
|
|
threshold = provider.Threshold,
|
|
documents = provider.Documents,
|
|
claims = provider.Claims,
|
|
error = provider.Error
|
|
})
|
|
});
|
|
}
|
|
|
|
internal static ImmutableArray<string> NormalizeProviders(IReadOnlyCollection<string>? providers)
|
|
{
|
|
if (providers is null || providers.Count == 0)
|
|
{
|
|
return ImmutableArray<string>.Empty;
|
|
}
|
|
|
|
var set = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var provider in providers)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(provider))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
set.Add(provider.Trim());
|
|
}
|
|
|
|
return set.ToImmutableArray();
|
|
}
|
|
|
|
internal static bool TryParseDateTimeOffset(string? value, out DateTimeOffset? result, out string? error)
|
|
{
|
|
result = null;
|
|
error = null;
|
|
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (DateTimeOffset.TryParse(
|
|
value.Trim(),
|
|
CultureInfo.InvariantCulture,
|
|
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
|
|
out var parsed))
|
|
{
|
|
result = parsed;
|
|
return true;
|
|
}
|
|
|
|
error = "Invalid 'since' value. Use ISO-8601 format (e.g. 2025-10-19T12:30:00Z).";
|
|
return false;
|
|
}
|
|
|
|
internal static bool TryParseTimeSpan(string? value, out TimeSpan? result, out string? error)
|
|
{
|
|
result = null;
|
|
error = null;
|
|
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (TimeSpan.TryParse(value.Trim(), CultureInfo.InvariantCulture, out var parsed) && parsed >= TimeSpan.Zero)
|
|
{
|
|
result = parsed;
|
|
return true;
|
|
}
|
|
|
|
error = "Invalid duration value. Use TimeSpan format (e.g. 1.00:00:00).";
|
|
return false;
|
|
}
|
|
|
|
internal sealed record ExcititorInitRequest(IReadOnlyList<string>? Providers, bool? Resume);
|
|
|
|
internal sealed record ExcititorIngestRunRequest(
|
|
IReadOnlyList<string>? Providers,
|
|
string? Since,
|
|
string? Window,
|
|
bool? Force);
|
|
|
|
internal sealed record ExcititorIngestResumeRequest(
|
|
IReadOnlyList<string>? Providers,
|
|
string? Checkpoint);
|
|
|
|
internal sealed record ExcititorReconcileRequest(
|
|
IReadOnlyList<string>? Providers,
|
|
string? MaxAge);
|
|
}
|