This commit is contained in:
master
2025-10-21 18:54:26 +03:00
parent 48f3071e2a
commit 104d5813c2
50 changed files with 3027 additions and 596 deletions

View File

@@ -8,9 +8,8 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Export;
using StellaOps.Excititor.Storage.Mongo;
using StellaOps.Excititor.WebService.Options;
using StellaOps.Excititor.WebService.Services;
using StellaOps.Excititor.Storage.Mongo;
using StellaOps.Excititor.WebService.Services;
namespace StellaOps.Excititor.WebService.Endpoints;
@@ -99,13 +98,13 @@ internal static class MirrorEndpoints
}
var resolvedExports = new List<MirrorExportIndexEntry>();
foreach (var exportOption in domain.Exports)
{
if (!TryBuildExportPlan(exportOption, out var plan, out var error))
{
resolvedExports.Add(new MirrorExportIndexEntry(
exportOption.Key,
null,
foreach (var exportOption in domain.Exports)
{
if (!MirrorExportPlanner.TryBuild(exportOption, out var plan, out var error))
{
resolvedExports.Add(new MirrorExportIndexEntry(
exportOption.Key,
null,
null,
exportOption.Format,
null,
@@ -117,7 +116,7 @@ internal static class MirrorEndpoints
continue;
}
var manifest = await exportStore.FindAsync(plan.Signature, plan.Format, cancellationToken).ConfigureAwait(false);
var manifest = await exportStore.FindAsync(plan.Signature, plan.Format, cancellationToken).ConfigureAwait(false);
if (manifest is null)
{
@@ -178,16 +177,16 @@ internal static class MirrorEndpoints
return Results.Unauthorized();
}
if (!TryFindExport(domain, exportKey, out var exportOptions))
{
return Results.NotFound();
}
if (!TryBuildExportPlan(exportOptions, out var plan, out var error))
{
await WritePlainTextAsync(httpContext, error ?? "invalid_export_configuration", StatusCodes.Status503ServiceUnavailable, cancellationToken).ConfigureAwait(false);
return Results.Empty;
}
if (!TryFindExport(domain, exportKey, out var exportOptions))
{
return Results.NotFound();
}
if (!MirrorExportPlanner.TryBuild(exportOptions, out var plan, out var error))
{
await WritePlainTextAsync(httpContext, error ?? "invalid_export_configuration", StatusCodes.Status503ServiceUnavailable, cancellationToken).ConfigureAwait(false);
return Results.Empty;
}
var manifest = await exportStore.FindAsync(plan.Signature, plan.Format, cancellationToken).ConfigureAwait(false);
if (manifest is null)
@@ -242,10 +241,10 @@ internal static class MirrorEndpoints
return Results.Empty;
}
if (!TryFindExport(domain, exportKey, out var exportOptions) || !TryBuildExportPlan(exportOptions, out var plan, out _))
{
return Results.NotFound();
}
if (!TryFindExport(domain, exportKey, out var exportOptions) || !MirrorExportPlanner.TryBuild(exportOptions, out var plan, out _))
{
return Results.NotFound();
}
var manifest = await exportStore.FindAsync(plan.Signature, plan.Format, cancellationToken).ConfigureAwait(false);
if (manifest is null)
@@ -287,37 +286,11 @@ internal static class MirrorEndpoints
return domain is not null;
}
private static bool TryFindExport(MirrorDomainOptions domain, string exportKey, out MirrorExportOptions export)
{
export = domain.Exports.FirstOrDefault(e => string.Equals(e.Key, exportKey, StringComparison.OrdinalIgnoreCase))!;
return export is not null;
}
private static bool TryBuildExportPlan(MirrorExportOptions exportOptions, out MirrorExportPlan plan, out string? error)
{
plan = null!;
error = null;
if (string.IsNullOrWhiteSpace(exportOptions.Key))
{
error = "missing_export_key";
return false;
}
if (string.IsNullOrWhiteSpace(exportOptions.Format) || !Enum.TryParse<VexExportFormat>(exportOptions.Format, ignoreCase: true, out var format))
{
error = "unsupported_export_format";
return false;
}
var filters = exportOptions.Filters.Select(pair => new KeyValuePair<string, string>(pair.Key, pair.Value)).ToArray();
var sorts = exportOptions.Sort.Select(pair => new VexQuerySort(pair.Key, pair.Value)).ToArray();
var query = VexQuery.Create(filters.Select(kv => new VexQueryFilter(kv.Key, kv.Value)), sorts, exportOptions.Limit, exportOptions.Offset, exportOptions.View);
var signature = VexQuerySignature.FromQuery(query);
plan = new MirrorExportPlan(format, query, signature);
return true;
}
private static bool TryFindExport(MirrorDomainOptions domain, string exportKey, out MirrorExportOptions export)
{
export = domain.Exports.FirstOrDefault(e => string.Equals(e.Key, exportKey, StringComparison.OrdinalIgnoreCase))!;
return export is not null;
}
private static string ResolveContentType(VexExportFormat format)
=> format switch
@@ -351,19 +324,15 @@ internal static class MirrorEndpoints
await context.Response.WriteAsync(message, cancellationToken);
}
private static async Task WriteJsonAsync<T>(HttpContext context, T payload, int statusCode, CancellationToken cancellationToken)
{
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json";
var json = VexCanonicalJsonSerializer.Serialize(payload);
await context.Response.WriteAsync(json, cancellationToken);
private static async Task WriteJsonAsync<T>(HttpContext context, T payload, int statusCode, CancellationToken cancellationToken)
{
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json";
var json = VexCanonicalJsonSerializer.Serialize(payload);
await context.Response.WriteAsync(json, cancellationToken);
}
private sealed record MirrorExportPlan(
VexExportFormat Format,
VexQuery Query,
VexQuerySignature Signature);
}
}
internal sealed record MirrorDomainListResponse(IReadOnlyList<MirrorDomainSummary> Domains);

View File

@@ -1,52 +0,0 @@
using System.Collections.Generic;
namespace StellaOps.Excititor.WebService.Options;
public sealed class MirrorDistributionOptions
{
public const string SectionName = "Excititor:Mirror";
public List<MirrorDomainOptions> Domains { get; } = new();
}
public sealed class MirrorDomainOptions
{
public string Id { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public bool RequireAuthentication { get; set; }
= false;
/// <summary>
/// Maximum index requests allowed per rolling window.
/// </summary>
public int MaxIndexRequestsPerHour { get; set; } = 120;
/// <summary>
/// Maximum export downloads allowed per rolling window.
/// </summary>
public int MaxDownloadRequestsPerHour { get; set; } = 600;
public List<MirrorExportOptions> Exports { get; } = new();
}
public sealed class MirrorExportOptions
{
public string Key { get; set; } = string.Empty;
public string Format { get; set; } = string.Empty;
public Dictionary<string, string> Filters { get; } = new();
public Dictionary<string, bool> Sort { get; } = new();
public int? Limit { get; set; }
= null;
public int? Offset { get; set; }
= null;
public string? View { get; set; }
= null;
}

View File

@@ -13,11 +13,11 @@ using StellaOps.Excititor.Export;
using StellaOps.Excititor.Formats.CSAF;
using StellaOps.Excititor.Formats.CycloneDX;
using StellaOps.Excititor.Formats.OpenVEX;
using StellaOps.Excititor.Policy;
using StellaOps.Excititor.Storage.Mongo;
using StellaOps.Excititor.WebService.Endpoints;
using StellaOps.Excititor.WebService.Options;
using StellaOps.Excititor.WebService.Services;
using StellaOps.Excititor.Policy;
using StellaOps.Excititor.Storage.Mongo;
using StellaOps.Excititor.WebService.Endpoints;
using StellaOps.Excititor.WebService.Services;
using StellaOps.Excititor.Core;
var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;