up the blokcing tasks
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Risk Bundle CI / risk-bundle-build (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Risk Bundle CI / risk-bundle-offline-kit (push) Has been cancelled
Risk Bundle CI / publish-checksums (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-11 02:32:18 +02:00
parent 92bc4d3a07
commit 49922dff5a
474 changed files with 76071 additions and 12411 deletions

View File

@@ -23,6 +23,8 @@ using Microsoft.Extensions.Options;
using Spectre.Console;
using Spectre.Console.Rendering;
using StellaOps.Auth.Client;
using StellaOps.ExportCenter.Client;
using StellaOps.ExportCenter.Client.Models;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Output;
using StellaOps.Cli.Prompts;
@@ -24774,8 +24776,485 @@ stella policy test {policyName}.stella
#endregion
#region Export Handlers (CLI-EXPORT-35-037)
internal static async Task<int> HandleExportProfilesListAsync(
IServiceProvider services,
int? limit,
string? cursor,
bool json,
bool verbose,
CancellationToken cancellationToken)
{
SetVerbosity(services, verbose);
var client = services.GetRequiredService<IExportCenterClient>();
var response = await client.ListProfilesAsync(cursor, limit, cancellationToken).ConfigureAwait(false);
if (json)
{
AnsiConsole.WriteLine(JsonSerializer.Serialize(response, JsonOptions));
return 0;
}
if (response.Profiles.Count == 0)
{
AnsiConsole.MarkupLine("[yellow]No export profiles found.[/]");
return 0;
}
var table = new Table();
table.AddColumn("Profile ID");
table.AddColumn("Name");
table.AddColumn("Adapter");
table.AddColumn("Format");
table.AddColumn("Signing");
table.AddColumn("Created");
table.AddColumn("Updated");
foreach (var profile in response.Profiles)
{
table.AddRow(
Markup.Escape(profile.ProfileId),
Markup.Escape(profile.Name),
Markup.Escape(profile.Adapter),
Markup.Escape(profile.OutputFormat),
profile.SigningEnabled ? "[green]Yes[/]" : "[grey]No[/]",
profile.CreatedAt.ToString("u", CultureInfo.InvariantCulture),
profile.UpdatedAt?.ToString("u", CultureInfo.InvariantCulture) ?? "[grey]-[/]");
}
AnsiConsole.Write(table);
return 0;
}
internal static async Task<int> HandleExportProfileShowAsync(
IServiceProvider services,
string profileId,
bool json,
bool verbose,
CancellationToken cancellationToken)
{
SetVerbosity(services, verbose);
var client = services.GetRequiredService<IExportCenterClient>();
var profile = await client.GetProfileAsync(profileId, cancellationToken).ConfigureAwait(false);
if (profile is null)
{
AnsiConsole.MarkupLine($"[red]Profile not found:[/] {Markup.Escape(profileId)}");
return 1;
}
if (json)
{
AnsiConsole.WriteLine(JsonSerializer.Serialize(profile, JsonOptions));
return 0;
}
var profileTable = new Table { Border = TableBorder.Rounded };
profileTable.AddColumn("Field");
profileTable.AddColumn("Value");
profileTable.AddRow("Profile ID", Markup.Escape(profile.ProfileId));
profileTable.AddRow("Name", Markup.Escape(profile.Name));
profileTable.AddRow("Description", string.IsNullOrWhiteSpace(profile.Description) ? "[grey]-[/]" : Markup.Escape(profile.Description));
profileTable.AddRow("Adapter", Markup.Escape(profile.Adapter));
profileTable.AddRow("Format", Markup.Escape(profile.OutputFormat));
profileTable.AddRow("Signing", profile.SigningEnabled ? "[green]Enabled[/]" : "[grey]Disabled[/]");
profileTable.AddRow("Created", profile.CreatedAt.ToString("u", CultureInfo.InvariantCulture));
profileTable.AddRow("Updated", profile.UpdatedAt?.ToString("u", CultureInfo.InvariantCulture) ?? "[grey]-[/]");
if (profile.Selectors is { Count: > 0 })
{
var selectorTable = new Table { Title = new TableTitle("Selectors") };
selectorTable.AddColumn("Key");
selectorTable.AddColumn("Value");
foreach (var selector in profile.Selectors)
{
selectorTable.AddRow(Markup.Escape(selector.Key), Markup.Escape(selector.Value));
}
AnsiConsole.Write(profileTable);
AnsiConsole.WriteLine();
AnsiConsole.Write(selectorTable);
}
else
{
AnsiConsole.Write(profileTable);
}
return 0;
}
internal static async Task<int> HandleExportRunsListAsync(
IServiceProvider services,
string? profileId,
int? limit,
string? cursor,
bool json,
bool verbose,
CancellationToken cancellationToken)
{
SetVerbosity(services, verbose);
var client = services.GetRequiredService<IExportCenterClient>();
var response = await client.ListRunsAsync(profileId, cursor, limit, cancellationToken).ConfigureAwait(false);
if (json)
{
AnsiConsole.WriteLine(JsonSerializer.Serialize(response, JsonOptions));
return 0;
}
if (response.Runs.Count == 0)
{
AnsiConsole.MarkupLine("[yellow]No export runs found.[/]");
return 0;
}
var table = new Table();
table.AddColumn("Run ID");
table.AddColumn("Profile");
table.AddColumn("Status");
table.AddColumn("Progress");
table.AddColumn("Started");
table.AddColumn("Completed");
table.AddColumn("Bundle");
foreach (var run in response.Runs)
{
table.AddRow(
Markup.Escape(run.RunId),
Markup.Escape(run.ProfileId),
Markup.Escape(run.Status),
run.Progress.HasValue ? $"{run.Progress.Value}%" : "[grey]-[/]",
run.StartedAt?.ToString("u", CultureInfo.InvariantCulture) ?? "[grey]-[/]",
run.CompletedAt?.ToString("u", CultureInfo.InvariantCulture) ?? "[grey]-[/]",
string.IsNullOrWhiteSpace(run.BundleHash) ? "[grey]-[/]" : Markup.Escape(run.BundleHash));
}
AnsiConsole.Write(table);
if (response.HasMore && !string.IsNullOrWhiteSpace(response.ContinuationToken))
{
AnsiConsole.MarkupLine($"[yellow]More available. Use --cursor {Markup.Escape(response.ContinuationToken)}[/]");
}
return 0;
}
internal static async Task<int> HandleExportRunShowAsync(
IServiceProvider services,
string runId,
bool json,
bool verbose,
CancellationToken cancellationToken)
{
SetVerbosity(services, verbose);
var client = services.GetRequiredService<IExportCenterClient>();
var run = await client.GetRunAsync(runId, cancellationToken).ConfigureAwait(false);
if (run is null)
{
AnsiConsole.MarkupLine($"[red]Run not found:[/] {Markup.Escape(runId)}");
return 1;
}
if (json)
{
AnsiConsole.WriteLine(JsonSerializer.Serialize(run, JsonOptions));
return 0;
}
var table = new Table { Border = TableBorder.Rounded };
table.AddColumn("Field");
table.AddColumn("Value");
table.AddRow("Run ID", Markup.Escape(run.RunId));
table.AddRow("Profile ID", Markup.Escape(run.ProfileId));
table.AddRow("Status", Markup.Escape(run.Status));
table.AddRow("Progress", run.Progress.HasValue ? $"{run.Progress.Value}%" : "[grey]-[/]");
table.AddRow("Started", run.StartedAt?.ToString("u", CultureInfo.InvariantCulture) ?? "[grey]-[/]");
table.AddRow("Completed", run.CompletedAt?.ToString("u", CultureInfo.InvariantCulture) ?? "[grey]-[/]");
table.AddRow("Bundle Hash", string.IsNullOrWhiteSpace(run.BundleHash) ? "[grey]-[/]" : Markup.Escape(run.BundleHash));
table.AddRow("Bundle URL", string.IsNullOrWhiteSpace(run.BundleUrl) ? "[grey]-[/]" : Markup.Escape(run.BundleUrl));
table.AddRow("Error Code", string.IsNullOrWhiteSpace(run.ErrorCode) ? "[grey]-[/]" : Markup.Escape(run.ErrorCode));
table.AddRow("Error Message", string.IsNullOrWhiteSpace(run.ErrorMessage) ? "[grey]-[/]" : Markup.Escape(run.ErrorMessage));
AnsiConsole.Write(table);
return 0;
}
internal static async Task<int> HandleExportRunDownloadAsync(
IServiceProvider services,
string runId,
string outputPath,
bool overwrite,
string? verifyHash,
string runType,
bool verbose,
CancellationToken cancellationToken)
{
SetVerbosity(services, verbose);
var client = services.GetRequiredService<IExportCenterClient>();
if (File.Exists(outputPath) && !overwrite)
{
AnsiConsole.MarkupLine($"[red]Output file already exists:[/] {Markup.Escape(outputPath)} (use --overwrite to replace)");
return 1;
}
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(outputPath)) ?? ".");
Stream? stream = null;
if (string.Equals(runType, "attestation", StringComparison.OrdinalIgnoreCase))
{
stream = await client.DownloadAttestationExportAsync(runId, cancellationToken).ConfigureAwait(false);
}
else
{
stream = await client.DownloadEvidenceExportAsync(runId, cancellationToken).ConfigureAwait(false);
}
if (stream is null)
{
AnsiConsole.MarkupLine($"[red]Export bundle not available for run:[/] {Markup.Escape(runId)}");
return 1;
}
await using (stream)
await using (var fileStream = File.Create(outputPath))
{
await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
if (!string.IsNullOrWhiteSpace(verifyHash))
{
await using var file = File.OpenRead(outputPath);
var hash = await SHA256.HashDataAsync(file, cancellationToken).ConfigureAwait(false);
var hashString = Convert.ToHexString(hash).ToLowerInvariant();
if (!string.Equals(hashString, verifyHash.Trim(), StringComparison.OrdinalIgnoreCase))
{
AnsiConsole.MarkupLine($"[red]Hash verification failed.[/] expected={Markup.Escape(verifyHash)}, actual={hashString}");
return 1;
}
}
AnsiConsole.MarkupLine($"[green]Bundle written to[/] {Markup.Escape(outputPath)}");
return 0;
}
internal static async Task<int> HandleExportStartEvidenceAsync(
IServiceProvider services,
string profileId,
string[]? selectors,
string? callbackUrl,
bool json,
bool verbose,
CancellationToken cancellationToken)
{
SetVerbosity(services, verbose);
var client = services.GetRequiredService<IExportCenterClient>();
var selectorMap = ParseSelectorMap(selectors);
var request = new CreateEvidenceExportRequest(profileId, selectorMap, callbackUrl);
var response = await client.CreateEvidenceExportAsync(request, cancellationToken).ConfigureAwait(false);
if (json)
{
AnsiConsole.WriteLine(JsonSerializer.Serialize(response, JsonOptions));
return 0;
}
AnsiConsole.MarkupLine($"[green]Export started.[/] runId={Markup.Escape(response.RunId)} status={Markup.Escape(response.Status)}");
return 0;
}
internal static async Task<int> HandleExportStartAttestationAsync(
IServiceProvider services,
string profileId,
string[]? selectors,
bool includeTransparencyLog,
string? callbackUrl,
bool json,
bool verbose,
CancellationToken cancellationToken)
{
SetVerbosity(services, verbose);
var client = services.GetRequiredService<IExportCenterClient>();
var selectorMap = ParseSelectorMap(selectors);
var request = new CreateAttestationExportRequest(profileId, selectorMap, includeTransparencyLog, callbackUrl);
var response = await client.CreateAttestationExportAsync(request, cancellationToken).ConfigureAwait(false);
if (json)
{
AnsiConsole.WriteLine(JsonSerializer.Serialize(response, JsonOptions));
return 0;
}
AnsiConsole.MarkupLine($"[green]Attestation export started.[/] runId={Markup.Escape(response.RunId)} status={Markup.Escape(response.Status)}");
return 0;
}
private static IReadOnlyDictionary<string, string>? ParseSelectorMap(string[]? selectors)
{
if (selectors is null || selectors.Length == 0)
{
return null;
}
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var selector in selectors)
{
if (string.IsNullOrWhiteSpace(selector))
{
continue;
}
var parts = selector.Split('=', 2, StringSplitOptions.TrimEntries);
if (parts.Length != 2 || string.IsNullOrWhiteSpace(parts[0]) || string.IsNullOrWhiteSpace(parts[1]))
{
AnsiConsole.MarkupLine($"[yellow]Ignoring selector with invalid format (expected key=value):[/] {Markup.Escape(selector)}");
continue;
}
result[parts[0]] = parts[1];
}
return result.Count == 0 ? null : result;
}
#endregion
#region Notify Handlers (CLI-PARITY-41-002)
internal static async Task<int> HandleNotifySimulateAsync(
IServiceProvider services,
string? tenant,
string? eventsFile,
string? rulesFile,
bool enabledOnly,
int? lookbackMinutes,
int? maxEvents,
string? eventKind,
bool includeNonMatches,
bool json,
bool verbose,
CancellationToken cancellationToken)
{
SetVerbosity(services, verbose);
var client = services.GetRequiredService<INotifyClient>();
var eventsPayload = LoadJsonElement(eventsFile);
var rulesPayload = LoadJsonElement(rulesFile);
var request = new NotifySimulationRequest
{
TenantId = tenant,
Events = eventsPayload,
Rules = rulesPayload,
EnabledRulesOnly = enabledOnly,
HistoricalLookbackMinutes = lookbackMinutes,
MaxEvents = maxEvents,
EventKindFilter = eventKind,
IncludeNonMatches = includeNonMatches
};
var result = await client.SimulateAsync(request, cancellationToken).ConfigureAwait(false);
if (json)
{
AnsiConsole.WriteLine(JsonSerializer.Serialize(result, JsonOptions));
return 0;
}
AnsiConsole.MarkupLine(result.SimulationId is null
? "[yellow]Simulation completed.[/]"
: $"[green]Simulation {Markup.Escape(result.SimulationId)} completed.[/]");
var table = new Table();
table.AddColumn("Total Events");
table.AddColumn("Total Rules");
table.AddColumn("Matched Events");
table.AddColumn("Actions");
table.AddColumn("Duration (ms)");
table.AddRow(
(result.TotalEvents ?? 0).ToString(CultureInfo.InvariantCulture),
(result.TotalRules ?? 0).ToString(CultureInfo.InvariantCulture),
(result.MatchedEvents ?? 0).ToString(CultureInfo.InvariantCulture),
(result.TotalActionsTriggered ?? 0).ToString(CultureInfo.InvariantCulture),
result.DurationMs?.ToString("0.00", CultureInfo.InvariantCulture) ?? "-");
AnsiConsole.Write(table);
return 0;
}
internal static async Task<int> HandleNotifyAckAsync(
IServiceProvider services,
string? tenant,
string? incidentId,
string? token,
string? acknowledgedBy,
string? comment,
bool json,
bool verbose,
CancellationToken cancellationToken)
{
SetVerbosity(services, verbose);
var client = services.GetRequiredService<INotifyClient>();
if (string.IsNullOrWhiteSpace(token) && string.IsNullOrWhiteSpace(incidentId))
{
AnsiConsole.MarkupLine("[red]Either --token or --incident-id is required.[/]");
return 1;
}
var request = new NotifyAckRequest
{
TenantId = tenant,
IncidentId = incidentId,
Token = token,
AcknowledgedBy = acknowledgedBy,
Comment = comment
};
var result = await client.AckAsync(request, cancellationToken).ConfigureAwait(false);
if (json)
{
AnsiConsole.WriteLine(JsonSerializer.Serialize(result, JsonOptions));
return 0;
}
if (!result.Success)
{
AnsiConsole.MarkupLine($"[red]Acknowledge failed:[/] {Markup.Escape(result.Error ?? "unknown error")}");
return 1;
}
AnsiConsole.MarkupLine($"[green]Acknowledged.[/] incidentId={Markup.Escape(result.IncidentId ?? incidentId ?? "n/a")}");
return 0;
}
private static JsonElement? LoadJsonElement(string? filePath)
{
if (string.IsNullOrWhiteSpace(filePath))
{
return null;
}
try
{
var content = File.ReadAllText(filePath);
using var doc = JsonDocument.Parse(content);
return doc.RootElement.Clone();
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[yellow]Failed to load JSON from {Markup.Escape(filePath)}:[/] {Markup.Escape(ex.Message)}");
return null;
}
}
internal static async Task<int> HandleNotifyChannelsListAsync(
IServiceProvider services,
string? tenant,