wip: doctor/cli/docs/api to vector db consolidation; api hardening for descriptions, tenant, and scopes; migrations and conversions of all DALs to EF v10
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Cli.Extensions;
|
||||
using StellaOps.Cli.Services;
|
||||
using StellaOps.Policy.Unknowns.Models;
|
||||
using System.CommandLine;
|
||||
using System.Net.Http.Json;
|
||||
@@ -24,6 +25,7 @@ namespace StellaOps.Cli.Commands;
|
||||
public static class UnknownsCommandGroup
|
||||
{
|
||||
private const string DefaultUnknownsExportSchemaVersion = "unknowns.export.v1";
|
||||
private const string TenantHeaderName = "X-Tenant-Id";
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
@@ -41,18 +43,23 @@ public static class UnknownsCommandGroup
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var unknownsCommand = new Command("unknowns", "Unknowns registry operations for unmatched vulnerabilities");
|
||||
var tenantOption = new Option<string?>("--tenant", new[] { "-t" })
|
||||
{
|
||||
Description = "Tenant context for unknowns operations. Overrides profile and STELLAOPS_TENANT."
|
||||
};
|
||||
unknownsCommand.Add(tenantOption);
|
||||
|
||||
unknownsCommand.Add(BuildListCommand(services, verboseOption, cancellationToken));
|
||||
unknownsCommand.Add(BuildEscalateCommand(services, verboseOption, cancellationToken));
|
||||
unknownsCommand.Add(BuildResolveCommand(services, verboseOption, cancellationToken));
|
||||
unknownsCommand.Add(BuildBudgetCommand(services, verboseOption, cancellationToken));
|
||||
unknownsCommand.Add(BuildListCommand(services, verboseOption, tenantOption, cancellationToken));
|
||||
unknownsCommand.Add(BuildEscalateCommand(services, verboseOption, tenantOption, cancellationToken));
|
||||
unknownsCommand.Add(BuildResolveCommand(services, verboseOption, tenantOption, cancellationToken));
|
||||
unknownsCommand.Add(BuildBudgetCommand(services, verboseOption, tenantOption, cancellationToken));
|
||||
|
||||
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-001, CLI-UNK-002, CLI-UNK-003)
|
||||
unknownsCommand.Add(BuildSummaryCommand(services, verboseOption, cancellationToken));
|
||||
unknownsCommand.Add(BuildShowCommand(services, verboseOption, cancellationToken));
|
||||
unknownsCommand.Add(BuildProofCommand(services, verboseOption, cancellationToken));
|
||||
unknownsCommand.Add(BuildExportCommand(services, verboseOption, cancellationToken));
|
||||
unknownsCommand.Add(BuildTriageCommand(services, verboseOption, cancellationToken));
|
||||
unknownsCommand.Add(BuildSummaryCommand(services, verboseOption, tenantOption, cancellationToken));
|
||||
unknownsCommand.Add(BuildShowCommand(services, verboseOption, tenantOption, cancellationToken));
|
||||
unknownsCommand.Add(BuildProofCommand(services, verboseOption, tenantOption, cancellationToken));
|
||||
unknownsCommand.Add(BuildExportCommand(services, verboseOption, tenantOption, cancellationToken));
|
||||
unknownsCommand.Add(BuildTriageCommand(services, verboseOption, tenantOption, cancellationToken));
|
||||
|
||||
return unknownsCommand;
|
||||
}
|
||||
@@ -64,17 +71,19 @@ public static class UnknownsCommandGroup
|
||||
private static Command BuildBudgetCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
Option<string?> tenantOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var budgetCommand = new Command("budget", "Unknowns budget operations for CI gates");
|
||||
budgetCommand.Add(BuildBudgetCheckCommand(services, verboseOption, cancellationToken));
|
||||
budgetCommand.Add(BuildBudgetStatusCommand(services, verboseOption, cancellationToken));
|
||||
budgetCommand.Add(BuildBudgetCheckCommand(services, verboseOption, tenantOption, cancellationToken));
|
||||
budgetCommand.Add(BuildBudgetStatusCommand(services, verboseOption, tenantOption, cancellationToken));
|
||||
return budgetCommand;
|
||||
}
|
||||
|
||||
private static Command BuildBudgetCheckCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
Option<string?> tenantOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var scanIdOption = new Option<string?>("--scan-id", new[] { "-s" })
|
||||
@@ -128,9 +137,11 @@ public static class UnknownsCommandGroup
|
||||
var failOnExceed = parseResult.GetValue(failOnExceedOption);
|
||||
var output = parseResult.GetValue(outputOption) ?? "text";
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
var tenant = parseResult.GetValue(tenantOption);
|
||||
|
||||
return await HandleBudgetCheckAsync(
|
||||
services,
|
||||
tenant,
|
||||
scanId,
|
||||
verdictPath,
|
||||
environment,
|
||||
@@ -147,6 +158,7 @@ public static class UnknownsCommandGroup
|
||||
private static Command BuildBudgetStatusCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
Option<string?> tenantOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var environmentOption = new Option<string>("--environment", new[] { "-e" })
|
||||
@@ -171,9 +183,11 @@ public static class UnknownsCommandGroup
|
||||
var environment = parseResult.GetValue(environmentOption) ?? "prod";
|
||||
var output = parseResult.GetValue(outputOption) ?? "text";
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
var tenant = parseResult.GetValue(tenantOption);
|
||||
|
||||
return await HandleBudgetStatusAsync(
|
||||
services,
|
||||
tenant,
|
||||
environment,
|
||||
output,
|
||||
verbose,
|
||||
@@ -186,6 +200,7 @@ public static class UnknownsCommandGroup
|
||||
private static Command BuildListCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
Option<string?> tenantOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var bandOption = new Option<string?>("--band", new[] { "-b" })
|
||||
@@ -229,11 +244,13 @@ public static class UnknownsCommandGroup
|
||||
var format = parseResult.GetValue(formatOption) ?? "table";
|
||||
var sort = parseResult.GetValue(sortOption) ?? "age";
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
var tenant = parseResult.GetValue(tenantOption);
|
||||
|
||||
if (limit <= 0) limit = 50;
|
||||
|
||||
return await HandleListAsync(
|
||||
services,
|
||||
tenant,
|
||||
band,
|
||||
limit,
|
||||
offset,
|
||||
@@ -249,6 +266,7 @@ public static class UnknownsCommandGroup
|
||||
private static Command BuildEscalateCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
Option<string?> tenantOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var idOption = new Option<string>("--id", new[] { "-i" })
|
||||
@@ -272,9 +290,11 @@ public static class UnknownsCommandGroup
|
||||
var id = parseResult.GetValue(idOption) ?? string.Empty;
|
||||
var reason = parseResult.GetValue(reasonOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
var tenant = parseResult.GetValue(tenantOption);
|
||||
|
||||
return await HandleEscalateAsync(
|
||||
services,
|
||||
tenant,
|
||||
id,
|
||||
reason,
|
||||
verbose,
|
||||
@@ -288,6 +308,7 @@ public static class UnknownsCommandGroup
|
||||
private static Command BuildSummaryCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
Option<string?> tenantOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var formatOption = new Option<string>("--format", new[] { "-f" })
|
||||
@@ -304,8 +325,9 @@ public static class UnknownsCommandGroup
|
||||
{
|
||||
var format = parseResult.GetValue(formatOption) ?? "table";
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
var tenant = parseResult.GetValue(tenantOption);
|
||||
|
||||
return await HandleSummaryAsync(services, format, verbose, cancellationToken);
|
||||
return await HandleSummaryAsync(services, tenant, format, verbose, cancellationToken);
|
||||
});
|
||||
|
||||
return summaryCommand;
|
||||
@@ -315,6 +337,7 @@ public static class UnknownsCommandGroup
|
||||
private static Command BuildShowCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
Option<string?> tenantOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var idOption = new Option<string>("--id", new[] { "-i" })
|
||||
@@ -339,8 +362,9 @@ public static class UnknownsCommandGroup
|
||||
var id = parseResult.GetValue(idOption) ?? string.Empty;
|
||||
var format = parseResult.GetValue(formatOption) ?? "table";
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
var tenant = parseResult.GetValue(tenantOption);
|
||||
|
||||
return await HandleShowAsync(services, id, format, verbose, cancellationToken);
|
||||
return await HandleShowAsync(services, tenant, id, format, verbose, cancellationToken);
|
||||
});
|
||||
|
||||
return showCommand;
|
||||
@@ -350,6 +374,7 @@ public static class UnknownsCommandGroup
|
||||
private static Command BuildProofCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
Option<string?> tenantOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var idOption = new Option<string>("--id", new[] { "-i" })
|
||||
@@ -374,8 +399,9 @@ public static class UnknownsCommandGroup
|
||||
var id = parseResult.GetValue(idOption) ?? string.Empty;
|
||||
var format = parseResult.GetValue(formatOption) ?? "json";
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
var tenant = parseResult.GetValue(tenantOption);
|
||||
|
||||
return await HandleProofAsync(services, id, format, verbose, cancellationToken);
|
||||
return await HandleProofAsync(services, tenant, id, format, verbose, cancellationToken);
|
||||
});
|
||||
|
||||
return proofCommand;
|
||||
@@ -385,6 +411,7 @@ public static class UnknownsCommandGroup
|
||||
private static Command BuildExportCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
Option<string?> tenantOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var bandOption = new Option<string?>("--band", new[] { "-b" })
|
||||
@@ -424,8 +451,9 @@ public static class UnknownsCommandGroup
|
||||
var schemaVersion = parseResult.GetValue(schemaVersionOption) ?? DefaultUnknownsExportSchemaVersion;
|
||||
var output = parseResult.GetValue(outputOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
var tenant = parseResult.GetValue(tenantOption);
|
||||
|
||||
return await HandleExportAsync(services, band, format, schemaVersion, output, verbose, cancellationToken);
|
||||
return await HandleExportAsync(services, tenant, band, format, schemaVersion, output, verbose, cancellationToken);
|
||||
});
|
||||
|
||||
return exportCommand;
|
||||
@@ -435,6 +463,7 @@ public static class UnknownsCommandGroup
|
||||
private static Command BuildTriageCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
Option<string?> tenantOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var idOption = new Option<string>("--id", new[] { "-i" })
|
||||
@@ -474,8 +503,9 @@ public static class UnknownsCommandGroup
|
||||
var reason = parseResult.GetValue(reasonOption) ?? string.Empty;
|
||||
var duration = parseResult.GetValue(durationOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
var tenant = parseResult.GetValue(tenantOption);
|
||||
|
||||
return await HandleTriageAsync(services, id, action, reason, duration, verbose, cancellationToken);
|
||||
return await HandleTriageAsync(services, tenant, id, action, reason, duration, verbose, cancellationToken);
|
||||
});
|
||||
|
||||
return triageCommand;
|
||||
@@ -484,6 +514,7 @@ public static class UnknownsCommandGroup
|
||||
private static Command BuildResolveCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
Option<string?> tenantOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var idOption = new Option<string>("--id", new[] { "-i" })
|
||||
@@ -515,9 +546,11 @@ public static class UnknownsCommandGroup
|
||||
var resolution = parseResult.GetValue(resolutionOption) ?? string.Empty;
|
||||
var note = parseResult.GetValue(noteOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
var tenant = parseResult.GetValue(tenantOption);
|
||||
|
||||
return await HandleResolveAsync(
|
||||
services,
|
||||
tenant,
|
||||
id,
|
||||
resolution,
|
||||
note,
|
||||
@@ -530,6 +563,7 @@ public static class UnknownsCommandGroup
|
||||
|
||||
private static async Task<int> HandleListAsync(
|
||||
IServiceProvider services,
|
||||
string? tenant,
|
||||
string? band,
|
||||
int limit,
|
||||
int offset,
|
||||
@@ -556,7 +590,7 @@ public static class UnknownsCommandGroup
|
||||
band ?? "all", limit, offset);
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient("PolicyApi");
|
||||
var client = CreatePolicyApiClient(httpClientFactory, tenant);
|
||||
var query = $"/api/v1/policy/unknowns?limit={limit}&offset={offset}&sort={sort}";
|
||||
|
||||
if (!string.IsNullOrEmpty(band))
|
||||
@@ -662,6 +696,7 @@ public static class UnknownsCommandGroup
|
||||
|
||||
private static async Task<int> HandleEscalateAsync(
|
||||
IServiceProvider services,
|
||||
string? tenant,
|
||||
string id,
|
||||
string? reason,
|
||||
bool verbose,
|
||||
@@ -684,7 +719,7 @@ public static class UnknownsCommandGroup
|
||||
logger?.LogDebug("Escalating unknown {Id}", id);
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient("PolicyApi");
|
||||
var client = CreatePolicyApiClient(httpClientFactory, tenant);
|
||||
var request = new EscalateRequest(reason);
|
||||
|
||||
var response = await client.PostAsJsonAsync(
|
||||
@@ -714,6 +749,7 @@ public static class UnknownsCommandGroup
|
||||
|
||||
private static async Task<int> HandleResolveAsync(
|
||||
IServiceProvider services,
|
||||
string? tenant,
|
||||
string id,
|
||||
string resolution,
|
||||
string? note,
|
||||
@@ -737,7 +773,7 @@ public static class UnknownsCommandGroup
|
||||
logger?.LogDebug("Resolving unknown {Id} as {Resolution}", id, resolution);
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient("PolicyApi");
|
||||
var client = CreatePolicyApiClient(httpClientFactory, tenant);
|
||||
var request = new ResolveRequest(resolution, note);
|
||||
|
||||
var response = await client.PostAsJsonAsync(
|
||||
@@ -768,6 +804,7 @@ public static class UnknownsCommandGroup
|
||||
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-001)
|
||||
private static async Task<int> HandleSummaryAsync(
|
||||
IServiceProvider services,
|
||||
string? tenant,
|
||||
string format,
|
||||
bool verbose,
|
||||
CancellationToken ct)
|
||||
@@ -789,7 +826,7 @@ public static class UnknownsCommandGroup
|
||||
logger?.LogDebug("Fetching unknowns summary");
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient("PolicyApi");
|
||||
var client = CreatePolicyApiClient(httpClientFactory, tenant);
|
||||
var response = await client.GetAsync("/api/v1/policy/unknowns/summary", ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
@@ -834,6 +871,7 @@ public static class UnknownsCommandGroup
|
||||
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-001)
|
||||
private static async Task<int> HandleShowAsync(
|
||||
IServiceProvider services,
|
||||
string? tenant,
|
||||
string id,
|
||||
string format,
|
||||
bool verbose,
|
||||
@@ -856,7 +894,7 @@ public static class UnknownsCommandGroup
|
||||
logger?.LogDebug("Fetching unknown {Id}", id);
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient("PolicyApi");
|
||||
var client = CreatePolicyApiClient(httpClientFactory, tenant);
|
||||
var response = await client.GetAsync($"/api/v1/policy/unknowns/{id}", ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
@@ -948,6 +986,7 @@ public static class UnknownsCommandGroup
|
||||
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-002)
|
||||
private static async Task<int> HandleProofAsync(
|
||||
IServiceProvider services,
|
||||
string? tenant,
|
||||
string id,
|
||||
string format,
|
||||
bool verbose,
|
||||
@@ -970,7 +1009,7 @@ public static class UnknownsCommandGroup
|
||||
logger?.LogDebug("Fetching proof for unknown {Id}", id);
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient("PolicyApi");
|
||||
var client = CreatePolicyApiClient(httpClientFactory, tenant);
|
||||
var response = await client.GetAsync($"/api/v1/policy/unknowns/{id}", ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
@@ -1018,6 +1057,7 @@ public static class UnknownsCommandGroup
|
||||
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-002)
|
||||
private static async Task<int> HandleExportAsync(
|
||||
IServiceProvider services,
|
||||
string? tenant,
|
||||
string? band,
|
||||
string format,
|
||||
string schemaVersion,
|
||||
@@ -1042,7 +1082,7 @@ public static class UnknownsCommandGroup
|
||||
logger?.LogDebug("Exporting unknowns: band={Band}, format={Format}", band ?? "all", format);
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient("PolicyApi");
|
||||
var client = CreatePolicyApiClient(httpClientFactory, tenant);
|
||||
var url = string.IsNullOrEmpty(band) || band == "all"
|
||||
? "/api/v1/policy/unknowns?limit=10000"
|
||||
: $"/api/v1/policy/unknowns?band={band}&limit=10000";
|
||||
@@ -1175,6 +1215,7 @@ public static class UnknownsCommandGroup
|
||||
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-003)
|
||||
private static async Task<int> HandleTriageAsync(
|
||||
IServiceProvider services,
|
||||
string? tenant,
|
||||
string id,
|
||||
string action,
|
||||
string reason,
|
||||
@@ -1207,7 +1248,7 @@ public static class UnknownsCommandGroup
|
||||
logger?.LogDebug("Triaging unknown {Id} with action {Action}", id, action);
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient("PolicyApi");
|
||||
var client = CreatePolicyApiClient(httpClientFactory, tenant);
|
||||
var request = new TriageRequest(action, reason, durationDays);
|
||||
|
||||
var response = await client.PostAsJsonAsync(
|
||||
@@ -1246,6 +1287,7 @@ public static class UnknownsCommandGroup
|
||||
/// </summary>
|
||||
private static async Task<int> HandleBudgetCheckAsync(
|
||||
IServiceProvider services,
|
||||
string? tenant,
|
||||
string? scanId,
|
||||
string? verdictPath,
|
||||
string environment,
|
||||
@@ -1298,7 +1340,7 @@ public static class UnknownsCommandGroup
|
||||
else if (!string.IsNullOrEmpty(scanId))
|
||||
{
|
||||
// Fetch from API
|
||||
var client = httpClientFactory.CreateClient("PolicyApi");
|
||||
var client = CreatePolicyApiClient(httpClientFactory, tenant);
|
||||
var response = await client.GetAsync($"/api/v1/policy/unknowns?scanId={scanId}&limit=1000", ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
@@ -1322,7 +1364,7 @@ public static class UnknownsCommandGroup
|
||||
}
|
||||
|
||||
// Check budget via API
|
||||
var budgetClient = httpClientFactory.CreateClient("PolicyApi");
|
||||
var budgetClient = CreatePolicyApiClient(httpClientFactory, tenant);
|
||||
var checkRequest = new BudgetCheckRequest(environment, unknowns);
|
||||
|
||||
var checkResponse = await budgetClient.PostAsJsonAsync(
|
||||
@@ -1471,6 +1513,7 @@ public static class UnknownsCommandGroup
|
||||
|
||||
private static async Task<int> HandleBudgetStatusAsync(
|
||||
IServiceProvider services,
|
||||
string? tenant,
|
||||
string environment,
|
||||
string output,
|
||||
bool verbose,
|
||||
@@ -1493,7 +1536,7 @@ public static class UnknownsCommandGroup
|
||||
logger?.LogDebug("Getting budget status for environment {Environment}", environment);
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient("PolicyApi");
|
||||
var client = CreatePolicyApiClient(httpClientFactory, tenant);
|
||||
var response = await client.GetAsync($"/api/v1/policy/unknowns/budget/status?environment={environment}", ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
@@ -1544,6 +1587,26 @@ public static class UnknownsCommandGroup
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpClient CreatePolicyApiClient(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
string? tenantOverride)
|
||||
{
|
||||
var client = httpClientFactory.CreateClient("PolicyApi");
|
||||
ApplyTenantHeader(client, tenantOverride);
|
||||
return client;
|
||||
}
|
||||
|
||||
private static void ApplyTenantHeader(HttpClient client, string? tenantOverride)
|
||||
{
|
||||
var effectiveTenant = TenantProfileStore.GetEffectiveTenant(tenantOverride);
|
||||
client.DefaultRequestHeaders.Remove(TenantHeaderName);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(effectiveTenant))
|
||||
{
|
||||
client.DefaultRequestHeaders.TryAddWithoutValidation(TenantHeaderName, effectiveTenant.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
#region DTOs
|
||||
|
||||
private sealed record LegacyUnknownsListResponse(
|
||||
|
||||
Reference in New Issue
Block a user