tests fixes and sprints work
This commit is contained in:
@@ -7,7 +7,6 @@
|
||||
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Binding;
|
||||
using System.CommandLine.Builder;
|
||||
using System.CommandLine.Parsing;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -22,6 +21,7 @@ public sealed class CliApplication
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ILogger<CliApplication> _logger;
|
||||
private Option<bool>? _verboseOption;
|
||||
|
||||
public CliApplication(IServiceProvider services, ILogger<CliApplication> logger)
|
||||
{
|
||||
@@ -35,39 +35,52 @@ public sealed class CliApplication
|
||||
public async Task<int> RunAsync(string[] args)
|
||||
{
|
||||
var rootCommand = BuildRootCommand();
|
||||
var parserConfig = new ParserConfiguration();
|
||||
var parseResult = rootCommand.Parse(args, parserConfig);
|
||||
var invocationConfig = new InvocationConfiguration
|
||||
{
|
||||
EnableDefaultExceptionHandler = false,
|
||||
Output = Console.Out,
|
||||
Error = Console.Error
|
||||
};
|
||||
|
||||
var parser = new CommandLineBuilder(rootCommand)
|
||||
.UseDefaults()
|
||||
.UseExceptionHandler(HandleException)
|
||||
.Build();
|
||||
|
||||
return await parser.InvokeAsync(args);
|
||||
try
|
||||
{
|
||||
return await parseResult.InvokeAsync(invocationConfig, CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HandleException(ex, parseResult);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private RootCommand BuildRootCommand()
|
||||
{
|
||||
var rootCommand = new RootCommand("Stella Ops - Release Control Plane CLI")
|
||||
{
|
||||
Name = "stella"
|
||||
};
|
||||
;
|
||||
|
||||
// Global options
|
||||
var configOption = new Option<string?>(
|
||||
aliases: ["--config", "-c"],
|
||||
description: "Path to config file");
|
||||
var configOption = new Option<string?>("--config", "-c")
|
||||
{
|
||||
Description = "Path to config file"
|
||||
};
|
||||
|
||||
var formatOption = new Option<OutputFormat>(
|
||||
aliases: ["--format", "-f"],
|
||||
getDefaultValue: () => OutputFormat.Table,
|
||||
description: "Output format (table, json, yaml)");
|
||||
var formatOption = new Option<OutputFormat>("--format", "-f")
|
||||
{
|
||||
Description = "Output format (table, json, yaml)"
|
||||
};
|
||||
formatOption.SetDefaultValue(OutputFormat.Table);
|
||||
|
||||
var verboseOption = new Option<bool>(
|
||||
aliases: ["--verbose", "-v"],
|
||||
description: "Enable verbose output");
|
||||
var verboseOption = new Option<bool>("--verbose", "-v")
|
||||
{
|
||||
Description = "Enable verbose output"
|
||||
};
|
||||
|
||||
rootCommand.AddGlobalOption(configOption);
|
||||
rootCommand.AddGlobalOption(formatOption);
|
||||
rootCommand.AddGlobalOption(verboseOption);
|
||||
_verboseOption = verboseOption;
|
||||
|
||||
// Add command groups
|
||||
rootCommand.AddCommand(BuildAuthCommand());
|
||||
@@ -90,17 +103,26 @@ public sealed class CliApplication
|
||||
|
||||
// Login command
|
||||
var loginCommand = new Command("login", "Authenticate with Stella server");
|
||||
var serverArg = new Argument<string>("server", "Server URL");
|
||||
var interactiveOption = new Option<bool>("--interactive", "Use interactive login");
|
||||
var tokenOption = new Option<string?>("--token", "API token for authentication");
|
||||
var serverArg = new Argument<string>("server")
|
||||
{
|
||||
Description = "Server URL"
|
||||
};
|
||||
var interactiveOption = new Option<bool>("--interactive")
|
||||
{
|
||||
Description = "Use interactive login"
|
||||
};
|
||||
var tokenOption = new Option<string?>("--token")
|
||||
{
|
||||
Description = "API token for authentication"
|
||||
};
|
||||
|
||||
loginCommand.AddArgument(serverArg);
|
||||
loginCommand.AddOption(interactiveOption);
|
||||
loginCommand.AddOption(tokenOption);
|
||||
|
||||
loginCommand.SetHandler(async (server, interactive, token) =>
|
||||
loginCommand.SetHandler<string, bool, string?>(async (server, interactive, token) =>
|
||||
{
|
||||
var handler = _services.GetRequiredService<AuthCommandHandler>();
|
||||
var handler = _services.GetRequiredService<AuthCommandHandler>();
|
||||
await handler.LoginAsync(server, interactive, token);
|
||||
}, serverArg, interactiveOption, tokenOption);
|
||||
|
||||
@@ -146,12 +168,15 @@ public sealed class CliApplication
|
||||
|
||||
// Init command
|
||||
var initCommand = new Command("init", "Initialize configuration file");
|
||||
var pathOption = new Option<string?>("--path", "Path to create config");
|
||||
var pathOption = new Option<string?>("--path")
|
||||
{
|
||||
Description = "Path to create config"
|
||||
};
|
||||
initCommand.AddOption(pathOption);
|
||||
|
||||
initCommand.SetHandler(async (path) =>
|
||||
initCommand.SetHandler<string?>(async (path) =>
|
||||
{
|
||||
var handler = _services.GetRequiredService<ConfigCommandHandler>();
|
||||
var handler = _services.GetRequiredService<ConfigCommandHandler>();
|
||||
await handler.InitAsync(path);
|
||||
}, pathOption);
|
||||
|
||||
@@ -165,25 +190,34 @@ public sealed class CliApplication
|
||||
|
||||
// Set command
|
||||
var setCommand = new Command("set", "Set a configuration value");
|
||||
var keyArg = new Argument<string>("key", "Configuration key");
|
||||
var valueArg = new Argument<string>("value", "Configuration value");
|
||||
var keyArg = new Argument<string>("key")
|
||||
{
|
||||
Description = "Configuration key"
|
||||
};
|
||||
var valueArg = new Argument<string>("value")
|
||||
{
|
||||
Description = "Configuration value"
|
||||
};
|
||||
setCommand.AddArgument(keyArg);
|
||||
setCommand.AddArgument(valueArg);
|
||||
|
||||
setCommand.SetHandler(async (key, value) =>
|
||||
setCommand.SetHandler<string, string>(async (key, value) =>
|
||||
{
|
||||
var handler = _services.GetRequiredService<ConfigCommandHandler>();
|
||||
var handler = _services.GetRequiredService<ConfigCommandHandler>();
|
||||
await handler.SetAsync(key, value);
|
||||
}, keyArg, valueArg);
|
||||
|
||||
// Get command
|
||||
var getCommand = new Command("get", "Get a configuration value");
|
||||
var getKeyArg = new Argument<string>("key", "Configuration key");
|
||||
var getKeyArg = new Argument<string>("key")
|
||||
{
|
||||
Description = "Configuration key"
|
||||
};
|
||||
getCommand.AddArgument(getKeyArg);
|
||||
|
||||
getCommand.SetHandler(async (key) =>
|
||||
getCommand.SetHandler<string>(async (key) =>
|
||||
{
|
||||
var handler = _services.GetRequiredService<ConfigCommandHandler>();
|
||||
var handler = _services.GetRequiredService<ConfigCommandHandler>();
|
||||
await handler.GetAsync(key);
|
||||
}, getKeyArg);
|
||||
|
||||
@@ -214,17 +248,29 @@ public sealed class CliApplication
|
||||
|
||||
// Create command
|
||||
var createCommand = new Command("create", "Create a new release");
|
||||
var serviceArg = new Argument<string>("service", "Service name");
|
||||
var versionArg = new Argument<string>("version", "Version");
|
||||
var notesOption = new Option<string?>("--notes", "Release notes");
|
||||
var draftOption = new Option<bool>("--draft", "Create as draft");
|
||||
var serviceArg = new Argument<string>("service")
|
||||
{
|
||||
Description = "Service name"
|
||||
};
|
||||
var versionArg = new Argument<string>("version")
|
||||
{
|
||||
Description = "Version"
|
||||
};
|
||||
var notesOption = new Option<string?>("--notes")
|
||||
{
|
||||
Description = "Release notes"
|
||||
};
|
||||
var draftOption = new Option<bool>("--draft")
|
||||
{
|
||||
Description = "Create as draft"
|
||||
};
|
||||
|
||||
createCommand.AddArgument(serviceArg);
|
||||
createCommand.AddArgument(versionArg);
|
||||
createCommand.AddOption(notesOption);
|
||||
createCommand.AddOption(draftOption);
|
||||
|
||||
createCommand.SetHandler(async (service, version, notes, draft) =>
|
||||
createCommand.SetHandler<string, string, string?, bool>(async (service, version, notes, draft) =>
|
||||
{
|
||||
var handler = _services.GetRequiredService<ReleaseCommandHandler>();
|
||||
await handler.CreateAsync(service, version, notes, draft);
|
||||
@@ -232,15 +278,25 @@ public sealed class CliApplication
|
||||
|
||||
// List command
|
||||
var listCommand = new Command("list", "List releases");
|
||||
var serviceOption = new Option<string?>("--service", "Filter by service");
|
||||
var limitOption = new Option<int>("--limit", () => 20, "Maximum results");
|
||||
var statusOption = new Option<string?>("--status", "Filter by status");
|
||||
var serviceOption = new Option<string?>("--service")
|
||||
{
|
||||
Description = "Filter by service"
|
||||
};
|
||||
var limitOption = new Option<int>("--limit")
|
||||
{
|
||||
Description = "Maximum results"
|
||||
};
|
||||
limitOption.SetDefaultValue(20);
|
||||
var statusOption = new Option<string?>("--status")
|
||||
{
|
||||
Description = "Filter by status"
|
||||
};
|
||||
|
||||
listCommand.AddOption(serviceOption);
|
||||
listCommand.AddOption(limitOption);
|
||||
listCommand.AddOption(statusOption);
|
||||
|
||||
listCommand.SetHandler(async (service, limit, status) =>
|
||||
listCommand.SetHandler<string?, int, string?>(async (service, limit, status) =>
|
||||
{
|
||||
var handler = _services.GetRequiredService<ReleaseCommandHandler>();
|
||||
await handler.ListAsync(service, limit, status);
|
||||
@@ -248,10 +304,13 @@ public sealed class CliApplication
|
||||
|
||||
// Get command
|
||||
var getCommand = new Command("get", "Get release details");
|
||||
var releaseIdArg = new Argument<string>("release-id", "Release ID");
|
||||
var releaseIdArg = new Argument<string>("release-id")
|
||||
{
|
||||
Description = "Release ID"
|
||||
};
|
||||
getCommand.AddArgument(releaseIdArg);
|
||||
|
||||
getCommand.SetHandler(async (releaseId) =>
|
||||
getCommand.SetHandler<string>(async (releaseId) =>
|
||||
{
|
||||
var handler = _services.GetRequiredService<ReleaseCommandHandler>();
|
||||
await handler.GetAsync(releaseId);
|
||||
@@ -259,13 +318,19 @@ public sealed class CliApplication
|
||||
|
||||
// Diff command
|
||||
var diffCommand = new Command("diff", "Compare two releases");
|
||||
var fromArg = new Argument<string>("from", "Source release");
|
||||
var toArg = new Argument<string>("to", "Target release");
|
||||
var fromArg = new Argument<string>("from")
|
||||
{
|
||||
Description = "Source release"
|
||||
};
|
||||
var toArg = new Argument<string>("to")
|
||||
{
|
||||
Description = "Target release"
|
||||
};
|
||||
|
||||
diffCommand.AddArgument(fromArg);
|
||||
diffCommand.AddArgument(toArg);
|
||||
|
||||
diffCommand.SetHandler(async (from, to) =>
|
||||
diffCommand.SetHandler<string, string>(async (from, to) =>
|
||||
{
|
||||
var handler = _services.GetRequiredService<ReleaseCommandHandler>();
|
||||
await handler.DiffAsync(from, to);
|
||||
@@ -273,10 +338,13 @@ public sealed class CliApplication
|
||||
|
||||
// History command
|
||||
var historyCommand = new Command("history", "Show release history");
|
||||
var historyServiceArg = new Argument<string>("service", "Service name");
|
||||
var historyServiceArg = new Argument<string>("service")
|
||||
{
|
||||
Description = "Service name"
|
||||
};
|
||||
historyCommand.AddArgument(historyServiceArg);
|
||||
|
||||
historyCommand.SetHandler(async (service) =>
|
||||
historyCommand.SetHandler<string>(async (service) =>
|
||||
{
|
||||
var handler = _services.GetRequiredService<ReleaseCommandHandler>();
|
||||
await handler.HistoryAsync(service);
|
||||
@@ -301,43 +369,64 @@ public sealed class CliApplication
|
||||
|
||||
// Start promotion
|
||||
var startCommand = new Command("start", "Start a promotion");
|
||||
var releaseArg = new Argument<string>("release", "Release to promote");
|
||||
var targetArg = new Argument<string>("target", "Target environment");
|
||||
var autoApproveOption = new Option<bool>("--auto-approve", "Skip approval");
|
||||
var releaseArg = new Argument<string>("release")
|
||||
{
|
||||
Description = "Release to promote"
|
||||
};
|
||||
var targetArg = new Argument<string>("target")
|
||||
{
|
||||
Description = "Target environment"
|
||||
};
|
||||
var autoApproveOption = new Option<bool>("--auto-approve")
|
||||
{
|
||||
Description = "Skip approval"
|
||||
};
|
||||
|
||||
startCommand.AddArgument(releaseArg);
|
||||
startCommand.AddArgument(targetArg);
|
||||
startCommand.AddOption(autoApproveOption);
|
||||
|
||||
startCommand.SetHandler(async (release, target, autoApprove) =>
|
||||
startCommand.SetHandler<string, string, bool>(async (release, target, autoApprove) =>
|
||||
{
|
||||
var handler = _services.GetRequiredService<PromoteCommandHandler>();
|
||||
var handler = _services.GetRequiredService<PromoteCommandHandler>();
|
||||
await handler.StartAsync(release, target, autoApprove);
|
||||
}, releaseArg, targetArg, autoApproveOption);
|
||||
|
||||
// Status command
|
||||
var statusCommand = new Command("status", "Get promotion status");
|
||||
var promotionIdArg = new Argument<string>("promotion-id", "Promotion ID");
|
||||
var watchOption = new Option<bool>("--watch", "Watch for updates");
|
||||
var promotionIdArg = new Argument<string>("promotion-id")
|
||||
{
|
||||
Description = "Promotion ID"
|
||||
};
|
||||
var watchOption = new Option<bool>("--watch")
|
||||
{
|
||||
Description = "Watch for updates"
|
||||
};
|
||||
|
||||
statusCommand.AddArgument(promotionIdArg);
|
||||
statusCommand.AddOption(watchOption);
|
||||
|
||||
statusCommand.SetHandler(async (promotionId, watch) =>
|
||||
statusCommand.SetHandler<string, bool>(async (promotionId, watch) =>
|
||||
{
|
||||
var handler = _services.GetRequiredService<PromoteCommandHandler>();
|
||||
var handler = _services.GetRequiredService<PromoteCommandHandler>();
|
||||
await handler.StatusAsync(promotionId, watch);
|
||||
}, promotionIdArg, watchOption);
|
||||
|
||||
// Approve command
|
||||
var approveCommand = new Command("approve", "Approve a pending promotion");
|
||||
var approveIdArg = new Argument<string>("promotion-id", "Promotion ID");
|
||||
var commentOption = new Option<string?>("--comment", "Approval comment");
|
||||
var approveIdArg = new Argument<string>("promotion-id")
|
||||
{
|
||||
Description = "Promotion ID"
|
||||
};
|
||||
var commentOption = new Option<string?>("--comment")
|
||||
{
|
||||
Description = "Approval comment"
|
||||
};
|
||||
|
||||
approveCommand.AddArgument(approveIdArg);
|
||||
approveCommand.AddOption(commentOption);
|
||||
|
||||
approveCommand.SetHandler(async (promotionId, comment) =>
|
||||
approveCommand.SetHandler<string, string?>(async (promotionId, comment) =>
|
||||
{
|
||||
var handler = _services.GetRequiredService<PromoteCommandHandler>();
|
||||
await handler.ApproveAsync(promotionId, comment);
|
||||
@@ -345,13 +434,20 @@ public sealed class CliApplication
|
||||
|
||||
// Reject command
|
||||
var rejectCommand = new Command("reject", "Reject a pending promotion");
|
||||
var rejectIdArg = new Argument<string>("promotion-id", "Promotion ID");
|
||||
var reasonOption = new Option<string>("--reason", "Rejection reason") { IsRequired = true };
|
||||
var rejectIdArg = new Argument<string>("promotion-id")
|
||||
{
|
||||
Description = "Promotion ID"
|
||||
};
|
||||
var reasonOption = new Option<string>("--reason")
|
||||
{
|
||||
Description = "Rejection reason",
|
||||
Required = true
|
||||
};
|
||||
|
||||
rejectCommand.AddArgument(rejectIdArg);
|
||||
rejectCommand.AddOption(reasonOption);
|
||||
|
||||
rejectCommand.SetHandler(async (promotionId, reason) =>
|
||||
rejectCommand.SetHandler<string, string>(async (promotionId, reason) =>
|
||||
{
|
||||
var handler = _services.GetRequiredService<PromoteCommandHandler>();
|
||||
await handler.RejectAsync(promotionId, reason);
|
||||
@@ -359,13 +455,19 @@ public sealed class CliApplication
|
||||
|
||||
// List command
|
||||
var listCommand = new Command("list", "List promotions");
|
||||
var envOption = new Option<string?>("--env", "Filter by environment");
|
||||
var pendingOption = new Option<bool>("--pending", "Show only pending");
|
||||
var envOption = new Option<string?>("--env")
|
||||
{
|
||||
Description = "Filter by environment"
|
||||
};
|
||||
var pendingOption = new Option<bool>("--pending")
|
||||
{
|
||||
Description = "Show only pending"
|
||||
};
|
||||
|
||||
listCommand.AddOption(envOption);
|
||||
listCommand.AddOption(pendingOption);
|
||||
|
||||
listCommand.SetHandler(async (env, pending) =>
|
||||
listCommand.SetHandler<string?, bool>(async (env, pending) =>
|
||||
{
|
||||
var handler = _services.GetRequiredService<PromoteCommandHandler>();
|
||||
await handler.ListAsync(env, pending);
|
||||
@@ -390,17 +492,30 @@ public sealed class CliApplication
|
||||
|
||||
// Start deployment
|
||||
var startCommand = new Command("start", "Start a deployment");
|
||||
var releaseArg = new Argument<string>("release", "Release to deploy");
|
||||
var targetArg = new Argument<string>("target", "Target environment");
|
||||
var strategyOption = new Option<string>("--strategy", () => "rolling", "Deployment strategy");
|
||||
var dryRunOption = new Option<bool>("--dry-run", "Simulate deployment");
|
||||
var releaseArg = new Argument<string>("release")
|
||||
{
|
||||
Description = "Release to deploy"
|
||||
};
|
||||
var targetArg = new Argument<string>("target")
|
||||
{
|
||||
Description = "Target environment"
|
||||
};
|
||||
var strategyOption = new Option<string>("--strategy")
|
||||
{
|
||||
Description = "Deployment strategy"
|
||||
};
|
||||
strategyOption.SetDefaultValue("rolling");
|
||||
var dryRunOption = new Option<bool>("--dry-run")
|
||||
{
|
||||
Description = "Simulate deployment"
|
||||
};
|
||||
|
||||
startCommand.AddArgument(releaseArg);
|
||||
startCommand.AddArgument(targetArg);
|
||||
startCommand.AddOption(strategyOption);
|
||||
startCommand.AddOption(dryRunOption);
|
||||
|
||||
startCommand.SetHandler(async (release, target, strategy, dryRun) =>
|
||||
startCommand.SetHandler<string, string, string, bool>(async (release, target, strategy, dryRun) =>
|
||||
{
|
||||
var handler = _services.GetRequiredService<DeployCommandHandler>();
|
||||
await handler.StartAsync(release, target, strategy, dryRun);
|
||||
@@ -408,57 +523,85 @@ public sealed class CliApplication
|
||||
|
||||
// Status command
|
||||
var statusCommand = new Command("status", "Get deployment status");
|
||||
var deploymentIdArg = new Argument<string>("deployment-id", "Deployment ID");
|
||||
var watchOption = new Option<bool>("--watch", "Watch for updates");
|
||||
var deploymentIdArg = new Argument<string>("deployment-id")
|
||||
{
|
||||
Description = "Deployment ID"
|
||||
};
|
||||
var watchOption = new Option<bool>("--watch")
|
||||
{
|
||||
Description = "Watch for updates"
|
||||
};
|
||||
|
||||
statusCommand.AddArgument(deploymentIdArg);
|
||||
statusCommand.AddOption(watchOption);
|
||||
|
||||
statusCommand.SetHandler(async (deploymentId, watch) =>
|
||||
statusCommand.SetHandler<string, bool>(async (deploymentId, watch) =>
|
||||
{
|
||||
var handler = _services.GetRequiredService<DeployCommandHandler>();
|
||||
var handler = _services.GetRequiredService<DeployCommandHandler>();
|
||||
await handler.StatusAsync(deploymentId, watch);
|
||||
}, deploymentIdArg, watchOption);
|
||||
|
||||
// Logs command
|
||||
var logsCommand = new Command("logs", "View deployment logs");
|
||||
var logsIdArg = new Argument<string>("deployment-id", "Deployment ID");
|
||||
var followOption = new Option<bool>("--follow", "Follow log output");
|
||||
var tailOption = new Option<int>("--tail", () => 100, "Lines to show");
|
||||
var logsIdArg = new Argument<string>("deployment-id")
|
||||
{
|
||||
Description = "Deployment ID"
|
||||
};
|
||||
var followOption = new Option<bool>("--follow")
|
||||
{
|
||||
Description = "Follow log output"
|
||||
};
|
||||
var tailOption = new Option<int>("--tail")
|
||||
{
|
||||
Description = "Lines to show"
|
||||
};
|
||||
tailOption.SetDefaultValue(100);
|
||||
|
||||
logsCommand.AddArgument(logsIdArg);
|
||||
logsCommand.AddOption(followOption);
|
||||
logsCommand.AddOption(tailOption);
|
||||
|
||||
logsCommand.SetHandler(async (deploymentId, follow, tail) =>
|
||||
logsCommand.SetHandler<string, bool, int>(async (deploymentId, follow, tail) =>
|
||||
{
|
||||
var handler = _services.GetRequiredService<DeployCommandHandler>();
|
||||
var handler = _services.GetRequiredService<DeployCommandHandler>();
|
||||
await handler.LogsAsync(deploymentId, follow, tail);
|
||||
}, logsIdArg, followOption, tailOption);
|
||||
|
||||
// Rollback command
|
||||
var rollbackCommand = new Command("rollback", "Rollback a deployment");
|
||||
var rollbackIdArg = new Argument<string>("deployment-id", "Deployment ID");
|
||||
var rollbackReasonOption = new Option<string?>("--reason", "Rollback reason");
|
||||
var rollbackIdArg = new Argument<string>("deployment-id")
|
||||
{
|
||||
Description = "Deployment ID"
|
||||
};
|
||||
var rollbackReasonOption = new Option<string?>("--reason")
|
||||
{
|
||||
Description = "Rollback reason"
|
||||
};
|
||||
|
||||
rollbackCommand.AddArgument(rollbackIdArg);
|
||||
rollbackCommand.AddOption(rollbackReasonOption);
|
||||
|
||||
rollbackCommand.SetHandler(async (deploymentId, reason) =>
|
||||
rollbackCommand.SetHandler<string, string?>(async (deploymentId, reason) =>
|
||||
{
|
||||
var handler = _services.GetRequiredService<DeployCommandHandler>();
|
||||
var handler = _services.GetRequiredService<DeployCommandHandler>();
|
||||
await handler.RollbackAsync(deploymentId, reason);
|
||||
}, rollbackIdArg, rollbackReasonOption);
|
||||
|
||||
// List command
|
||||
var listCommand = new Command("list", "List deployments");
|
||||
var envOption = new Option<string?>("--env", "Filter by environment");
|
||||
var activeOption = new Option<bool>("--active", "Show only active");
|
||||
var envOption = new Option<string?>("--env")
|
||||
{
|
||||
Description = "Filter by environment"
|
||||
};
|
||||
var activeOption = new Option<bool>("--active")
|
||||
{
|
||||
Description = "Show only active"
|
||||
};
|
||||
|
||||
listCommand.AddOption(envOption);
|
||||
listCommand.AddOption(activeOption);
|
||||
|
||||
listCommand.SetHandler(async (env, active) =>
|
||||
listCommand.SetHandler<string?, bool>(async (env, active) =>
|
||||
{
|
||||
var handler = _services.GetRequiredService<DeployCommandHandler>();
|
||||
await handler.ListAsync(env, active);
|
||||
@@ -483,15 +626,25 @@ public sealed class CliApplication
|
||||
|
||||
// Run scan
|
||||
var runCommand = new Command("run", "Run a security scan");
|
||||
var imageArg = new Argument<string>("image", "Image to scan");
|
||||
var outputOption = new Option<string?>("--output", "Output file");
|
||||
var failOnOption = new Option<string>("--fail-on", () => "high", "Fail on severity");
|
||||
var imageArg = new Argument<string>("image")
|
||||
{
|
||||
Description = "Image to scan"
|
||||
};
|
||||
var outputOption = new Option<string?>("--output")
|
||||
{
|
||||
Description = "Output file"
|
||||
};
|
||||
var failOnOption = new Option<string>("--fail-on")
|
||||
{
|
||||
Description = "Fail on severity"
|
||||
};
|
||||
failOnOption.SetDefaultValue("high");
|
||||
|
||||
runCommand.AddArgument(imageArg);
|
||||
runCommand.AddOption(outputOption);
|
||||
runCommand.AddOption(failOnOption);
|
||||
|
||||
runCommand.SetHandler(async (image, output, failOn) =>
|
||||
runCommand.SetHandler<string, string?, string>(async (image, output, failOn) =>
|
||||
{
|
||||
var handler = _services.GetRequiredService<ScanCommandHandler>();
|
||||
await handler.RunAsync(image, output, failOn);
|
||||
@@ -499,11 +652,14 @@ public sealed class CliApplication
|
||||
|
||||
// Results command
|
||||
var resultsCommand = new Command("results", "Get scan results");
|
||||
var scanIdArg = new Argument<string>("scan-id", "Scan ID");
|
||||
var scanIdArg = new Argument<string>("scan-id")
|
||||
{
|
||||
Description = "Scan ID"
|
||||
};
|
||||
|
||||
resultsCommand.AddArgument(scanIdArg);
|
||||
|
||||
resultsCommand.SetHandler(async (scanId) =>
|
||||
resultsCommand.SetHandler<string>(async (scanId) =>
|
||||
{
|
||||
var handler = _services.GetRequiredService<ScanCommandHandler>();
|
||||
await handler.ResultsAsync(scanId);
|
||||
@@ -525,11 +681,14 @@ public sealed class CliApplication
|
||||
|
||||
// Check command
|
||||
var checkCommand = new Command("check", "Check policy compliance");
|
||||
var releaseArg = new Argument<string>("release", "Release to check");
|
||||
var releaseArg = new Argument<string>("release")
|
||||
{
|
||||
Description = "Release to check"
|
||||
};
|
||||
|
||||
checkCommand.AddArgument(releaseArg);
|
||||
|
||||
checkCommand.SetHandler(async (release) =>
|
||||
checkCommand.SetHandler<string>(async (release) =>
|
||||
{
|
||||
var handler = _services.GetRequiredService<PolicyCommandHandler>();
|
||||
await handler.CheckAsync(release);
|
||||
@@ -569,18 +728,17 @@ public sealed class CliApplication
|
||||
|
||||
#endregion
|
||||
|
||||
private void HandleException(Exception exception, InvocationContext context)
|
||||
private void HandleException(Exception exception, ParseResult parseResult)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.Error.WriteLine($"Error: {exception.Message}");
|
||||
Console.ResetColor();
|
||||
|
||||
if (context.ParseResult.HasOption(new Option<bool>("--verbose")))
|
||||
if (_verboseOption is not null && parseResult.GetValue(_verboseOption))
|
||||
{
|
||||
Console.Error.WriteLine(exception.StackTrace);
|
||||
}
|
||||
|
||||
context.ExitCode = 1;
|
||||
Environment.ExitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,28 +17,33 @@ public static class BootstrapCommands
|
||||
{
|
||||
var command = new Command("bootstrap", "Bootstrap a new agent with zero-touch deployment");
|
||||
|
||||
var nameOption = new Option<string>(
|
||||
["--name", "-n"],
|
||||
"Agent name")
|
||||
{ IsRequired = true };
|
||||
var nameOption = new Option<string>("--name", "-n")
|
||||
{
|
||||
Description = "Agent name",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var envOption = new Option<string>(
|
||||
["--env", "-e"],
|
||||
() => "production",
|
||||
"Target environment");
|
||||
var envOption = new Option<string>("--env", "-e")
|
||||
{
|
||||
Description = "Target environment"
|
||||
};
|
||||
envOption.SetDefaultValue("production");
|
||||
|
||||
var platformOption = new Option<string>(
|
||||
["--platform", "-p"],
|
||||
"Target platform (linux, windows, docker). Auto-detected if not specified.");
|
||||
var platformOption = new Option<string>("--platform", "-p")
|
||||
{
|
||||
Description = "Target platform (linux, windows, docker). Auto-detected if not specified."
|
||||
};
|
||||
|
||||
var outputOption = new Option<string>(
|
||||
["--output", "-o"],
|
||||
"Output file for install script");
|
||||
var outputOption = new Option<string>("--output", "-o")
|
||||
{
|
||||
Description = "Output file for install script"
|
||||
};
|
||||
|
||||
var capabilitiesOption = new Option<string[]>(
|
||||
["--capabilities", "-c"],
|
||||
() => ["docker", "scripts"],
|
||||
"Agent capabilities");
|
||||
var capabilitiesOption = new Option<string[]>("--capabilities", "-c")
|
||||
{
|
||||
Description = "Agent capabilities"
|
||||
};
|
||||
capabilitiesOption.SetDefaultValue(["docker", "scripts"]);
|
||||
|
||||
command.AddOption(nameOption);
|
||||
command.AddOption(envOption);
|
||||
@@ -61,19 +66,22 @@ public static class BootstrapCommands
|
||||
{
|
||||
var command = new Command("install-script", "Generate an install script from a bootstrap token");
|
||||
|
||||
var tokenOption = new Option<string>(
|
||||
["--token", "-t"],
|
||||
"Bootstrap token")
|
||||
{ IsRequired = true };
|
||||
var tokenOption = new Option<string>("--token", "-t")
|
||||
{
|
||||
Description = "Bootstrap token",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var platformOption = new Option<string>(
|
||||
["--platform", "-p"],
|
||||
() => DetectPlatform(),
|
||||
"Target platform (linux, windows, docker)");
|
||||
var platformOption = new Option<string>("--platform", "-p")
|
||||
{
|
||||
Description = "Target platform (linux, windows, docker)"
|
||||
};
|
||||
platformOption.SetDefaultValue(DetectPlatform());
|
||||
|
||||
var outputOption = new Option<string>(
|
||||
["--output", "-o"],
|
||||
"Output file path");
|
||||
var outputOption = new Option<string>("--output", "-o")
|
||||
{
|
||||
Description = "Output file path"
|
||||
};
|
||||
|
||||
command.AddOption(tokenOption);
|
||||
command.AddOption(platformOption);
|
||||
@@ -225,3 +233,5 @@ public static class BootstrapCommands
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
""";
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -16,14 +16,15 @@ public static class CertificateCommands
|
||||
{
|
||||
var command = new Command("renew-cert", "Renew agent mTLS certificate");
|
||||
|
||||
var forceOption = new Option<bool>(
|
||||
["--force", "-f"],
|
||||
() => false,
|
||||
"Force renewal even if certificate is not near expiry");
|
||||
var forceOption = new Option<bool>("--force", "-f")
|
||||
{
|
||||
Description = "Force renewal even if certificate is not near expiry"
|
||||
};
|
||||
forceOption.SetDefaultValue(false);
|
||||
|
||||
command.AddOption(forceOption);
|
||||
|
||||
command.SetHandler(async (force) =>
|
||||
command.SetHandler<bool>(async (force) =>
|
||||
{
|
||||
await HandleRenewCertAsync(force);
|
||||
}, forceOption);
|
||||
|
||||
@@ -17,20 +17,22 @@ public static class ConfigCommands
|
||||
{
|
||||
var command = new Command("config", "Show agent configuration");
|
||||
|
||||
var diffOption = new Option<bool>(
|
||||
["--diff", "-d"],
|
||||
() => false,
|
||||
"Show drift between current and desired configuration");
|
||||
var diffOption = new Option<bool>("--diff", "-d")
|
||||
{
|
||||
Description = "Show drift between current and desired configuration"
|
||||
};
|
||||
diffOption.SetDefaultValue(false);
|
||||
|
||||
var formatOption = new Option<string>(
|
||||
["--format"],
|
||||
() => "yaml",
|
||||
"Output format (yaml, json)");
|
||||
var formatOption = new Option<string>("--format")
|
||||
{
|
||||
Description = "Output format (yaml, json)"
|
||||
};
|
||||
formatOption.SetDefaultValue("yaml");
|
||||
|
||||
command.AddOption(diffOption);
|
||||
command.AddOption(formatOption);
|
||||
|
||||
command.SetHandler(async (diff, format) =>
|
||||
command.SetHandler<bool, string>(async (diff, format) =>
|
||||
{
|
||||
await HandleConfigAsync(diff, format);
|
||||
}, diffOption, formatOption);
|
||||
@@ -45,20 +47,22 @@ public static class ConfigCommands
|
||||
{
|
||||
var command = new Command("apply", "Apply agent configuration");
|
||||
|
||||
var fileOption = new Option<string>(
|
||||
["--file", "-f"],
|
||||
"Configuration file path")
|
||||
{ IsRequired = true };
|
||||
var fileOption = new Option<string>("--file", "-f")
|
||||
{
|
||||
Description = "Configuration file path",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var dryRunOption = new Option<bool>(
|
||||
["--dry-run"],
|
||||
() => false,
|
||||
"Validate without applying");
|
||||
var dryRunOption = new Option<bool>("--dry-run")
|
||||
{
|
||||
Description = "Validate without applying"
|
||||
};
|
||||
dryRunOption.SetDefaultValue(false);
|
||||
|
||||
command.AddOption(fileOption);
|
||||
command.AddOption(dryRunOption);
|
||||
|
||||
command.SetHandler(async (file, dryRun) =>
|
||||
command.SetHandler<string, bool>(async (file, dryRun) =>
|
||||
{
|
||||
await HandleApplyAsync(file, dryRun);
|
||||
}, fileOption, dryRunOption);
|
||||
|
||||
@@ -17,30 +17,34 @@ public static class DoctorCommands
|
||||
{
|
||||
var command = new Command("doctor", "Run agent health diagnostics");
|
||||
|
||||
var agentIdOption = new Option<string?>(
|
||||
["--agent-id", "-a"],
|
||||
"Run diagnostics on a remote agent (omit for local)");
|
||||
var agentIdOption = new Option<string?>("--agent-id", "-a")
|
||||
{
|
||||
Description = "Run diagnostics on a remote agent (omit for local)"
|
||||
};
|
||||
|
||||
var categoryOption = new Option<string?>(
|
||||
["--category", "-c"],
|
||||
"Filter by category (security, network, runtime, resources, configuration)");
|
||||
var categoryOption = new Option<string?>("--category", "-c")
|
||||
{
|
||||
Description = "Filter by category (security, network, runtime, resources, configuration)"
|
||||
};
|
||||
|
||||
var fixOption = new Option<bool>(
|
||||
["--fix", "-f"],
|
||||
() => false,
|
||||
"Apply automated fixes for detected issues");
|
||||
var fixOption = new Option<bool>("--fix", "-f")
|
||||
{
|
||||
Description = "Apply automated fixes for detected issues"
|
||||
};
|
||||
fixOption.SetDefaultValue(false);
|
||||
|
||||
var formatOption = new Option<string>(
|
||||
["--format"],
|
||||
() => "table",
|
||||
"Output format (table, json, yaml)");
|
||||
var formatOption = new Option<string>("--format")
|
||||
{
|
||||
Description = "Output format (table, json, yaml)"
|
||||
};
|
||||
formatOption.SetDefaultValue("table");
|
||||
|
||||
command.AddOption(agentIdOption);
|
||||
command.AddOption(categoryOption);
|
||||
command.AddOption(fixOption);
|
||||
command.AddOption(formatOption);
|
||||
|
||||
command.SetHandler(async (agentId, category, fix, format) =>
|
||||
command.SetHandler<string?, string?, bool, string>(async (agentId, category, fix, format) =>
|
||||
{
|
||||
await HandleDoctorAsync(agentId, category, fix, format);
|
||||
}, agentIdOption, categoryOption, fixOption, formatOption);
|
||||
|
||||
@@ -16,25 +16,28 @@ public static class UpdateCommands
|
||||
{
|
||||
var command = new Command("update", "Check and apply agent updates");
|
||||
|
||||
var versionOption = new Option<string?>(
|
||||
["--version", "-v"],
|
||||
"Update to a specific version");
|
||||
var versionOption = new Option<string?>("--version", "-v")
|
||||
{
|
||||
Description = "Update to a specific version"
|
||||
};
|
||||
|
||||
var checkOption = new Option<bool>(
|
||||
["--check", "-c"],
|
||||
() => false,
|
||||
"Check for updates without applying");
|
||||
var checkOption = new Option<bool>("--check", "-c")
|
||||
{
|
||||
Description = "Check for updates without applying"
|
||||
};
|
||||
checkOption.SetDefaultValue(false);
|
||||
|
||||
var forceOption = new Option<bool>(
|
||||
["--force", "-f"],
|
||||
() => false,
|
||||
"Force update even outside maintenance window");
|
||||
var forceOption = new Option<bool>("--force", "-f")
|
||||
{
|
||||
Description = "Force update even outside maintenance window"
|
||||
};
|
||||
forceOption.SetDefaultValue(false);
|
||||
|
||||
command.AddOption(versionOption);
|
||||
command.AddOption(checkOption);
|
||||
command.AddOption(forceOption);
|
||||
|
||||
command.SetHandler(async (version, check, force) =>
|
||||
command.SetHandler<string?, bool, bool>(async (version, check, force) =>
|
||||
{
|
||||
await HandleUpdateAsync(version, check, force);
|
||||
}, versionOption, checkOption, forceOption);
|
||||
|
||||
1243
src/Cli/StellaOps.Cli/Commands/AnalyticsCommandGroup.cs
Normal file
1243
src/Cli/StellaOps.Cli/Commands/AnalyticsCommandGroup.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -702,7 +702,7 @@ public static class AttestCommandGroup
|
||||
}
|
||||
else
|
||||
{
|
||||
checks.Add(new OfflineVerificationCheck("Rekor inclusion proof", true, "Skipped (not present)", optional: true));
|
||||
checks.Add(new OfflineVerificationCheck("Rekor inclusion proof", true, "Skipped (not present)", Optional: true));
|
||||
}
|
||||
|
||||
// Check 4: Validate content hash matches
|
||||
@@ -714,7 +714,7 @@ public static class AttestCommandGroup
|
||||
}
|
||||
else
|
||||
{
|
||||
checks.Add(new OfflineVerificationCheck("Content hash", true, "Skipped (no metadata.json)", optional: true));
|
||||
checks.Add(new OfflineVerificationCheck("Content hash", true, "Skipped (no metadata.json)", Optional: true));
|
||||
}
|
||||
|
||||
// Determine overall status
|
||||
|
||||
@@ -459,9 +459,9 @@ internal static class AuditCommandGroup
|
||||
decision = "BLOCKED",
|
||||
gates = new[]
|
||||
{
|
||||
new { name = "SbomPresent", result = "PASS" },
|
||||
new { name = "VulnScan", result = "PASS" },
|
||||
new { name = "VexTrust", result = "FAIL", reason = "Trust score below threshold" }
|
||||
new { name = "SbomPresent", result = "PASS", reason = (string?)null },
|
||||
new { name = "VulnScan", result = "PASS", reason = (string?)null },
|
||||
new { name = "VexTrust", result = "FAIL", reason = (string?)"Trust score below threshold" }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -547,9 +547,9 @@ internal static class AuditCommandGroup
|
||||
overallResult = "FAIL",
|
||||
gateResults = new[]
|
||||
{
|
||||
new { gate = "SbomPresent", result = "PASS", durationMs = 15 },
|
||||
new { gate = "VulnScan", result = "PASS", durationMs = 250 },
|
||||
new { gate = "VexTrust", result = "FAIL", durationMs = 45, reason = "Trust score 0.45 < 0.70" }
|
||||
new { gate = "SbomPresent", result = "PASS", durationMs = 15, reason = (string?)null },
|
||||
new { gate = "VulnScan", result = "PASS", durationMs = 250, reason = (string?)null },
|
||||
new { gate = "VexTrust", result = "FAIL", durationMs = 45, reason = (string?)"Trust score 0.45 < 0.70" }
|
||||
}
|
||||
};
|
||||
await File.WriteAllTextAsync(
|
||||
|
||||
@@ -50,7 +50,7 @@ internal static class BenchCommandBuilder
|
||||
{
|
||||
var corpusOption = new Option<string>("--corpus", "Path to corpus.json index file")
|
||||
{
|
||||
IsRequired = true
|
||||
Required = true
|
||||
};
|
||||
var outputOption = new Option<string?>("--output", "Output path for results JSON");
|
||||
var categoryOption = new Option<string[]?>("--category", "Filter to specific categories");
|
||||
@@ -157,11 +157,11 @@ internal static class BenchCommandBuilder
|
||||
{
|
||||
var resultsOption = new Option<string>("--results", "Path to benchmark results JSON")
|
||||
{
|
||||
IsRequired = true
|
||||
Required = true
|
||||
};
|
||||
var baselineOption = new Option<string>("--baseline", "Path to baseline JSON")
|
||||
{
|
||||
IsRequired = true
|
||||
Required = true
|
||||
};
|
||||
var strictOption = new Option<bool>("--strict", () => false, "Fail on any metric degradation");
|
||||
var outputOption = new Option<string?>("--output", "Output path for regression report");
|
||||
@@ -249,11 +249,11 @@ internal static class BenchCommandBuilder
|
||||
// baseline update
|
||||
var resultsOption = new Option<string>("--results", "Path to benchmark results JSON")
|
||||
{
|
||||
IsRequired = true
|
||||
Required = true
|
||||
};
|
||||
var outputOption = new Option<string>("--output", "Output path for new baseline")
|
||||
{
|
||||
IsRequired = true
|
||||
Required = true
|
||||
};
|
||||
var noteOption = new Option<string?>("--note", "Note explaining the baseline update");
|
||||
|
||||
@@ -305,7 +305,7 @@ internal static class BenchCommandBuilder
|
||||
// baseline show
|
||||
var baselinePathOption = new Option<string>("--path", "Path to baseline JSON")
|
||||
{
|
||||
IsRequired = true
|
||||
Required = true
|
||||
};
|
||||
|
||||
var show = new Command("show", "Display baseline metrics");
|
||||
@@ -359,7 +359,7 @@ internal static class BenchCommandBuilder
|
||||
{
|
||||
var resultsOption = new Option<string>("--results", "Path to benchmark results JSON")
|
||||
{
|
||||
IsRequired = true
|
||||
Required = true
|
||||
};
|
||||
var formatOption = new Option<string>("--format", () => "markdown", "Output format: markdown, html");
|
||||
var outputOption = new Option<string?>("--output", "Output path for report");
|
||||
@@ -473,3 +473,4 @@ internal static class BenchCommandBuilder
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -552,7 +552,7 @@ internal static class DeltaSigCommandGroup
|
||||
}
|
||||
else
|
||||
{
|
||||
await console.WriteLineAsync($"✗ Verification FAILED: {result.FailureReason}");
|
||||
await console.WriteLineAsync($"✗ Verification FAILED: {result.Message ?? "Unknown failure"}");
|
||||
Environment.ExitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ public static class BundleExportCommand
|
||||
var imageOption = new Option<string>("--image", "-i")
|
||||
{
|
||||
Description = "Image reference (registry/repo@sha256:...)",
|
||||
IsRequired = true
|
||||
Required = true
|
||||
};
|
||||
|
||||
var outputOption = new Option<string>("--output", "-o")
|
||||
|
||||
@@ -45,7 +45,7 @@ public static class BundleVerifyCommand
|
||||
var bundleOption = new Option<string>("--bundle", "-b")
|
||||
{
|
||||
Description = "Path to bundle (tar.gz or directory)",
|
||||
IsRequired = true
|
||||
Required = true
|
||||
};
|
||||
|
||||
var trustRootOption = new Option<string?>("--trust-root")
|
||||
@@ -267,10 +267,9 @@ public static class BundleVerifyCommand
|
||||
using var reader = new StreamReader(gz);
|
||||
|
||||
// Simple extraction (matches our simple tar format)
|
||||
while (!reader.EndOfStream)
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync(ct)) != null)
|
||||
{
|
||||
var line = await reader.ReadLineAsync(ct);
|
||||
if (line == null) break;
|
||||
|
||||
if (line.StartsWith("FILE:"))
|
||||
{
|
||||
@@ -288,7 +287,7 @@ public static class BundleVerifyCommand
|
||||
}
|
||||
|
||||
var buffer = new char[size];
|
||||
await reader.ReadBlockAsync(buffer, 0, size, ct);
|
||||
await reader.ReadBlockAsync(buffer, 0, size);
|
||||
await File.WriteAllTextAsync(fullPath, new string(buffer), ct);
|
||||
}
|
||||
}
|
||||
@@ -472,9 +471,10 @@ public static class BundleVerifyCommand
|
||||
|
||||
// Check that required payload types are present
|
||||
var present = manifest?.Bundle?.Artifacts?
|
||||
.Where(a => !string.IsNullOrEmpty(a.MediaType))
|
||||
.Select(a => a.MediaType)
|
||||
.ToHashSet() ?? [];
|
||||
.Where(mediaType => !string.IsNullOrWhiteSpace(mediaType))
|
||||
.Select(mediaType => mediaType!)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase) ?? [];
|
||||
|
||||
var missing = expected.Where(e => !present.Any(p =>
|
||||
p.Contains(e.Split(';')[0], StringComparison.OrdinalIgnoreCase))).ToList();
|
||||
|
||||
@@ -61,7 +61,7 @@ public static class CheckpointCommands
|
||||
var outputOption = new Option<string>("--output", "-o")
|
||||
{
|
||||
Description = "Output path for checkpoint bundle",
|
||||
IsRequired = true
|
||||
Required = true
|
||||
};
|
||||
|
||||
var includeTilesOption = new Option<bool>("--include-tiles")
|
||||
@@ -109,7 +109,7 @@ public static class CheckpointCommands
|
||||
var inputOption = new Option<string>("--input", "-i")
|
||||
{
|
||||
Description = "Path to checkpoint bundle",
|
||||
IsRequired = true
|
||||
Required = true
|
||||
};
|
||||
|
||||
var verifySignatureOption = new Option<bool>("--verify-signature")
|
||||
|
||||
@@ -82,7 +82,6 @@ internal static class CommandFactory
|
||||
root.Add(BuildExportCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildAttestCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BundleCommandGroup.BuildBundleCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(TimestampCommandGroup.BuildTimestampCommand(verboseOption, cancellationToken));
|
||||
root.Add(BuildRiskProfileCommand(verboseOption, cancellationToken));
|
||||
root.Add(BuildAdvisoryCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildForensicCommand(services, verboseOption, cancellationToken));
|
||||
@@ -93,11 +92,12 @@ internal static class CommandFactory
|
||||
root.Add(BuildExceptionsCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildOrchCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildSbomCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(LicenseCommandGroup.BuildLicenseCommand(verboseOption, cancellationToken)); // Sprint: SPRINT_20260119_024 - License detection
|
||||
root.Add(AnalyticsCommandGroup.BuildAnalyticsCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildNotifyCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildSbomerCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildCvssCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildRiskCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildReachabilityCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildGraphCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(DeltaSigCommandGroup.BuildDeltaSigCommand(services, verboseOption, cancellationToken)); // Sprint: SPRINT_20260102_001_BE - Delta signatures
|
||||
root.Add(Binary.BinaryCommandGroup.BuildBinaryCommand(services, verboseOption, cancellationToken)); // Sprint: SPRINT_3850_0001_0001
|
||||
@@ -105,6 +105,10 @@ internal static class CommandFactory
|
||||
root.Add(BuildSdkCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildMirrorCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildAirgapCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(TrustProfileCommandGroup.BuildTrustProfileCommand(
|
||||
services,
|
||||
verboseOption,
|
||||
cancellationToken));
|
||||
root.Add(OfflineCommandGroup.BuildOfflineCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(VerifyCommandGroup.BuildVerifyCommand(services, verboseOption, cancellationToken));
|
||||
root.Add(BuildDevPortalCommand(services, verboseOption, cancellationToken));
|
||||
@@ -276,7 +280,7 @@ internal static class CommandFactory
|
||||
var countOption = new Option<int>("--count", "-c")
|
||||
{
|
||||
Description = "Number of scanner workers",
|
||||
IsRequired = true
|
||||
Required = true
|
||||
};
|
||||
var poolOption = new Option<string?>("--pool")
|
||||
{
|
||||
@@ -490,6 +494,50 @@ internal static class CommandFactory
|
||||
{
|
||||
Description = "Override scanner worker count for this run"
|
||||
};
|
||||
var serviceAnalysisOption = new Option<bool>("--service-analysis")
|
||||
{
|
||||
Description = "Enable service endpoint security analysis."
|
||||
};
|
||||
var cryptoAnalysisOption = new Option<bool>("--crypto-analysis")
|
||||
{
|
||||
Description = "Enable CBOM cryptographic analysis."
|
||||
};
|
||||
var cryptoPolicyOption = new Option<string?>("--crypto-policy")
|
||||
{
|
||||
Description = "Path to crypto policy file (YAML/JSON)."
|
||||
};
|
||||
var fipsModeOption = new Option<bool>("--fips-mode")
|
||||
{
|
||||
Description = "Force FIPS compliance checks for crypto analysis."
|
||||
};
|
||||
var pqcAnalysisOption = new Option<bool>("--pqc-analysis")
|
||||
{
|
||||
Description = "Enable post-quantum crypto analysis."
|
||||
};
|
||||
var aiGovernancePolicyOption = new Option<string?>("--ai-governance-policy")
|
||||
{
|
||||
Description = "Path to AI governance policy file (YAML/JSON)."
|
||||
};
|
||||
var aiRiskAssessmentOption = new Option<bool>("--ai-risk-assessment")
|
||||
{
|
||||
Description = "Require AI risk assessments during AI/ML analysis."
|
||||
};
|
||||
var skipAiAnalysisOption = new Option<bool>("--skip-ai-analysis")
|
||||
{
|
||||
Description = "Skip AI/ML supply chain analysis."
|
||||
};
|
||||
var verifyProvenanceOption = new Option<bool>("--verify-provenance")
|
||||
{
|
||||
Description = "Enable build provenance verification."
|
||||
};
|
||||
var slsaPolicyOption = new Option<string?>("--slsa-policy")
|
||||
{
|
||||
Description = "Path to build provenance (SLSA) policy file (YAML/JSON)."
|
||||
};
|
||||
var verifyReproducibilityOption = new Option<bool>("--verify-reproducibility")
|
||||
{
|
||||
Description = "Trigger reproducibility verification (rebuild) when possible."
|
||||
};
|
||||
|
||||
var argsArgument = new Argument<string[]>("scanner-args")
|
||||
{
|
||||
@@ -500,21 +548,44 @@ internal static class CommandFactory
|
||||
run.Add(entryOption);
|
||||
run.Add(targetOption);
|
||||
run.Add(workersOption);
|
||||
run.Add(serviceAnalysisOption);
|
||||
run.Add(cryptoAnalysisOption);
|
||||
run.Add(cryptoPolicyOption);
|
||||
run.Add(fipsModeOption);
|
||||
run.Add(pqcAnalysisOption);
|
||||
run.Add(aiGovernancePolicyOption);
|
||||
run.Add(aiRiskAssessmentOption);
|
||||
run.Add(skipAiAnalysisOption);
|
||||
run.Add(verifyProvenanceOption);
|
||||
run.Add(slsaPolicyOption);
|
||||
run.Add(verifyReproducibilityOption);
|
||||
run.Add(argsArgument);
|
||||
|
||||
run.SetAction((parseResult, _) =>
|
||||
run.SetAction(async (parseResult, _) =>
|
||||
{
|
||||
var runner = parseResult.GetValue(runnerOption) ?? options.DefaultRunner;
|
||||
var entry = parseResult.GetValue(entryOption) ?? string.Empty;
|
||||
var target = parseResult.GetValue(targetOption) ?? string.Empty;
|
||||
var forwardedArgs = parseResult.GetValue(argsArgument) ?? Array.Empty<string>();
|
||||
var workers = parseResult.GetValue(workersOption);
|
||||
var serviceAnalysis = parseResult.GetValue(serviceAnalysisOption);
|
||||
var cryptoAnalysis = parseResult.GetValue(cryptoAnalysisOption);
|
||||
var cryptoPolicy = parseResult.GetValue(cryptoPolicyOption);
|
||||
var fipsMode = parseResult.GetValue(fipsModeOption);
|
||||
var pqcAnalysis = parseResult.GetValue(pqcAnalysisOption);
|
||||
var aiGovernancePolicy = parseResult.GetValue(aiGovernancePolicyOption);
|
||||
var aiRiskAssessment = parseResult.GetValue(aiRiskAssessmentOption);
|
||||
var skipAiAnalysis = parseResult.GetValue(skipAiAnalysisOption);
|
||||
var verifyProvenance = parseResult.GetValue(verifyProvenanceOption);
|
||||
var slsaPolicy = parseResult.GetValue(slsaPolicyOption);
|
||||
var verifyReproducibility = parseResult.GetValue(verifyReproducibilityOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
if (workers.HasValue && workers.Value <= 0)
|
||||
{
|
||||
Console.Error.WriteLine("--workers must be greater than zero.");
|
||||
return 1;
|
||||
Environment.ExitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
var effectiveArgs = new List<string>(forwardedArgs);
|
||||
@@ -533,7 +604,78 @@ internal static class CommandFactory
|
||||
}
|
||||
}
|
||||
|
||||
return CommandHandlers.HandleScannerRunAsync(services, runner, entry, target, effectiveArgs, verbose, cancellationToken);
|
||||
if (serviceAnalysis)
|
||||
{
|
||||
effectiveArgs.Add("--Scanner:Worker:ServiceSecurity:Enabled");
|
||||
effectiveArgs.Add("true");
|
||||
}
|
||||
|
||||
if (cryptoAnalysis || !string.IsNullOrWhiteSpace(cryptoPolicy) || fipsMode || pqcAnalysis)
|
||||
{
|
||||
effectiveArgs.Add("--Scanner:Worker:CryptoAnalysis:Enabled");
|
||||
effectiveArgs.Add("true");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(cryptoPolicy))
|
||||
{
|
||||
effectiveArgs.Add("--Scanner:Worker:CryptoAnalysis:PolicyPath");
|
||||
effectiveArgs.Add(cryptoPolicy);
|
||||
}
|
||||
|
||||
if (fipsMode)
|
||||
{
|
||||
effectiveArgs.Add("--Scanner:Worker:CryptoAnalysis:RequireFips");
|
||||
effectiveArgs.Add("true");
|
||||
}
|
||||
|
||||
if (pqcAnalysis)
|
||||
{
|
||||
effectiveArgs.Add("--Scanner:Worker:CryptoAnalysis:EnablePostQuantumAnalysis");
|
||||
effectiveArgs.Add("true");
|
||||
}
|
||||
|
||||
if (skipAiAnalysis)
|
||||
{
|
||||
effectiveArgs.Add("--Scanner:Worker:AiMlSecurity:Enabled");
|
||||
effectiveArgs.Add("false");
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(aiGovernancePolicy) || aiRiskAssessment)
|
||||
{
|
||||
effectiveArgs.Add("--Scanner:Worker:AiMlSecurity:Enabled");
|
||||
effectiveArgs.Add("true");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(aiGovernancePolicy))
|
||||
{
|
||||
effectiveArgs.Add("--Scanner:Worker:AiMlSecurity:PolicyPath");
|
||||
effectiveArgs.Add(aiGovernancePolicy);
|
||||
}
|
||||
|
||||
if (aiRiskAssessment)
|
||||
{
|
||||
effectiveArgs.Add("--Scanner:Worker:AiMlSecurity:RequireRiskAssessment");
|
||||
effectiveArgs.Add("true");
|
||||
}
|
||||
|
||||
if (verifyProvenance || !string.IsNullOrWhiteSpace(slsaPolicy) || verifyReproducibility)
|
||||
{
|
||||
effectiveArgs.Add("--Scanner:Worker:BuildProvenance:Enabled");
|
||||
effectiveArgs.Add("true");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(slsaPolicy))
|
||||
{
|
||||
effectiveArgs.Add("--Scanner:Worker:BuildProvenance:PolicyPath");
|
||||
effectiveArgs.Add(slsaPolicy);
|
||||
}
|
||||
|
||||
if (verifyReproducibility)
|
||||
{
|
||||
effectiveArgs.Add("--Scanner:Worker:BuildProvenance:VerifyReproducibility");
|
||||
effectiveArgs.Add("true");
|
||||
}
|
||||
|
||||
await CommandHandlers.HandleScannerRunAsync(services, runner, entry, target, effectiveArgs, verbose, cancellationToken);
|
||||
});
|
||||
|
||||
var upload = new Command("upload", "Upload completed scan results to the backend.");
|
||||
@@ -1470,12 +1612,12 @@ internal static class CommandFactory
|
||||
var typeOption = new Option<string>("--type")
|
||||
{
|
||||
Description = "Key type (rsa, ecdsa, eddsa)",
|
||||
IsRequired = true
|
||||
Required = true
|
||||
};
|
||||
var nameOption = new Option<string>("--name")
|
||||
{
|
||||
Description = "Key name",
|
||||
IsRequired = true
|
||||
Required = true
|
||||
};
|
||||
|
||||
var create = new Command("create", "Create a new issuer key")
|
||||
@@ -3288,81 +3430,6 @@ internal static class CommandFactory
|
||||
|
||||
policy.Add(publish);
|
||||
|
||||
// CLI-POLICY-27-004: promote command
|
||||
var promote = new Command("promote", "Promote a policy to a target environment.");
|
||||
var promotePolicyIdArg = new Argument<string>("policy-id")
|
||||
{
|
||||
Description = "Policy identifier."
|
||||
};
|
||||
var promoteVersionOption = new Option<int>("--version")
|
||||
{
|
||||
Description = "Version to promote.",
|
||||
Required = true
|
||||
};
|
||||
var promoteEnvOption = new Option<string>("--env")
|
||||
{
|
||||
Description = "Target environment (e.g. staging, production).",
|
||||
Required = true
|
||||
};
|
||||
var promoteCanaryOption = new Option<bool>("--canary")
|
||||
{
|
||||
Description = "Enable canary deployment."
|
||||
};
|
||||
var promoteCanaryPercentOption = new Option<int?>("--canary-percent")
|
||||
{
|
||||
Description = "Canary traffic percentage (1-99)."
|
||||
};
|
||||
var promoteNoteOption = new Option<string?>("--note")
|
||||
{
|
||||
Description = "Promotion note."
|
||||
};
|
||||
var promoteTenantOption = new Option<string?>("--tenant")
|
||||
{
|
||||
Description = "Tenant context."
|
||||
};
|
||||
var promoteJsonOption = new Option<bool>("--json")
|
||||
{
|
||||
Description = "Output as JSON."
|
||||
};
|
||||
|
||||
promote.Add(promotePolicyIdArg);
|
||||
promote.Add(promoteVersionOption);
|
||||
promote.Add(promoteEnvOption);
|
||||
promote.Add(promoteCanaryOption);
|
||||
promote.Add(promoteCanaryPercentOption);
|
||||
promote.Add(promoteNoteOption);
|
||||
promote.Add(promoteTenantOption);
|
||||
promote.Add(promoteJsonOption);
|
||||
promote.Add(verboseOption);
|
||||
|
||||
promote.SetAction((parseResult, _) =>
|
||||
{
|
||||
var policyId = parseResult.GetValue(promotePolicyIdArg) ?? string.Empty;
|
||||
var version = parseResult.GetValue(promoteVersionOption);
|
||||
var env = parseResult.GetValue(promoteEnvOption) ?? string.Empty;
|
||||
var canary = parseResult.GetValue(promoteCanaryOption);
|
||||
var canaryPercent = parseResult.GetValue(promoteCanaryPercentOption);
|
||||
var note = parseResult.GetValue(promoteNoteOption);
|
||||
var tenant = parseResult.GetValue(promoteTenantOption);
|
||||
var json = parseResult.GetValue(promoteJsonOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return CommandHandlers.HandlePolicyPromoteAsync(
|
||||
services,
|
||||
policyId,
|
||||
version,
|
||||
env,
|
||||
canary,
|
||||
canaryPercent,
|
||||
note,
|
||||
tenant,
|
||||
json,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
policy.Add(promote);
|
||||
|
||||
// CLI-POLICY-27-004: rollback command
|
||||
var rollback = new Command("rollback", "Rollback a policy to a previous version.");
|
||||
var rollbackPolicyIdArg = new Argument<string>("policy-id")
|
||||
@@ -3692,9 +3759,11 @@ flowchart TB
|
||||
|
||||
DateTimeOffset? from = null;
|
||||
DateTimeOffset? to = null;
|
||||
DateTimeOffset fromParsed = default;
|
||||
DateTimeOffset toParsed = default;
|
||||
|
||||
if (!string.IsNullOrEmpty(fromText) &&
|
||||
!DateTimeOffset.TryParse(fromText, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var fromParsed))
|
||||
!DateTimeOffset.TryParse(fromText, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out fromParsed))
|
||||
{
|
||||
Console.Error.WriteLine("Invalid --from value. Use ISO-8601 UTC timestamps.");
|
||||
return 1;
|
||||
@@ -3705,7 +3774,7 @@ flowchart TB
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(toText) &&
|
||||
!DateTimeOffset.TryParse(toText, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var toParsed))
|
||||
!DateTimeOffset.TryParse(toText, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out toParsed))
|
||||
{
|
||||
Console.Error.WriteLine("Invalid --to value. Use ISO-8601 UTC timestamps.");
|
||||
return 1;
|
||||
|
||||
@@ -9,14 +9,14 @@ using StellaOps.Cli.Services;
|
||||
|
||||
namespace StellaOps.Cli.Commands;
|
||||
|
||||
public static partial class CommandHandlers
|
||||
internal static partial class CommandHandlers
|
||||
{
|
||||
public static class Config
|
||||
internal static class Config
|
||||
{
|
||||
/// <summary>
|
||||
/// Lists all available configuration paths.
|
||||
/// </summary>
|
||||
public static Task<int> ListAsync(string? category)
|
||||
internal static Task<int> ListAsync(string? category)
|
||||
{
|
||||
var catalog = ConfigCatalog.GetAll();
|
||||
|
||||
@@ -75,7 +75,7 @@ public static partial class CommandHandlers
|
||||
/// <summary>
|
||||
/// Shows configuration for a specific path.
|
||||
/// </summary>
|
||||
public static async Task<int> ShowAsync(
|
||||
internal static async Task<int> ShowAsync(
|
||||
IBackendOperationsClient client,
|
||||
string path,
|
||||
string format,
|
||||
|
||||
@@ -1088,7 +1088,7 @@ internal static partial class CommandHandlers
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task HandleAdviseRunAsync(
|
||||
internal static async Task HandleAdviseRunAsync(
|
||||
IServiceProvider services,
|
||||
AdvisoryAiTaskType taskType,
|
||||
string advisoryKey,
|
||||
@@ -1244,7 +1244,7 @@ internal static partial class CommandHandlers
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task HandleAdviseBatchAsync(
|
||||
internal static async Task HandleAdviseBatchAsync(
|
||||
IServiceProvider services,
|
||||
AdvisoryAiTaskType taskType,
|
||||
IReadOnlyList<string> advisoryKeys,
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace StellaOps.Cli.Commands;
|
||||
/// <summary>
|
||||
/// CLI commands for inspecting StellaOps configuration.
|
||||
/// </summary>
|
||||
public static class ConfigCommandGroup
|
||||
internal static class ConfigCommandGroup
|
||||
{
|
||||
public static Command Create(IBackendOperationsClient client)
|
||||
{
|
||||
@@ -19,26 +19,32 @@ public static class ConfigCommandGroup
|
||||
|
||||
// stella config list
|
||||
var listCommand = new Command("list", "List all available configuration paths");
|
||||
var categoryOption = new Option<string?>(
|
||||
["--category", "-c"],
|
||||
"Filter by category (e.g., policy, scanner, notifier)");
|
||||
var categoryOption = new Option<string?>("--category", "-c")
|
||||
{
|
||||
Description = "Filter by category (e.g., policy, scanner, notifier)"
|
||||
};
|
||||
listCommand.AddOption(categoryOption);
|
||||
listCommand.SetHandler(
|
||||
async (string? category) => await CommandHandlers.Config.ListAsync(category),
|
||||
categoryOption);
|
||||
|
||||
// stella config <path> show
|
||||
var pathArgument = new Argument<string>("path", "Configuration path (e.g., policy.determinization, scanner.epss)");
|
||||
var pathArgument = new Argument<string>("path")
|
||||
{
|
||||
Description = "Configuration path (e.g., policy.determinization, scanner.epss)"
|
||||
};
|
||||
var showCommand = new Command("show", "Show configuration for a specific path");
|
||||
showCommand.AddArgument(pathArgument);
|
||||
var formatOption = new Option<string>(
|
||||
["--format", "-f"],
|
||||
() => "table",
|
||||
"Output format: table, json, yaml");
|
||||
var showSecretsOption = new Option<bool>(
|
||||
"--show-secrets",
|
||||
() => false,
|
||||
"Show secret values (default: redacted)");
|
||||
var formatOption = new Option<string>("--format", "-f")
|
||||
{
|
||||
Description = "Output format: table, json, yaml"
|
||||
};
|
||||
formatOption.SetDefaultValue("table");
|
||||
var showSecretsOption = new Option<bool>("--show-secrets")
|
||||
{
|
||||
Description = "Show secret values (default: redacted)"
|
||||
};
|
||||
showSecretsOption.SetDefaultValue(false);
|
||||
showCommand.AddOption(formatOption);
|
||||
showCommand.AddOption(showSecretsOption);
|
||||
showCommand.SetHandler(
|
||||
|
||||
@@ -483,20 +483,32 @@ internal static class DeltaSigCommandHandlers
|
||||
// Load signatures
|
||||
var signatures = await LoadSignaturesAsync(sigpackPath, cveFilter, ct);
|
||||
|
||||
if (semantic)
|
||||
{
|
||||
var semanticSignatures = signatures
|
||||
.Where(s => s.Symbols.Any(sym => !string.IsNullOrWhiteSpace(sym.SemanticHashHex)))
|
||||
.ToList();
|
||||
|
||||
if (semanticSignatures.Count > 0)
|
||||
{
|
||||
signatures = semanticSignatures;
|
||||
}
|
||||
}
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[dim]Loaded {signatures.Count} signatures[/]");
|
||||
if (semantic)
|
||||
{
|
||||
var withSemantic = signatures.Count(s => s.SemanticFingerprint != null);
|
||||
var withSemantic = signatures.Count(s =>
|
||||
s.Symbols.Any(sym => !string.IsNullOrWhiteSpace(sym.SemanticHashHex)));
|
||||
AnsiConsole.MarkupLine($"[dim]Signatures with semantic fingerprints: {withSemantic}[/]");
|
||||
}
|
||||
}
|
||||
|
||||
// Match with semantic preference
|
||||
var matchOptions = new MatchOptions(PreferSemantic: semantic);
|
||||
// Match signatures
|
||||
using var binaryStream = new MemoryStream(binaryBytes);
|
||||
var results = await matcher.MatchAsync(binaryStream, signatures, cveFilter, matchOptions, ct);
|
||||
var results = await matcher.MatchAsync(binaryStream, signatures, cveFilter, ct);
|
||||
|
||||
// Output results
|
||||
var matchedResults = results.Where(r => r.Matched).ToList();
|
||||
|
||||
@@ -1820,26 +1820,31 @@ public static class EvidenceCommandGroup
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var dryRunOption = new Option<bool>(
|
||||
aliases: ["--dry-run", "-n"],
|
||||
description: "Perform a dry run without making changes, showing impact assessment");
|
||||
var dryRunOption = new Option<bool>("--dry-run", "-n")
|
||||
{
|
||||
Description = "Perform a dry run without making changes, showing impact assessment"
|
||||
};
|
||||
|
||||
var sinceOption = new Option<DateTimeOffset?>(
|
||||
aliases: ["--since", "-s"],
|
||||
description: "Only reindex evidence created after this date (ISO 8601 format)");
|
||||
var sinceOption = new Option<DateTimeOffset?>("--since", "-s")
|
||||
{
|
||||
Description = "Only reindex evidence created after this date (ISO 8601 format)"
|
||||
};
|
||||
|
||||
var batchSizeOption = new Option<int>(
|
||||
aliases: ["--batch-size", "-b"],
|
||||
getDefaultValue: () => 100,
|
||||
description: "Number of evidence records to process per batch");
|
||||
var batchSizeOption = new Option<int>("--batch-size", "-b")
|
||||
{
|
||||
Description = "Number of evidence records to process per batch"
|
||||
};
|
||||
batchSizeOption.SetDefaultValue(100);
|
||||
|
||||
var outputOption = new Option<string?>(
|
||||
aliases: ["--output", "-o"],
|
||||
description: "Output file for dry-run report (JSON format)");
|
||||
var outputOption = new Option<string?>("--output", "-o")
|
||||
{
|
||||
Description = "Output file for dry-run report (JSON format)"
|
||||
};
|
||||
|
||||
var serverOption = new Option<string?>(
|
||||
aliases: ["--server"],
|
||||
description: "Evidence Locker server URL (default: from config)");
|
||||
var serverOption = new Option<string?>("--server")
|
||||
{
|
||||
Description = "Evidence Locker server URL (default: from config)"
|
||||
};
|
||||
|
||||
var cmd = new Command("reindex", "Re-index evidence bundles after schema or algorithm changes")
|
||||
{
|
||||
@@ -1851,7 +1856,7 @@ public static class EvidenceCommandGroup
|
||||
verboseOption
|
||||
};
|
||||
|
||||
cmd.SetHandler(async (dryRun, since, batchSize, output, server, verbose) =>
|
||||
cmd.SetHandler<bool, DateTimeOffset?, int, string?, string?, bool>(async (dryRun, since, batchSize, output, server, verbose) =>
|
||||
{
|
||||
var logger = services.GetRequiredService<ILoggerFactory>().CreateLogger("EvidenceReindex");
|
||||
|
||||
@@ -1864,7 +1869,13 @@ public static class EvidenceCommandGroup
|
||||
AnsiConsole.WriteLine();
|
||||
}
|
||||
|
||||
var serverUrl = server ?? options.EvidenceLockerUrl ?? "http://localhost:5080";
|
||||
var serverUrl = !string.IsNullOrWhiteSpace(server)
|
||||
? server
|
||||
: !string.IsNullOrWhiteSpace(options.BackendUrl)
|
||||
? options.BackendUrl
|
||||
: Environment.GetEnvironmentVariable("STELLAOPS_EVIDENCE_URL")
|
||||
?? Environment.GetEnvironmentVariable("STELLAOPS_BACKEND_URL")
|
||||
?? "http://localhost:5080";
|
||||
|
||||
// Show configuration
|
||||
var configTable = new Table()
|
||||
@@ -1982,26 +1993,33 @@ public static class EvidenceCommandGroup
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var oldRootOption = new Option<string>(
|
||||
aliases: ["--old-root"],
|
||||
description: "Previous Merkle root hash (sha256:...)") { IsRequired = true };
|
||||
var oldRootOption = new Option<string>("--old-root")
|
||||
{
|
||||
Description = "Previous Merkle root hash (sha256:...)",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var newRootOption = new Option<string>(
|
||||
aliases: ["--new-root"],
|
||||
description: "New Merkle root hash after reindex (sha256:...)") { IsRequired = true };
|
||||
var newRootOption = new Option<string>("--new-root")
|
||||
{
|
||||
Description = "New Merkle root hash after reindex (sha256:...)",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var outputOption = new Option<string?>(
|
||||
aliases: ["--output", "-o"],
|
||||
description: "Output file for verification report");
|
||||
var outputOption = new Option<string?>("--output", "-o")
|
||||
{
|
||||
Description = "Output file for verification report"
|
||||
};
|
||||
|
||||
var formatOption = new Option<string>(
|
||||
aliases: ["--format", "-f"],
|
||||
getDefaultValue: () => "json",
|
||||
description: "Report format: json, html, or text");
|
||||
var formatOption = new Option<string>("--format", "-f")
|
||||
{
|
||||
Description = "Report format: json, html, or text"
|
||||
};
|
||||
formatOption.SetDefaultValue("json");
|
||||
|
||||
var serverOption = new Option<string?>(
|
||||
aliases: ["--server"],
|
||||
description: "Evidence Locker server URL (default: from config)");
|
||||
var serverOption = new Option<string?>("--server")
|
||||
{
|
||||
Description = "Evidence Locker server URL (default: from config)"
|
||||
};
|
||||
|
||||
var cmd = new Command("verify-continuity", "Verify chain-of-custody after evidence reindex or upgrade")
|
||||
{
|
||||
@@ -2013,14 +2031,20 @@ public static class EvidenceCommandGroup
|
||||
verboseOption
|
||||
};
|
||||
|
||||
cmd.SetHandler(async (oldRoot, newRoot, output, format, server, verbose) =>
|
||||
cmd.SetHandler<string, string, string?, string, string?, bool>(async (oldRoot, newRoot, output, format, server, verbose) =>
|
||||
{
|
||||
var logger = services.GetRequiredService<ILoggerFactory>().CreateLogger("EvidenceContinuity");
|
||||
|
||||
AnsiConsole.MarkupLine("[bold blue]Evidence Continuity Verification[/]");
|
||||
AnsiConsole.WriteLine();
|
||||
|
||||
var serverUrl = server ?? options.EvidenceLockerUrl ?? "http://localhost:5080";
|
||||
var serverUrl = !string.IsNullOrWhiteSpace(server)
|
||||
? server
|
||||
: !string.IsNullOrWhiteSpace(options.BackendUrl)
|
||||
? options.BackendUrl
|
||||
: Environment.GetEnvironmentVariable("STELLAOPS_EVIDENCE_URL")
|
||||
?? Environment.GetEnvironmentVariable("STELLAOPS_BACKEND_URL")
|
||||
?? "http://localhost:5080";
|
||||
|
||||
AnsiConsole.MarkupLine($"Old Root: [cyan]{oldRoot}[/]");
|
||||
AnsiConsole.MarkupLine($"New Root: [cyan]{newRoot}[/]");
|
||||
@@ -2136,25 +2160,31 @@ public static class EvidenceCommandGroup
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var fromVersionOption = new Option<string>(
|
||||
aliases: ["--from-version"],
|
||||
description: "Source schema version") { IsRequired = true };
|
||||
var fromVersionOption = new Option<string>("--from-version")
|
||||
{
|
||||
Description = "Source schema version",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var toVersionOption = new Option<string?>(
|
||||
aliases: ["--to-version"],
|
||||
description: "Target schema version (default: latest)");
|
||||
var toVersionOption = new Option<string?>("--to-version")
|
||||
{
|
||||
Description = "Target schema version (default: latest)"
|
||||
};
|
||||
|
||||
var dryRunOption = new Option<bool>(
|
||||
aliases: ["--dry-run", "-n"],
|
||||
description: "Show migration plan without executing");
|
||||
var dryRunOption = new Option<bool>("--dry-run", "-n")
|
||||
{
|
||||
Description = "Show migration plan without executing"
|
||||
};
|
||||
|
||||
var rollbackOption = new Option<bool>(
|
||||
aliases: ["--rollback"],
|
||||
description: "Roll back a previously failed migration");
|
||||
var rollbackOption = new Option<bool>("--rollback")
|
||||
{
|
||||
Description = "Roll back a previously failed migration"
|
||||
};
|
||||
|
||||
var serverOption = new Option<string?>(
|
||||
aliases: ["--server"],
|
||||
description: "Evidence Locker server URL (default: from config)");
|
||||
var serverOption = new Option<string?>("--server")
|
||||
{
|
||||
Description = "Evidence Locker server URL (default: from config)"
|
||||
};
|
||||
|
||||
var cmd = new Command("migrate", "Migrate evidence schema between versions")
|
||||
{
|
||||
@@ -2166,14 +2196,20 @@ public static class EvidenceCommandGroup
|
||||
verboseOption
|
||||
};
|
||||
|
||||
cmd.SetHandler(async (fromVersion, toVersion, dryRun, rollback, server, verbose) =>
|
||||
cmd.SetHandler<string, string?, bool, bool, string?, bool>(async (fromVersion, toVersion, dryRun, rollback, server, verbose) =>
|
||||
{
|
||||
var logger = services.GetRequiredService<ILoggerFactory>().CreateLogger("EvidenceMigrate");
|
||||
|
||||
AnsiConsole.MarkupLine("[bold blue]Evidence Schema Migration[/]");
|
||||
AnsiConsole.WriteLine();
|
||||
|
||||
var serverUrl = server ?? options.EvidenceLockerUrl ?? "http://localhost:5080";
|
||||
var serverUrl = !string.IsNullOrWhiteSpace(server)
|
||||
? server
|
||||
: !string.IsNullOrWhiteSpace(options.BackendUrl)
|
||||
? options.BackendUrl
|
||||
: Environment.GetEnvironmentVariable("STELLAOPS_EVIDENCE_URL")
|
||||
?? Environment.GetEnvironmentVariable("STELLAOPS_BACKEND_URL")
|
||||
?? "http://localhost:5080";
|
||||
|
||||
if (rollback)
|
||||
{
|
||||
|
||||
2099
src/Cli/StellaOps.Cli/Commands/GroundTruthCommandGroup.cs
Normal file
2099
src/Cli/StellaOps.Cli/Commands/GroundTruthCommandGroup.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -39,16 +39,28 @@ public static class IrCommandGroup
|
||||
{
|
||||
var command = new Command("lift", "Lift a binary to intermediate representation");
|
||||
|
||||
var inOption = new Option<FileInfo>("--in", "Input binary file path") { IsRequired = true };
|
||||
inOption.AddAlias("-i");
|
||||
var inOption = new Option<FileInfo>("--in", new[] { "-i" })
|
||||
{
|
||||
Description = "Input binary file path",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var outOption = new Option<DirectoryInfo>("--out", "Output directory for IR cache") { IsRequired = true };
|
||||
outOption.AddAlias("-o");
|
||||
var outOption = new Option<DirectoryInfo>("--out", new[] { "-o" })
|
||||
{
|
||||
Description = "Output directory for IR cache",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var archOption = new Option<string?>("--arch", "Architecture override (x86-64, arm64, arm32, auto)");
|
||||
var archOption = new Option<string?>("--arch")
|
||||
{
|
||||
Description = "Architecture override (x86-64, arm64, arm32, auto)"
|
||||
};
|
||||
archOption.SetDefaultValue("auto");
|
||||
|
||||
var formatOption = new Option<string>("--format", "Output format (json, binary)");
|
||||
var formatOption = new Option<string>("--format")
|
||||
{
|
||||
Description = "Output format (json, binary)"
|
||||
};
|
||||
formatOption.SetDefaultValue("json");
|
||||
|
||||
command.AddOption(inOption);
|
||||
@@ -56,7 +68,7 @@ public static class IrCommandGroup
|
||||
command.AddOption(archOption);
|
||||
command.AddOption(formatOption);
|
||||
|
||||
command.SetHandler(HandleLiftAsync, inOption, outOption, archOption, formatOption);
|
||||
command.SetHandler<FileInfo, DirectoryInfo, string?, string>(HandleLiftAsync, inOption, outOption, archOption, formatOption);
|
||||
|
||||
return command;
|
||||
}
|
||||
@@ -68,20 +80,29 @@ public static class IrCommandGroup
|
||||
{
|
||||
var command = new Command("canon", "Canonicalize IR with SSA transformation and CFG ordering");
|
||||
|
||||
var inOption = new Option<DirectoryInfo>("--in", "Input IR cache directory") { IsRequired = true };
|
||||
inOption.AddAlias("-i");
|
||||
var inOption = new Option<DirectoryInfo>("--in", new[] { "-i" })
|
||||
{
|
||||
Description = "Input IR cache directory",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var outOption = new Option<DirectoryInfo>("--out", "Output directory for canonicalized IR") { IsRequired = true };
|
||||
outOption.AddAlias("-o");
|
||||
var outOption = new Option<DirectoryInfo>("--out", new[] { "-o" })
|
||||
{
|
||||
Description = "Output directory for canonicalized IR",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var recipeOption = new Option<string?>("--recipe", "Normalization recipe version");
|
||||
var recipeOption = new Option<string?>("--recipe")
|
||||
{
|
||||
Description = "Normalization recipe version"
|
||||
};
|
||||
recipeOption.SetDefaultValue("v1");
|
||||
|
||||
command.AddOption(inOption);
|
||||
command.AddOption(outOption);
|
||||
command.AddOption(recipeOption);
|
||||
|
||||
command.SetHandler(HandleCanonAsync, inOption, outOption, recipeOption);
|
||||
command.SetHandler<DirectoryInfo, DirectoryInfo, string?>(HandleCanonAsync, inOption, outOption, recipeOption);
|
||||
|
||||
return command;
|
||||
}
|
||||
@@ -93,16 +114,28 @@ public static class IrCommandGroup
|
||||
{
|
||||
var command = new Command("fp", "Generate semantic fingerprints using Weisfeiler-Lehman hashing");
|
||||
|
||||
var inOption = new Option<DirectoryInfo>("--in", "Input canonicalized IR directory") { IsRequired = true };
|
||||
inOption.AddAlias("-i");
|
||||
var inOption = new Option<DirectoryInfo>("--in", new[] { "-i" })
|
||||
{
|
||||
Description = "Input canonicalized IR directory",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var outOption = new Option<FileInfo>("--out", "Output fingerprint file path") { IsRequired = true };
|
||||
outOption.AddAlias("-o");
|
||||
var outOption = new Option<FileInfo>("--out", new[] { "-o" })
|
||||
{
|
||||
Description = "Output fingerprint file path",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var iterationsOption = new Option<int>("--iterations", "Number of WL iterations");
|
||||
var iterationsOption = new Option<int>("--iterations")
|
||||
{
|
||||
Description = "Number of WL iterations"
|
||||
};
|
||||
iterationsOption.SetDefaultValue(3);
|
||||
|
||||
var formatOption = new Option<string>("--format", "Output format (json, hex, binary)");
|
||||
var formatOption = new Option<string>("--format")
|
||||
{
|
||||
Description = "Output format (json, hex, binary)"
|
||||
};
|
||||
formatOption.SetDefaultValue("json");
|
||||
|
||||
command.AddOption(inOption);
|
||||
@@ -110,7 +143,7 @@ public static class IrCommandGroup
|
||||
command.AddOption(iterationsOption);
|
||||
command.AddOption(formatOption);
|
||||
|
||||
command.SetHandler(HandleFpAsync, inOption, outOption, iterationsOption, formatOption);
|
||||
command.SetHandler<DirectoryInfo, FileInfo, int, string>(HandleFpAsync, inOption, outOption, iterationsOption, formatOption);
|
||||
|
||||
return command;
|
||||
}
|
||||
@@ -122,18 +155,33 @@ public static class IrCommandGroup
|
||||
{
|
||||
var command = new Command("pipeline", "Run full IR pipeline: lift → canon → fp");
|
||||
|
||||
var inOption = new Option<FileInfo>("--in", "Input binary file path") { IsRequired = true };
|
||||
inOption.AddAlias("-i");
|
||||
var inOption = new Option<FileInfo>("--in", new[] { "-i" })
|
||||
{
|
||||
Description = "Input binary file path",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var outOption = new Option<FileInfo>("--out", "Output fingerprint file path") { IsRequired = true };
|
||||
outOption.AddAlias("-o");
|
||||
var outOption = new Option<FileInfo>("--out", new[] { "-o" })
|
||||
{
|
||||
Description = "Output fingerprint file path",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var cacheOption = new Option<DirectoryInfo?>("--cache", "Cache directory for intermediate artifacts");
|
||||
var cacheOption = new Option<DirectoryInfo?>("--cache")
|
||||
{
|
||||
Description = "Cache directory for intermediate artifacts"
|
||||
};
|
||||
|
||||
var archOption = new Option<string?>("--arch", "Architecture override");
|
||||
var archOption = new Option<string?>("--arch")
|
||||
{
|
||||
Description = "Architecture override"
|
||||
};
|
||||
archOption.SetDefaultValue("auto");
|
||||
|
||||
var cleanupOption = new Option<bool>("--cleanup", "Remove intermediate cache after completion");
|
||||
var cleanupOption = new Option<bool>("--cleanup")
|
||||
{
|
||||
Description = "Remove intermediate cache after completion"
|
||||
};
|
||||
cleanupOption.SetDefaultValue(false);
|
||||
|
||||
command.AddOption(inOption);
|
||||
@@ -142,7 +190,7 @@ public static class IrCommandGroup
|
||||
command.AddOption(archOption);
|
||||
command.AddOption(cleanupOption);
|
||||
|
||||
command.SetHandler(HandlePipelineAsync, inOption, outOption, cacheOption, archOption, cleanupOption);
|
||||
command.SetHandler<FileInfo, FileInfo, DirectoryInfo?, string?, bool>(HandlePipelineAsync, inOption, outOption, cacheOption, archOption, cleanupOption);
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
798
src/Cli/StellaOps.Cli/Commands/LicenseCommandGroup.cs
Normal file
798
src/Cli/StellaOps.Cli/Commands/LicenseCommandGroup.cs
Normal file
@@ -0,0 +1,798 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// LicenseCommandGroup.cs
|
||||
// Sprint: SPRINT_20260119_024_Scanner_license_detection_enhancements
|
||||
// Task: TASK-024-012 - Create license detection CLI commands
|
||||
// Description: CLI commands for license detection, categorization, and validation
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.CommandLine;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Spectre.Console;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Core.Licensing;
|
||||
|
||||
namespace StellaOps.Cli.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Command group for license detection and management operations.
|
||||
/// Implements `stella license detect`, `stella license categorize`,
|
||||
/// `stella license validate`, and `stella license extract`.
|
||||
/// </summary>
|
||||
public static class LicenseCommandGroup
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Build the 'license' command group.
|
||||
/// </summary>
|
||||
public static Command BuildLicenseCommand(Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||
{
|
||||
var license = new Command("license", "License detection and compliance commands");
|
||||
|
||||
license.Add(BuildDetectCommand(verboseOption, cancellationToken));
|
||||
license.Add(BuildCategorizeCommand(verboseOption));
|
||||
license.Add(BuildValidateCommand(verboseOption));
|
||||
license.Add(BuildExtractCommand(verboseOption, cancellationToken));
|
||||
license.Add(BuildSummaryCommand(verboseOption, cancellationToken));
|
||||
|
||||
return license;
|
||||
}
|
||||
|
||||
#region Detect Command
|
||||
|
||||
/// <summary>
|
||||
/// Build the 'license detect' command for detecting licenses in a directory.
|
||||
/// </summary>
|
||||
private static Command BuildDetectCommand(Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||
{
|
||||
var pathArg = new Argument<string>("path")
|
||||
{
|
||||
Description = "Path to directory or file to scan for licenses"
|
||||
};
|
||||
|
||||
var formatOption = new Option<OutputFormat>("--format", "-f")
|
||||
{
|
||||
Description = "Output format: table, json, or spdx"
|
||||
};
|
||||
formatOption.SetDefaultValue(OutputFormat.Table);
|
||||
|
||||
var recursiveOption = new Option<bool>("--recursive", "-r")
|
||||
{
|
||||
Description = "Recursively scan subdirectories"
|
||||
};
|
||||
recursiveOption.SetDefaultValue(true);
|
||||
|
||||
var detect = new Command("detect", "Detect licenses in a directory or file")
|
||||
{
|
||||
pathArg,
|
||||
formatOption,
|
||||
recursiveOption,
|
||||
verboseOption
|
||||
};
|
||||
|
||||
detect.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var path = parseResult.GetValue(pathArg) ?? ".";
|
||||
var format = parseResult.GetValue(formatOption);
|
||||
var recursive = parseResult.GetValue(recursiveOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return await ExecuteDetectAsync(path, format, recursive, verbose, cancellationToken);
|
||||
});
|
||||
|
||||
return detect;
|
||||
}
|
||||
|
||||
private static async Task<int> ExecuteDetectAsync(
|
||||
string path,
|
||||
OutputFormat format,
|
||||
bool recursive,
|
||||
bool verbose,
|
||||
CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
path = Path.GetFullPath(path);
|
||||
|
||||
if (!Directory.Exists(path) && !File.Exists(path))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Path not found: {path}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
Console.WriteLine($"Scanning for licenses in: {path}");
|
||||
}
|
||||
|
||||
var extractor = new LicenseTextExtractor();
|
||||
var categorizationService = new LicenseCategorizationService();
|
||||
var results = new List<LicenseDetectionResult>();
|
||||
|
||||
if (File.Exists(path))
|
||||
{
|
||||
// Single file
|
||||
var result = await extractor.ExtractAsync(path, ct);
|
||||
if (result is not null && !string.IsNullOrWhiteSpace(result.DetectedLicenseId))
|
||||
{
|
||||
var detection = new LicenseDetectionResult
|
||||
{
|
||||
SpdxId = result.DetectedLicenseId,
|
||||
Confidence = result.Confidence,
|
||||
Method = LicenseDetectionMethod.LicenseFile,
|
||||
SourceFile = Path.GetFileName(path),
|
||||
LicenseText = result.FullText,
|
||||
LicenseTextHash = result.TextHash,
|
||||
CopyrightNotice = result.CopyrightNotices.Length > 0
|
||||
? result.CopyrightNotices[0].FullText
|
||||
: null
|
||||
};
|
||||
results.Add(categorizationService.Enrich(detection));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Directory
|
||||
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
||||
var licenseFiles = await extractor.ExtractFromDirectoryAsync(path, ct);
|
||||
|
||||
foreach (var result in licenseFiles)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(result.DetectedLicenseId))
|
||||
{
|
||||
var detection = new LicenseDetectionResult
|
||||
{
|
||||
SpdxId = result.DetectedLicenseId,
|
||||
Confidence = result.Confidence,
|
||||
Method = LicenseDetectionMethod.LicenseFile,
|
||||
SourceFile = result.SourceFile,
|
||||
LicenseTextHash = result.TextHash,
|
||||
CopyrightNotice = result.CopyrightNotices.Length > 0
|
||||
? result.CopyrightNotices[0].FullText
|
||||
: null
|
||||
};
|
||||
results.Add(categorizationService.Enrich(detection));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (results.Count == 0)
|
||||
{
|
||||
Console.WriteLine("No licenses detected.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
OutputResults(results, format);
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Categorize Command
|
||||
|
||||
/// <summary>
|
||||
/// Build the 'license categorize' command for showing license category and obligations.
|
||||
/// </summary>
|
||||
private static Command BuildCategorizeCommand(Option<bool> verboseOption)
|
||||
{
|
||||
var spdxIdArg = new Argument<string>("spdx-id")
|
||||
{
|
||||
Description = "SPDX license identifier (e.g., MIT, Apache-2.0, GPL-3.0-only)"
|
||||
};
|
||||
|
||||
var formatOption = new Option<OutputFormat>("--format", "-f")
|
||||
{
|
||||
Description = "Output format: table or json"
|
||||
};
|
||||
formatOption.SetDefaultValue(OutputFormat.Table);
|
||||
|
||||
var categorize = new Command("categorize", "Show category and obligations for a license")
|
||||
{
|
||||
spdxIdArg,
|
||||
formatOption,
|
||||
verboseOption
|
||||
};
|
||||
|
||||
categorize.SetAction((parseResult, ct) =>
|
||||
{
|
||||
var spdxId = parseResult.GetValue(spdxIdArg) ?? string.Empty;
|
||||
var format = parseResult.GetValue(formatOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return Task.FromResult(ExecuteCategorize(spdxId, format, verbose));
|
||||
});
|
||||
|
||||
return categorize;
|
||||
}
|
||||
|
||||
private static int ExecuteCategorize(string spdxId, OutputFormat format, bool verbose)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(spdxId))
|
||||
{
|
||||
Console.Error.WriteLine("Error: SPDX license identifier is required.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var service = new LicenseCategorizationService();
|
||||
var category = service.Categorize(spdxId);
|
||||
var obligations = service.GetObligations(spdxId);
|
||||
var isOsiApproved = service.IsOsiApproved(spdxId);
|
||||
var isFsfFree = service.IsFsfFree(spdxId);
|
||||
var isDeprecated = service.IsDeprecated(spdxId);
|
||||
|
||||
if (format == OutputFormat.Json)
|
||||
{
|
||||
var output = new
|
||||
{
|
||||
SpdxId = spdxId,
|
||||
Category = category.ToString(),
|
||||
Obligations = obligations.Select(o => o.ToString()).ToArray(),
|
||||
IsOsiApproved = isOsiApproved,
|
||||
IsFsfFree = isFsfFree,
|
||||
IsDeprecated = isDeprecated
|
||||
};
|
||||
Console.WriteLine(JsonSerializer.Serialize(output, JsonOptions));
|
||||
}
|
||||
else
|
||||
{
|
||||
var table = new Table();
|
||||
table.AddColumn("Property");
|
||||
table.AddColumn("Value");
|
||||
|
||||
table.AddRow("SPDX ID", spdxId);
|
||||
table.AddRow("Category", GetCategoryDisplay(category));
|
||||
table.AddRow("Obligations", obligations.Count > 0
|
||||
? string.Join(", ", obligations.Select(GetObligationDisplay))
|
||||
: "[dim]None[/]");
|
||||
table.AddRow("OSI Approved", (isOsiApproved ?? false) ? "[green]Yes[/]" : "[dim]No[/]");
|
||||
table.AddRow("FSF Free", (isFsfFree ?? false) ? "[green]Yes[/]" : "[dim]No[/]");
|
||||
table.AddRow("Deprecated", isDeprecated ? "[yellow]Yes[/]" : "[dim]No[/]");
|
||||
|
||||
AnsiConsole.Write(table);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Validate Command
|
||||
|
||||
/// <summary>
|
||||
/// Build the 'license validate' command for validating SPDX expressions.
|
||||
/// </summary>
|
||||
private static Command BuildValidateCommand(Option<bool> verboseOption)
|
||||
{
|
||||
var expressionArg = new Argument<string>("expression")
|
||||
{
|
||||
Description = "SPDX license expression to validate (e.g., 'MIT OR Apache-2.0')"
|
||||
};
|
||||
|
||||
var formatOption = new Option<OutputFormat>("--format", "-f")
|
||||
{
|
||||
Description = "Output format: table or json"
|
||||
};
|
||||
formatOption.SetDefaultValue(OutputFormat.Table);
|
||||
|
||||
var validate = new Command("validate", "Validate an SPDX license expression")
|
||||
{
|
||||
expressionArg,
|
||||
formatOption,
|
||||
verboseOption
|
||||
};
|
||||
|
||||
validate.SetAction((parseResult, ct) =>
|
||||
{
|
||||
var expression = parseResult.GetValue(expressionArg) ?? string.Empty;
|
||||
var format = parseResult.GetValue(formatOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return Task.FromResult(ExecuteValidate(expression, format, verbose));
|
||||
});
|
||||
|
||||
return validate;
|
||||
}
|
||||
|
||||
private static int ExecuteValidate(string expression, OutputFormat format, bool verbose)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(expression))
|
||||
{
|
||||
Console.Error.WriteLine("Error: SPDX expression is required.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var service = new LicenseCategorizationService();
|
||||
var isValid = true;
|
||||
var components = new List<string>();
|
||||
var errors = new List<string>();
|
||||
|
||||
// Parse the expression
|
||||
var tokens = expression
|
||||
.Replace("(", " ")
|
||||
.Replace(")", " ")
|
||||
.Split([' '], StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
var upper = token.ToUpperInvariant();
|
||||
if (upper is "OR" or "AND" or "WITH")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
components.Add(token);
|
||||
|
||||
// Check if it's a known license
|
||||
var category = service.Categorize(token);
|
||||
if (category == LicenseCategory.Unknown && !token.StartsWith("LicenseRef-", StringComparison.Ordinal))
|
||||
{
|
||||
errors.Add($"Unknown license identifier: {token}");
|
||||
}
|
||||
|
||||
if (service.IsDeprecated(token))
|
||||
{
|
||||
errors.Add($"Deprecated license identifier: {token}");
|
||||
}
|
||||
}
|
||||
|
||||
isValid = errors.Count == 0;
|
||||
|
||||
if (format == OutputFormat.Json)
|
||||
{
|
||||
var output = new
|
||||
{
|
||||
Expression = expression,
|
||||
IsValid = isValid,
|
||||
Components = components.ToArray(),
|
||||
Errors = errors.ToArray()
|
||||
};
|
||||
Console.WriteLine(JsonSerializer.Serialize(output, JsonOptions));
|
||||
}
|
||||
else
|
||||
{
|
||||
var panel = new Panel(new Markup(isValid
|
||||
? $"[green]Valid[/] SPDX expression: [bold]{Markup.Escape(expression)}[/]"
|
||||
: $"[red]Invalid[/] SPDX expression: [bold]{Markup.Escape(expression)}[/]"));
|
||||
panel.Header = new PanelHeader("Validation Result");
|
||||
AnsiConsole.Write(panel);
|
||||
|
||||
if (components.Count > 0)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Components:");
|
||||
foreach (var component in components)
|
||||
{
|
||||
var cat = service.Categorize(component);
|
||||
Console.WriteLine($" - {component}: {GetCategoryDisplay(cat)}");
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
Console.WriteLine();
|
||||
AnsiConsole.MarkupLine("[yellow]Warnings/Errors:[/]");
|
||||
foreach (var error in errors)
|
||||
{
|
||||
AnsiConsole.MarkupLine($" [yellow]![/] {Markup.Escape(error)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return isValid ? 0 : 1;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Extract Command
|
||||
|
||||
/// <summary>
|
||||
/// Build the 'license extract' command for extracting license text and copyright.
|
||||
/// </summary>
|
||||
private static Command BuildExtractCommand(Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||
{
|
||||
var fileArg = new Argument<string>("file")
|
||||
{
|
||||
Description = "Path to license file to extract"
|
||||
};
|
||||
|
||||
var formatOption = new Option<OutputFormat>("--format", "-f")
|
||||
{
|
||||
Description = "Output format: table or json"
|
||||
};
|
||||
formatOption.SetDefaultValue(OutputFormat.Table);
|
||||
|
||||
var extract = new Command("extract", "Extract license text and copyright from a file")
|
||||
{
|
||||
fileArg,
|
||||
formatOption,
|
||||
verboseOption
|
||||
};
|
||||
|
||||
extract.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var file = parseResult.GetValue(fileArg) ?? string.Empty;
|
||||
var format = parseResult.GetValue(formatOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return await ExecuteExtractAsync(file, format, verbose, cancellationToken);
|
||||
});
|
||||
|
||||
return extract;
|
||||
}
|
||||
|
||||
private static async Task<int> ExecuteExtractAsync(
|
||||
string file,
|
||||
OutputFormat format,
|
||||
bool verbose,
|
||||
CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
file = Path.GetFullPath(file);
|
||||
|
||||
if (!File.Exists(file))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: File not found: {file}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var extractor = new LicenseTextExtractor();
|
||||
var result = await extractor.ExtractAsync(file, ct);
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
Console.Error.WriteLine("Error: Could not extract license information.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (format == OutputFormat.Json)
|
||||
{
|
||||
var output = new
|
||||
{
|
||||
File = Path.GetFileName(file),
|
||||
DetectedLicense = result.DetectedLicenseId,
|
||||
Confidence = result.Confidence.ToString(),
|
||||
TextHash = result.TextHash,
|
||||
CopyrightNotices = result.CopyrightNotices.Select(c => new
|
||||
{
|
||||
c.FullText,
|
||||
c.Year,
|
||||
c.Holder,
|
||||
c.LineNumber
|
||||
}).ToArray(),
|
||||
TextPreview = result.FullText?.Length > 500
|
||||
? result.FullText[..500] + "..."
|
||||
: result.FullText
|
||||
};
|
||||
Console.WriteLine(JsonSerializer.Serialize(output, JsonOptions));
|
||||
}
|
||||
else
|
||||
{
|
||||
var table = new Table();
|
||||
table.AddColumn("Property");
|
||||
table.AddColumn("Value");
|
||||
|
||||
table.AddRow("File", Path.GetFileName(file));
|
||||
table.AddRow("Detected License", result.DetectedLicenseId ?? "[dim]Unknown[/]");
|
||||
table.AddRow("Confidence", result.Confidence.ToString());
|
||||
table.AddRow("Text Hash", result.TextHash ?? "[dim]N/A[/]");
|
||||
|
||||
if (result.CopyrightNotices.Length > 0)
|
||||
{
|
||||
table.AddRow("Copyright Notices", string.Join("\n", result.CopyrightNotices.Select(c => c.FullText)));
|
||||
}
|
||||
else
|
||||
{
|
||||
table.AddRow("Copyright Notices", "[dim]None found[/]");
|
||||
}
|
||||
|
||||
AnsiConsole.Write(table);
|
||||
|
||||
if (verbose && !string.IsNullOrWhiteSpace(result.FullText))
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("License Text Preview:");
|
||||
Console.WriteLine(new string('-', 40));
|
||||
var preview = result.FullText.Length > 1000
|
||||
? result.FullText[..1000] + "\n... (truncated)"
|
||||
: result.FullText;
|
||||
Console.WriteLine(preview);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Summary Command
|
||||
|
||||
/// <summary>
|
||||
/// Build the 'license summary' command for aggregated license statistics.
|
||||
/// </summary>
|
||||
private static Command BuildSummaryCommand(Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||
{
|
||||
var pathArg = new Argument<string>("path")
|
||||
{
|
||||
Description = "Path to directory to analyze"
|
||||
};
|
||||
pathArg.SetDefaultValue(".");
|
||||
|
||||
var formatOption = new Option<OutputFormat>("--format", "-f")
|
||||
{
|
||||
Description = "Output format: table or json"
|
||||
};
|
||||
formatOption.SetDefaultValue(OutputFormat.Table);
|
||||
|
||||
var summary = new Command("summary", "Show aggregated license statistics for a directory")
|
||||
{
|
||||
pathArg,
|
||||
formatOption,
|
||||
verboseOption
|
||||
};
|
||||
|
||||
summary.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var path = parseResult.GetValue(pathArg) ?? ".";
|
||||
var format = parseResult.GetValue(formatOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return await ExecuteSummaryAsync(path, format, verbose, cancellationToken);
|
||||
});
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
private static async Task<int> ExecuteSummaryAsync(
|
||||
string path,
|
||||
OutputFormat format,
|
||||
bool verbose,
|
||||
CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
path = Path.GetFullPath(path);
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Directory not found: {path}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
Console.WriteLine($"Analyzing licenses in: {path}");
|
||||
}
|
||||
|
||||
var extractor = new LicenseTextExtractor();
|
||||
var categorizationService = new LicenseCategorizationService();
|
||||
var aggregator = new LicenseDetectionAggregator();
|
||||
var results = new List<LicenseDetectionResult>();
|
||||
|
||||
var licenseFiles = await extractor.ExtractFromDirectoryAsync(path, ct);
|
||||
|
||||
foreach (var result in licenseFiles)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(result.DetectedLicenseId))
|
||||
{
|
||||
var detection = new LicenseDetectionResult
|
||||
{
|
||||
SpdxId = result.DetectedLicenseId,
|
||||
Confidence = result.Confidence,
|
||||
Method = LicenseDetectionMethod.LicenseFile,
|
||||
SourceFile = result.SourceFile,
|
||||
LicenseTextHash = result.TextHash,
|
||||
CopyrightNotice = result.CopyrightNotices.Length > 0
|
||||
? result.CopyrightNotices[0].FullText
|
||||
: null
|
||||
};
|
||||
results.Add(categorizationService.Enrich(detection));
|
||||
}
|
||||
}
|
||||
|
||||
var summary = aggregator.Aggregate(results);
|
||||
var risk = aggregator.GetComplianceRisk(summary);
|
||||
|
||||
if (format == OutputFormat.Json)
|
||||
{
|
||||
var output = new
|
||||
{
|
||||
TotalLicenses = summary.TotalComponents,
|
||||
UniqueCount = summary.DistinctLicenses.Length,
|
||||
UnknownCount = summary.UnknownLicenses,
|
||||
CopyleftCount = summary.CopyleftComponentCount,
|
||||
ByCategory = summary.ByCategory.ToDictionary(k => k.Key.ToString(), k => k.Value),
|
||||
BySpdxId = summary.BySpdxId,
|
||||
DistinctLicenses = summary.DistinctLicenses,
|
||||
CopyrightNotices = summary.AllCopyrightNotices,
|
||||
Risk = new
|
||||
{
|
||||
risk.HasStrongCopyleft,
|
||||
risk.HasNetworkCopyleft,
|
||||
risk.UnknownLicensePercentage,
|
||||
risk.CopyleftPercentage,
|
||||
risk.RequiresReview
|
||||
}
|
||||
};
|
||||
Console.WriteLine(JsonSerializer.Serialize(output, JsonOptions));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Summary table
|
||||
var summaryTable = new Table();
|
||||
summaryTable.Title = new TableTitle("License Summary");
|
||||
summaryTable.AddColumn("Metric");
|
||||
summaryTable.AddColumn("Value");
|
||||
|
||||
summaryTable.AddRow("Total Licenses Found", summary.TotalComponents.ToString());
|
||||
summaryTable.AddRow("Unique Licenses", summary.DistinctLicenses.Length.ToString());
|
||||
summaryTable.AddRow("Unknown Licenses", summary.UnknownLicenses.ToString());
|
||||
summaryTable.AddRow("Copyleft Licenses", summary.CopyleftComponentCount.ToString());
|
||||
|
||||
AnsiConsole.Write(summaryTable);
|
||||
|
||||
// Category breakdown
|
||||
if (summary.ByCategory.Count > 0)
|
||||
{
|
||||
Console.WriteLine();
|
||||
var categoryTable = new Table();
|
||||
categoryTable.Title = new TableTitle("By Category");
|
||||
categoryTable.AddColumn("Category");
|
||||
categoryTable.AddColumn("Count");
|
||||
|
||||
foreach (var kvp in summary.ByCategory.OrderByDescending(k => k.Value))
|
||||
{
|
||||
categoryTable.AddRow(GetCategoryDisplay(kvp.Key), kvp.Value.ToString());
|
||||
}
|
||||
|
||||
AnsiConsole.Write(categoryTable);
|
||||
}
|
||||
|
||||
// Risk assessment
|
||||
Console.WriteLine();
|
||||
var riskPanel = new Panel(new Markup(
|
||||
$"Strong Copyleft: {(risk.HasStrongCopyleft ? "[red]Yes[/]" : "[green]No[/]")}\n" +
|
||||
$"Network Copyleft (AGPL): {(risk.HasNetworkCopyleft ? "[red]Yes[/]" : "[green]No[/]")}\n" +
|
||||
$"Unknown License %: {risk.UnknownLicensePercentage:F1}%\n" +
|
||||
$"Requires Review: {(risk.RequiresReview ? "[yellow]Yes[/]" : "[green]No[/]")}"));
|
||||
riskPanel.Header = new PanelHeader("Compliance Risk");
|
||||
AnsiConsole.Write(riskPanel);
|
||||
|
||||
// Distinct licenses
|
||||
if (summary.DistinctLicenses.Length > 0 && verbose)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Distinct Licenses:");
|
||||
foreach (var license in summary.DistinctLicenses)
|
||||
{
|
||||
Console.WriteLine($" - {license}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Output Helpers
|
||||
|
||||
private static void OutputResults(List<LicenseDetectionResult> results, OutputFormat format)
|
||||
{
|
||||
if (format == OutputFormat.Json)
|
||||
{
|
||||
Console.WriteLine(JsonSerializer.Serialize(results, JsonOptions));
|
||||
}
|
||||
else if (format == OutputFormat.Spdx)
|
||||
{
|
||||
// SPDX format output
|
||||
Console.WriteLine("SPDXVersion: SPDX-2.3");
|
||||
Console.WriteLine("DataLicense: CC0-1.0");
|
||||
Console.WriteLine($"SPDXID: SPDXRef-DOCUMENT");
|
||||
Console.WriteLine($"DocumentName: license-detection-{DateTime.UtcNow:yyyyMMdd}");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("# Detected Licenses");
|
||||
foreach (var result in results)
|
||||
{
|
||||
Console.WriteLine($"LicenseID: {result.SpdxId}");
|
||||
if (!string.IsNullOrWhiteSpace(result.SourceFile))
|
||||
{
|
||||
Console.WriteLine($"LicenseComment: Detected in {result.SourceFile}");
|
||||
}
|
||||
Console.WriteLine();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Table format
|
||||
var table = new Table();
|
||||
table.AddColumn("License");
|
||||
table.AddColumn("Category");
|
||||
table.AddColumn("Confidence");
|
||||
table.AddColumn("Source");
|
||||
|
||||
foreach (var result in results)
|
||||
{
|
||||
table.AddRow(
|
||||
result.SpdxId,
|
||||
GetCategoryDisplay(result.Category),
|
||||
result.Confidence.ToString(),
|
||||
result.SourceFile ?? "[dim]Unknown[/]");
|
||||
}
|
||||
|
||||
AnsiConsole.Write(table);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetCategoryDisplay(LicenseCategory category)
|
||||
{
|
||||
return category switch
|
||||
{
|
||||
LicenseCategory.Permissive => "[green]Permissive[/]",
|
||||
LicenseCategory.WeakCopyleft => "[yellow]Weak Copyleft[/]",
|
||||
LicenseCategory.StrongCopyleft => "[red]Strong Copyleft[/]",
|
||||
LicenseCategory.NetworkCopyleft => "[red]Network Copyleft[/]",
|
||||
LicenseCategory.PublicDomain => "[blue]Public Domain[/]",
|
||||
LicenseCategory.Proprietary => "[magenta]Proprietary[/]",
|
||||
_ => "[dim]Unknown[/]"
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetObligationDisplay(LicenseObligation obligation)
|
||||
{
|
||||
return obligation switch
|
||||
{
|
||||
LicenseObligation.Attribution => "Attribution",
|
||||
LicenseObligation.SourceDisclosure => "Source Disclosure",
|
||||
LicenseObligation.SameLicense => "Same License",
|
||||
LicenseObligation.PatentGrant => "Patent Grant",
|
||||
LicenseObligation.NoWarranty => "No Warranty",
|
||||
LicenseObligation.StateChanges => "State Changes",
|
||||
LicenseObligation.IncludeLicense => "Include License",
|
||||
LicenseObligation.NetworkCopyleft => "Network Copyleft",
|
||||
LicenseObligation.IncludeNotice => "Include Notice",
|
||||
_ => obligation.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Output format for license commands.
|
||||
/// </summary>
|
||||
public enum OutputFormat
|
||||
{
|
||||
/// <summary>Table format for terminal display.</summary>
|
||||
Table,
|
||||
|
||||
/// <summary>JSON format for programmatic use.</summary>
|
||||
Json,
|
||||
|
||||
/// <summary>SPDX format for license documentation.</summary>
|
||||
Spdx
|
||||
}
|
||||
@@ -24,12 +24,11 @@ public static class MigrateArtifactsCommand
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var sourceOption = new Option<string>("--source", "-s")
|
||||
var sourceOption = new Option<string>("--source", new[] { "-s" })
|
||||
{
|
||||
Description = "Source store type: evidence, attestor, vex, all",
|
||||
IsRequired = true
|
||||
Required = true
|
||||
};
|
||||
sourceOption.AddAlias("-s");
|
||||
|
||||
var dryRunOption = new Option<bool>("--dry-run")
|
||||
{
|
||||
|
||||
@@ -33,55 +33,62 @@ public class AnchorCommandGroup
|
||||
|
||||
private Command BuildListCommand()
|
||||
{
|
||||
var outputOption = new Option<string>(
|
||||
name: "--output",
|
||||
getDefaultValue: () => "text",
|
||||
description: "Output format: text, json");
|
||||
var outputOption = new Option<string>("--output")
|
||||
{
|
||||
Description = "Output format: text, json"
|
||||
};
|
||||
outputOption.SetDefaultValue("text");
|
||||
|
||||
var listCommand = new Command("list", "List trust anchors")
|
||||
{
|
||||
outputOption
|
||||
};
|
||||
|
||||
listCommand.SetHandler(async (context) =>
|
||||
{
|
||||
var output = context.ParseResult.GetValueForOption(outputOption) ?? "text";
|
||||
context.ExitCode = await ListAnchorsAsync(output, context.GetCancellationToken());
|
||||
});
|
||||
listCommand.SetAction(async (parseResult, ct) =>
|
||||
await ListAnchorsAsync(
|
||||
parseResult.GetValue(outputOption) ?? "text",
|
||||
ct));
|
||||
|
||||
return listCommand;
|
||||
}
|
||||
|
||||
private Command BuildShowCommand()
|
||||
{
|
||||
var anchorArg = new Argument<Guid>("anchorId", "Trust anchor ID");
|
||||
var anchorArg = new Argument<Guid>("anchorId")
|
||||
{
|
||||
Description = "Trust anchor ID"
|
||||
};
|
||||
|
||||
var showCommand = new Command("show", "Show trust anchor details")
|
||||
{
|
||||
anchorArg
|
||||
};
|
||||
|
||||
showCommand.SetHandler(async (context) =>
|
||||
{
|
||||
var anchorId = context.ParseResult.GetValueForArgument(anchorArg);
|
||||
context.ExitCode = await ShowAnchorAsync(anchorId, context.GetCancellationToken());
|
||||
});
|
||||
showCommand.SetAction(async (parseResult, ct) =>
|
||||
await ShowAnchorAsync(
|
||||
parseResult.GetValue(anchorArg),
|
||||
ct));
|
||||
|
||||
return showCommand;
|
||||
}
|
||||
|
||||
private Command BuildCreateCommand()
|
||||
{
|
||||
var patternArg = new Argument<string>("pattern", "PURL glob pattern (e.g., pkg:npm/*)");
|
||||
var patternArg = new Argument<string>("pattern")
|
||||
{
|
||||
Description = "PURL glob pattern (e.g., pkg:npm/*)"
|
||||
};
|
||||
|
||||
var keyIdsOption = new Option<string[]>(
|
||||
aliases: ["-k", "--key-id"],
|
||||
description: "Allowed key IDs (can be repeated)")
|
||||
{ AllowMultipleArgumentsPerToken = true };
|
||||
var keyIdsOption = new Option<string[]>("--key-id", "-k")
|
||||
{
|
||||
Description = "Allowed key IDs (can be repeated)",
|
||||
AllowMultipleArgumentsPerToken = true
|
||||
};
|
||||
|
||||
var policyVersionOption = new Option<string?>(
|
||||
name: "--policy-version",
|
||||
description: "Policy version for this anchor");
|
||||
var policyVersionOption = new Option<string?>("--policy-version")
|
||||
{
|
||||
Description = "Policy version for this anchor"
|
||||
};
|
||||
|
||||
var createCommand = new Command("create", "Create a new trust anchor")
|
||||
{
|
||||
@@ -90,26 +97,32 @@ public class AnchorCommandGroup
|
||||
policyVersionOption
|
||||
};
|
||||
|
||||
createCommand.SetHandler(async (context) =>
|
||||
{
|
||||
var pattern = context.ParseResult.GetValueForArgument(patternArg);
|
||||
var keyIds = context.ParseResult.GetValueForOption(keyIdsOption) ?? [];
|
||||
var policyVersion = context.ParseResult.GetValueForOption(policyVersionOption);
|
||||
context.ExitCode = await CreateAnchorAsync(pattern, keyIds, policyVersion, context.GetCancellationToken());
|
||||
});
|
||||
createCommand.SetAction(async (parseResult, ct) =>
|
||||
await CreateAnchorAsync(
|
||||
parseResult.GetValue(patternArg),
|
||||
parseResult.GetValue(keyIdsOption) ?? [],
|
||||
parseResult.GetValue(policyVersionOption),
|
||||
ct));
|
||||
|
||||
return createCommand;
|
||||
}
|
||||
|
||||
private Command BuildRevokeKeyCommand()
|
||||
{
|
||||
var anchorArg = new Argument<Guid>("anchorId", "Trust anchor ID");
|
||||
var keyArg = new Argument<string>("keyId", "Key ID to revoke");
|
||||
var anchorArg = new Argument<Guid>("anchorId")
|
||||
{
|
||||
Description = "Trust anchor ID"
|
||||
};
|
||||
var keyArg = new Argument<string>("keyId")
|
||||
{
|
||||
Description = "Key ID to revoke"
|
||||
};
|
||||
|
||||
var reasonOption = new Option<string>(
|
||||
aliases: ["-r", "--reason"],
|
||||
getDefaultValue: () => "manual-revocation",
|
||||
description: "Reason for revocation");
|
||||
var reasonOption = new Option<string>("--reason", "-r")
|
||||
{
|
||||
Description = "Reason for revocation"
|
||||
};
|
||||
reasonOption.SetDefaultValue("manual-revocation");
|
||||
|
||||
var revokeCommand = new Command("revoke-key", "Revoke a key in a trust anchor")
|
||||
{
|
||||
@@ -118,13 +131,12 @@ public class AnchorCommandGroup
|
||||
reasonOption
|
||||
};
|
||||
|
||||
revokeCommand.SetHandler(async (context) =>
|
||||
{
|
||||
var anchorId = context.ParseResult.GetValueForArgument(anchorArg);
|
||||
var keyId = context.ParseResult.GetValueForArgument(keyArg);
|
||||
var reason = context.ParseResult.GetValueForOption(reasonOption) ?? "manual-revocation";
|
||||
context.ExitCode = await RevokeKeyAsync(anchorId, keyId, reason, context.GetCancellationToken());
|
||||
});
|
||||
revokeCommand.SetAction(async (parseResult, ct) =>
|
||||
await RevokeKeyAsync(
|
||||
parseResult.GetValue(anchorArg),
|
||||
parseResult.GetValue(keyArg),
|
||||
parseResult.GetValue(reasonOption) ?? "manual-revocation",
|
||||
ct));
|
||||
|
||||
return revokeCommand;
|
||||
}
|
||||
|
||||
@@ -31,12 +31,16 @@ public class ReceiptCommandGroup
|
||||
|
||||
private Command BuildGetCommand()
|
||||
{
|
||||
var bundleArg = new Argument<string>("bundleId", "Proof bundle ID");
|
||||
var bundleArg = new Argument<string>("bundleId")
|
||||
{
|
||||
Description = "Proof bundle ID"
|
||||
};
|
||||
|
||||
var outputOption = new Option<string>(
|
||||
name: "--output",
|
||||
getDefaultValue: () => "text",
|
||||
description: "Output format: text, json, cbor");
|
||||
var outputOption = new Option<string>("--output")
|
||||
{
|
||||
Description = "Output format: text, json, cbor"
|
||||
};
|
||||
outputOption.SetDefaultValue("text");
|
||||
|
||||
var getCommand = new Command("get", "Get a verification receipt")
|
||||
{
|
||||
@@ -44,23 +48,26 @@ public class ReceiptCommandGroup
|
||||
outputOption
|
||||
};
|
||||
|
||||
getCommand.SetHandler(async (context) =>
|
||||
{
|
||||
var bundleId = context.ParseResult.GetValueForArgument(bundleArg);
|
||||
var output = context.ParseResult.GetValueForOption(outputOption) ?? "text";
|
||||
context.ExitCode = await GetReceiptAsync(bundleId, output, context.GetCancellationToken());
|
||||
});
|
||||
getCommand.SetAction(async (parseResult, ct) =>
|
||||
await GetReceiptAsync(
|
||||
parseResult.GetValue(bundleArg),
|
||||
parseResult.GetValue(outputOption) ?? "text",
|
||||
ct));
|
||||
|
||||
return getCommand;
|
||||
}
|
||||
|
||||
private Command BuildVerifyCommand()
|
||||
{
|
||||
var receiptFileArg = new Argument<FileInfo>("receiptFile", "Path to receipt file");
|
||||
var receiptFileArg = new Argument<FileInfo>("receiptFile")
|
||||
{
|
||||
Description = "Path to receipt file"
|
||||
};
|
||||
|
||||
var offlineOption = new Option<bool>(
|
||||
name: "--offline",
|
||||
description: "Offline mode (skip Rekor verification)");
|
||||
var offlineOption = new Option<bool>("--offline")
|
||||
{
|
||||
Description = "Offline mode (skip Rekor verification)"
|
||||
};
|
||||
|
||||
var verifyCommand = new Command("verify", "Verify a stored receipt")
|
||||
{
|
||||
@@ -68,12 +75,11 @@ public class ReceiptCommandGroup
|
||||
offlineOption
|
||||
};
|
||||
|
||||
verifyCommand.SetHandler(async (context) =>
|
||||
{
|
||||
var receiptFile = context.ParseResult.GetValueForArgument(receiptFileArg);
|
||||
var offline = context.ParseResult.GetValueForOption(offlineOption);
|
||||
context.ExitCode = await VerifyReceiptAsync(receiptFile, offline, context.GetCancellationToken());
|
||||
});
|
||||
verifyCommand.SetAction(async (parseResult, ct) =>
|
||||
await VerifyReceiptAsync(
|
||||
parseResult.GetValue(receiptFileArg),
|
||||
parseResult.GetValue(offlineOption),
|
||||
ct));
|
||||
|
||||
return verifyCommand;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
|
||||
namespace StellaOps.Cli.Commands.Sbom;
|
||||
|
||||
@@ -43,35 +42,39 @@ public static class SbomCommandGroup
|
||||
var generateCommand = new Command("generate", "Generate a deterministic SBOM from an image or directory");
|
||||
|
||||
// Options
|
||||
var imageOption = new Option<string?>(
|
||||
aliases: ["--image", "-i"],
|
||||
description: "Container image reference (e.g., registry/repo@sha256:...)");
|
||||
|
||||
var directoryOption = new Option<string?>(
|
||||
aliases: ["--directory", "-d"],
|
||||
description: "Local directory to scan");
|
||||
|
||||
var formatOption = new Option<SbomOutputFormat>(
|
||||
aliases: ["--format", "-f"],
|
||||
getDefaultValue: () => SbomOutputFormat.CycloneDx,
|
||||
description: "Output format: cyclonedx, spdx, or both");
|
||||
|
||||
var outputOption = new Option<string>(
|
||||
aliases: ["--output", "-o"],
|
||||
description: "Output file path or directory (for 'both' format)")
|
||||
var imageOption = new Option<string?>("--image", "-i")
|
||||
{
|
||||
IsRequired = true
|
||||
Description = "Container image reference (e.g., registry/repo@sha256:...)"
|
||||
};
|
||||
|
||||
var forceOption = new Option<bool>(
|
||||
aliases: ["--force"],
|
||||
getDefaultValue: () => false,
|
||||
description: "Overwrite existing output file");
|
||||
var directoryOption = new Option<string?>("--directory", "-d")
|
||||
{
|
||||
Description = "Local directory to scan"
|
||||
};
|
||||
|
||||
var showHashOption = new Option<bool>(
|
||||
aliases: ["--show-hash"],
|
||||
getDefaultValue: () => true,
|
||||
description: "Display golden hash after generation");
|
||||
var formatOption = new Option<SbomOutputFormat>("--format", "-f")
|
||||
{
|
||||
Description = "Output format: cyclonedx, spdx, or both"
|
||||
};
|
||||
formatOption.SetDefaultValue(SbomOutputFormat.CycloneDx);
|
||||
|
||||
var outputOption = new Option<string>("--output", "-o")
|
||||
{
|
||||
Description = "Output file path or directory (for 'both' format)",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var forceOption = new Option<bool>("--force")
|
||||
{
|
||||
Description = "Overwrite existing output file"
|
||||
};
|
||||
forceOption.SetDefaultValue(false);
|
||||
|
||||
var showHashOption = new Option<bool>("--show-hash")
|
||||
{
|
||||
Description = "Display golden hash after generation"
|
||||
};
|
||||
showHashOption.SetDefaultValue(true);
|
||||
|
||||
generateCommand.AddOption(imageOption);
|
||||
generateCommand.AddOption(directoryOption);
|
||||
@@ -80,28 +83,26 @@ public static class SbomCommandGroup
|
||||
generateCommand.AddOption(forceOption);
|
||||
generateCommand.AddOption(showHashOption);
|
||||
|
||||
generateCommand.SetHandler(async (InvocationContext context) =>
|
||||
generateCommand.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var image = context.ParseResult.GetValueForOption(imageOption);
|
||||
var directory = context.ParseResult.GetValueForOption(directoryOption);
|
||||
var format = context.ParseResult.GetValueForOption(formatOption);
|
||||
var output = context.ParseResult.GetValueForOption(outputOption)!;
|
||||
var force = context.ParseResult.GetValueForOption(forceOption);
|
||||
var showHash = context.ParseResult.GetValueForOption(showHashOption);
|
||||
var image = parseResult.GetValue(imageOption);
|
||||
var directory = parseResult.GetValue(directoryOption);
|
||||
var format = parseResult.GetValue(formatOption);
|
||||
var output = parseResult.GetValue(outputOption)!;
|
||||
var force = parseResult.GetValue(forceOption);
|
||||
var showHash = parseResult.GetValue(showHashOption);
|
||||
|
||||
// Validate input
|
||||
if (string.IsNullOrEmpty(image) && string.IsNullOrEmpty(directory))
|
||||
{
|
||||
Console.Error.WriteLine("Error: Either --image or --directory must be specified.");
|
||||
context.ExitCode = 1;
|
||||
return;
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(image) && !string.IsNullOrEmpty(directory))
|
||||
{
|
||||
Console.Error.WriteLine("Error: Specify either --image or --directory, not both.");
|
||||
context.ExitCode = 1;
|
||||
return;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Check output exists
|
||||
@@ -109,19 +110,18 @@ public static class SbomCommandGroup
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Output file already exists: {output}");
|
||||
Console.Error.WriteLine("Use --force to overwrite.");
|
||||
context.ExitCode = 1;
|
||||
return;
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await GenerateSbomAsync(image, directory, format, output, showHash, context.GetCancellationToken());
|
||||
context.ExitCode = 0;
|
||||
await GenerateSbomAsync(image, directory, format, output, showHash, ct);
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
context.ExitCode = 1;
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -139,36 +139,34 @@ public static class SbomCommandGroup
|
||||
{
|
||||
var hashCommand = new Command("hash", "Compute the golden hash of an SBOM file");
|
||||
|
||||
var inputOption = new Option<string>(
|
||||
aliases: ["--input", "-i"],
|
||||
description: "SBOM file to hash")
|
||||
var inputOption = new Option<string>("--input", "-i")
|
||||
{
|
||||
IsRequired = true
|
||||
Description = "SBOM file to hash",
|
||||
Required = true
|
||||
};
|
||||
|
||||
hashCommand.AddOption(inputOption);
|
||||
|
||||
hashCommand.SetHandler(async (InvocationContext context) =>
|
||||
hashCommand.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var input = context.ParseResult.GetValueForOption(inputOption)!;
|
||||
var input = parseResult.GetValue(inputOption)!;
|
||||
|
||||
if (!File.Exists(input))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: File not found: {input}");
|
||||
context.ExitCode = 1;
|
||||
return;
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var hash = await ComputeGoldenHashAsync(input, context.GetCancellationToken());
|
||||
var hash = await ComputeGoldenHashAsync(input, ct);
|
||||
Console.WriteLine($"Golden Hash (SHA-256): {hash}");
|
||||
context.ExitCode = 0;
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
context.ExitCode = 1;
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -182,57 +180,54 @@ public static class SbomCommandGroup
|
||||
{
|
||||
var verifyCommand = new Command("verify", "Verify an SBOM's golden hash matches expected value");
|
||||
|
||||
var inputOption = new Option<string>(
|
||||
aliases: ["--input", "-i"],
|
||||
description: "SBOM file to verify")
|
||||
var inputOption = new Option<string>("--input", "-i")
|
||||
{
|
||||
IsRequired = true
|
||||
Description = "SBOM file to verify",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var expectedOption = new Option<string>(
|
||||
aliases: ["--expected", "-e"],
|
||||
description: "Expected golden hash (SHA-256)")
|
||||
var expectedOption = new Option<string>("--expected", "-e")
|
||||
{
|
||||
IsRequired = true
|
||||
Description = "Expected golden hash (SHA-256)",
|
||||
Required = true
|
||||
};
|
||||
|
||||
verifyCommand.AddOption(inputOption);
|
||||
verifyCommand.AddOption(expectedOption);
|
||||
|
||||
verifyCommand.SetHandler(async (InvocationContext context) =>
|
||||
verifyCommand.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var input = context.ParseResult.GetValueForOption(inputOption)!;
|
||||
var expected = context.ParseResult.GetValueForOption(expectedOption)!;
|
||||
var input = parseResult.GetValue(inputOption)!;
|
||||
var expected = parseResult.GetValue(expectedOption)!;
|
||||
|
||||
if (!File.Exists(input))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: File not found: {input}");
|
||||
context.ExitCode = 1;
|
||||
return;
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var actual = await ComputeGoldenHashAsync(input, context.GetCancellationToken());
|
||||
var actual = await ComputeGoldenHashAsync(input, ct);
|
||||
var match = string.Equals(actual, expected, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (match)
|
||||
{
|
||||
Console.WriteLine("✓ Golden hash verified successfully.");
|
||||
context.ExitCode = 0;
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine("✗ Golden hash mismatch!");
|
||||
Console.Error.WriteLine($" Expected: {expected}");
|
||||
Console.Error.WriteLine($" Actual: {actual}");
|
||||
context.ExitCode = 1;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
context.ExitCode = 1;
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -329,3 +324,5 @@ public enum SbomOutputFormat
|
||||
/// <summary>Both CycloneDX and SPDX.</summary>
|
||||
Both
|
||||
}
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -108,7 +108,7 @@ public sealed class AgentsSetupStep : SetupStepBase
|
||||
if (context.ConfigValues.TryGetValue("agents.count", out var countStr) &&
|
||||
int.TryParse(countStr, out var count) && count > 0)
|
||||
{
|
||||
var agents = new List<AgentConfig>();
|
||||
var preconfiguredAgents = new List<AgentConfig>();
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var name = context.ConfigValues.GetValueOrDefault($"agents.{i}.name", $"agent-{i}");
|
||||
@@ -116,9 +116,9 @@ public sealed class AgentsSetupStep : SetupStepBase
|
||||
var type = context.ConfigValues.GetValueOrDefault($"agents.{i}.type", "docker");
|
||||
var labels = context.ConfigValues.GetValueOrDefault($"agents.{i}.labels", "").Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
agents.Add(new AgentConfig(name, environment, type, new List<string>(labels)));
|
||||
preconfiguredAgents.Add(new AgentConfig(name, environment, type, new List<string>(labels)));
|
||||
}
|
||||
return agents;
|
||||
return preconfiguredAgents;
|
||||
}
|
||||
|
||||
if (context.NonInteractive)
|
||||
|
||||
@@ -20,23 +20,28 @@ public enum SetupCategory
|
||||
/// </summary>
|
||||
Integration = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Release orchestration (agents, environments).
|
||||
/// </summary>
|
||||
Orchestration = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Settings store and configuration.
|
||||
/// </summary>
|
||||
Configuration = 3,
|
||||
Configuration = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Observability (telemetry, logging).
|
||||
/// </summary>
|
||||
Observability = 4,
|
||||
Observability = 5,
|
||||
|
||||
/// <summary>
|
||||
/// Optional features and enhancements.
|
||||
/// </summary>
|
||||
Optional = 5,
|
||||
Optional = 6,
|
||||
|
||||
/// <summary>
|
||||
/// Data sources (advisory feeds, CVE databases).
|
||||
/// </summary>
|
||||
Data = 6
|
||||
Data = 7
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Cli.Services;
|
||||
using StellaOps.Cli.Extensions;
|
||||
using StellaOps.Infrastructure.Postgres.Migrations;
|
||||
using InfraMigrationResult = StellaOps.Infrastructure.Postgres.Migrations.MigrationResult;
|
||||
|
||||
namespace StellaOps.Cli.Commands;
|
||||
|
||||
@@ -157,7 +158,7 @@ internal static class SystemCommandBuilder
|
||||
return system;
|
||||
}
|
||||
|
||||
private static void WriteRunResult(MigrationModuleInfo module, MigrationResult result, bool verbose)
|
||||
private static void WriteRunResult(MigrationModuleInfo module, InfraMigrationResult result, bool verbose)
|
||||
{
|
||||
var prefix = $"[{module.Name}]";
|
||||
|
||||
|
||||
480
src/Cli/StellaOps.Cli/Commands/TrustProfileCommandGroup.cs
Normal file
480
src/Cli/StellaOps.Cli/Commands/TrustProfileCommandGroup.cs
Normal file
@@ -0,0 +1,480 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.CommandLine;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.AirGap.Bundle.Models;
|
||||
using StellaOps.AirGap.Bundle.Services;
|
||||
|
||||
namespace StellaOps.Cli.Commands;
|
||||
|
||||
public static class TrustProfileCommandGroup
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
public static Command BuildTrustProfileCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var command = new Command("trust-profile", "Manage trust profiles for offline verification.");
|
||||
|
||||
command.Add(BuildListCommand(services, verboseOption, cancellationToken));
|
||||
command.Add(BuildShowCommand(services, verboseOption, cancellationToken));
|
||||
command.Add(BuildApplyCommand(services, verboseOption, cancellationToken));
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private static Command BuildListCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var profilesDirOption = new Option<string?>("--profiles-dir")
|
||||
{
|
||||
Description = "Directory containing trust profile definitions."
|
||||
};
|
||||
var formatOption = new Option<string>("--format", "-f")
|
||||
{
|
||||
Description = "Output format: table (default), json"
|
||||
};
|
||||
formatOption.SetDefaultValue("table");
|
||||
|
||||
var list = new Command("list", "List available trust profiles")
|
||||
{
|
||||
profilesDirOption,
|
||||
formatOption,
|
||||
verboseOption
|
||||
};
|
||||
|
||||
list.SetAction((parseResult, _) =>
|
||||
{
|
||||
var profilesDir = ResolveProfilesDirectory(parseResult.GetValue(profilesDirOption));
|
||||
var format = parseResult.GetValue(formatOption) ?? "table";
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
return HandleListAsync(services, profilesDir, format, verbose, cancellationToken);
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static Command BuildShowCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var profileArg = new Argument<string>("profile-id")
|
||||
{
|
||||
Description = "Trust profile identifier."
|
||||
};
|
||||
var profilesDirOption = new Option<string?>("--profiles-dir")
|
||||
{
|
||||
Description = "Directory containing trust profile definitions."
|
||||
};
|
||||
var formatOption = new Option<string>("--format", "-f")
|
||||
{
|
||||
Description = "Output format: text (default), json"
|
||||
};
|
||||
formatOption.SetDefaultValue("text");
|
||||
|
||||
var show = new Command("show", "Show trust profile details")
|
||||
{
|
||||
profileArg,
|
||||
profilesDirOption,
|
||||
formatOption,
|
||||
verboseOption
|
||||
};
|
||||
|
||||
show.SetAction((parseResult, _) =>
|
||||
{
|
||||
var profileId = parseResult.GetValue(profileArg) ?? string.Empty;
|
||||
var profilesDir = ResolveProfilesDirectory(parseResult.GetValue(profilesDirOption));
|
||||
var format = parseResult.GetValue(formatOption) ?? "text";
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
return HandleShowAsync(services, profileId, profilesDir, format, verbose, cancellationToken);
|
||||
});
|
||||
|
||||
return show;
|
||||
}
|
||||
|
||||
private static Command BuildApplyCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var profileArg = new Argument<string>("profile-id")
|
||||
{
|
||||
Description = "Trust profile identifier."
|
||||
};
|
||||
var profilesDirOption = new Option<string?>("--profiles-dir")
|
||||
{
|
||||
Description = "Directory containing trust profile definitions."
|
||||
};
|
||||
var outputOption = new Option<string?>("--output", "-o")
|
||||
{
|
||||
Description = "Output directory for the applied trust store."
|
||||
};
|
||||
var overwriteOption = new Option<bool>("--overwrite")
|
||||
{
|
||||
Description = "Overwrite existing trust store directory."
|
||||
};
|
||||
|
||||
var apply = new Command("apply", "Apply a trust profile to a local trust store")
|
||||
{
|
||||
profileArg,
|
||||
profilesDirOption,
|
||||
outputOption,
|
||||
overwriteOption,
|
||||
verboseOption
|
||||
};
|
||||
|
||||
apply.SetAction((parseResult, _) =>
|
||||
{
|
||||
var profileId = parseResult.GetValue(profileArg) ?? string.Empty;
|
||||
var profilesDir = ResolveProfilesDirectory(parseResult.GetValue(profilesDirOption));
|
||||
var output = parseResult.GetValue(outputOption);
|
||||
var overwrite = parseResult.GetValue(overwriteOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
return HandleApplyAsync(services, profileId, profilesDir, output, overwrite, verbose, cancellationToken);
|
||||
});
|
||||
|
||||
return apply;
|
||||
}
|
||||
|
||||
private static Task<int> HandleListAsync(
|
||||
IServiceProvider services,
|
||||
string profilesDir,
|
||||
string format,
|
||||
bool verbose,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var loader = services.GetRequiredService<TrustProfileLoader>();
|
||||
var profiles = loader.LoadProfiles(profilesDir);
|
||||
|
||||
if (profiles.Count == 0)
|
||||
{
|
||||
Console.WriteLine($"No trust profiles found in {profilesDir}.");
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
if (format.Equals("json", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Console.WriteLine(JsonSerializer.Serialize(profiles, JsonOptions));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
Console.WriteLine("Trust Profiles");
|
||||
Console.WriteLine("==============");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"Profiles directory: {profilesDir}");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("ID Name Roots Rekor TSA");
|
||||
Console.WriteLine("---------------------------------------------------------------");
|
||||
|
||||
foreach (var profile in profiles)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"{profile.ProfileId,-16} {profile.Name,-30} {profile.TrustRoots.Length,5} {profile.RekorKeys.Length,6} {profile.TsaRoots.Length,4}");
|
||||
}
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
Console.WriteLine();
|
||||
foreach (var profile in profiles)
|
||||
{
|
||||
Console.WriteLine($"- {profile.ProfileId}: {profile.Description ?? "n/a"}");
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
private static Task<int> HandleShowAsync(
|
||||
IServiceProvider services,
|
||||
string profileId,
|
||||
string profilesDir,
|
||||
string format,
|
||||
bool verbose,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(profileId))
|
||||
{
|
||||
Console.Error.WriteLine("Error: profile-id is required.");
|
||||
return Task.FromResult(1);
|
||||
}
|
||||
|
||||
var loader = services.GetRequiredService<TrustProfileLoader>();
|
||||
var profile = loader.LoadProfiles(profilesDir)
|
||||
.FirstOrDefault(p => p.ProfileId.Equals(profileId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (profile is null)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: trust profile not found: {profileId}");
|
||||
return Task.FromResult(1);
|
||||
}
|
||||
|
||||
if (format.Equals("json", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Console.WriteLine(JsonSerializer.Serialize(profile, JsonOptions));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
Console.WriteLine("Trust Profile");
|
||||
Console.WriteLine("=============");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"ID: {profile.ProfileId}");
|
||||
Console.WriteLine($"Name: {profile.Name}");
|
||||
if (!string.IsNullOrWhiteSpace(profile.Description))
|
||||
{
|
||||
Console.WriteLine($"Description: {profile.Description}");
|
||||
}
|
||||
|
||||
PrintEntries("Trust Roots", profile.TrustRoots, verbose);
|
||||
PrintEntries("Rekor Keys", profile.RekorKeys, verbose);
|
||||
PrintEntries("TSA Roots", profile.TsaRoots, verbose);
|
||||
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
private static Task<int> HandleApplyAsync(
|
||||
IServiceProvider services,
|
||||
string profileId,
|
||||
string profilesDir,
|
||||
string? output,
|
||||
bool overwrite,
|
||||
bool verbose,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(profileId))
|
||||
{
|
||||
Console.Error.WriteLine("Error: profile-id is required.");
|
||||
return Task.FromResult(1);
|
||||
}
|
||||
|
||||
var loader = services.GetRequiredService<TrustProfileLoader>();
|
||||
var profile = loader.LoadProfiles(profilesDir)
|
||||
.FirstOrDefault(p => p.ProfileId.Equals(profileId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (profile is null)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: trust profile not found: {profileId}");
|
||||
return Task.FromResult(1);
|
||||
}
|
||||
|
||||
var outputDir = output ?? GetDefaultApplyDirectory(profile.ProfileId);
|
||||
outputDir = Path.GetFullPath(outputDir);
|
||||
|
||||
if (Directory.Exists(outputDir) && Directory.EnumerateFileSystemEntries(outputDir).Any() && !overwrite)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: output directory is not empty: {outputDir}");
|
||||
Console.Error.WriteLine("Use --overwrite to replace existing contents.");
|
||||
return Task.FromResult(1);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(outputDir);
|
||||
|
||||
var trustRootsDir = Path.Combine(outputDir, "trust-roots");
|
||||
var rekorDir = Path.Combine(outputDir, "rekor");
|
||||
var tsaDir = Path.Combine(outputDir, "tsa");
|
||||
Directory.CreateDirectory(trustRootsDir);
|
||||
Directory.CreateDirectory(rekorDir);
|
||||
Directory.CreateDirectory(tsaDir);
|
||||
|
||||
var trustRoots = CopyEntries(loader, profile, profile.TrustRoots, trustRootsDir, verbose);
|
||||
CopyEntries(loader, profile, profile.RekorKeys, rekorDir, verbose);
|
||||
CopyEntries(loader, profile, profile.TsaRoots, tsaDir, verbose);
|
||||
|
||||
var manifest = new TrustManifest
|
||||
{
|
||||
Roots = trustRoots
|
||||
};
|
||||
var manifestPath = Path.Combine(outputDir, "trust-manifest.json");
|
||||
File.WriteAllText(manifestPath, JsonSerializer.Serialize(manifest, JsonOptions));
|
||||
|
||||
var combinedRootPath = Path.Combine(outputDir, "trust-root.pem");
|
||||
WriteCombinedTrustRoots(trustRoots, outputDir, combinedRootPath);
|
||||
|
||||
var profilePath = Path.Combine(outputDir, "trust-profile.json");
|
||||
File.WriteAllText(profilePath, JsonSerializer.Serialize(profile, JsonOptions));
|
||||
|
||||
Console.WriteLine("Trust profile applied.");
|
||||
Console.WriteLine($"Profile: {profile.ProfileId}");
|
||||
Console.WriteLine($"Output: {outputDir}");
|
||||
Console.WriteLine($"Roots: {trustRoots.Count}");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Use the trust store with:");
|
||||
Console.WriteLine($" stella bundle verify --trust-root \"{combinedRootPath}\"");
|
||||
Console.WriteLine($" trust manifest: {manifestPath}");
|
||||
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
private static List<TrustManifestEntry> CopyEntries(
|
||||
TrustProfileLoader loader,
|
||||
TrustProfile profile,
|
||||
ImmutableArray<TrustProfileEntry> entries,
|
||||
string targetDir,
|
||||
bool verbose)
|
||||
{
|
||||
var manifestEntries = new List<TrustManifestEntry>();
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
var sourcePath = loader.ResolveEntryPath(profile, entry);
|
||||
var fileName = Path.GetFileName(entry.Path);
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid entry path: {entry.Path}");
|
||||
}
|
||||
|
||||
var targetPath = Path.Combine(targetDir, fileName);
|
||||
File.Copy(sourcePath, targetPath, overwrite: true);
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
Console.WriteLine($"Copied {entry.Id} -> {targetPath}");
|
||||
}
|
||||
|
||||
manifestEntries.Add(new TrustManifestEntry
|
||||
{
|
||||
KeyId = entry.Id,
|
||||
RelativePath = Path.GetRelativePath(Path.GetDirectoryName(targetDir)!, targetPath)
|
||||
.Replace('\\', '/'),
|
||||
Algorithm = entry.Algorithm,
|
||||
ExpiresAt = entry.ValidUntil,
|
||||
Purpose = entry.Purpose
|
||||
});
|
||||
}
|
||||
|
||||
return manifestEntries;
|
||||
}
|
||||
|
||||
private static void WriteCombinedTrustRoots(
|
||||
IReadOnlyList<TrustManifestEntry> trustRoots,
|
||||
string outputDir,
|
||||
string combinedPath)
|
||||
{
|
||||
if (trustRoots.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var builder = new System.Text.StringBuilder();
|
||||
foreach (var entry in trustRoots)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(entry.RelativePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fullPath = Path.Combine(
|
||||
outputDir,
|
||||
entry.RelativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var pem = File.ReadAllText(fullPath).Trim();
|
||||
if (pem.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (builder.Length > 0)
|
||||
{
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
builder.AppendLine(pem);
|
||||
}
|
||||
|
||||
if (builder.Length > 0)
|
||||
{
|
||||
File.WriteAllText(combinedPath, builder.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private static void PrintEntries(string title, ImmutableArray<TrustProfileEntry> entries, bool verbose)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"{title}: {entries.Length}");
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
Console.WriteLine($" - {entry.Id} ({entry.Path})");
|
||||
if (verbose && (!string.IsNullOrWhiteSpace(entry.Algorithm) || !string.IsNullOrWhiteSpace(entry.Purpose)))
|
||||
{
|
||||
Console.WriteLine($" Algorithm: {entry.Algorithm ?? "n/a"}");
|
||||
Console.WriteLine($" Purpose: {entry.Purpose ?? "n/a"}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveProfilesDirectory(string? profilesDir)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(profilesDir))
|
||||
{
|
||||
return Path.GetFullPath(profilesDir);
|
||||
}
|
||||
|
||||
var envOverride = Environment.GetEnvironmentVariable("STELLAOPS_TRUST_PROFILES");
|
||||
if (!string.IsNullOrWhiteSpace(envOverride))
|
||||
{
|
||||
return Path.GetFullPath(envOverride);
|
||||
}
|
||||
|
||||
var candidate = Path.Combine(Directory.GetCurrentDirectory(), "etc", "trust-profiles");
|
||||
if (Directory.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
|
||||
var baseDirCandidate = Path.Combine(AppContext.BaseDirectory, "etc", "trust-profiles");
|
||||
if (Directory.Exists(baseDirCandidate))
|
||||
{
|
||||
return baseDirCandidate;
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private static string GetDefaultApplyDirectory(string profileId)
|
||||
{
|
||||
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
if (string.IsNullOrWhiteSpace(home))
|
||||
{
|
||||
home = Directory.GetCurrentDirectory();
|
||||
}
|
||||
|
||||
return Path.Combine(home, ".stellaops", "trust-profiles", profileId);
|
||||
}
|
||||
|
||||
private sealed class TrustManifest
|
||||
{
|
||||
[JsonPropertyName("roots")]
|
||||
public List<TrustManifestEntry> Roots { get; init; } = new();
|
||||
}
|
||||
|
||||
private sealed class TrustManifestEntry
|
||||
{
|
||||
[JsonPropertyName("keyId")]
|
||||
public string KeyId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("relativePath")]
|
||||
public string? RelativePath { get; init; }
|
||||
|
||||
[JsonPropertyName("algorithm")]
|
||||
public string? Algorithm { get; init; }
|
||||
|
||||
[JsonPropertyName("expiresAt")]
|
||||
public DateTimeOffset? ExpiresAt { get; init; }
|
||||
|
||||
[JsonPropertyName("purpose")]
|
||||
public string? Purpose { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -576,7 +576,7 @@ public static class UnknownsCommandGroup
|
||||
return 1;
|
||||
}
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<UnknownsListResponse>(JsonOptions, ct);
|
||||
var result = await response.Content.ReadFromJsonAsync<LegacyUnknownsListResponse>(JsonOptions, ct);
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
@@ -603,7 +603,7 @@ public static class UnknownsCommandGroup
|
||||
}
|
||||
}
|
||||
|
||||
private static void PrintUnknownsTable(UnknownsListResponse result)
|
||||
private static void PrintUnknownsTable(LegacyUnknownsListResponse result)
|
||||
{
|
||||
Console.WriteLine($"Unknowns Registry ({result.TotalCount} total, showing {result.Items.Count})");
|
||||
Console.WriteLine(new string('=', 80));
|
||||
@@ -1269,10 +1269,10 @@ public static class UnknownsCommandGroup
|
||||
}
|
||||
|
||||
var listResponse = await response.Content.ReadFromJsonAsync<UnknownsListResponse>(JsonOptions, ct);
|
||||
unknowns = listResponse?.Items.Select(i => new BudgetUnknownDto
|
||||
unknowns = listResponse?.Items?.Select(i => new BudgetUnknownDto
|
||||
{
|
||||
Id = i.Id,
|
||||
ReasonCode = "Reachability" // Default if not provided
|
||||
Id = i.Id.ToString("D"),
|
||||
ReasonCode = "Reachability" // Default if not provided
|
||||
}).ToList() ?? [];
|
||||
}
|
||||
else
|
||||
@@ -1506,7 +1506,7 @@ public static class UnknownsCommandGroup
|
||||
|
||||
#region DTOs
|
||||
|
||||
private sealed record UnknownsListResponse(
|
||||
private sealed record LegacyUnknownsListResponse(
|
||||
IReadOnlyList<UnknownItem> Items,
|
||||
int TotalCount,
|
||||
int Offset,
|
||||
|
||||
@@ -67,8 +67,8 @@ public static class VexCommandGroup
|
||||
generate.Add(signOption);
|
||||
generate.SetAction((parseResult, _) =>
|
||||
{
|
||||
var scan = parseResult.GetValue(scanOption);
|
||||
var format = parseResult.GetValue(formatOption);
|
||||
var scan = parseResult.GetValue(scanOption) ?? string.Empty;
|
||||
var format = parseResult.GetValue(formatOption) ?? "openvex";
|
||||
var output = parseResult.GetValue(outputOption);
|
||||
var sign = parseResult.GetValue(signOption);
|
||||
|
||||
|
||||
437
src/Cli/StellaOps.Cli/Extensions/CommandLineCompatExtensions.cs
Normal file
437
src/Cli/StellaOps.Cli/Extensions/CommandLineCompatExtensions.cs
Normal file
@@ -0,0 +1,437 @@
|
||||
using System;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Parsing;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Cli.Extensions;
|
||||
|
||||
public static class CommandLineCompatExtensions
|
||||
{
|
||||
public static Command AddCommand(this Command command, Command subcommand)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentNullException.ThrowIfNull(subcommand);
|
||||
command.Add(subcommand);
|
||||
return command;
|
||||
}
|
||||
|
||||
public static Command AddOption(this Command command, Option option)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentNullException.ThrowIfNull(option);
|
||||
command.Add(option);
|
||||
return command;
|
||||
}
|
||||
|
||||
public static Command AddArgument(this Command command, Argument argument)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentNullException.ThrowIfNull(argument);
|
||||
command.Add(argument);
|
||||
return command;
|
||||
}
|
||||
|
||||
public static RootCommand AddGlobalOption(this RootCommand command, Option option)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentNullException.ThrowIfNull(option);
|
||||
option.Recursive = true;
|
||||
command.Add(option);
|
||||
return command;
|
||||
}
|
||||
|
||||
public static void SetHandler(this Command command, Action handler)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
command.SetAction(_ => handler());
|
||||
}
|
||||
|
||||
public static void SetHandler(this Command command, Func<Task> handler)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
command.SetAction(_ => handler());
|
||||
}
|
||||
|
||||
public static void SetHandler(this Command command, Func<Task<int>> handler)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
command.SetAction(_ => handler());
|
||||
}
|
||||
|
||||
public static void SetHandler<T1>(this Command command, Action<T1> handler, Symbol symbol1)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
command.SetAction(parseResult => handler(GetValue<T1>(parseResult, symbol1)));
|
||||
}
|
||||
|
||||
public static void SetHandler<T1>(this Command command, Func<T1, Task> handler, Symbol symbol1)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
command.SetAction(parseResult => handler(GetValue<T1>(parseResult, symbol1)));
|
||||
}
|
||||
|
||||
public static void SetHandler<T1>(this Command command, Func<T1, Task<int>> handler, Symbol symbol1)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
command.SetAction(parseResult => handler(GetValue<T1>(parseResult, symbol1)));
|
||||
}
|
||||
|
||||
public static void SetHandler<T1, T2>(this Command command, Action<T1, T2> handler, Symbol symbol1, Symbol symbol2)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
command.SetAction(parseResult =>
|
||||
handler(
|
||||
GetValue<T1>(parseResult, symbol1),
|
||||
GetValue<T2>(parseResult, symbol2)));
|
||||
}
|
||||
|
||||
public static void SetHandler<T1, T2>(this Command command, Func<T1, T2, Task> handler, Symbol symbol1, Symbol symbol2)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
command.SetAction(parseResult =>
|
||||
handler(
|
||||
GetValue<T1>(parseResult, symbol1),
|
||||
GetValue<T2>(parseResult, symbol2)));
|
||||
}
|
||||
|
||||
public static void SetHandler<T1, T2>(this Command command, Func<T1, T2, Task<int>> handler, Symbol symbol1, Symbol symbol2)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
command.SetAction(parseResult =>
|
||||
handler(
|
||||
GetValue<T1>(parseResult, symbol1),
|
||||
GetValue<T2>(parseResult, symbol2)));
|
||||
}
|
||||
|
||||
public static void SetHandler<T1, T2, T3>(
|
||||
this Command command,
|
||||
Action<T1, T2, T3> handler,
|
||||
Symbol symbol1,
|
||||
Symbol symbol2,
|
||||
Symbol symbol3)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
command.SetAction(parseResult =>
|
||||
handler(
|
||||
GetValue<T1>(parseResult, symbol1),
|
||||
GetValue<T2>(parseResult, symbol2),
|
||||
GetValue<T3>(parseResult, symbol3)));
|
||||
}
|
||||
|
||||
public static void SetHandler<T1, T2, T3>(
|
||||
this Command command,
|
||||
Func<T1, T2, T3, Task> handler,
|
||||
Symbol symbol1,
|
||||
Symbol symbol2,
|
||||
Symbol symbol3)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
command.SetAction(parseResult =>
|
||||
handler(
|
||||
GetValue<T1>(parseResult, symbol1),
|
||||
GetValue<T2>(parseResult, symbol2),
|
||||
GetValue<T3>(parseResult, symbol3)));
|
||||
}
|
||||
|
||||
public static void SetHandler<T1, T2, T3>(
|
||||
this Command command,
|
||||
Func<T1, T2, T3, Task<int>> handler,
|
||||
Symbol symbol1,
|
||||
Symbol symbol2,
|
||||
Symbol symbol3)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
command.SetAction(parseResult =>
|
||||
handler(
|
||||
GetValue<T1>(parseResult, symbol1),
|
||||
GetValue<T2>(parseResult, symbol2),
|
||||
GetValue<T3>(parseResult, symbol3)));
|
||||
}
|
||||
|
||||
public static void SetHandler<T1, T2, T3, T4>(
|
||||
this Command command,
|
||||
Func<T1, T2, T3, T4, Task> handler,
|
||||
Symbol symbol1,
|
||||
Symbol symbol2,
|
||||
Symbol symbol3,
|
||||
Symbol symbol4)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
command.SetAction(parseResult =>
|
||||
handler(
|
||||
GetValue<T1>(parseResult, symbol1),
|
||||
GetValue<T2>(parseResult, symbol2),
|
||||
GetValue<T3>(parseResult, symbol3),
|
||||
GetValue<T4>(parseResult, symbol4)));
|
||||
}
|
||||
|
||||
public static void SetHandler<T1, T2, T3, T4>(
|
||||
this Command command,
|
||||
Func<T1, T2, T3, T4, Task<int>> handler,
|
||||
Symbol symbol1,
|
||||
Symbol symbol2,
|
||||
Symbol symbol3,
|
||||
Symbol symbol4)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
command.SetAction(parseResult =>
|
||||
handler(
|
||||
GetValue<T1>(parseResult, symbol1),
|
||||
GetValue<T2>(parseResult, symbol2),
|
||||
GetValue<T3>(parseResult, symbol3),
|
||||
GetValue<T4>(parseResult, symbol4)));
|
||||
}
|
||||
|
||||
public static void SetHandler<T1, T2, T3, T4, T5>(
|
||||
this Command command,
|
||||
Func<T1, T2, T3, T4, T5, Task> handler,
|
||||
Symbol symbol1,
|
||||
Symbol symbol2,
|
||||
Symbol symbol3,
|
||||
Symbol symbol4,
|
||||
Symbol symbol5)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
command.SetAction(parseResult =>
|
||||
handler(
|
||||
GetValue<T1>(parseResult, symbol1),
|
||||
GetValue<T2>(parseResult, symbol2),
|
||||
GetValue<T3>(parseResult, symbol3),
|
||||
GetValue<T4>(parseResult, symbol4),
|
||||
GetValue<T5>(parseResult, symbol5)));
|
||||
}
|
||||
|
||||
public static void SetHandler<T1, T2, T3, T4, T5>(
|
||||
this Command command,
|
||||
Func<T1, T2, T3, T4, T5, Task<int>> handler,
|
||||
Symbol symbol1,
|
||||
Symbol symbol2,
|
||||
Symbol symbol3,
|
||||
Symbol symbol4,
|
||||
Symbol symbol5)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
command.SetAction(parseResult =>
|
||||
handler(
|
||||
GetValue<T1>(parseResult, symbol1),
|
||||
GetValue<T2>(parseResult, symbol2),
|
||||
GetValue<T3>(parseResult, symbol3),
|
||||
GetValue<T4>(parseResult, symbol4),
|
||||
GetValue<T5>(parseResult, symbol5)));
|
||||
}
|
||||
|
||||
public static void SetHandler<T1, T2, T3, T4, T5, T6>(
|
||||
this Command command,
|
||||
Func<T1, T2, T3, T4, T5, T6, Task> handler,
|
||||
Symbol symbol1,
|
||||
Symbol symbol2,
|
||||
Symbol symbol3,
|
||||
Symbol symbol4,
|
||||
Symbol symbol5,
|
||||
Symbol symbol6)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
command.SetAction(parseResult =>
|
||||
handler(
|
||||
GetValue<T1>(parseResult, symbol1),
|
||||
GetValue<T2>(parseResult, symbol2),
|
||||
GetValue<T3>(parseResult, symbol3),
|
||||
GetValue<T4>(parseResult, symbol4),
|
||||
GetValue<T5>(parseResult, symbol5),
|
||||
GetValue<T6>(parseResult, symbol6)));
|
||||
}
|
||||
|
||||
public static void SetHandler<T1, T2, T3, T4, T5, T6>(
|
||||
this Command command,
|
||||
Func<T1, T2, T3, T4, T5, T6, Task<int>> handler,
|
||||
Symbol symbol1,
|
||||
Symbol symbol2,
|
||||
Symbol symbol3,
|
||||
Symbol symbol4,
|
||||
Symbol symbol5,
|
||||
Symbol symbol6)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
command.SetAction(parseResult =>
|
||||
handler(
|
||||
GetValue<T1>(parseResult, symbol1),
|
||||
GetValue<T2>(parseResult, symbol2),
|
||||
GetValue<T3>(parseResult, symbol3),
|
||||
GetValue<T4>(parseResult, symbol4),
|
||||
GetValue<T5>(parseResult, symbol5),
|
||||
GetValue<T6>(parseResult, symbol6)));
|
||||
}
|
||||
|
||||
public static void SetHandler(this Command command, Action<InvocationContext> handler)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
command.SetAction((parseResult, cancellationToken) =>
|
||||
{
|
||||
handler(new InvocationContext(parseResult, cancellationToken));
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
public static void SetHandler(this Command command, Func<InvocationContext, Task> handler)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
command.SetAction((parseResult, cancellationToken) => handler(new InvocationContext(parseResult, cancellationToken)));
|
||||
}
|
||||
|
||||
public static void SetHandler<T1>(this Command command, Action<T1> handler, Option<T1> option1)
|
||||
=> command.SetHandler(handler, (Symbol)option1);
|
||||
|
||||
public static void SetHandler<T1>(this Command command, Func<T1, Task> handler, Option<T1> option1)
|
||||
=> command.SetHandler(handler, (Symbol)option1);
|
||||
|
||||
public static void SetHandler<T1>(this Command command, Func<T1, Task<int>> handler, Option<T1> option1)
|
||||
=> command.SetHandler(handler, (Symbol)option1);
|
||||
|
||||
public static void SetHandler<T1, T2>(this Command command, Action<T1, T2> handler, Option<T1> option1, Option<T2> option2)
|
||||
=> command.SetHandler(handler, (Symbol)option1, (Symbol)option2);
|
||||
|
||||
public static void SetHandler<T1, T2>(this Command command, Func<T1, T2, Task> handler, Option<T1> option1, Option<T2> option2)
|
||||
=> command.SetHandler(handler, (Symbol)option1, (Symbol)option2);
|
||||
|
||||
public static void SetHandler<T1, T2>(this Command command, Func<T1, T2, Task<int>> handler, Option<T1> option1, Option<T2> option2)
|
||||
=> command.SetHandler(handler, (Symbol)option1, (Symbol)option2);
|
||||
|
||||
public static void SetHandler<T1, T2, T3>(
|
||||
this Command command,
|
||||
Action<T1, T2, T3> handler,
|
||||
Option<T1> option1,
|
||||
Option<T2> option2,
|
||||
Option<T3> option3)
|
||||
=> command.SetHandler(handler, (Symbol)option1, (Symbol)option2, (Symbol)option3);
|
||||
|
||||
public static void SetHandler<T1, T2, T3>(
|
||||
this Command command,
|
||||
Func<T1, T2, T3, Task> handler,
|
||||
Option<T1> option1,
|
||||
Option<T2> option2,
|
||||
Option<T3> option3)
|
||||
=> command.SetHandler(handler, (Symbol)option1, (Symbol)option2, (Symbol)option3);
|
||||
|
||||
public static void SetHandler<T1, T2, T3>(
|
||||
this Command command,
|
||||
Func<T1, T2, T3, Task<int>> handler,
|
||||
Option<T1> option1,
|
||||
Option<T2> option2,
|
||||
Option<T3> option3)
|
||||
=> command.SetHandler(handler, (Symbol)option1, (Symbol)option2, (Symbol)option3);
|
||||
|
||||
public static void SetHandler<T1, T2, T3, T4>(
|
||||
this Command command,
|
||||
Func<T1, T2, T3, T4, Task> handler,
|
||||
Option<T1> option1,
|
||||
Option<T2> option2,
|
||||
Option<T3> option3,
|
||||
Option<T4> option4)
|
||||
=> command.SetHandler(handler, (Symbol)option1, (Symbol)option2, (Symbol)option3, (Symbol)option4);
|
||||
|
||||
public static void SetHandler<T1, T2, T3, T4>(
|
||||
this Command command,
|
||||
Func<T1, T2, T3, T4, Task<int>> handler,
|
||||
Option<T1> option1,
|
||||
Option<T2> option2,
|
||||
Option<T3> option3,
|
||||
Option<T4> option4)
|
||||
=> command.SetHandler(handler, (Symbol)option1, (Symbol)option2, (Symbol)option3, (Symbol)option4);
|
||||
|
||||
public static void SetHandler<T1, T2, T3, T4, T5>(
|
||||
this Command command,
|
||||
Func<T1, T2, T3, T4, T5, Task> handler,
|
||||
Option<T1> option1,
|
||||
Option<T2> option2,
|
||||
Option<T3> option3,
|
||||
Option<T4> option4,
|
||||
Option<T5> option5)
|
||||
=> command.SetHandler(handler, (Symbol)option1, (Symbol)option2, (Symbol)option3, (Symbol)option4, (Symbol)option5);
|
||||
|
||||
public static void SetHandler<T1, T2, T3, T4, T5>(
|
||||
this Command command,
|
||||
Func<T1, T2, T3, T4, T5, Task<int>> handler,
|
||||
Option<T1> option1,
|
||||
Option<T2> option2,
|
||||
Option<T3> option3,
|
||||
Option<T4> option4,
|
||||
Option<T5> option5)
|
||||
=> command.SetHandler(handler, (Symbol)option1, (Symbol)option2, (Symbol)option3, (Symbol)option4, (Symbol)option5);
|
||||
|
||||
public static void SetHandler<T1, T2, T3, T4, T5, T6>(
|
||||
this Command command,
|
||||
Func<T1, T2, T3, T4, T5, T6, Task> handler,
|
||||
Option<T1> option1,
|
||||
Option<T2> option2,
|
||||
Option<T3> option3,
|
||||
Option<T4> option4,
|
||||
Option<T5> option5,
|
||||
Option<T6> option6)
|
||||
=> command.SetHandler(handler, (Symbol)option1, (Symbol)option2, (Symbol)option3, (Symbol)option4, (Symbol)option5, (Symbol)option6);
|
||||
|
||||
public static void SetHandler<T1, T2, T3, T4, T5, T6>(
|
||||
this Command command,
|
||||
Func<T1, T2, T3, T4, T5, T6, Task<int>> handler,
|
||||
Option<T1> option1,
|
||||
Option<T2> option2,
|
||||
Option<T3> option3,
|
||||
Option<T4> option4,
|
||||
Option<T5> option5,
|
||||
Option<T6> option6)
|
||||
=> command.SetHandler(handler, (Symbol)option1, (Symbol)option2, (Symbol)option3, (Symbol)option4, (Symbol)option5, (Symbol)option6);
|
||||
|
||||
private static T GetValue<T>(ParseResult parseResult, Symbol symbol)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(parseResult);
|
||||
ArgumentNullException.ThrowIfNull(symbol);
|
||||
|
||||
if (symbol is Option<T> option)
|
||||
{
|
||||
return parseResult.GetValue(option)!;
|
||||
}
|
||||
|
||||
if (symbol is Argument<T> argument)
|
||||
{
|
||||
return parseResult.GetValue(argument)!;
|
||||
}
|
||||
|
||||
return parseResult.GetValue<T>(symbol.Name)!;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class InvocationContext
|
||||
{
|
||||
public InvocationContext(ParseResult parseResult, CancellationToken cancellationToken)
|
||||
{
|
||||
ParseResult = parseResult ?? throw new ArgumentNullException(nameof(parseResult));
|
||||
CancellationToken = cancellationToken;
|
||||
}
|
||||
|
||||
public ParseResult ParseResult { get; }
|
||||
|
||||
public CancellationToken CancellationToken { get; }
|
||||
|
||||
public int ExitCode { get; set; }
|
||||
|
||||
public CancellationToken GetCancellationToken() => CancellationToken;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Parsing;
|
||||
namespace StellaOps.Cli.Extensions;
|
||||
|
||||
/// <summary>
|
||||
@@ -18,6 +19,16 @@ public static class CommandLineExtensions
|
||||
return option;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a default value for an argument.
|
||||
/// </summary>
|
||||
public static Argument<T> SetDefaultValue<T>(this Argument<T> argument, T defaultValue)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(argument);
|
||||
argument.DefaultValueFactory = _ => defaultValue;
|
||||
return argument;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restrict the option to a fixed set of values and add completions.
|
||||
/// </summary>
|
||||
@@ -41,4 +52,35 @@ public static class CommandLineExtensions
|
||||
option.Required = isRequired;
|
||||
return option;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an alias to an option.
|
||||
/// </summary>
|
||||
public static Option<T> AddAlias<T>(this Option<T> option, string alias)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(option);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(alias);
|
||||
option.Aliases.Add(alias);
|
||||
return option;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compatibility shim for GetValueForOption.
|
||||
/// </summary>
|
||||
public static T? GetValueForOption<T>(this ParseResult parseResult, Option<T> option)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(parseResult);
|
||||
ArgumentNullException.ThrowIfNull(option);
|
||||
return parseResult.GetValue(option);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compatibility shim for GetValueForArgument.
|
||||
/// </summary>
|
||||
public static T? GetValueForArgument<T>(this ParseResult parseResult, Argument<T> argument)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(parseResult);
|
||||
ArgumentNullException.ThrowIfNull(argument);
|
||||
return parseResult.GetValue(argument);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ public sealed class CommandGroupBuilder
|
||||
{
|
||||
var command = new Command(_name, _description)
|
||||
{
|
||||
IsHidden = _isHidden,
|
||||
Hidden = _isHidden,
|
||||
};
|
||||
|
||||
// Add all subcommands
|
||||
@@ -159,7 +159,7 @@ public sealed class CommandGroupBuilder
|
||||
{
|
||||
var clone = new Command(newName, original.Description)
|
||||
{
|
||||
IsHidden = original.IsHidden,
|
||||
Hidden = original.Hidden,
|
||||
};
|
||||
|
||||
foreach (var option in original.Options)
|
||||
@@ -177,9 +177,9 @@ public sealed class CommandGroupBuilder
|
||||
clone.AddCommand(subcommand);
|
||||
}
|
||||
|
||||
if (original.Handler is not null)
|
||||
if (original.Action is not null)
|
||||
{
|
||||
clone.Handler = original.Handler;
|
||||
clone.Action = original.Action;
|
||||
}
|
||||
|
||||
return clone;
|
||||
|
||||
@@ -103,7 +103,7 @@ public sealed class CommandRouter : ICommandRouter
|
||||
|
||||
var aliasCommand = new Command(aliasName, $"Alias for '{canonicalCommand.Name}'")
|
||||
{
|
||||
IsHidden = route?.IsDeprecated ?? false, // Hide deprecated commands from help
|
||||
Hidden = route?.IsDeprecated ?? false, // Hide deprecated commands from help
|
||||
};
|
||||
|
||||
// Copy all options from canonical command
|
||||
@@ -119,7 +119,7 @@ public sealed class CommandRouter : ICommandRouter
|
||||
}
|
||||
|
||||
// Set handler that shows warning (if deprecated) and delegates to canonical
|
||||
aliasCommand.SetHandler(async (context) =>
|
||||
aliasCommand.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
if (route?.IsDeprecated == true)
|
||||
{
|
||||
@@ -127,9 +127,13 @@ public sealed class CommandRouter : ICommandRouter
|
||||
}
|
||||
|
||||
// Delegate to canonical command's handler
|
||||
if (canonicalCommand.Handler is not null)
|
||||
if (canonicalCommand.Action is AsynchronousCommandLineAction asyncAction)
|
||||
{
|
||||
await canonicalCommand.Handler.InvokeAsync(context);
|
||||
await asyncAction.InvokeAsync(parseResult, ct);
|
||||
}
|
||||
else if (canonicalCommand.Action is SynchronousCommandLineAction syncAction)
|
||||
{
|
||||
syncAction.Invoke(parseResult);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ using StellaOps.Cli.Configuration;
|
||||
using StellaOps.Cli.Services;
|
||||
using StellaOps.Cli.Telemetry;
|
||||
using StellaOps.AirGap.Policy;
|
||||
using StellaOps.AirGap.Bundle.Services;
|
||||
using StellaOps.Configuration;
|
||||
using StellaOps.Attestor.StandardPredicates.BinaryDiff;
|
||||
using StellaOps.Policy.Scoring.Engine;
|
||||
@@ -29,9 +30,6 @@ using StellaOps.Doctor.DependencyInjection;
|
||||
using StellaOps.Doctor.Plugins.Core.DependencyInjection;
|
||||
using StellaOps.Doctor.Plugins.Database.DependencyInjection;
|
||||
using StellaOps.Doctor.Plugin.BinaryAnalysis.DependencyInjection;
|
||||
#if DEBUG || STELLAOPS_ENABLE_SIMULATOR
|
||||
using StellaOps.Cryptography.Plugin.SimRemote.DependencyInjection;
|
||||
#endif
|
||||
|
||||
namespace StellaOps.Cli;
|
||||
|
||||
@@ -42,6 +40,7 @@ internal static class Program
|
||||
var (options, configuration) = CliBootstrapper.Build(args);
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IConfiguration>(configuration);
|
||||
services.AddSingleton(configuration);
|
||||
services.AddSingleton(options);
|
||||
services.AddOptions();
|
||||
@@ -65,9 +64,6 @@ internal static class Program
|
||||
services.AddSmRemoteCryptoProvider(configuration);
|
||||
#endif
|
||||
|
||||
#if DEBUG || STELLAOPS_ENABLE_SIMULATOR
|
||||
services.AddSimRemoteCryptoProvider(configuration);
|
||||
#endif
|
||||
|
||||
// CLI-AIRGAP-56-002: Add sealed mode telemetry for air-gapped operation
|
||||
services.AddSealedModeTelemetryIfOffline(
|
||||
@@ -343,6 +339,7 @@ internal static class Program
|
||||
services.AddSingleton<StellaOps.AirGap.Importer.Repositories.IBundleItemRepository,
|
||||
StellaOps.AirGap.Importer.Repositories.InMemoryBundleItemRepository>();
|
||||
services.AddSingleton<IMirrorBundleImportService, MirrorBundleImportService>();
|
||||
services.AddSingleton<TrustProfileLoader>();
|
||||
|
||||
// CLI-CRYPTO-4100-001: Crypto profile validator
|
||||
services.AddSingleton<CryptoProfileValidator>();
|
||||
|
||||
@@ -896,6 +896,270 @@ internal sealed class BackendOperationsClient : IBackendOperationsClient
|
||||
return MapPolicyFindingExplain(document);
|
||||
}
|
||||
|
||||
public async Task<AnalyticsListResponse<AnalyticsSupplierConcentration>> GetAnalyticsSuppliersAsync(
|
||||
int? limit,
|
||||
string? environment,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
EnsureBackendConfigured();
|
||||
|
||||
var query = BuildAnalyticsQueryString(environment: environment, limit: limit);
|
||||
using var request = CreateRequest(HttpMethod.Get, $"api/analytics/suppliers{query}");
|
||||
await AuthorizeRequestAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var (message, _) = await CreateFailureDetailsAsync(response, cancellationToken).ConfigureAwait(false);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
AnalyticsListResponse<AnalyticsSupplierConcentration>? result;
|
||||
try
|
||||
{
|
||||
result = await response.Content.ReadFromJsonAsync<AnalyticsListResponse<AnalyticsSupplierConcentration>>(SerializerOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
var raw = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
throw new InvalidOperationException($"Failed to parse analytics suppliers response: {ex.Message}", ex)
|
||||
{
|
||||
Data = { ["payload"] = raw }
|
||||
};
|
||||
}
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
throw new InvalidOperationException("Analytics suppliers response was empty.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<AnalyticsListResponse<AnalyticsLicenseDistribution>> GetAnalyticsLicensesAsync(
|
||||
string? environment,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
EnsureBackendConfigured();
|
||||
|
||||
var query = BuildAnalyticsQueryString(environment: environment);
|
||||
using var request = CreateRequest(HttpMethod.Get, $"api/analytics/licenses{query}");
|
||||
await AuthorizeRequestAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var (message, _) = await CreateFailureDetailsAsync(response, cancellationToken).ConfigureAwait(false);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
AnalyticsListResponse<AnalyticsLicenseDistribution>? result;
|
||||
try
|
||||
{
|
||||
result = await response.Content.ReadFromJsonAsync<AnalyticsListResponse<AnalyticsLicenseDistribution>>(SerializerOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
var raw = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
throw new InvalidOperationException($"Failed to parse analytics licenses response: {ex.Message}", ex)
|
||||
{
|
||||
Data = { ["payload"] = raw }
|
||||
};
|
||||
}
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
throw new InvalidOperationException("Analytics licenses response was empty.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<AnalyticsListResponse<AnalyticsVulnerabilityExposure>> GetAnalyticsVulnerabilitiesAsync(string? environment, string? minSeverity, CancellationToken cancellationToken)
|
||||
{
|
||||
EnsureBackendConfigured();
|
||||
|
||||
var query = BuildAnalyticsQueryString(environment: environment, minSeverity: minSeverity);
|
||||
using var request = CreateRequest(HttpMethod.Get, $"api/analytics/vulnerabilities{query}");
|
||||
await AuthorizeRequestAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var (message, _) = await CreateFailureDetailsAsync(response, cancellationToken).ConfigureAwait(false);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
AnalyticsListResponse<AnalyticsVulnerabilityExposure>? result;
|
||||
try
|
||||
{
|
||||
result = await response.Content.ReadFromJsonAsync<AnalyticsListResponse<AnalyticsVulnerabilityExposure>>(SerializerOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
var raw = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
throw new InvalidOperationException($"Failed to parse analytics vulnerabilities response: {ex.Message}", ex)
|
||||
{
|
||||
Data = { ["payload"] = raw }
|
||||
};
|
||||
}
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
throw new InvalidOperationException("Analytics vulnerabilities response was empty.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<AnalyticsListResponse<AnalyticsFixableBacklogItem>> GetAnalyticsBacklogAsync(string? environment, CancellationToken cancellationToken)
|
||||
{
|
||||
EnsureBackendConfigured();
|
||||
|
||||
var query = BuildAnalyticsQueryString(environment: environment);
|
||||
using var request = CreateRequest(HttpMethod.Get, $"api/analytics/backlog{query}");
|
||||
await AuthorizeRequestAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var (message, _) = await CreateFailureDetailsAsync(response, cancellationToken).ConfigureAwait(false);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
AnalyticsListResponse<AnalyticsFixableBacklogItem>? result;
|
||||
try
|
||||
{
|
||||
result = await response.Content.ReadFromJsonAsync<AnalyticsListResponse<AnalyticsFixableBacklogItem>>(SerializerOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
var raw = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
throw new InvalidOperationException($"Failed to parse analytics backlog response: {ex.Message}", ex)
|
||||
{
|
||||
Data = { ["payload"] = raw }
|
||||
};
|
||||
}
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
throw new InvalidOperationException("Analytics backlog response was empty.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<AnalyticsListResponse<AnalyticsAttestationCoverage>> GetAnalyticsAttestationCoverageAsync(string? environment, CancellationToken cancellationToken)
|
||||
{
|
||||
EnsureBackendConfigured();
|
||||
|
||||
var query = BuildAnalyticsQueryString(environment: environment);
|
||||
using var request = CreateRequest(HttpMethod.Get, $"api/analytics/attestation-coverage{query}");
|
||||
await AuthorizeRequestAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var (message, _) = await CreateFailureDetailsAsync(response, cancellationToken).ConfigureAwait(false);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
AnalyticsListResponse<AnalyticsAttestationCoverage>? result;
|
||||
try
|
||||
{
|
||||
result = await response.Content.ReadFromJsonAsync<AnalyticsListResponse<AnalyticsAttestationCoverage>>(SerializerOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
var raw = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
throw new InvalidOperationException($"Failed to parse analytics attestation coverage response: {ex.Message}", ex)
|
||||
{
|
||||
Data = { ["payload"] = raw }
|
||||
};
|
||||
}
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
throw new InvalidOperationException("Analytics attestation coverage response was empty.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<AnalyticsListResponse<AnalyticsVulnerabilityTrendPoint>> GetAnalyticsVulnerabilityTrendsAsync(string? environment, int? days, CancellationToken cancellationToken)
|
||||
{
|
||||
EnsureBackendConfigured();
|
||||
|
||||
var query = BuildAnalyticsQueryString(environment: environment, days: days);
|
||||
using var request = CreateRequest(HttpMethod.Get, $"api/analytics/trends/vulnerabilities{query}");
|
||||
await AuthorizeRequestAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var (message, _) = await CreateFailureDetailsAsync(response, cancellationToken).ConfigureAwait(false);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
AnalyticsListResponse<AnalyticsVulnerabilityTrendPoint>? result;
|
||||
try
|
||||
{
|
||||
result = await response.Content.ReadFromJsonAsync<AnalyticsListResponse<AnalyticsVulnerabilityTrendPoint>>(SerializerOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
var raw = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
throw new InvalidOperationException($"Failed to parse analytics vulnerability trends response: {ex.Message}", ex)
|
||||
{
|
||||
Data = { ["payload"] = raw }
|
||||
};
|
||||
}
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
throw new InvalidOperationException("Analytics vulnerability trends response was empty.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<AnalyticsListResponse<AnalyticsComponentTrendPoint>> GetAnalyticsComponentTrendsAsync(string? environment, int? days, CancellationToken cancellationToken)
|
||||
{
|
||||
EnsureBackendConfigured();
|
||||
|
||||
var query = BuildAnalyticsQueryString(environment: environment, days: days);
|
||||
using var request = CreateRequest(HttpMethod.Get, $"api/analytics/trends/components{query}");
|
||||
await AuthorizeRequestAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var (message, _) = await CreateFailureDetailsAsync(response, cancellationToken).ConfigureAwait(false);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
AnalyticsListResponse<AnalyticsComponentTrendPoint>? result;
|
||||
try
|
||||
{
|
||||
result = await response.Content.ReadFromJsonAsync<AnalyticsListResponse<AnalyticsComponentTrendPoint>>(SerializerOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
var raw = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
throw new InvalidOperationException($"Failed to parse analytics component trends response: {ex.Message}", ex)
|
||||
{
|
||||
Data = { ["payload"] = raw }
|
||||
};
|
||||
}
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
throw new InvalidOperationException("Analytics component trends response was empty.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<EntryTraceResponseModel?> GetEntryTraceAsync(string scanId, CancellationToken cancellationToken)
|
||||
{
|
||||
EnsureBackendConfigured();
|
||||
@@ -2055,6 +2319,21 @@ internal sealed class BackendOperationsClient : IBackendOperationsClient
|
||||
request.Headers.TryAddWithoutValidation(AdvisoryScopesHeader, combined);
|
||||
}
|
||||
|
||||
private static void ApplyTenantHeader(HttpRequestMessage request, string? tenantId)
|
||||
{
|
||||
if (request is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tenantId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
request.Headers.TryAddWithoutValidation("X-Tenant-Id", tenantId.Trim());
|
||||
}
|
||||
|
||||
private HttpRequestMessage CreateRequest(HttpMethod method, string relativeUri)
|
||||
{
|
||||
if (!Uri.TryCreate(relativeUri, UriKind.RelativeOrAbsolute, out var requestUri))
|
||||
@@ -2427,6 +2706,42 @@ internal sealed class BackendOperationsClient : IBackendOperationsClient
|
||||
return "?" + string.Join("&", parameters);
|
||||
}
|
||||
|
||||
private static string BuildAnalyticsQueryString(
|
||||
string? environment = null,
|
||||
string? minSeverity = null,
|
||||
int? days = null,
|
||||
int? limit = null)
|
||||
{
|
||||
var parameters = new List<string>();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(environment))
|
||||
{
|
||||
parameters.Add($"environment={Uri.EscapeDataString(environment.Trim())}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(minSeverity))
|
||||
{
|
||||
parameters.Add($"minSeverity={Uri.EscapeDataString(minSeverity.Trim())}");
|
||||
}
|
||||
|
||||
if (days.HasValue)
|
||||
{
|
||||
parameters.Add($"days={days.Value.ToString(CultureInfo.InvariantCulture)}");
|
||||
}
|
||||
|
||||
if (limit.HasValue)
|
||||
{
|
||||
parameters.Add($"limit={limit.Value.ToString(CultureInfo.InvariantCulture)}");
|
||||
}
|
||||
|
||||
if (parameters.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return "?" + string.Join("&", parameters);
|
||||
}
|
||||
|
||||
private static PolicyFindingsPage MapPolicyFindings(PolicyFindingsResponseDocument document)
|
||||
{
|
||||
var items = document.Items is null
|
||||
|
||||
@@ -48,6 +48,15 @@ internal interface IBackendOperationsClient
|
||||
|
||||
Task<PolicyFindingExplainResult> GetPolicyFindingExplainAsync(string policyId, string findingId, string? mode, CancellationToken cancellationToken);
|
||||
|
||||
// CLI-ANALYTICS-32-001: SBOM lake analytics endpoints
|
||||
Task<AnalyticsListResponse<AnalyticsSupplierConcentration>> GetAnalyticsSuppliersAsync(int? limit, string? environment, CancellationToken cancellationToken);
|
||||
Task<AnalyticsListResponse<AnalyticsLicenseDistribution>> GetAnalyticsLicensesAsync(string? environment, CancellationToken cancellationToken);
|
||||
Task<AnalyticsListResponse<AnalyticsVulnerabilityExposure>> GetAnalyticsVulnerabilitiesAsync(string? environment, string? minSeverity, CancellationToken cancellationToken);
|
||||
Task<AnalyticsListResponse<AnalyticsFixableBacklogItem>> GetAnalyticsBacklogAsync(string? environment, CancellationToken cancellationToken);
|
||||
Task<AnalyticsListResponse<AnalyticsAttestationCoverage>> GetAnalyticsAttestationCoverageAsync(string? environment, CancellationToken cancellationToken);
|
||||
Task<AnalyticsListResponse<AnalyticsVulnerabilityTrendPoint>> GetAnalyticsVulnerabilityTrendsAsync(string? environment, int? days, CancellationToken cancellationToken);
|
||||
Task<AnalyticsListResponse<AnalyticsComponentTrendPoint>> GetAnalyticsComponentTrendsAsync(string? environment, int? days, CancellationToken cancellationToken);
|
||||
|
||||
Task<EntryTraceResponseModel?> GetEntryTraceAsync(string scanId, CancellationToken cancellationToken);
|
||||
|
||||
Task<RubyPackageInventoryModel?> GetRubyPackagesAsync(string scanId, CancellationToken cancellationToken);
|
||||
|
||||
81
src/Cli/StellaOps.Cli/Services/Models/AnalyticsModels.cs
Normal file
81
src/Cli/StellaOps.Cli/Services/Models/AnalyticsModels.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Cli.Services.Models;
|
||||
|
||||
// CLI-ANALYTICS-32-001: Analytics SBOM lake response models.
|
||||
internal sealed record AnalyticsListResponse<T>(
|
||||
[property: JsonPropertyName("tenantId")] string TenantId,
|
||||
[property: JsonPropertyName("actorId")] string ActorId,
|
||||
[property: JsonPropertyName("dataAsOf")] DateTimeOffset DataAsOf,
|
||||
[property: JsonPropertyName("cached")] bool Cached,
|
||||
[property: JsonPropertyName("cacheTtlSeconds")] int CacheTtlSeconds,
|
||||
[property: JsonPropertyName("items")] IReadOnlyList<T> Items,
|
||||
[property: JsonPropertyName("count")] int Count,
|
||||
[property: JsonPropertyName("limit")] int? Limit = null,
|
||||
[property: JsonPropertyName("offset")] int? Offset = null,
|
||||
[property: JsonPropertyName("query")] string? Query = null);
|
||||
|
||||
internal sealed record AnalyticsSupplierConcentration(
|
||||
[property: JsonPropertyName("supplier")] string Supplier,
|
||||
[property: JsonPropertyName("componentCount")] int ComponentCount,
|
||||
[property: JsonPropertyName("artifactCount")] int ArtifactCount,
|
||||
[property: JsonPropertyName("teamCount")] int TeamCount,
|
||||
[property: JsonPropertyName("criticalVulnCount")] int CriticalVulnCount,
|
||||
[property: JsonPropertyName("highVulnCount")] int HighVulnCount,
|
||||
[property: JsonPropertyName("environments")] IReadOnlyList<string>? Environments);
|
||||
|
||||
internal sealed record AnalyticsLicenseDistribution(
|
||||
[property: JsonPropertyName("licenseConcluded")] string? LicenseConcluded,
|
||||
[property: JsonPropertyName("licenseCategory")] string LicenseCategory,
|
||||
[property: JsonPropertyName("componentCount")] int ComponentCount,
|
||||
[property: JsonPropertyName("artifactCount")] int ArtifactCount,
|
||||
[property: JsonPropertyName("ecosystems")] IReadOnlyList<string>? Ecosystems);
|
||||
|
||||
internal sealed record AnalyticsVulnerabilityExposure(
|
||||
[property: JsonPropertyName("vulnId")] string VulnId,
|
||||
[property: JsonPropertyName("severity")] string Severity,
|
||||
[property: JsonPropertyName("cvssScore")] decimal? CvssScore,
|
||||
[property: JsonPropertyName("epssScore")] decimal? EpssScore,
|
||||
[property: JsonPropertyName("kevListed")] bool KevListed,
|
||||
[property: JsonPropertyName("fixAvailable")] bool FixAvailable,
|
||||
[property: JsonPropertyName("rawComponentCount")] int RawComponentCount,
|
||||
[property: JsonPropertyName("rawArtifactCount")] int RawArtifactCount,
|
||||
[property: JsonPropertyName("effectiveComponentCount")] int EffectiveComponentCount,
|
||||
[property: JsonPropertyName("effectiveArtifactCount")] int EffectiveArtifactCount,
|
||||
[property: JsonPropertyName("vexMitigated")] int VexMitigated);
|
||||
|
||||
internal sealed record AnalyticsFixableBacklogItem(
|
||||
[property: JsonPropertyName("service")] string Service,
|
||||
[property: JsonPropertyName("environment")] string Environment,
|
||||
[property: JsonPropertyName("component")] string Component,
|
||||
[property: JsonPropertyName("version")] string? Version,
|
||||
[property: JsonPropertyName("vulnId")] string VulnId,
|
||||
[property: JsonPropertyName("severity")] string Severity,
|
||||
[property: JsonPropertyName("fixedVersion")] string? FixedVersion);
|
||||
|
||||
internal sealed record AnalyticsAttestationCoverage(
|
||||
[property: JsonPropertyName("environment")] string Environment,
|
||||
[property: JsonPropertyName("team")] string? Team,
|
||||
[property: JsonPropertyName("totalArtifacts")] int TotalArtifacts,
|
||||
[property: JsonPropertyName("withProvenance")] int WithProvenance,
|
||||
[property: JsonPropertyName("provenancePct")] decimal? ProvenancePct,
|
||||
[property: JsonPropertyName("slsaLevel2Plus")] int SlsaLevel2Plus,
|
||||
[property: JsonPropertyName("slsa2Pct")] decimal? Slsa2Pct,
|
||||
[property: JsonPropertyName("missingProvenance")] int MissingProvenance);
|
||||
|
||||
internal sealed record AnalyticsVulnerabilityTrendPoint(
|
||||
[property: JsonPropertyName("snapshotDate")] DateTimeOffset SnapshotDate,
|
||||
[property: JsonPropertyName("environment")] string Environment,
|
||||
[property: JsonPropertyName("totalVulns")] int TotalVulns,
|
||||
[property: JsonPropertyName("fixableVulns")] int FixableVulns,
|
||||
[property: JsonPropertyName("vexMitigated")] int VexMitigated,
|
||||
[property: JsonPropertyName("netExposure")] int NetExposure,
|
||||
[property: JsonPropertyName("kevVulns")] int KevVulns);
|
||||
|
||||
internal sealed record AnalyticsComponentTrendPoint(
|
||||
[property: JsonPropertyName("snapshotDate")] DateTimeOffset SnapshotDate,
|
||||
[property: JsonPropertyName("environment")] string Environment,
|
||||
[property: JsonPropertyName("totalComponents")] int TotalComponents,
|
||||
[property: JsonPropertyName("uniqueSuppliers")] int UniqueSuppliers);
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Commands\\BenchCommandBuilder.cs" />
|
||||
<Compile Remove="Commands\\Agent\\BootstrapCommands.cs" />
|
||||
<Compile Remove="Commands\\Proof\\AnchorCommandGroup.cs" />
|
||||
<!-- ProofCommandGroup enabled for SPRINT_3500_0004_0001_cli_verbs T4 -->
|
||||
<Compile Remove="Commands\\Proof\\ReceiptCommandGroup.cs" />
|
||||
@@ -60,6 +61,7 @@
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Canonical.Json/StellaOps.Canonical.Json.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.DeltaVerdict/StellaOps.DeltaVerdict.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Verdict/StellaOps.Verdict.csproj" />
|
||||
<ProjectReference Include="../../AirGap/__Libraries/StellaOps.AirGap.Bundle/StellaOps.AirGap.Bundle.csproj" />
|
||||
<ProjectReference Include="../../AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.csproj" />
|
||||
<ProjectReference Include="../../AirGap/StellaOps.AirGap.Importer/StellaOps.AirGap.Importer.csproj" />
|
||||
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOps.Auth.Abstractions.csproj" />
|
||||
@@ -76,6 +78,8 @@
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Bun/StellaOps.Scanner.Analyzers.Lang.Bun.csproj" />
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Surface.Env/StellaOps.Scanner.Surface.Env.csproj" />
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Surface.Validation/StellaOps.Scanner.Surface.Validation.csproj" />
|
||||
<!-- Sprint: SPRINT_20260119_022_Scanner_dependency_reachability (TASK-022-009) -->
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Reachability/StellaOps.Scanner.Reachability.csproj" />
|
||||
<ProjectReference Include="../../Policy/StellaOps.PolicyDsl/StellaOps.PolicyDsl.csproj" />
|
||||
<ProjectReference Include="../../Policy/__Libraries/StellaOps.Policy/StellaOps.Policy.csproj" />
|
||||
<ProjectReference Include="../../Policy/StellaOps.Policy.RiskProfile/StellaOps.Policy.RiskProfile.csproj" />
|
||||
@@ -83,6 +87,7 @@
|
||||
<ProjectReference Include="../../Attestor/StellaOps.Attestation/StellaOps.Attestation.csproj" />
|
||||
<ProjectReference Include="../../Attestor/StellaOps.Attestor.Envelope/StellaOps.Attestor.Envelope.csproj" />
|
||||
<ProjectReference Include="../../Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/StellaOps.Attestor.Core.csproj" />
|
||||
<ProjectReference Include="../../Attestor/__Libraries/StellaOps.Attestor.Timestamping/StellaOps.Attestor.Timestamping.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Infrastructure.Postgres/StellaOps.Infrastructure.Postgres.csproj" />
|
||||
<ProjectReference Include="../../Authority/__Libraries/StellaOps.Authority.Persistence/StellaOps.Authority.Persistence.csproj" />
|
||||
<ProjectReference Include="../../Scheduler/__Libraries/StellaOps.Scheduler.Persistence/StellaOps.Scheduler.Persistence.csproj" />
|
||||
@@ -121,9 +126,12 @@
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Doctor/StellaOps.Doctor.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Doctor.Plugins.Core/StellaOps.Doctor.Plugins.Core.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Doctor.Plugins.Database/StellaOps.Doctor.Plugins.Database.csproj" />
|
||||
<ProjectReference Include="../Doctor/__Plugins/StellaOps.Doctor.Plugin.BinaryAnalysis/StellaOps.Doctor.Plugin.BinaryAnalysis.csproj" />
|
||||
<ProjectReference Include="../../Doctor/__Plugins/StellaOps.Doctor.Plugin.BinaryAnalysis/StellaOps.Doctor.Plugin.BinaryAnalysis.csproj" />
|
||||
<!-- Delta Scanning Engine (Sprint: SPRINT_20260118_026_Scanner_delta_scanning_engine) -->
|
||||
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Delta/StellaOps.Scanner.Delta.csproj" />
|
||||
<!-- Ground-Truth Corpus (Sprint: SPRINT_20260121_036_BinaryIndex_golden_corpus_bundle_verification) -->
|
||||
<ProjectReference Include="../../BinaryIndex/__Libraries/StellaOps.BinaryIndex.GroundTruth.Abstractions/StellaOps.BinaryIndex.GroundTruth.Abstractions.csproj" />
|
||||
<ProjectReference Include="../../BinaryIndex/__Libraries/StellaOps.BinaryIndex.GroundTruth.Reproducible/StellaOps.BinaryIndex.GroundTruth.Reproducible.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- GOST Crypto Plugins (Russia distribution) -->
|
||||
|
||||
@@ -48,3 +48,11 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| CLI-VEX-WEBHOOKS-0001 | DONE | SPRINT_20260117_009 - Add VEX webhooks commands. |
|
||||
| CLI-BINARY-ANALYSIS-0001 | DONE | SPRINT_20260117_007 - Add binary fingerprint/diff tests. |
|
||||
| ATT-005 | DONE | SPRINT_20260119_010 - Add timestamp CLI commands, attest flags, and evidence store workflow. |
|
||||
| TASK-029-003 | DONE | SPRINT_20260120_029 - Add `stella bundle verify` report signing options. |
|
||||
| TASK-029-004 | DONE | SPRINT_20260120_029 - Add trust profile commands and apply flow. |
|
||||
| TASK-032-001 | BLOCKED | Analytics command tree delivered; validation blocked pending stable ingestion datasets. |
|
||||
| TASK-032-002 | BLOCKED | Analytics handlers delivered; validation blocked pending endpoint stability. |
|
||||
| TASK-032-003 | BLOCKED | Output formats delivered; validation blocked pending real datasets. |
|
||||
| TASK-032-004 | BLOCKED | Fixtures/tests delivered; refresh blocked pending stabilized API responses. |
|
||||
| TASK-032-005 | BLOCKED | Docs delivered; validation blocked pending stable API filters. |
|
||||
| TASK-033-007 | DONE | Updated CLI compatibility shims; CLI + plugins build (SPRINT_20260120_033). |
|
||||
|
||||
@@ -163,6 +163,9 @@ public sealed class LocalValidator
|
||||
{
|
||||
DirectoryPath = directoryPath,
|
||||
IsValid = false,
|
||||
TotalFiles = 0,
|
||||
ValidFiles = 0,
|
||||
InvalidFiles = 1,
|
||||
Results = [new ValidationResult
|
||||
{
|
||||
IsValid = false,
|
||||
|
||||
@@ -119,6 +119,15 @@
|
||||
"type": "alias",
|
||||
"reason": "Both paths remain valid"
|
||||
},
|
||||
// =============================================
|
||||
// Analytics aliases (Sprint 032)
|
||||
// =============================================
|
||||
{
|
||||
"old": "analytics sbom",
|
||||
"new": "analytics sbom-lake",
|
||||
"type": "alias",
|
||||
"reason": "SBOM lake analytics group"
|
||||
},
|
||||
|
||||
// =============================================
|
||||
// Scanning consolidation (Sprint 013)
|
||||
@@ -732,13 +741,6 @@
|
||||
"removeIn": "3.0",
|
||||
"reason": "Replay commands consolidated under evidence"
|
||||
},
|
||||
{
|
||||
"old": "prove",
|
||||
"new": "evidence proof",
|
||||
"type": "deprecated",
|
||||
"removeIn": "3.0",
|
||||
"reason": "Proof commands consolidated under evidence"
|
||||
},
|
||||
{
|
||||
"old": "proof",
|
||||
"new": "evidence proof",
|
||||
|
||||
Reference in New Issue
Block a user