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:
master
2026-02-23 15:30:50 +02:00
parent bd8fee6ed8
commit e746577380
1424 changed files with 81225 additions and 25251 deletions

View File

@@ -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(