Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
10640 lines
407 KiB
C#
10640 lines
407 KiB
C#
using System;
|
|
using System.CommandLine;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
using StellaOps.Cli.Configuration;
|
|
using StellaOps.Cli.Extensions;
|
|
using StellaOps.Cli.Plugins;
|
|
using StellaOps.Cli.Services.Models.AdvisoryAi;
|
|
|
|
namespace StellaOps.Cli.Commands;
|
|
|
|
internal static class CommandFactory
|
|
{
|
|
public static RootCommand Create(
|
|
IServiceProvider services,
|
|
StellaOpsCliOptions options,
|
|
CancellationToken cancellationToken,
|
|
ILoggerFactory loggerFactory)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(loggerFactory);
|
|
|
|
var verboseOption = new Option<bool>("--verbose", new[] { "-v" })
|
|
{
|
|
Description = "Enable verbose logging output."
|
|
};
|
|
|
|
var globalTenantOption = new Option<string?>("--tenant", new[] { "-t" })
|
|
{
|
|
Description = "Tenant context for the operation. Overrides profile and STELLAOPS_TENANT environment variable."
|
|
};
|
|
|
|
var root = new RootCommand("StellaOps command-line interface")
|
|
{
|
|
TreatUnmatchedTokensAsErrors = true
|
|
};
|
|
root.Add(verboseOption);
|
|
root.Add(globalTenantOption);
|
|
|
|
root.Add(BuildScannerCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildScanCommand(services, options, verboseOption, cancellationToken));
|
|
root.Add(BuildRubyCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildPhpCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildPythonCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildBunCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildDatabaseCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildSourcesCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildAocCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildAuthCommand(services, options, verboseOption, cancellationToken));
|
|
root.Add(BuildTenantsCommand(services, options, verboseOption, cancellationToken));
|
|
root.Add(BuildPolicyCommand(services, options, verboseOption, cancellationToken));
|
|
root.Add(BuildTaskRunnerCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildFindingsCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildAdviseCommand(services, options, verboseOption, cancellationToken));
|
|
root.Add(BuildConfigCommand(options));
|
|
root.Add(BuildKmsCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildVulnCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildVexCommand(services, options, verboseOption, cancellationToken));
|
|
root.Add(BuildCryptoCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildAttestCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildRiskProfileCommand(verboseOption, cancellationToken));
|
|
root.Add(BuildAdvisoryCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildForensicCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildPromotionCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildDetscoreCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildObsCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildPackCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildExceptionsCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildOrchCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildSbomCommand(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(BuildApiCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildSdkCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildMirrorCommand(services, verboseOption, cancellationToken));
|
|
root.Add(BuildAirgapCommand(services, verboseOption, cancellationToken));
|
|
root.Add(SystemCommandBuilder.BuildSystemCommand(services, verboseOption, cancellationToken));
|
|
|
|
var pluginLogger = loggerFactory.CreateLogger<CliCommandModuleLoader>();
|
|
var pluginLoader = new CliCommandModuleLoader(services, options, pluginLogger);
|
|
pluginLoader.RegisterModules(root, verboseOption, cancellationToken);
|
|
|
|
return root;
|
|
}
|
|
|
|
private static Command BuildScannerCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var scanner = new Command("scanner", "Manage scanner artifacts and lifecycle.");
|
|
|
|
var download = new Command("download", "Download the latest scanner bundle.");
|
|
var channelOption = new Option<string>("--channel", new[] { "-c" })
|
|
{
|
|
Description = "Scanner channel (stable, beta, nightly)."
|
|
};
|
|
|
|
var outputOption = new Option<string?>("--output")
|
|
{
|
|
Description = "Optional output path for the downloaded bundle."
|
|
};
|
|
|
|
var overwriteOption = new Option<bool>("--overwrite")
|
|
{
|
|
Description = "Overwrite existing bundle if present."
|
|
};
|
|
|
|
var noInstallOption = new Option<bool>("--no-install")
|
|
{
|
|
Description = "Skip installing the scanner container after download."
|
|
};
|
|
|
|
download.Add(channelOption);
|
|
download.Add(outputOption);
|
|
download.Add(overwriteOption);
|
|
download.Add(noInstallOption);
|
|
|
|
download.SetAction((parseResult, _) =>
|
|
{
|
|
var channel = parseResult.GetValue(channelOption) ?? "stable";
|
|
var output = parseResult.GetValue(outputOption);
|
|
var overwrite = parseResult.GetValue(overwriteOption);
|
|
var install = !parseResult.GetValue(noInstallOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleScannerDownloadAsync(services, channel, output, overwrite, install, verbose, cancellationToken);
|
|
});
|
|
|
|
scanner.Add(download);
|
|
return scanner;
|
|
}
|
|
|
|
private static Command BuildCvssCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var cvss = new Command("cvss", "CVSS v4.0 receipt operations (score, show, history, export)." );
|
|
|
|
var score = new Command("score", "Create a CVSS v4 receipt for a vulnerability.");
|
|
var vulnOption = new Option<string>("--vuln") { Description = "Vulnerability identifier (e.g., CVE).", IsRequired = true };
|
|
var policyFileOption = new Option<string>("--policy-file") { Description = "Path to CvssPolicy JSON file.", IsRequired = true };
|
|
var vectorOption = new Option<string>("--vector") { Description = "CVSS:4.0 vector string.", IsRequired = true };
|
|
var jsonOption = new Option<bool>("--json") { Description = "Emit JSON output." };
|
|
score.Add(vulnOption);
|
|
score.Add(policyFileOption);
|
|
score.Add(vectorOption);
|
|
score.Add(jsonOption);
|
|
score.SetAction((parseResult, _) =>
|
|
{
|
|
var vuln = parseResult.GetValue(vulnOption) ?? string.Empty;
|
|
var policyPath = parseResult.GetValue(policyFileOption) ?? string.Empty;
|
|
var vector = parseResult.GetValue(vectorOption) ?? string.Empty;
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
return CommandHandlers.HandleCvssScoreAsync(services, vuln, policyPath, vector, json, verbose, cancellationToken);
|
|
});
|
|
|
|
var show = new Command("show", "Fetch a CVSS receipt by ID.");
|
|
var receiptArg = new Argument<string>("receipt-id") { Description = "Receipt identifier." };
|
|
show.Add(receiptArg);
|
|
var showJsonOption = new Option<bool>("--json") { Description = "Emit JSON output." };
|
|
show.Add(showJsonOption);
|
|
show.SetAction((parseResult, _) =>
|
|
{
|
|
var receiptId = parseResult.GetValue(receiptArg) ?? string.Empty;
|
|
var json = parseResult.GetValue(showJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
return CommandHandlers.HandleCvssShowAsync(services, receiptId, json, verbose, cancellationToken);
|
|
});
|
|
|
|
var history = new Command("history", "Show receipt amendment history.");
|
|
history.Add(receiptArg);
|
|
var historyJsonOption = new Option<bool>("--json") { Description = "Emit JSON output." };
|
|
history.Add(historyJsonOption);
|
|
history.SetAction((parseResult, _) =>
|
|
{
|
|
var receiptId = parseResult.GetValue(receiptArg) ?? string.Empty;
|
|
var json = parseResult.GetValue(historyJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
return CommandHandlers.HandleCvssHistoryAsync(services, receiptId, json, verbose, cancellationToken);
|
|
});
|
|
|
|
var export = new Command("export", "Export a CVSS receipt to JSON (pdf not yet supported).");
|
|
export.Add(receiptArg);
|
|
var formatOption = new Option<string>("--format") { Description = "json|pdf (json default)." };
|
|
var outOption = new Option<string>("--out") { Description = "Output file path." };
|
|
export.Add(formatOption);
|
|
export.Add(outOption);
|
|
export.SetAction((parseResult, _) =>
|
|
{
|
|
var receiptId = parseResult.GetValue(receiptArg) ?? string.Empty;
|
|
var format = parseResult.GetValue(formatOption) ?? "json";
|
|
var output = parseResult.GetValue(outOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
return CommandHandlers.HandleCvssExportAsync(services, receiptId, format, output, verbose, cancellationToken);
|
|
});
|
|
|
|
cvss.Add(score);
|
|
cvss.Add(show);
|
|
cvss.Add(history);
|
|
cvss.Add(export);
|
|
return cvss;
|
|
}
|
|
|
|
private static Command BuildScanCommand(IServiceProvider services, StellaOpsCliOptions options, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var scan = new Command("scan", "Execute scanners and manage scan outputs.");
|
|
|
|
var run = new Command("run", "Execute a scanner bundle with the configured runner.");
|
|
var runnerOption = new Option<string>("--runner")
|
|
{
|
|
Description = "Execution runtime (dotnet, self, docker)."
|
|
};
|
|
var entryOption = new Option<string>("--entry")
|
|
{
|
|
Description = "Path to the scanner entrypoint or Docker image.",
|
|
Required = true
|
|
};
|
|
var targetOption = new Option<string>("--target")
|
|
{
|
|
Description = "Directory to scan.",
|
|
Required = true
|
|
};
|
|
|
|
var argsArgument = new Argument<string[]>("scanner-args")
|
|
{
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
|
|
run.Add(runnerOption);
|
|
run.Add(entryOption);
|
|
run.Add(targetOption);
|
|
run.Add(argsArgument);
|
|
|
|
run.SetAction((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 verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleScannerRunAsync(services, runner, entry, target, forwardedArgs, verbose, cancellationToken);
|
|
});
|
|
|
|
var upload = new Command("upload", "Upload completed scan results to the backend.");
|
|
var fileOption = new Option<string>("--file")
|
|
{
|
|
Description = "Path to the scan result artifact.",
|
|
Required = true
|
|
};
|
|
upload.Add(fileOption);
|
|
upload.SetAction((parseResult, _) =>
|
|
{
|
|
var file = parseResult.GetValue(fileOption) ?? string.Empty;
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
return CommandHandlers.HandleScanUploadAsync(services, file, verbose, cancellationToken);
|
|
});
|
|
|
|
var entryTrace = new Command("entrytrace", "Show entry trace summary for a scan.");
|
|
var scanIdOption = new Option<string>("--scan-id")
|
|
{
|
|
Description = "Scan identifier.",
|
|
Required = true
|
|
};
|
|
var includeNdjsonOption = new Option<bool>("--include-ndjson")
|
|
{
|
|
Description = "Include raw NDJSON output."
|
|
};
|
|
|
|
entryTrace.Add(scanIdOption);
|
|
entryTrace.Add(includeNdjsonOption);
|
|
|
|
entryTrace.SetAction((parseResult, _) =>
|
|
{
|
|
var id = parseResult.GetValue(scanIdOption) ?? string.Empty;
|
|
var includeNdjson = parseResult.GetValue(includeNdjsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
return CommandHandlers.HandleScanEntryTraceAsync(services, id, includeNdjson, verbose, cancellationToken);
|
|
});
|
|
|
|
scan.Add(entryTrace);
|
|
|
|
scan.Add(run);
|
|
scan.Add(upload);
|
|
return scan;
|
|
}
|
|
|
|
private static Command BuildRubyCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var ruby = new Command("ruby", "Work with Ruby analyzer outputs.");
|
|
|
|
var inspect = new Command("inspect", "Inspect a local Ruby workspace.");
|
|
var inspectRootOption = new Option<string?>("--root")
|
|
{
|
|
Description = "Path to the Ruby workspace (defaults to current directory)."
|
|
};
|
|
var inspectFormatOption = new Option<string?>("--format")
|
|
{
|
|
Description = "Output format (table or json)."
|
|
};
|
|
|
|
inspect.Add(inspectRootOption);
|
|
inspect.Add(inspectFormatOption);
|
|
inspect.SetAction((parseResult, _) =>
|
|
{
|
|
var root = parseResult.GetValue(inspectRootOption);
|
|
var format = parseResult.GetValue(inspectFormatOption) ?? "table";
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleRubyInspectAsync(
|
|
services,
|
|
root,
|
|
format,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
var resolve = new Command("resolve", "Fetch Ruby packages for a completed scan.");
|
|
var resolveImageOption = new Option<string?>("--image")
|
|
{
|
|
Description = "Image reference (digest or tag) used by the scan."
|
|
};
|
|
var resolveScanIdOption = new Option<string?>("--scan-id")
|
|
{
|
|
Description = "Explicit scan identifier."
|
|
};
|
|
var resolveFormatOption = new Option<string?>("--format")
|
|
{
|
|
Description = "Output format (table or json)."
|
|
};
|
|
|
|
resolve.Add(resolveImageOption);
|
|
resolve.Add(resolveScanIdOption);
|
|
resolve.Add(resolveFormatOption);
|
|
resolve.SetAction((parseResult, _) =>
|
|
{
|
|
var image = parseResult.GetValue(resolveImageOption);
|
|
var scanId = parseResult.GetValue(resolveScanIdOption);
|
|
var format = parseResult.GetValue(resolveFormatOption) ?? "table";
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleRubyResolveAsync(
|
|
services,
|
|
image,
|
|
scanId,
|
|
format,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
ruby.Add(inspect);
|
|
ruby.Add(resolve);
|
|
return ruby;
|
|
}
|
|
|
|
private static Command BuildPhpCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var php = new Command("php", "Work with PHP analyzer outputs.");
|
|
|
|
var inspect = new Command("inspect", "Inspect a local PHP workspace.");
|
|
var inspectRootOption = new Option<string?>("--root")
|
|
{
|
|
Description = "Path to the PHP workspace (defaults to current directory)."
|
|
};
|
|
var inspectFormatOption = new Option<string?>("--format")
|
|
{
|
|
Description = "Output format (table or json)."
|
|
};
|
|
|
|
inspect.Add(inspectRootOption);
|
|
inspect.Add(inspectFormatOption);
|
|
inspect.SetAction((parseResult, _) =>
|
|
{
|
|
var root = parseResult.GetValue(inspectRootOption);
|
|
var format = parseResult.GetValue(inspectFormatOption) ?? "table";
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePhpInspectAsync(
|
|
services,
|
|
root,
|
|
format,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
php.Add(inspect);
|
|
return php;
|
|
}
|
|
|
|
private static Command BuildPythonCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var python = new Command("python", "Work with Python analyzer outputs.");
|
|
|
|
var inspect = new Command("inspect", "Inspect a local Python workspace or virtual environment.");
|
|
var inspectRootOption = new Option<string?>("--root")
|
|
{
|
|
Description = "Path to the Python workspace (defaults to current directory)."
|
|
};
|
|
var inspectFormatOption = new Option<string?>("--format")
|
|
{
|
|
Description = "Output format (table, json, or aoc)."
|
|
};
|
|
var inspectSitePackagesOption = new Option<string[]?>("--site-packages")
|
|
{
|
|
Description = "Additional site-packages directories to scan."
|
|
};
|
|
var inspectIncludeFrameworksOption = new Option<bool>("--include-frameworks")
|
|
{
|
|
Description = "Include detected framework hints in output."
|
|
};
|
|
var inspectIncludeCapabilitiesOption = new Option<bool>("--include-capabilities")
|
|
{
|
|
Description = "Include detected capability signals in output."
|
|
};
|
|
|
|
inspect.Add(inspectRootOption);
|
|
inspect.Add(inspectFormatOption);
|
|
inspect.Add(inspectSitePackagesOption);
|
|
inspect.Add(inspectIncludeFrameworksOption);
|
|
inspect.Add(inspectIncludeCapabilitiesOption);
|
|
inspect.SetAction((parseResult, _) =>
|
|
{
|
|
var root = parseResult.GetValue(inspectRootOption);
|
|
var format = parseResult.GetValue(inspectFormatOption) ?? "table";
|
|
var sitePackages = parseResult.GetValue(inspectSitePackagesOption);
|
|
var includeFrameworks = parseResult.GetValue(inspectIncludeFrameworksOption);
|
|
var includeCapabilities = parseResult.GetValue(inspectIncludeCapabilitiesOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePythonInspectAsync(
|
|
services,
|
|
root,
|
|
format,
|
|
sitePackages,
|
|
includeFrameworks,
|
|
includeCapabilities,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
python.Add(inspect);
|
|
return python;
|
|
}
|
|
|
|
private static Command BuildBunCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var bun = new Command("bun", "Work with Bun analyzer outputs.");
|
|
|
|
var inspect = new Command("inspect", "Inspect a local Bun workspace.");
|
|
var inspectRootOption = new Option<string?>("--root")
|
|
{
|
|
Description = "Path to the Bun workspace (defaults to current directory)."
|
|
};
|
|
var inspectFormatOption = new Option<string?>("--format")
|
|
{
|
|
Description = "Output format (table or json)."
|
|
};
|
|
|
|
inspect.Add(inspectRootOption);
|
|
inspect.Add(inspectFormatOption);
|
|
inspect.SetAction((parseResult, _) =>
|
|
{
|
|
var root = parseResult.GetValue(inspectRootOption);
|
|
var format = parseResult.GetValue(inspectFormatOption) ?? "table";
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleBunInspectAsync(
|
|
services,
|
|
root,
|
|
format,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
var resolve = new Command("resolve", "Fetch Bun packages for a completed scan.");
|
|
var resolveImageOption = new Option<string?>("--image")
|
|
{
|
|
Description = "Image reference (digest or tag) used by the scan."
|
|
};
|
|
var resolveScanIdOption = new Option<string?>("--scan-id")
|
|
{
|
|
Description = "Explicit scan identifier."
|
|
};
|
|
var resolveFormatOption = new Option<string?>("--format")
|
|
{
|
|
Description = "Output format (table or json)."
|
|
};
|
|
|
|
resolve.Add(resolveImageOption);
|
|
resolve.Add(resolveScanIdOption);
|
|
resolve.Add(resolveFormatOption);
|
|
resolve.SetAction((parseResult, _) =>
|
|
{
|
|
var image = parseResult.GetValue(resolveImageOption);
|
|
var scanId = parseResult.GetValue(resolveScanIdOption);
|
|
var format = parseResult.GetValue(resolveFormatOption) ?? "table";
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleBunResolveAsync(
|
|
services,
|
|
image,
|
|
scanId,
|
|
format,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
bun.Add(inspect);
|
|
bun.Add(resolve);
|
|
return bun;
|
|
}
|
|
|
|
private static Command BuildKmsCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var kms = new Command("kms", "Manage file-backed signing keys.");
|
|
|
|
var export = new Command("export", "Export key material to a portable bundle.");
|
|
var exportRootOption = new Option<string>("--root")
|
|
{
|
|
Description = "Root directory containing file-based KMS material."
|
|
};
|
|
var exportKeyOption = new Option<string>("--key-id")
|
|
{
|
|
Description = "Logical KMS key identifier to export.",
|
|
Required = true
|
|
};
|
|
var exportVersionOption = new Option<string?>("--version")
|
|
{
|
|
Description = "Key version identifier to export (defaults to active version)."
|
|
};
|
|
var exportOutputOption = new Option<string>("--output")
|
|
{
|
|
Description = "Destination file path for exported key material.",
|
|
Required = true
|
|
};
|
|
var exportForceOption = new Option<bool>("--force")
|
|
{
|
|
Description = "Overwrite the destination file if it already exists."
|
|
};
|
|
var exportPassphraseOption = new Option<string?>("--passphrase")
|
|
{
|
|
Description = "File KMS passphrase (falls back to STELLAOPS_KMS_PASSPHRASE or interactive prompt)."
|
|
};
|
|
|
|
export.Add(exportRootOption);
|
|
export.Add(exportKeyOption);
|
|
export.Add(exportVersionOption);
|
|
export.Add(exportOutputOption);
|
|
export.Add(exportForceOption);
|
|
export.Add(exportPassphraseOption);
|
|
|
|
export.SetAction((parseResult, _) =>
|
|
{
|
|
var root = parseResult.GetValue(exportRootOption);
|
|
var keyId = parseResult.GetValue(exportKeyOption) ?? string.Empty;
|
|
var versionId = parseResult.GetValue(exportVersionOption);
|
|
var output = parseResult.GetValue(exportOutputOption) ?? string.Empty;
|
|
var force = parseResult.GetValue(exportForceOption);
|
|
var passphrase = parseResult.GetValue(exportPassphraseOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleKmsExportAsync(
|
|
services,
|
|
root,
|
|
keyId,
|
|
versionId,
|
|
output,
|
|
force,
|
|
passphrase,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
var import = new Command("import", "Import key material from a bundle.");
|
|
var importRootOption = new Option<string>("--root")
|
|
{
|
|
Description = "Root directory containing file-based KMS material."
|
|
};
|
|
var importKeyOption = new Option<string>("--key-id")
|
|
{
|
|
Description = "Logical KMS key identifier to import into.",
|
|
Required = true
|
|
};
|
|
var importInputOption = new Option<string>("--input")
|
|
{
|
|
Description = "Path to exported key material JSON.",
|
|
Required = true
|
|
};
|
|
var importVersionOption = new Option<string?>("--version")
|
|
{
|
|
Description = "Override the imported version identifier."
|
|
};
|
|
var importPassphraseOption = new Option<string?>("--passphrase")
|
|
{
|
|
Description = "File KMS passphrase (falls back to STELLAOPS_KMS_PASSPHRASE or interactive prompt)."
|
|
};
|
|
|
|
import.Add(importRootOption);
|
|
import.Add(importKeyOption);
|
|
import.Add(importInputOption);
|
|
import.Add(importVersionOption);
|
|
import.Add(importPassphraseOption);
|
|
|
|
import.SetAction((parseResult, _) =>
|
|
{
|
|
var root = parseResult.GetValue(importRootOption);
|
|
var keyId = parseResult.GetValue(importKeyOption) ?? string.Empty;
|
|
var input = parseResult.GetValue(importInputOption) ?? string.Empty;
|
|
var versionOverride = parseResult.GetValue(importVersionOption);
|
|
var passphrase = parseResult.GetValue(importPassphraseOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleKmsImportAsync(
|
|
services,
|
|
root,
|
|
keyId,
|
|
input,
|
|
versionOverride,
|
|
passphrase,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
kms.Add(export);
|
|
kms.Add(import);
|
|
return kms;
|
|
}
|
|
|
|
private static Command BuildDatabaseCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var db = new Command("db", "Trigger Concelier database operations via backend jobs.");
|
|
|
|
var fetch = new Command("fetch", "Trigger connector fetch/parse/map stages.");
|
|
var sourceOption = new Option<string>("--source")
|
|
{
|
|
Description = "Connector source identifier (e.g. redhat, osv, vmware).",
|
|
Required = true
|
|
};
|
|
var stageOption = new Option<string>("--stage")
|
|
{
|
|
Description = "Stage to trigger: fetch, parse, or map."
|
|
};
|
|
var modeOption = new Option<string?>("--mode")
|
|
{
|
|
Description = "Optional connector-specific mode (init, resume, cursor)."
|
|
};
|
|
|
|
fetch.Add(sourceOption);
|
|
fetch.Add(stageOption);
|
|
fetch.Add(modeOption);
|
|
fetch.SetAction((parseResult, _) =>
|
|
{
|
|
var source = parseResult.GetValue(sourceOption) ?? string.Empty;
|
|
var stage = parseResult.GetValue(stageOption) ?? "fetch";
|
|
var mode = parseResult.GetValue(modeOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleConnectorJobAsync(services, source, stage, mode, verbose, cancellationToken);
|
|
});
|
|
|
|
var merge = new Command("merge", "Run canonical merge reconciliation.");
|
|
merge.SetAction((parseResult, _) =>
|
|
{
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
return CommandHandlers.HandleMergeJobAsync(services, verbose, cancellationToken);
|
|
});
|
|
|
|
var export = new Command("export", "Run Concelier export jobs.");
|
|
var formatOption = new Option<string>("--format")
|
|
{
|
|
Description = "Export format: json or trivy-db."
|
|
};
|
|
var deltaOption = new Option<bool>("--delta")
|
|
{
|
|
Description = "Request a delta export when supported."
|
|
};
|
|
var publishFullOption = new Option<bool?>("--publish-full")
|
|
{
|
|
Description = "Override whether full exports push to ORAS (true/false)."
|
|
};
|
|
var publishDeltaOption = new Option<bool?>("--publish-delta")
|
|
{
|
|
Description = "Override whether delta exports push to ORAS (true/false)."
|
|
};
|
|
var includeFullOption = new Option<bool?>("--bundle-full")
|
|
{
|
|
Description = "Override whether offline bundles include full exports (true/false)."
|
|
};
|
|
var includeDeltaOption = new Option<bool?>("--bundle-delta")
|
|
{
|
|
Description = "Override whether offline bundles include delta exports (true/false)."
|
|
};
|
|
|
|
export.Add(formatOption);
|
|
export.Add(deltaOption);
|
|
export.Add(publishFullOption);
|
|
export.Add(publishDeltaOption);
|
|
export.Add(includeFullOption);
|
|
export.Add(includeDeltaOption);
|
|
export.SetAction((parseResult, _) =>
|
|
{
|
|
var format = parseResult.GetValue(formatOption) ?? "json";
|
|
var delta = parseResult.GetValue(deltaOption);
|
|
var publishFull = parseResult.GetValue(publishFullOption);
|
|
var publishDelta = parseResult.GetValue(publishDeltaOption);
|
|
var includeFull = parseResult.GetValue(includeFullOption);
|
|
var includeDelta = parseResult.GetValue(includeDeltaOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
return CommandHandlers.HandleExportJobAsync(services, format, delta, publishFull, publishDelta, includeFull, includeDelta, verbose, cancellationToken);
|
|
});
|
|
|
|
db.Add(fetch);
|
|
db.Add(merge);
|
|
db.Add(export);
|
|
return db;
|
|
}
|
|
|
|
private static Command BuildCryptoCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var crypto = new Command("crypto", "Inspect StellaOps cryptography providers.");
|
|
var providers = new Command("providers", "List registered crypto providers and keys.");
|
|
|
|
var jsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Emit JSON output."
|
|
};
|
|
|
|
var profileOption = new Option<string?>("--profile")
|
|
{
|
|
Description = "Temporarily override the active registry profile when computing provider order."
|
|
};
|
|
|
|
providers.Add(jsonOption);
|
|
providers.Add(profileOption);
|
|
|
|
providers.SetAction((parseResult, _) =>
|
|
{
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
var profile = parseResult.GetValue(profileOption);
|
|
return CommandHandlers.HandleCryptoProvidersAsync(services, verbose, json, profile, cancellationToken);
|
|
});
|
|
|
|
crypto.Add(providers);
|
|
return crypto;
|
|
}
|
|
|
|
private static Command BuildSourcesCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var sources = new Command("sources", "Interact with source ingestion workflows.");
|
|
|
|
var ingest = new Command("ingest", "Validate source documents before ingestion.");
|
|
|
|
var dryRunOption = new Option<bool>("--dry-run")
|
|
{
|
|
Description = "Evaluate guard rules without writing to persistent storage."
|
|
};
|
|
|
|
var sourceOption = new Option<string>("--source")
|
|
{
|
|
Description = "Logical source identifier (e.g. redhat, ubuntu, osv).",
|
|
Required = true
|
|
};
|
|
|
|
var inputOption = new Option<string>("--input")
|
|
{
|
|
Description = "Path to a local document or HTTPS URI.",
|
|
Required = true
|
|
};
|
|
|
|
var tenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Tenant identifier override."
|
|
};
|
|
|
|
var formatOption = new Option<string>("--format")
|
|
{
|
|
Description = "Output format: table or json."
|
|
};
|
|
|
|
var noColorOption = new Option<bool>("--no-color")
|
|
{
|
|
Description = "Disable ANSI colouring in console output."
|
|
};
|
|
|
|
var outputOption = new Option<string?>("--output")
|
|
{
|
|
Description = "Write the JSON report to the specified file path."
|
|
};
|
|
|
|
ingest.Add(dryRunOption);
|
|
ingest.Add(sourceOption);
|
|
ingest.Add(inputOption);
|
|
ingest.Add(tenantOption);
|
|
ingest.Add(formatOption);
|
|
ingest.Add(noColorOption);
|
|
ingest.Add(outputOption);
|
|
|
|
ingest.SetAction((parseResult, _) =>
|
|
{
|
|
var dryRun = parseResult.GetValue(dryRunOption);
|
|
var source = parseResult.GetValue(sourceOption) ?? string.Empty;
|
|
var input = parseResult.GetValue(inputOption) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var format = parseResult.GetValue(formatOption) ?? "table";
|
|
var noColor = parseResult.GetValue(noColorOption);
|
|
var output = parseResult.GetValue(outputOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleSourcesIngestAsync(
|
|
services,
|
|
dryRun,
|
|
source,
|
|
input,
|
|
tenant,
|
|
format,
|
|
noColor,
|
|
output,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
sources.Add(ingest);
|
|
return sources;
|
|
}
|
|
|
|
private static Command BuildAocCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var aoc = new Command("aoc", "Aggregation-Only Contract verification commands.");
|
|
|
|
var verify = new Command("verify", "Verify stored raw documents against AOC guardrails.");
|
|
|
|
var sinceOption = new Option<string?>("--since")
|
|
{
|
|
Description = "Verification window start (ISO-8601 timestamp) or relative duration (e.g. 24h, 7d)."
|
|
};
|
|
|
|
var limitOption = new Option<int?>("--limit")
|
|
{
|
|
Description = "Maximum number of violations to include per code (0 = no limit)."
|
|
};
|
|
|
|
var sourcesOption = new Option<string?>("--sources")
|
|
{
|
|
Description = "Comma-separated list of sources (e.g. redhat,ubuntu,osv)."
|
|
};
|
|
|
|
var codesOption = new Option<string?>("--codes")
|
|
{
|
|
Description = "Comma-separated list of violation codes (ERR_AOC_00x)."
|
|
};
|
|
|
|
var formatOption = new Option<string>("--format")
|
|
{
|
|
Description = "Output format: table or json."
|
|
};
|
|
|
|
var exportOption = new Option<string?>("--export")
|
|
{
|
|
Description = "Write the JSON report to the specified file path."
|
|
};
|
|
|
|
var tenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Tenant identifier override."
|
|
};
|
|
|
|
var noColorOption = new Option<bool>("--no-color")
|
|
{
|
|
Description = "Disable ANSI colouring in console output."
|
|
};
|
|
|
|
verify.Add(sinceOption);
|
|
verify.Add(limitOption);
|
|
verify.Add(sourcesOption);
|
|
verify.Add(codesOption);
|
|
verify.Add(formatOption);
|
|
verify.Add(exportOption);
|
|
verify.Add(tenantOption);
|
|
verify.Add(noColorOption);
|
|
|
|
verify.SetAction((parseResult, _) =>
|
|
{
|
|
var since = parseResult.GetValue(sinceOption);
|
|
var limit = parseResult.GetValue(limitOption);
|
|
var sources = parseResult.GetValue(sourcesOption);
|
|
var codes = parseResult.GetValue(codesOption);
|
|
var format = parseResult.GetValue(formatOption) ?? "table";
|
|
var export = parseResult.GetValue(exportOption);
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var noColor = parseResult.GetValue(noColorOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleAocVerifyAsync(
|
|
services,
|
|
since,
|
|
limit,
|
|
sources,
|
|
codes,
|
|
format,
|
|
export,
|
|
tenant,
|
|
noColor,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
aoc.Add(verify);
|
|
return aoc;
|
|
}
|
|
|
|
private static Command BuildAuthCommand(IServiceProvider services, StellaOpsCliOptions options, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var auth = new Command("auth", "Manage authentication with StellaOps Authority.");
|
|
|
|
var login = new Command("login", "Acquire and cache access tokens using the configured credentials.");
|
|
var forceOption = new Option<bool>("--force")
|
|
{
|
|
Description = "Ignore existing cached tokens and force re-authentication."
|
|
};
|
|
login.Add(forceOption);
|
|
login.SetAction((parseResult, _) =>
|
|
{
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
var force = parseResult.GetValue(forceOption);
|
|
return CommandHandlers.HandleAuthLoginAsync(services, options, verbose, force, cancellationToken);
|
|
});
|
|
|
|
var logout = new Command("logout", "Remove cached tokens for the current credentials.");
|
|
logout.SetAction((parseResult, _) =>
|
|
{
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
return CommandHandlers.HandleAuthLogoutAsync(services, options, verbose, cancellationToken);
|
|
});
|
|
|
|
var status = new Command("status", "Display cached token status.");
|
|
status.SetAction((parseResult, _) =>
|
|
{
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
return CommandHandlers.HandleAuthStatusAsync(services, options, verbose, cancellationToken);
|
|
});
|
|
|
|
var whoami = new Command("whoami", "Display cached token claims (subject, scopes, expiry).");
|
|
whoami.SetAction((parseResult, _) =>
|
|
{
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
return CommandHandlers.HandleAuthWhoAmIAsync(services, options, verbose, cancellationToken);
|
|
});
|
|
|
|
var revoke = new Command("revoke", "Manage revocation exports.");
|
|
var export = new Command("export", "Export the revocation bundle and signature to disk.");
|
|
var outputOption = new Option<string?>("--output")
|
|
{
|
|
Description = "Directory to write exported revocation files (defaults to current directory)."
|
|
};
|
|
export.Add(outputOption);
|
|
export.SetAction((parseResult, _) =>
|
|
{
|
|
var output = parseResult.GetValue(outputOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
return CommandHandlers.HandleAuthRevokeExportAsync(services, options, output, verbose, cancellationToken);
|
|
});
|
|
revoke.Add(export);
|
|
var verify = new Command("verify", "Verify a revocation bundle against a detached JWS signature.");
|
|
var bundleOption = new Option<string>("--bundle")
|
|
{
|
|
Description = "Path to the revocation-bundle.json file."
|
|
};
|
|
var signatureOption = new Option<string>("--signature")
|
|
{
|
|
Description = "Path to the revocation-bundle.json.jws file."
|
|
};
|
|
var keyOption = new Option<string>("--key")
|
|
{
|
|
Description = "Path to the PEM-encoded public/private key used for verification."
|
|
};
|
|
verify.Add(bundleOption);
|
|
verify.Add(signatureOption);
|
|
verify.Add(keyOption);
|
|
verify.SetAction((parseResult, _) =>
|
|
{
|
|
var bundlePath = parseResult.GetValue(bundleOption) ?? string.Empty;
|
|
var signaturePath = parseResult.GetValue(signatureOption) ?? string.Empty;
|
|
var keyPath = parseResult.GetValue(keyOption) ?? string.Empty;
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
return CommandHandlers.HandleAuthRevokeVerifyAsync(bundlePath, signaturePath, keyPath, verbose, cancellationToken);
|
|
});
|
|
revoke.Add(verify);
|
|
|
|
// CLI-TEN-49-001: Token minting and delegation commands
|
|
var token = new Command("token", "Service account token operations (CLI-TEN-49-001).");
|
|
|
|
var mint = new Command("mint", "Mint a service account token.");
|
|
var serviceAccountOption = new Option<string>("--service-account", new[] { "-s" })
|
|
{
|
|
Description = "Service account identifier to mint token for.",
|
|
Required = true
|
|
};
|
|
var mintScopesOption = new Option<string[]>("--scope")
|
|
{
|
|
Description = "Scopes to include in the minted token (can be specified multiple times).",
|
|
AllowMultipleArgumentsPerToken = true
|
|
};
|
|
var mintExpiresOption = new Option<int?>("--expires-in")
|
|
{
|
|
Description = "Token expiry in seconds (defaults to server default)."
|
|
};
|
|
var mintTenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Tenant context for the token."
|
|
};
|
|
var mintReasonOption = new Option<string?>("--reason")
|
|
{
|
|
Description = "Audit reason for minting the token."
|
|
};
|
|
var mintOutputOption = new Option<bool>("--raw")
|
|
{
|
|
Description = "Output only the raw token value (for automation)."
|
|
};
|
|
mint.Add(serviceAccountOption);
|
|
mint.Add(mintScopesOption);
|
|
mint.Add(mintExpiresOption);
|
|
mint.Add(mintTenantOption);
|
|
mint.Add(mintReasonOption);
|
|
mint.Add(mintOutputOption);
|
|
mint.SetAction((parseResult, _) =>
|
|
{
|
|
var serviceAccount = parseResult.GetValue(serviceAccountOption) ?? string.Empty;
|
|
var scopes = parseResult.GetValue(mintScopesOption) ?? Array.Empty<string>();
|
|
var expiresIn = parseResult.GetValue(mintExpiresOption);
|
|
var tenant = parseResult.GetValue(mintTenantOption);
|
|
var reason = parseResult.GetValue(mintReasonOption);
|
|
var raw = parseResult.GetValue(mintOutputOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
return CommandHandlers.HandleTokenMintAsync(services, options, serviceAccount, scopes, expiresIn, tenant, reason, raw, verbose, cancellationToken);
|
|
});
|
|
|
|
var delegateCmd = new Command("delegate", "Delegate your token to another principal.");
|
|
var delegateToOption = new Option<string>("--to")
|
|
{
|
|
Description = "Principal identifier to delegate to.",
|
|
Required = true
|
|
};
|
|
var delegateScopesOption = new Option<string[]>("--scope")
|
|
{
|
|
Description = "Scopes to include in the delegation (must be subset of current token).",
|
|
AllowMultipleArgumentsPerToken = true
|
|
};
|
|
var delegateExpiresOption = new Option<int?>("--expires-in")
|
|
{
|
|
Description = "Delegation expiry in seconds (defaults to remaining token lifetime)."
|
|
};
|
|
var delegateTenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Tenant context for the delegation."
|
|
};
|
|
var delegateReasonOption = new Option<string?>("--reason")
|
|
{
|
|
Description = "Audit reason for the delegation.",
|
|
Required = true
|
|
};
|
|
var delegateRawOption = new Option<bool>("--raw")
|
|
{
|
|
Description = "Output only the raw token value (for automation)."
|
|
};
|
|
delegateCmd.Add(delegateToOption);
|
|
delegateCmd.Add(delegateScopesOption);
|
|
delegateCmd.Add(delegateExpiresOption);
|
|
delegateCmd.Add(delegateTenantOption);
|
|
delegateCmd.Add(delegateReasonOption);
|
|
delegateCmd.Add(delegateRawOption);
|
|
delegateCmd.SetAction((parseResult, _) =>
|
|
{
|
|
var delegateTo = parseResult.GetValue(delegateToOption) ?? string.Empty;
|
|
var scopes = parseResult.GetValue(delegateScopesOption) ?? Array.Empty<string>();
|
|
var expiresIn = parseResult.GetValue(delegateExpiresOption);
|
|
var tenant = parseResult.GetValue(delegateTenantOption);
|
|
var reason = parseResult.GetValue(delegateReasonOption);
|
|
var raw = parseResult.GetValue(delegateRawOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
return CommandHandlers.HandleTokenDelegateAsync(services, options, delegateTo, scopes, expiresIn, tenant, reason, raw, verbose, cancellationToken);
|
|
});
|
|
|
|
token.Add(mint);
|
|
token.Add(delegateCmd);
|
|
|
|
auth.Add(login);
|
|
auth.Add(logout);
|
|
auth.Add(status);
|
|
auth.Add(whoami);
|
|
auth.Add(revoke);
|
|
auth.Add(token);
|
|
return auth;
|
|
}
|
|
|
|
private static Command BuildTenantsCommand(IServiceProvider services, StellaOpsCliOptions options, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
_ = options;
|
|
var tenants = new Command("tenants", "Manage tenant contexts (CLI-TEN-47-001).");
|
|
|
|
var list = new Command("list", "List available tenants for the authenticated principal.");
|
|
var tenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Tenant context to use for the request (required for multi-tenant environments)."
|
|
};
|
|
var jsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output tenant list in JSON format."
|
|
};
|
|
|
|
list.Add(tenantOption);
|
|
list.Add(jsonOption);
|
|
|
|
list.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
return CommandHandlers.HandleTenantsListAsync(services, options, tenant, json, verbose, cancellationToken);
|
|
});
|
|
|
|
var use = new Command("use", "Set the active tenant context for subsequent commands.");
|
|
var tenantIdArgument = new Argument<string>("tenant-id")
|
|
{
|
|
Description = "Tenant identifier to use as the default context."
|
|
};
|
|
use.Add(tenantIdArgument);
|
|
|
|
use.SetAction((parseResult, _) =>
|
|
{
|
|
var tenantId = parseResult.GetValue(tenantIdArgument) ?? string.Empty;
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
return CommandHandlers.HandleTenantsUseAsync(services, options, tenantId, verbose, cancellationToken);
|
|
});
|
|
|
|
var current = new Command("current", "Show the currently active tenant context.");
|
|
var currentJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output profile in JSON format."
|
|
};
|
|
current.Add(currentJsonOption);
|
|
|
|
current.SetAction((parseResult, _) =>
|
|
{
|
|
var json = parseResult.GetValue(currentJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
return CommandHandlers.HandleTenantsCurrentAsync(json, verbose, cancellationToken);
|
|
});
|
|
|
|
var clear = new Command("clear", "Clear the active tenant context (use default or require --tenant).");
|
|
|
|
clear.SetAction((_, _) =>
|
|
{
|
|
return CommandHandlers.HandleTenantsClearAsync(cancellationToken);
|
|
});
|
|
|
|
tenants.Add(list);
|
|
tenants.Add(use);
|
|
tenants.Add(current);
|
|
tenants.Add(clear);
|
|
return tenants;
|
|
}
|
|
|
|
private static Command BuildPolicyCommand(IServiceProvider services, StellaOpsCliOptions options, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
_ = options;
|
|
var policy = new Command("policy", "Interact with Policy Engine operations.");
|
|
|
|
var simulate = new Command("simulate", "Simulate a policy revision against selected SBOMs and environment.");
|
|
var policyIdArgument = new Argument<string>("policy-id")
|
|
{
|
|
Description = "Policy identifier (e.g. P-7)."
|
|
};
|
|
simulate.Add(policyIdArgument);
|
|
|
|
var baseOption = new Option<int?>("--base")
|
|
{
|
|
Description = "Base policy version for diff calculations."
|
|
};
|
|
var candidateOption = new Option<int?>("--candidate")
|
|
{
|
|
Description = "Candidate policy version. Defaults to latest approved."
|
|
};
|
|
var sbomOption = new Option<string[]>("--sbom")
|
|
{
|
|
Description = "SBOM identifier to include (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
sbomOption.AllowMultipleArgumentsPerToken = true;
|
|
|
|
var envOption = new Option<string[]>("--env")
|
|
{
|
|
Description = "Environment override (key=value, repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
envOption.AllowMultipleArgumentsPerToken = true;
|
|
|
|
var formatOption = new Option<string?>("--format")
|
|
{
|
|
Description = "Output format: table, json, or markdown."
|
|
};
|
|
var outputOption = new Option<string?>("--output")
|
|
{
|
|
Description = "Write output to the specified file."
|
|
};
|
|
var explainOption = new Option<bool>("--explain")
|
|
{
|
|
Description = "Request explain traces for diffed findings."
|
|
};
|
|
var failOnDiffOption = new Option<bool>("--fail-on-diff")
|
|
{
|
|
Description = "Exit with code 20 when findings are added or removed."
|
|
};
|
|
|
|
// CLI-EXC-25-002: Exception preview flags
|
|
var withExceptionOption = new Option<string[]>("--with-exception")
|
|
{
|
|
Description = "Include exception ID in simulation (repeatable). Shows what-if the exception were applied.",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
withExceptionOption.AllowMultipleArgumentsPerToken = true;
|
|
|
|
var withoutExceptionOption = new Option<string[]>("--without-exception")
|
|
{
|
|
Description = "Exclude exception ID from simulation (repeatable). Shows what-if the exception were removed.",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
withoutExceptionOption.AllowMultipleArgumentsPerToken = true;
|
|
|
|
// CLI-POLICY-27-003: Enhanced simulation options
|
|
var modeOption = new Option<string?>("--mode")
|
|
{
|
|
Description = "Simulation mode: quick (sample SBOMs) or batch (all matching SBOMs)."
|
|
};
|
|
var sbomSelectorOption = new Option<string[]>("--sbom-selector")
|
|
{
|
|
Description = "SBOM selector pattern (e.g. 'registry:docker.io/*', 'tag:production'). Repeatable.",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
sbomSelectorOption.AllowMultipleArgumentsPerToken = true;
|
|
|
|
var heatmapOption = new Option<bool>("--heatmap")
|
|
{
|
|
Description = "Include severity heatmap summary in output."
|
|
};
|
|
var manifestDownloadOption = new Option<bool>("--manifest-download")
|
|
{
|
|
Description = "Request manifest download URI for offline analysis."
|
|
};
|
|
|
|
// CLI-SIG-26-002: Reachability override options
|
|
var reachabilityStateOption = new Option<string[]>("--reachability-state")
|
|
{
|
|
Description = "Override reachability state for vuln/package (format: 'CVE-XXXX:reachable' or 'pkg:npm/lodash@4.17.0:unreachable'). Repeatable.",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
reachabilityStateOption.AllowMultipleArgumentsPerToken = true;
|
|
|
|
var reachabilityScoreOption = new Option<string[]>("--reachability-score")
|
|
{
|
|
Description = "Override reachability score for vuln/package (format: 'CVE-XXXX:0.85' or 'pkg:npm/lodash@4.17.0:0.5'). Repeatable.",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
reachabilityScoreOption.AllowMultipleArgumentsPerToken = true;
|
|
|
|
simulate.Add(baseOption);
|
|
simulate.Add(candidateOption);
|
|
simulate.Add(sbomOption);
|
|
simulate.Add(envOption);
|
|
simulate.Add(formatOption);
|
|
simulate.Add(outputOption);
|
|
simulate.Add(explainOption);
|
|
simulate.Add(failOnDiffOption);
|
|
simulate.Add(withExceptionOption);
|
|
simulate.Add(withoutExceptionOption);
|
|
simulate.Add(modeOption);
|
|
simulate.Add(sbomSelectorOption);
|
|
simulate.Add(heatmapOption);
|
|
simulate.Add(manifestDownloadOption);
|
|
simulate.Add(reachabilityStateOption);
|
|
simulate.Add(reachabilityScoreOption);
|
|
|
|
simulate.SetAction((parseResult, _) =>
|
|
{
|
|
var policyId = parseResult.GetValue(policyIdArgument) ?? string.Empty;
|
|
var baseVersion = parseResult.GetValue(baseOption);
|
|
var candidateVersion = parseResult.GetValue(candidateOption);
|
|
var sbomSet = parseResult.GetValue(sbomOption) ?? Array.Empty<string>();
|
|
var environment = parseResult.GetValue(envOption) ?? Array.Empty<string>();
|
|
var format = parseResult.GetValue(formatOption);
|
|
var output = parseResult.GetValue(outputOption);
|
|
var explain = parseResult.GetValue(explainOption);
|
|
var failOnDiff = parseResult.GetValue(failOnDiffOption);
|
|
var withExceptions = parseResult.GetValue(withExceptionOption) ?? Array.Empty<string>();
|
|
var withoutExceptions = parseResult.GetValue(withoutExceptionOption) ?? Array.Empty<string>();
|
|
var mode = parseResult.GetValue(modeOption);
|
|
var sbomSelectors = parseResult.GetValue(sbomSelectorOption) ?? Array.Empty<string>();
|
|
var heatmap = parseResult.GetValue(heatmapOption);
|
|
var manifestDownload = parseResult.GetValue(manifestDownloadOption);
|
|
var reachabilityStates = parseResult.GetValue(reachabilityStateOption) ?? Array.Empty<string>();
|
|
var reachabilityScores = parseResult.GetValue(reachabilityScoreOption) ?? Array.Empty<string>();
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePolicySimulateAsync(
|
|
services,
|
|
policyId,
|
|
baseVersion,
|
|
candidateVersion,
|
|
sbomSet,
|
|
environment,
|
|
format,
|
|
output,
|
|
explain,
|
|
failOnDiff,
|
|
withExceptions,
|
|
withoutExceptions,
|
|
mode,
|
|
sbomSelectors,
|
|
heatmap,
|
|
manifestDownload,
|
|
reachabilityStates,
|
|
reachabilityScores,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
policy.Add(simulate);
|
|
|
|
var activate = new Command("activate", "Activate an approved policy revision.");
|
|
var activatePolicyIdArgument = new Argument<string>("policy-id")
|
|
{
|
|
Description = "Policy identifier (e.g. P-7)."
|
|
};
|
|
activate.Add(activatePolicyIdArgument);
|
|
|
|
var activateVersionOption = new Option<int>("--version")
|
|
{
|
|
Description = "Revision version to activate.",
|
|
Arity = ArgumentArity.ExactlyOne
|
|
};
|
|
|
|
var activationNoteOption = new Option<string?>("--note")
|
|
{
|
|
Description = "Optional activation note recorded with the approval."
|
|
};
|
|
|
|
var runNowOption = new Option<bool>("--run-now")
|
|
{
|
|
Description = "Trigger an immediate full policy run after activation."
|
|
};
|
|
|
|
var scheduledAtOption = new Option<string?>("--scheduled-at")
|
|
{
|
|
Description = "Schedule activation at the provided ISO-8601 timestamp."
|
|
};
|
|
|
|
var priorityOption = new Option<string?>("--priority")
|
|
{
|
|
Description = "Optional activation priority label (e.g. low, standard, high)."
|
|
};
|
|
|
|
var rollbackOption = new Option<bool>("--rollback")
|
|
{
|
|
Description = "Indicate that this activation is a rollback to a previous version."
|
|
};
|
|
|
|
var incidentOption = new Option<string?>("--incident")
|
|
{
|
|
Description = "Associate the activation with an incident identifier."
|
|
};
|
|
|
|
activate.Add(activateVersionOption);
|
|
activate.Add(activationNoteOption);
|
|
activate.Add(runNowOption);
|
|
activate.Add(scheduledAtOption);
|
|
activate.Add(priorityOption);
|
|
activate.Add(rollbackOption);
|
|
activate.Add(incidentOption);
|
|
|
|
activate.SetAction((parseResult, _) =>
|
|
{
|
|
var policyId = parseResult.GetValue(activatePolicyIdArgument) ?? string.Empty;
|
|
var version = parseResult.GetValue(activateVersionOption);
|
|
var note = parseResult.GetValue(activationNoteOption);
|
|
var runNow = parseResult.GetValue(runNowOption);
|
|
var scheduledAt = parseResult.GetValue(scheduledAtOption);
|
|
var priority = parseResult.GetValue(priorityOption);
|
|
var rollback = parseResult.GetValue(rollbackOption);
|
|
var incident = parseResult.GetValue(incidentOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePolicyActivateAsync(
|
|
services,
|
|
policyId,
|
|
version,
|
|
note,
|
|
runNow,
|
|
scheduledAt,
|
|
priority,
|
|
rollback,
|
|
incident,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
policy.Add(activate);
|
|
|
|
// lint subcommand - validates policy DSL files locally
|
|
var lint = new Command("lint", "Validate a policy DSL file locally without contacting the backend.");
|
|
var lintFileArgument = new Argument<string>("file")
|
|
{
|
|
Description = "Path to the policy DSL file to validate."
|
|
};
|
|
var lintFormatOption = new Option<string?>("--format", new[] { "-f" })
|
|
{
|
|
Description = "Output format: table (default), json."
|
|
};
|
|
var lintOutputOption = new Option<string?>("--output", new[] { "-o" })
|
|
{
|
|
Description = "Write JSON output to the specified file."
|
|
};
|
|
|
|
lint.Add(lintFileArgument);
|
|
lint.Add(lintFormatOption);
|
|
lint.Add(lintOutputOption);
|
|
|
|
lint.SetAction((parseResult, _) =>
|
|
{
|
|
var file = parseResult.GetValue(lintFileArgument) ?? string.Empty;
|
|
var format = parseResult.GetValue(lintFormatOption);
|
|
var output = parseResult.GetValue(lintOutputOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePolicyLintAsync(file, format, output, verbose, cancellationToken);
|
|
});
|
|
|
|
policy.Add(lint);
|
|
|
|
// edit subcommand - Git-backed DSL file editing with validation and commit
|
|
var edit = new Command("edit", "Open a policy DSL file in $EDITOR, validate, and optionally commit with SemVer metadata.");
|
|
var editFileArgument = new Argument<string>("file")
|
|
{
|
|
Description = "Path to the policy DSL file to edit."
|
|
};
|
|
var editCommitOption = new Option<bool>("--commit", new[] { "-c" })
|
|
{
|
|
Description = "Commit changes after successful validation."
|
|
};
|
|
var editVersionOption = new Option<string?>("--version", new[] { "-V" })
|
|
{
|
|
Description = "SemVer version for commit metadata (e.g. 1.2.0)."
|
|
};
|
|
var editMessageOption = new Option<string?>("--message", new[] { "-m" })
|
|
{
|
|
Description = "Commit message (auto-generated if not provided)."
|
|
};
|
|
var editNoValidateOption = new Option<bool>("--no-validate")
|
|
{
|
|
Description = "Skip validation after editing (not recommended)."
|
|
};
|
|
|
|
edit.Add(editFileArgument);
|
|
edit.Add(editCommitOption);
|
|
edit.Add(editVersionOption);
|
|
edit.Add(editMessageOption);
|
|
edit.Add(editNoValidateOption);
|
|
|
|
edit.SetAction((parseResult, _) =>
|
|
{
|
|
var file = parseResult.GetValue(editFileArgument) ?? string.Empty;
|
|
var commit = parseResult.GetValue(editCommitOption);
|
|
var version = parseResult.GetValue(editVersionOption);
|
|
var message = parseResult.GetValue(editMessageOption);
|
|
var noValidate = parseResult.GetValue(editNoValidateOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePolicyEditAsync(file, commit, version, message, noValidate, verbose, cancellationToken);
|
|
});
|
|
|
|
policy.Add(edit);
|
|
|
|
// test subcommand - run coverage fixtures against a policy DSL file
|
|
var test = new Command("test", "Run coverage test fixtures against a policy DSL file.");
|
|
var testFileArgument = new Argument<string>("file")
|
|
{
|
|
Description = "Path to the policy DSL file to test."
|
|
};
|
|
var testFixturesOption = new Option<string?>("--fixtures", new[] { "-d" })
|
|
{
|
|
Description = "Path to fixtures directory (defaults to tests/policy/<policy-name>/cases)."
|
|
};
|
|
var testFilterOption = new Option<string?>("--filter")
|
|
{
|
|
Description = "Run only fixtures matching this pattern."
|
|
};
|
|
var testFormatOption = new Option<string?>("--format", new[] { "-f" })
|
|
{
|
|
Description = "Output format: table (default), json."
|
|
};
|
|
var testOutputOption = new Option<string?>("--output", new[] { "-o" })
|
|
{
|
|
Description = "Write test results to the specified file."
|
|
};
|
|
var testFailFastOption = new Option<bool>("--fail-fast")
|
|
{
|
|
Description = "Stop on first test failure."
|
|
};
|
|
|
|
test.Add(testFileArgument);
|
|
test.Add(testFixturesOption);
|
|
test.Add(testFilterOption);
|
|
test.Add(testFormatOption);
|
|
test.Add(testOutputOption);
|
|
test.Add(testFailFastOption);
|
|
|
|
test.SetAction((parseResult, _) =>
|
|
{
|
|
var file = parseResult.GetValue(testFileArgument) ?? string.Empty;
|
|
var fixtures = parseResult.GetValue(testFixturesOption);
|
|
var filter = parseResult.GetValue(testFilterOption);
|
|
var format = parseResult.GetValue(testFormatOption);
|
|
var output = parseResult.GetValue(testOutputOption);
|
|
var failFast = parseResult.GetValue(testFailFastOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePolicyTestAsync(file, fixtures, filter, format, output, failFast, verbose, cancellationToken);
|
|
});
|
|
|
|
policy.Add(test);
|
|
|
|
// CLI-POLICY-20-001: policy new - scaffold new policy from template
|
|
var newCmd = new Command("new", "Create a new policy file from a template.");
|
|
var newNameArgument = new Argument<string>("name")
|
|
{
|
|
Description = "Name for the new policy (e.g. 'my-org-policy')."
|
|
};
|
|
var newTemplateOption = new Option<string?>("--template", new[] { "-t" })
|
|
{
|
|
Description = "Template to use: minimal (default), baseline, vex-precedence, reachability, secret-leak, full."
|
|
};
|
|
var newOutputOption = new Option<string?>("--output", new[] { "-o" })
|
|
{
|
|
Description = "Output path for the policy file. Defaults to ./<name>.stella"
|
|
};
|
|
var newDescriptionOption = new Option<string?>("--description", new[] { "-d" })
|
|
{
|
|
Description = "Policy description for metadata block."
|
|
};
|
|
var newTagsOption = new Option<string[]>("--tag")
|
|
{
|
|
Description = "Policy tag for metadata block (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
newTagsOption.AllowMultipleArgumentsPerToken = true;
|
|
var newShadowOption = new Option<bool>("--shadow")
|
|
{
|
|
Description = "Enable shadow mode in settings (default: true)."
|
|
};
|
|
newShadowOption.SetDefaultValue(true);
|
|
var newFixturesOption = new Option<bool>("--fixtures")
|
|
{
|
|
Description = "Create test fixtures directory alongside the policy file."
|
|
};
|
|
var newGitInitOption = new Option<bool>("--git-init")
|
|
{
|
|
Description = "Initialize a Git repository in the output directory."
|
|
};
|
|
var newFormatOption = new Option<string?>("--format", new[] { "-f" })
|
|
{
|
|
Description = "Output format: table (default), json."
|
|
};
|
|
|
|
newCmd.Add(newNameArgument);
|
|
newCmd.Add(newTemplateOption);
|
|
newCmd.Add(newOutputOption);
|
|
newCmd.Add(newDescriptionOption);
|
|
newCmd.Add(newTagsOption);
|
|
newCmd.Add(newShadowOption);
|
|
newCmd.Add(newFixturesOption);
|
|
newCmd.Add(newGitInitOption);
|
|
newCmd.Add(newFormatOption);
|
|
newCmd.Add(verboseOption);
|
|
|
|
newCmd.SetAction((parseResult, _) =>
|
|
{
|
|
var name = parseResult.GetValue(newNameArgument) ?? string.Empty;
|
|
var template = parseResult.GetValue(newTemplateOption);
|
|
var output = parseResult.GetValue(newOutputOption);
|
|
var description = parseResult.GetValue(newDescriptionOption);
|
|
var tags = parseResult.GetValue(newTagsOption) ?? Array.Empty<string>();
|
|
var shadow = parseResult.GetValue(newShadowOption);
|
|
var fixtures = parseResult.GetValue(newFixturesOption);
|
|
var gitInit = parseResult.GetValue(newGitInitOption);
|
|
var format = parseResult.GetValue(newFormatOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePolicyNewAsync(
|
|
name,
|
|
template,
|
|
output,
|
|
description,
|
|
tags,
|
|
shadow,
|
|
fixtures,
|
|
gitInit,
|
|
format,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
policy.Add(newCmd);
|
|
|
|
// CLI-POLICY-23-006: policy history - view policy run history
|
|
var history = new Command("history", "View policy run history.");
|
|
var historyPolicyIdArgument = new Argument<string>("policy-id")
|
|
{
|
|
Description = "Policy identifier (e.g. P-7)."
|
|
};
|
|
var historyTenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Filter by tenant."
|
|
};
|
|
var historyFromOption = new Option<string?>("--from")
|
|
{
|
|
Description = "Filter runs from this timestamp (ISO-8601)."
|
|
};
|
|
var historyToOption = new Option<string?>("--to")
|
|
{
|
|
Description = "Filter runs to this timestamp (ISO-8601)."
|
|
};
|
|
var historyStatusOption = new Option<string?>("--status")
|
|
{
|
|
Description = "Filter by run status (completed, failed, running)."
|
|
};
|
|
var historyLimitOption = new Option<int?>("--limit", new[] { "-l" })
|
|
{
|
|
Description = "Maximum number of runs to return."
|
|
};
|
|
var historyCursorOption = new Option<string?>("--cursor")
|
|
{
|
|
Description = "Pagination cursor for next page."
|
|
};
|
|
var historyFormatOption = new Option<string?>("--format", new[] { "-f" })
|
|
{
|
|
Description = "Output format: table (default), json."
|
|
};
|
|
|
|
history.Add(historyPolicyIdArgument);
|
|
history.Add(historyTenantOption);
|
|
history.Add(historyFromOption);
|
|
history.Add(historyToOption);
|
|
history.Add(historyStatusOption);
|
|
history.Add(historyLimitOption);
|
|
history.Add(historyCursorOption);
|
|
history.Add(historyFormatOption);
|
|
history.Add(verboseOption);
|
|
|
|
history.SetAction((parseResult, _) =>
|
|
{
|
|
var policyId = parseResult.GetValue(historyPolicyIdArgument) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(historyTenantOption);
|
|
var from = parseResult.GetValue(historyFromOption);
|
|
var to = parseResult.GetValue(historyToOption);
|
|
var status = parseResult.GetValue(historyStatusOption);
|
|
var limit = parseResult.GetValue(historyLimitOption);
|
|
var cursor = parseResult.GetValue(historyCursorOption);
|
|
var format = parseResult.GetValue(historyFormatOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePolicyHistoryAsync(
|
|
services,
|
|
policyId,
|
|
tenant,
|
|
from,
|
|
to,
|
|
status,
|
|
limit,
|
|
cursor,
|
|
format,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
policy.Add(history);
|
|
|
|
// CLI-POLICY-23-006: policy explain - show explanation tree for a decision
|
|
var explain = new Command("explain", "Show explanation tree for a policy decision.");
|
|
var explainPolicyIdOption = new Option<string>("--policy", new[] { "-p" })
|
|
{
|
|
Description = "Policy identifier (e.g. P-7).",
|
|
Required = true
|
|
};
|
|
var explainRunIdOption = new Option<string?>("--run-id")
|
|
{
|
|
Description = "Specific run ID to explain from."
|
|
};
|
|
var explainFindingIdOption = new Option<string?>("--finding-id")
|
|
{
|
|
Description = "Finding ID to explain."
|
|
};
|
|
var explainSbomIdOption = new Option<string?>("--sbom-id")
|
|
{
|
|
Description = "SBOM ID for context."
|
|
};
|
|
var explainPurlOption = new Option<string?>("--purl")
|
|
{
|
|
Description = "Component PURL to explain."
|
|
};
|
|
var explainAdvisoryOption = new Option<string?>("--advisory")
|
|
{
|
|
Description = "Advisory ID to explain."
|
|
};
|
|
var explainTenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Tenant context."
|
|
};
|
|
var explainDepthOption = new Option<int?>("--depth")
|
|
{
|
|
Description = "Maximum depth of explanation tree."
|
|
};
|
|
var explainFormatOption = new Option<string?>("--format", new[] { "-f" })
|
|
{
|
|
Description = "Output format: table (default), json."
|
|
};
|
|
|
|
explain.Add(explainPolicyIdOption);
|
|
explain.Add(explainRunIdOption);
|
|
explain.Add(explainFindingIdOption);
|
|
explain.Add(explainSbomIdOption);
|
|
explain.Add(explainPurlOption);
|
|
explain.Add(explainAdvisoryOption);
|
|
explain.Add(explainTenantOption);
|
|
explain.Add(explainDepthOption);
|
|
explain.Add(explainFormatOption);
|
|
explain.Add(verboseOption);
|
|
|
|
explain.SetAction((parseResult, _) =>
|
|
{
|
|
var policyId = parseResult.GetValue(explainPolicyIdOption) ?? string.Empty;
|
|
var runId = parseResult.GetValue(explainRunIdOption);
|
|
var findingId = parseResult.GetValue(explainFindingIdOption);
|
|
var sbomId = parseResult.GetValue(explainSbomIdOption);
|
|
var purl = parseResult.GetValue(explainPurlOption);
|
|
var advisory = parseResult.GetValue(explainAdvisoryOption);
|
|
var tenant = parseResult.GetValue(explainTenantOption);
|
|
var depth = parseResult.GetValue(explainDepthOption);
|
|
var format = parseResult.GetValue(explainFormatOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePolicyExplainTreeAsync(
|
|
services,
|
|
policyId,
|
|
runId,
|
|
findingId,
|
|
sbomId,
|
|
purl,
|
|
advisory,
|
|
tenant,
|
|
depth,
|
|
format,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
policy.Add(explain);
|
|
|
|
// CLI-POLICY-27-001: policy init - initialize policy workspace
|
|
var init = new Command("init", "Initialize a policy workspace directory.");
|
|
var initPathArgument = new Argument<string?>("path")
|
|
{
|
|
Description = "Directory path for the workspace (defaults to current directory).",
|
|
Arity = ArgumentArity.ZeroOrOne
|
|
};
|
|
var initNameOption = new Option<string?>("--name", new[] { "-n" })
|
|
{
|
|
Description = "Policy name (defaults to directory name)."
|
|
};
|
|
var initTemplateOption = new Option<string?>("--template", new[] { "-t" })
|
|
{
|
|
Description = "Template to use: minimal (default), baseline, vex-precedence, reachability, secret-leak, full."
|
|
};
|
|
var initNoGitOption = new Option<bool>("--no-git")
|
|
{
|
|
Description = "Skip Git repository initialization."
|
|
};
|
|
var initNoReadmeOption = new Option<bool>("--no-readme")
|
|
{
|
|
Description = "Skip README.md creation."
|
|
};
|
|
var initNoFixturesOption = new Option<bool>("--no-fixtures")
|
|
{
|
|
Description = "Skip test fixtures directory creation."
|
|
};
|
|
var initFormatOption = new Option<string?>("--format", new[] { "-f" })
|
|
{
|
|
Description = "Output format: table (default), json."
|
|
};
|
|
|
|
init.Add(initPathArgument);
|
|
init.Add(initNameOption);
|
|
init.Add(initTemplateOption);
|
|
init.Add(initNoGitOption);
|
|
init.Add(initNoReadmeOption);
|
|
init.Add(initNoFixturesOption);
|
|
init.Add(initFormatOption);
|
|
init.Add(verboseOption);
|
|
|
|
init.SetAction((parseResult, _) =>
|
|
{
|
|
var path = parseResult.GetValue(initPathArgument);
|
|
var name = parseResult.GetValue(initNameOption);
|
|
var template = parseResult.GetValue(initTemplateOption);
|
|
var noGit = parseResult.GetValue(initNoGitOption);
|
|
var noReadme = parseResult.GetValue(initNoReadmeOption);
|
|
var noFixtures = parseResult.GetValue(initNoFixturesOption);
|
|
var format = parseResult.GetValue(initFormatOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePolicyInitAsync(
|
|
path,
|
|
name,
|
|
template,
|
|
noGit,
|
|
noReadme,
|
|
noFixtures,
|
|
format,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
policy.Add(init);
|
|
|
|
// CLI-POLICY-27-001: policy compile - compile DSL to IR
|
|
var compile = new Command("compile", "Compile a policy DSL file to IR.");
|
|
var compileFileArgument = new Argument<string>("file")
|
|
{
|
|
Description = "Path to the policy DSL file to compile."
|
|
};
|
|
var compileOutputOption = new Option<string?>("--output", new[] { "-o" })
|
|
{
|
|
Description = "Output path for the compiled IR file."
|
|
};
|
|
var compileNoIrOption = new Option<bool>("--no-ir")
|
|
{
|
|
Description = "Skip IR file generation (validation only)."
|
|
};
|
|
var compileNoDigestOption = new Option<bool>("--no-digest")
|
|
{
|
|
Description = "Skip SHA-256 digest output."
|
|
};
|
|
var compileOptimizeOption = new Option<bool>("--optimize")
|
|
{
|
|
Description = "Enable optimization passes on the IR."
|
|
};
|
|
var compileStrictOption = new Option<bool>("--strict")
|
|
{
|
|
Description = "Treat warnings as errors."
|
|
};
|
|
var compileFormatOption = new Option<string?>("--format", new[] { "-f" })
|
|
{
|
|
Description = "Output format: table (default), json."
|
|
};
|
|
|
|
compile.Add(compileFileArgument);
|
|
compile.Add(compileOutputOption);
|
|
compile.Add(compileNoIrOption);
|
|
compile.Add(compileNoDigestOption);
|
|
compile.Add(compileOptimizeOption);
|
|
compile.Add(compileStrictOption);
|
|
compile.Add(compileFormatOption);
|
|
compile.Add(verboseOption);
|
|
|
|
compile.SetAction((parseResult, _) =>
|
|
{
|
|
var file = parseResult.GetValue(compileFileArgument) ?? string.Empty;
|
|
var output = parseResult.GetValue(compileOutputOption);
|
|
var noIr = parseResult.GetValue(compileNoIrOption);
|
|
var noDigest = parseResult.GetValue(compileNoDigestOption);
|
|
var optimize = parseResult.GetValue(compileOptimizeOption);
|
|
var strict = parseResult.GetValue(compileStrictOption);
|
|
var format = parseResult.GetValue(compileFormatOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePolicyCompileAsync(
|
|
file,
|
|
output,
|
|
noIr,
|
|
noDigest,
|
|
optimize,
|
|
strict,
|
|
format,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
policy.Add(compile);
|
|
|
|
// CLI-POLICY-27-002: policy version bump
|
|
var versionCmd = new Command("version", "Manage policy versions.");
|
|
var versionBump = new Command("bump", "Bump the policy version (patch, minor, major).");
|
|
var bumpPolicyIdArg = new Argument<string>("policy-id")
|
|
{
|
|
Description = "Policy identifier (e.g. P-7)."
|
|
};
|
|
var bumpTypeOption = new Option<string?>("--type", new[] { "-t" })
|
|
{
|
|
Description = "Bump type: patch (default), minor, major."
|
|
};
|
|
var bumpChangelogOption = new Option<string?>("--changelog", new[] { "-m" })
|
|
{
|
|
Description = "Changelog message for this version."
|
|
};
|
|
var bumpFileOption = new Option<string?>("--file", new[] { "-f" })
|
|
{
|
|
Description = "Path to policy DSL file to upload."
|
|
};
|
|
var bumpTenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Tenant context."
|
|
};
|
|
var bumpJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON."
|
|
};
|
|
|
|
versionBump.Add(bumpPolicyIdArg);
|
|
versionBump.Add(bumpTypeOption);
|
|
versionBump.Add(bumpChangelogOption);
|
|
versionBump.Add(bumpFileOption);
|
|
versionBump.Add(bumpTenantOption);
|
|
versionBump.Add(bumpJsonOption);
|
|
versionBump.Add(verboseOption);
|
|
|
|
versionBump.SetAction((parseResult, _) =>
|
|
{
|
|
var policyId = parseResult.GetValue(bumpPolicyIdArg) ?? string.Empty;
|
|
var bumpType = parseResult.GetValue(bumpTypeOption);
|
|
var changelog = parseResult.GetValue(bumpChangelogOption);
|
|
var filePath = parseResult.GetValue(bumpFileOption);
|
|
var tenant = parseResult.GetValue(bumpTenantOption);
|
|
var json = parseResult.GetValue(bumpJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePolicyVersionBumpAsync(
|
|
services,
|
|
policyId,
|
|
bumpType,
|
|
changelog,
|
|
filePath,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
versionCmd.Add(versionBump);
|
|
policy.Add(versionCmd);
|
|
|
|
// CLI-POLICY-27-002: policy submit
|
|
var submit = new Command("submit", "Submit policy for review.");
|
|
var submitPolicyIdArg = new Argument<string>("policy-id")
|
|
{
|
|
Description = "Policy identifier (e.g. P-7)."
|
|
};
|
|
var submitVersionOption = new Option<int?>("--version", new[] { "-v" })
|
|
{
|
|
Description = "Specific version to submit (defaults to latest)."
|
|
};
|
|
var submitReviewersOption = new Option<string[]>("--reviewer", new[] { "-r" })
|
|
{
|
|
Description = "Reviewer username(s) (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
submitReviewersOption.AllowMultipleArgumentsPerToken = true;
|
|
var submitMessageOption = new Option<string?>("--message", new[] { "-m" })
|
|
{
|
|
Description = "Submission message."
|
|
};
|
|
var submitUrgentOption = new Option<bool>("--urgent")
|
|
{
|
|
Description = "Mark submission as urgent."
|
|
};
|
|
var submitTenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Tenant context."
|
|
};
|
|
var submitJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON."
|
|
};
|
|
|
|
submit.Add(submitPolicyIdArg);
|
|
submit.Add(submitVersionOption);
|
|
submit.Add(submitReviewersOption);
|
|
submit.Add(submitMessageOption);
|
|
submit.Add(submitUrgentOption);
|
|
submit.Add(submitTenantOption);
|
|
submit.Add(submitJsonOption);
|
|
submit.Add(verboseOption);
|
|
|
|
submit.SetAction((parseResult, _) =>
|
|
{
|
|
var policyId = parseResult.GetValue(submitPolicyIdArg) ?? string.Empty;
|
|
var version = parseResult.GetValue(submitVersionOption);
|
|
var reviewers = parseResult.GetValue(submitReviewersOption) ?? Array.Empty<string>();
|
|
var message = parseResult.GetValue(submitMessageOption);
|
|
var urgent = parseResult.GetValue(submitUrgentOption);
|
|
var tenant = parseResult.GetValue(submitTenantOption);
|
|
var json = parseResult.GetValue(submitJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePolicySubmitAsync(
|
|
services,
|
|
policyId,
|
|
version,
|
|
reviewers,
|
|
message,
|
|
urgent,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
policy.Add(submit);
|
|
|
|
// CLI-POLICY-27-002: policy review command group
|
|
var review = new Command("review", "Manage policy reviews.");
|
|
|
|
// review status
|
|
var reviewStatus = new Command("status", "Get current review status.");
|
|
var reviewStatusPolicyIdArg = new Argument<string>("policy-id")
|
|
{
|
|
Description = "Policy identifier."
|
|
};
|
|
var reviewStatusIdOption = new Option<string?>("--review-id")
|
|
{
|
|
Description = "Specific review ID (defaults to latest)."
|
|
};
|
|
var reviewStatusTenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Tenant context."
|
|
};
|
|
var reviewStatusJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON."
|
|
};
|
|
|
|
reviewStatus.Add(reviewStatusPolicyIdArg);
|
|
reviewStatus.Add(reviewStatusIdOption);
|
|
reviewStatus.Add(reviewStatusTenantOption);
|
|
reviewStatus.Add(reviewStatusJsonOption);
|
|
reviewStatus.Add(verboseOption);
|
|
|
|
reviewStatus.SetAction((parseResult, _) =>
|
|
{
|
|
var policyId = parseResult.GetValue(reviewStatusPolicyIdArg) ?? string.Empty;
|
|
var reviewId = parseResult.GetValue(reviewStatusIdOption);
|
|
var tenant = parseResult.GetValue(reviewStatusTenantOption);
|
|
var json = parseResult.GetValue(reviewStatusJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePolicyReviewStatusAsync(
|
|
services,
|
|
policyId,
|
|
reviewId,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
review.Add(reviewStatus);
|
|
|
|
// review comment
|
|
var reviewComment = new Command("comment", "Add a review comment.");
|
|
var commentPolicyIdArg = new Argument<string>("policy-id")
|
|
{
|
|
Description = "Policy identifier."
|
|
};
|
|
var commentReviewIdOption = new Option<string>("--review-id")
|
|
{
|
|
Description = "Review ID to comment on.",
|
|
Required = true
|
|
};
|
|
var commentTextOption = new Option<string>("--comment", new[] { "-c" })
|
|
{
|
|
Description = "Comment text.",
|
|
Required = true
|
|
};
|
|
var commentLineOption = new Option<int?>("--line")
|
|
{
|
|
Description = "Line number in the policy file."
|
|
};
|
|
var commentRuleOption = new Option<string?>("--rule")
|
|
{
|
|
Description = "Rule name reference."
|
|
};
|
|
var commentBlockingOption = new Option<bool>("--blocking")
|
|
{
|
|
Description = "Mark comment as blocking (must be addressed before approval)."
|
|
};
|
|
var commentTenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Tenant context."
|
|
};
|
|
var commentJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON."
|
|
};
|
|
|
|
reviewComment.Add(commentPolicyIdArg);
|
|
reviewComment.Add(commentReviewIdOption);
|
|
reviewComment.Add(commentTextOption);
|
|
reviewComment.Add(commentLineOption);
|
|
reviewComment.Add(commentRuleOption);
|
|
reviewComment.Add(commentBlockingOption);
|
|
reviewComment.Add(commentTenantOption);
|
|
reviewComment.Add(commentJsonOption);
|
|
reviewComment.Add(verboseOption);
|
|
|
|
reviewComment.SetAction((parseResult, _) =>
|
|
{
|
|
var policyId = parseResult.GetValue(commentPolicyIdArg) ?? string.Empty;
|
|
var reviewId = parseResult.GetValue(commentReviewIdOption) ?? string.Empty;
|
|
var comment = parseResult.GetValue(commentTextOption) ?? string.Empty;
|
|
var line = parseResult.GetValue(commentLineOption);
|
|
var rule = parseResult.GetValue(commentRuleOption);
|
|
var blocking = parseResult.GetValue(commentBlockingOption);
|
|
var tenant = parseResult.GetValue(commentTenantOption);
|
|
var json = parseResult.GetValue(commentJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePolicyReviewCommentAsync(
|
|
services,
|
|
policyId,
|
|
reviewId,
|
|
comment,
|
|
line,
|
|
rule,
|
|
blocking,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
review.Add(reviewComment);
|
|
|
|
// review approve
|
|
var reviewApprove = new Command("approve", "Approve a policy review.");
|
|
var approvePolicyIdArg = new Argument<string>("policy-id")
|
|
{
|
|
Description = "Policy identifier."
|
|
};
|
|
var approveReviewIdOption = new Option<string>("--review-id")
|
|
{
|
|
Description = "Review ID to approve.",
|
|
Required = true
|
|
};
|
|
var approveCommentOption = new Option<string?>("--comment", new[] { "-c" })
|
|
{
|
|
Description = "Approval comment."
|
|
};
|
|
var approveTenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Tenant context."
|
|
};
|
|
var approveJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON."
|
|
};
|
|
|
|
reviewApprove.Add(approvePolicyIdArg);
|
|
reviewApprove.Add(approveReviewIdOption);
|
|
reviewApprove.Add(approveCommentOption);
|
|
reviewApprove.Add(approveTenantOption);
|
|
reviewApprove.Add(approveJsonOption);
|
|
reviewApprove.Add(verboseOption);
|
|
|
|
reviewApprove.SetAction((parseResult, _) =>
|
|
{
|
|
var policyId = parseResult.GetValue(approvePolicyIdArg) ?? string.Empty;
|
|
var reviewId = parseResult.GetValue(approveReviewIdOption) ?? string.Empty;
|
|
var comment = parseResult.GetValue(approveCommentOption);
|
|
var tenant = parseResult.GetValue(approveTenantOption);
|
|
var json = parseResult.GetValue(approveJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePolicyReviewApproveAsync(
|
|
services,
|
|
policyId,
|
|
reviewId,
|
|
comment,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
review.Add(reviewApprove);
|
|
|
|
// review reject
|
|
var reviewReject = new Command("reject", "Reject a policy review.");
|
|
var rejectPolicyIdArg = new Argument<string>("policy-id")
|
|
{
|
|
Description = "Policy identifier."
|
|
};
|
|
var rejectReviewIdOption = new Option<string>("--review-id")
|
|
{
|
|
Description = "Review ID to reject.",
|
|
Required = true
|
|
};
|
|
var rejectReasonOption = new Option<string>("--reason", new[] { "-r" })
|
|
{
|
|
Description = "Rejection reason.",
|
|
Required = true
|
|
};
|
|
var rejectTenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Tenant context."
|
|
};
|
|
var rejectJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON."
|
|
};
|
|
|
|
reviewReject.Add(rejectPolicyIdArg);
|
|
reviewReject.Add(rejectReviewIdOption);
|
|
reviewReject.Add(rejectReasonOption);
|
|
reviewReject.Add(rejectTenantOption);
|
|
reviewReject.Add(rejectJsonOption);
|
|
reviewReject.Add(verboseOption);
|
|
|
|
reviewReject.SetAction((parseResult, _) =>
|
|
{
|
|
var policyId = parseResult.GetValue(rejectPolicyIdArg) ?? string.Empty;
|
|
var reviewId = parseResult.GetValue(rejectReviewIdOption) ?? string.Empty;
|
|
var reason = parseResult.GetValue(rejectReasonOption) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(rejectTenantOption);
|
|
var json = parseResult.GetValue(rejectJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePolicyReviewRejectAsync(
|
|
services,
|
|
policyId,
|
|
reviewId,
|
|
reason,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
review.Add(reviewReject);
|
|
|
|
policy.Add(review);
|
|
|
|
// CLI-POLICY-27-004: publish command
|
|
var publish = new Command("publish", "Publish an approved policy revision.");
|
|
var publishPolicyIdArg = new Argument<string>("policy-id")
|
|
{
|
|
Description = "Policy identifier."
|
|
};
|
|
var publishVersionOption = new Option<int>("--version")
|
|
{
|
|
Description = "Version to publish.",
|
|
Required = true
|
|
};
|
|
var publishSignOption = new Option<bool>("--sign")
|
|
{
|
|
Description = "Sign the policy during publish."
|
|
};
|
|
var publishAlgorithmOption = new Option<string?>("--algorithm")
|
|
{
|
|
Description = "Signature algorithm (e.g. ecdsa-sha256, ed25519)."
|
|
};
|
|
var publishKeyIdOption = new Option<string?>("--key-id")
|
|
{
|
|
Description = "Key identifier for signing."
|
|
};
|
|
var publishNoteOption = new Option<string?>("--note")
|
|
{
|
|
Description = "Publish note."
|
|
};
|
|
var publishTenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Tenant context."
|
|
};
|
|
var publishJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON."
|
|
};
|
|
|
|
publish.Add(publishPolicyIdArg);
|
|
publish.Add(publishVersionOption);
|
|
publish.Add(publishSignOption);
|
|
publish.Add(publishAlgorithmOption);
|
|
publish.Add(publishKeyIdOption);
|
|
publish.Add(publishNoteOption);
|
|
publish.Add(publishTenantOption);
|
|
publish.Add(publishJsonOption);
|
|
publish.Add(verboseOption);
|
|
|
|
publish.SetAction((parseResult, _) =>
|
|
{
|
|
var policyId = parseResult.GetValue(publishPolicyIdArg) ?? string.Empty;
|
|
var version = parseResult.GetValue(publishVersionOption);
|
|
var sign = parseResult.GetValue(publishSignOption);
|
|
var algorithm = parseResult.GetValue(publishAlgorithmOption);
|
|
var keyId = parseResult.GetValue(publishKeyIdOption);
|
|
var note = parseResult.GetValue(publishNoteOption);
|
|
var tenant = parseResult.GetValue(publishTenantOption);
|
|
var json = parseResult.GetValue(publishJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePolicyPublishAsync(
|
|
services,
|
|
policyId,
|
|
version,
|
|
sign,
|
|
algorithm,
|
|
keyId,
|
|
note,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
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")
|
|
{
|
|
Description = "Policy identifier."
|
|
};
|
|
var rollbackTargetVersionOption = new Option<int?>("--target-version")
|
|
{
|
|
Description = "Target version to rollback to. Defaults to previous version."
|
|
};
|
|
var rollbackEnvOption = new Option<string?>("--env")
|
|
{
|
|
Description = "Environment scope for rollback."
|
|
};
|
|
var rollbackReasonOption = new Option<string?>("--reason")
|
|
{
|
|
Description = "Reason for rollback."
|
|
};
|
|
var rollbackIncidentOption = new Option<string?>("--incident")
|
|
{
|
|
Description = "Associated incident ID."
|
|
};
|
|
var rollbackTenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Tenant context."
|
|
};
|
|
var rollbackJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON."
|
|
};
|
|
|
|
rollback.Add(rollbackPolicyIdArg);
|
|
rollback.Add(rollbackTargetVersionOption);
|
|
rollback.Add(rollbackEnvOption);
|
|
rollback.Add(rollbackReasonOption);
|
|
rollback.Add(rollbackIncidentOption);
|
|
rollback.Add(rollbackTenantOption);
|
|
rollback.Add(rollbackJsonOption);
|
|
rollback.Add(verboseOption);
|
|
|
|
rollback.SetAction((parseResult, _) =>
|
|
{
|
|
var policyId = parseResult.GetValue(rollbackPolicyIdArg) ?? string.Empty;
|
|
var targetVersion = parseResult.GetValue(rollbackTargetVersionOption);
|
|
var env = parseResult.GetValue(rollbackEnvOption);
|
|
var reason = parseResult.GetValue(rollbackReasonOption);
|
|
var incident = parseResult.GetValue(rollbackIncidentOption);
|
|
var tenant = parseResult.GetValue(rollbackTenantOption);
|
|
var json = parseResult.GetValue(rollbackJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePolicyRollbackAsync(
|
|
services,
|
|
policyId,
|
|
targetVersion,
|
|
env,
|
|
reason,
|
|
incident,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
policy.Add(rollback);
|
|
|
|
// CLI-POLICY-27-004: sign command
|
|
var sign = new Command("sign", "Sign a policy revision.");
|
|
var signPolicyIdArg = new Argument<string>("policy-id")
|
|
{
|
|
Description = "Policy identifier."
|
|
};
|
|
var signVersionOption = new Option<int>("--version")
|
|
{
|
|
Description = "Version to sign.",
|
|
Required = true
|
|
};
|
|
var signKeyIdOption = new Option<string?>("--key-id")
|
|
{
|
|
Description = "Key identifier for signing."
|
|
};
|
|
var signAlgorithmOption = new Option<string?>("--algorithm")
|
|
{
|
|
Description = "Signature algorithm (e.g. ecdsa-sha256, ed25519)."
|
|
};
|
|
var signRekorOption = new Option<bool>("--rekor")
|
|
{
|
|
Description = "Upload signature to Sigstore Rekor transparency log."
|
|
};
|
|
var signTenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Tenant context."
|
|
};
|
|
var signJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON."
|
|
};
|
|
|
|
sign.Add(signPolicyIdArg);
|
|
sign.Add(signVersionOption);
|
|
sign.Add(signKeyIdOption);
|
|
sign.Add(signAlgorithmOption);
|
|
sign.Add(signRekorOption);
|
|
sign.Add(signTenantOption);
|
|
sign.Add(signJsonOption);
|
|
sign.Add(verboseOption);
|
|
|
|
sign.SetAction((parseResult, _) =>
|
|
{
|
|
var policyId = parseResult.GetValue(signPolicyIdArg) ?? string.Empty;
|
|
var version = parseResult.GetValue(signVersionOption);
|
|
var keyId = parseResult.GetValue(signKeyIdOption);
|
|
var algorithm = parseResult.GetValue(signAlgorithmOption);
|
|
var rekor = parseResult.GetValue(signRekorOption);
|
|
var tenant = parseResult.GetValue(signTenantOption);
|
|
var json = parseResult.GetValue(signJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePolicySignAsync(
|
|
services,
|
|
policyId,
|
|
version,
|
|
keyId,
|
|
algorithm,
|
|
rekor,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
policy.Add(sign);
|
|
|
|
// CLI-POLICY-27-004: verify-signature command
|
|
var verifySignature = new Command("verify-signature", "Verify a policy signature.");
|
|
var verifyPolicyIdArg = new Argument<string>("policy-id")
|
|
{
|
|
Description = "Policy identifier."
|
|
};
|
|
var verifyVersionOption = new Option<int>("--version")
|
|
{
|
|
Description = "Version to verify.",
|
|
Required = true
|
|
};
|
|
var verifySignatureIdOption = new Option<string?>("--signature-id")
|
|
{
|
|
Description = "Signature ID to verify. Defaults to latest."
|
|
};
|
|
var verifyCheckRekorOption = new Option<bool>("--check-rekor")
|
|
{
|
|
Description = "Verify against Sigstore Rekor transparency log."
|
|
};
|
|
var verifyTenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Tenant context."
|
|
};
|
|
var verifyJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON."
|
|
};
|
|
|
|
verifySignature.Add(verifyPolicyIdArg);
|
|
verifySignature.Add(verifyVersionOption);
|
|
verifySignature.Add(verifySignatureIdOption);
|
|
verifySignature.Add(verifyCheckRekorOption);
|
|
verifySignature.Add(verifyTenantOption);
|
|
verifySignature.Add(verifyJsonOption);
|
|
verifySignature.Add(verboseOption);
|
|
|
|
verifySignature.SetAction((parseResult, _) =>
|
|
{
|
|
var policyId = parseResult.GetValue(verifyPolicyIdArg) ?? string.Empty;
|
|
var version = parseResult.GetValue(verifyVersionOption);
|
|
var signatureId = parseResult.GetValue(verifySignatureIdOption);
|
|
var checkRekor = parseResult.GetValue(verifyCheckRekorOption);
|
|
var tenant = parseResult.GetValue(verifyTenantOption);
|
|
var json = parseResult.GetValue(verifyJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePolicyVerifySignatureAsync(
|
|
services,
|
|
policyId,
|
|
version,
|
|
signatureId,
|
|
checkRekor,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
policy.Add(verifySignature);
|
|
|
|
return policy;
|
|
}
|
|
|
|
private static Command BuildTaskRunnerCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var taskRunner = new Command("task-runner", "Interact with Task Runner operations.");
|
|
|
|
var simulate = new Command("simulate", "Simulate a task pack and inspect the execution graph.");
|
|
var manifestOption = new Option<string>("--manifest")
|
|
{
|
|
Description = "Path to the task pack manifest (YAML).",
|
|
Arity = ArgumentArity.ExactlyOne
|
|
};
|
|
var inputsOption = new Option<string?>("--inputs")
|
|
{
|
|
Description = "Optional JSON file containing Task Pack input values."
|
|
};
|
|
var formatOption = new Option<string?>("--format")
|
|
{
|
|
Description = "Output format: table or json."
|
|
};
|
|
var outputOption = new Option<string?>("--output")
|
|
{
|
|
Description = "Write JSON payload to the specified file."
|
|
};
|
|
|
|
simulate.Add(manifestOption);
|
|
simulate.Add(inputsOption);
|
|
simulate.Add(formatOption);
|
|
simulate.Add(outputOption);
|
|
|
|
simulate.SetAction((parseResult, _) =>
|
|
{
|
|
var manifestPath = parseResult.GetValue(manifestOption) ?? string.Empty;
|
|
var inputsPath = parseResult.GetValue(inputsOption);
|
|
var selectedFormat = parseResult.GetValue(formatOption);
|
|
var output = parseResult.GetValue(outputOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleTaskRunnerSimulateAsync(
|
|
services,
|
|
manifestPath,
|
|
inputsPath,
|
|
selectedFormat,
|
|
output,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
taskRunner.Add(simulate);
|
|
return taskRunner;
|
|
}
|
|
|
|
private static Command BuildFindingsCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var findings = new Command("findings", "Inspect policy findings.");
|
|
|
|
var list = new Command("ls", "List effective findings that match the provided filters.");
|
|
var policyOption = new Option<string>("--policy")
|
|
{
|
|
Description = "Policy identifier (e.g. P-7).",
|
|
Required = true
|
|
};
|
|
var sbomOption = new Option<string[]>("--sbom")
|
|
{
|
|
Description = "Filter by SBOM identifier (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
sbomOption.AllowMultipleArgumentsPerToken = true;
|
|
|
|
var statusOption = new Option<string[]>("--status")
|
|
{
|
|
Description = "Filter by finding status (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
statusOption.AllowMultipleArgumentsPerToken = true;
|
|
|
|
var severityOption = new Option<string[]>("--severity")
|
|
{
|
|
Description = "Filter by severity label (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
severityOption.AllowMultipleArgumentsPerToken = true;
|
|
|
|
var sinceOption = new Option<string?>("--since")
|
|
{
|
|
Description = "Filter by last-updated timestamp (ISO-8601)."
|
|
};
|
|
var cursorOption = new Option<string?>("--cursor")
|
|
{
|
|
Description = "Resume listing from the provided cursor."
|
|
};
|
|
var pageOption = new Option<int?>("--page")
|
|
{
|
|
Description = "Page number (starts at 1)."
|
|
};
|
|
var pageSizeOption = new Option<int?>("--page-size")
|
|
{
|
|
Description = "Results per page (default backend limit applies)."
|
|
};
|
|
var formatOption = new Option<string?>("--format")
|
|
{
|
|
Description = "Output format: table or json."
|
|
};
|
|
var outputOption = new Option<string?>("--output")
|
|
{
|
|
Description = "Write JSON payload to the specified file."
|
|
};
|
|
|
|
list.Add(policyOption);
|
|
list.Add(sbomOption);
|
|
list.Add(statusOption);
|
|
list.Add(severityOption);
|
|
list.Add(sinceOption);
|
|
list.Add(cursorOption);
|
|
list.Add(pageOption);
|
|
list.Add(pageSizeOption);
|
|
list.Add(formatOption);
|
|
list.Add(outputOption);
|
|
|
|
list.SetAction((parseResult, _) =>
|
|
{
|
|
var policy = parseResult.GetValue(policyOption) ?? string.Empty;
|
|
var sboms = parseResult.GetValue(sbomOption) ?? Array.Empty<string>();
|
|
var statuses = parseResult.GetValue(statusOption) ?? Array.Empty<string>();
|
|
var severities = parseResult.GetValue(severityOption) ?? Array.Empty<string>();
|
|
var since = parseResult.GetValue(sinceOption);
|
|
var cursor = parseResult.GetValue(cursorOption);
|
|
var page = parseResult.GetValue(pageOption);
|
|
var pageSize = parseResult.GetValue(pageSizeOption);
|
|
var selectedFormat = parseResult.GetValue(formatOption);
|
|
var output = parseResult.GetValue(outputOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePolicyFindingsListAsync(
|
|
services,
|
|
policy,
|
|
sboms,
|
|
statuses,
|
|
severities,
|
|
since,
|
|
cursor,
|
|
page,
|
|
pageSize,
|
|
selectedFormat,
|
|
output,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
var get = new Command("get", "Retrieve a specific finding.");
|
|
var findingArgument = new Argument<string>("finding-id")
|
|
{
|
|
Description = "Finding identifier (e.g. P-7:S-42:pkg:...)."
|
|
};
|
|
var getPolicyOption = new Option<string>("--policy")
|
|
{
|
|
Description = "Policy identifier for the finding.",
|
|
Required = true
|
|
};
|
|
var getFormatOption = new Option<string?>("--format")
|
|
{
|
|
Description = "Output format: table or json."
|
|
};
|
|
var getOutputOption = new Option<string?>("--output")
|
|
{
|
|
Description = "Write JSON payload to the specified file."
|
|
};
|
|
|
|
get.Add(findingArgument);
|
|
get.Add(getPolicyOption);
|
|
get.Add(getFormatOption);
|
|
get.Add(getOutputOption);
|
|
|
|
get.SetAction((parseResult, _) =>
|
|
{
|
|
var policy = parseResult.GetValue(getPolicyOption) ?? string.Empty;
|
|
var finding = parseResult.GetValue(findingArgument) ?? string.Empty;
|
|
var selectedFormat = parseResult.GetValue(getFormatOption);
|
|
var output = parseResult.GetValue(getOutputOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePolicyFindingsGetAsync(
|
|
services,
|
|
policy,
|
|
finding,
|
|
selectedFormat,
|
|
output,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
var explain = new Command("explain", "Fetch explain trace for a finding.");
|
|
var explainFindingArgument = new Argument<string>("finding-id")
|
|
{
|
|
Description = "Finding identifier."
|
|
};
|
|
var explainPolicyOption = new Option<string>("--policy")
|
|
{
|
|
Description = "Policy identifier.",
|
|
Required = true
|
|
};
|
|
var modeOption = new Option<string?>("--mode")
|
|
{
|
|
Description = "Explain mode (for example: verbose)."
|
|
};
|
|
var explainFormatOption = new Option<string?>("--format")
|
|
{
|
|
Description = "Output format: table or json."
|
|
};
|
|
var explainOutputOption = new Option<string?>("--output")
|
|
{
|
|
Description = "Write JSON payload to the specified file."
|
|
};
|
|
|
|
explain.Add(explainFindingArgument);
|
|
explain.Add(explainPolicyOption);
|
|
explain.Add(modeOption);
|
|
explain.Add(explainFormatOption);
|
|
explain.Add(explainOutputOption);
|
|
|
|
explain.SetAction((parseResult, _) =>
|
|
{
|
|
var policy = parseResult.GetValue(explainPolicyOption) ?? string.Empty;
|
|
var finding = parseResult.GetValue(explainFindingArgument) ?? string.Empty;
|
|
var mode = parseResult.GetValue(modeOption);
|
|
var selectedFormat = parseResult.GetValue(explainFormatOption);
|
|
var output = parseResult.GetValue(explainOutputOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePolicyFindingsExplainAsync(
|
|
services,
|
|
policy,
|
|
finding,
|
|
mode,
|
|
selectedFormat,
|
|
output,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
findings.Add(list);
|
|
findings.Add(get);
|
|
findings.Add(explain);
|
|
return findings;
|
|
}
|
|
|
|
private static Command BuildAdviseCommand(IServiceProvider services, StellaOpsCliOptions options, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var advise = new Command("advise", "Interact with Advisory AI pipelines.");
|
|
_ = options;
|
|
|
|
var runOptions = CreateAdvisoryOptions();
|
|
var runTaskArgument = new Argument<string>("task")
|
|
{
|
|
Description = "Task to run (summary, conflict, remediation)."
|
|
};
|
|
|
|
var run = new Command("run", "Generate Advisory AI output for the specified task.");
|
|
run.Add(runTaskArgument);
|
|
AddAdvisoryOptions(run, runOptions);
|
|
|
|
run.SetAction((parseResult, _) =>
|
|
{
|
|
var taskValue = parseResult.GetValue(runTaskArgument);
|
|
var advisoryKey = parseResult.GetValue(runOptions.AdvisoryKey) ?? string.Empty;
|
|
var artifactId = parseResult.GetValue(runOptions.ArtifactId);
|
|
var artifactPurl = parseResult.GetValue(runOptions.ArtifactPurl);
|
|
var policyVersion = parseResult.GetValue(runOptions.PolicyVersion);
|
|
var profile = parseResult.GetValue(runOptions.Profile) ?? "default";
|
|
var sections = parseResult.GetValue(runOptions.Sections) ?? Array.Empty<string>();
|
|
var forceRefresh = parseResult.GetValue(runOptions.ForceRefresh);
|
|
var timeoutSeconds = parseResult.GetValue(runOptions.TimeoutSeconds) ?? 120;
|
|
var outputFormat = ParseAdvisoryOutputFormat(parseResult.GetValue(runOptions.Format));
|
|
var outputPath = parseResult.GetValue(runOptions.Output);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
if (!Enum.TryParse<AdvisoryAiTaskType>(taskValue, ignoreCase: true, out var taskType))
|
|
{
|
|
throw new InvalidOperationException($"Unknown advisory task '{taskValue}'. Expected summary, conflict, or remediation.");
|
|
}
|
|
|
|
return CommandHandlers.HandleAdviseRunAsync(
|
|
services,
|
|
taskType,
|
|
advisoryKey,
|
|
artifactId,
|
|
artifactPurl,
|
|
policyVersion,
|
|
profile,
|
|
sections,
|
|
forceRefresh,
|
|
timeoutSeconds,
|
|
outputFormat,
|
|
outputPath,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
var summarizeOptions = CreateAdvisoryOptions();
|
|
var summarize = new Command("summarize", "Summarize an advisory with JSON/Markdown outputs and citations.");
|
|
AddAdvisoryOptions(summarize, summarizeOptions);
|
|
summarize.SetAction((parseResult, _) =>
|
|
{
|
|
var advisoryKey = parseResult.GetValue(summarizeOptions.AdvisoryKey) ?? string.Empty;
|
|
var artifactId = parseResult.GetValue(summarizeOptions.ArtifactId);
|
|
var artifactPurl = parseResult.GetValue(summarizeOptions.ArtifactPurl);
|
|
var policyVersion = parseResult.GetValue(summarizeOptions.PolicyVersion);
|
|
var profile = parseResult.GetValue(summarizeOptions.Profile) ?? "default";
|
|
var sections = parseResult.GetValue(summarizeOptions.Sections) ?? Array.Empty<string>();
|
|
var forceRefresh = parseResult.GetValue(summarizeOptions.ForceRefresh);
|
|
var timeoutSeconds = parseResult.GetValue(summarizeOptions.TimeoutSeconds) ?? 120;
|
|
var outputFormat = ParseAdvisoryOutputFormat(parseResult.GetValue(summarizeOptions.Format));
|
|
var outputPath = parseResult.GetValue(summarizeOptions.Output);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleAdviseRunAsync(
|
|
services,
|
|
AdvisoryAiTaskType.Summary,
|
|
advisoryKey,
|
|
artifactId,
|
|
artifactPurl,
|
|
policyVersion,
|
|
profile,
|
|
sections,
|
|
forceRefresh,
|
|
timeoutSeconds,
|
|
outputFormat,
|
|
outputPath,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
var explainOptions = CreateAdvisoryOptions();
|
|
var explain = new Command("explain", "Explain an advisory conflict set with narrative and rationale.");
|
|
AddAdvisoryOptions(explain, explainOptions);
|
|
explain.SetAction((parseResult, _) =>
|
|
{
|
|
var advisoryKey = parseResult.GetValue(explainOptions.AdvisoryKey) ?? string.Empty;
|
|
var artifactId = parseResult.GetValue(explainOptions.ArtifactId);
|
|
var artifactPurl = parseResult.GetValue(explainOptions.ArtifactPurl);
|
|
var policyVersion = parseResult.GetValue(explainOptions.PolicyVersion);
|
|
var profile = parseResult.GetValue(explainOptions.Profile) ?? "default";
|
|
var sections = parseResult.GetValue(explainOptions.Sections) ?? Array.Empty<string>();
|
|
var forceRefresh = parseResult.GetValue(explainOptions.ForceRefresh);
|
|
var timeoutSeconds = parseResult.GetValue(explainOptions.TimeoutSeconds) ?? 120;
|
|
var outputFormat = ParseAdvisoryOutputFormat(parseResult.GetValue(explainOptions.Format));
|
|
var outputPath = parseResult.GetValue(explainOptions.Output);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleAdviseRunAsync(
|
|
services,
|
|
AdvisoryAiTaskType.Conflict,
|
|
advisoryKey,
|
|
artifactId,
|
|
artifactPurl,
|
|
policyVersion,
|
|
profile,
|
|
sections,
|
|
forceRefresh,
|
|
timeoutSeconds,
|
|
outputFormat,
|
|
outputPath,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
var remediateOptions = CreateAdvisoryOptions();
|
|
var remediate = new Command("remediate", "Generate remediation guidance for an advisory.");
|
|
AddAdvisoryOptions(remediate, remediateOptions);
|
|
remediate.SetAction((parseResult, _) =>
|
|
{
|
|
var advisoryKey = parseResult.GetValue(remediateOptions.AdvisoryKey) ?? string.Empty;
|
|
var artifactId = parseResult.GetValue(remediateOptions.ArtifactId);
|
|
var artifactPurl = parseResult.GetValue(remediateOptions.ArtifactPurl);
|
|
var policyVersion = parseResult.GetValue(remediateOptions.PolicyVersion);
|
|
var profile = parseResult.GetValue(remediateOptions.Profile) ?? "default";
|
|
var sections = parseResult.GetValue(remediateOptions.Sections) ?? Array.Empty<string>();
|
|
var forceRefresh = parseResult.GetValue(remediateOptions.ForceRefresh);
|
|
var timeoutSeconds = parseResult.GetValue(remediateOptions.TimeoutSeconds) ?? 120;
|
|
var outputFormat = ParseAdvisoryOutputFormat(parseResult.GetValue(remediateOptions.Format));
|
|
var outputPath = parseResult.GetValue(remediateOptions.Output);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleAdviseRunAsync(
|
|
services,
|
|
AdvisoryAiTaskType.Remediation,
|
|
advisoryKey,
|
|
artifactId,
|
|
artifactPurl,
|
|
policyVersion,
|
|
profile,
|
|
sections,
|
|
forceRefresh,
|
|
timeoutSeconds,
|
|
outputFormat,
|
|
outputPath,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
var batchOptions = CreateAdvisoryOptions();
|
|
var batchKeys = new Argument<string[]>("advisory-keys")
|
|
{
|
|
Description = "One or more advisory identifiers.",
|
|
Arity = ArgumentArity.OneOrMore
|
|
};
|
|
var batch = new Command("batch", "Run Advisory AI over multiple advisories with a single invocation.");
|
|
batch.Add(batchKeys);
|
|
batch.Add(batchOptions.Output);
|
|
batch.Add(batchOptions.AdvisoryKey);
|
|
batch.Add(batchOptions.ArtifactId);
|
|
batch.Add(batchOptions.ArtifactPurl);
|
|
batch.Add(batchOptions.PolicyVersion);
|
|
batch.Add(batchOptions.Profile);
|
|
batch.Add(batchOptions.Sections);
|
|
batch.Add(batchOptions.ForceRefresh);
|
|
batch.Add(batchOptions.TimeoutSeconds);
|
|
batch.Add(batchOptions.Format);
|
|
batch.SetAction((parseResult, _) =>
|
|
{
|
|
var advisoryKeys = parseResult.GetValue(batchKeys) ?? Array.Empty<string>();
|
|
var artifactId = parseResult.GetValue(batchOptions.ArtifactId);
|
|
var artifactPurl = parseResult.GetValue(batchOptions.ArtifactPurl);
|
|
var policyVersion = parseResult.GetValue(batchOptions.PolicyVersion);
|
|
var profile = parseResult.GetValue(batchOptions.Profile) ?? "default";
|
|
var sections = parseResult.GetValue(batchOptions.Sections) ?? Array.Empty<string>();
|
|
var forceRefresh = parseResult.GetValue(batchOptions.ForceRefresh);
|
|
var timeoutSeconds = parseResult.GetValue(batchOptions.TimeoutSeconds) ?? 120;
|
|
var outputFormat = ParseAdvisoryOutputFormat(parseResult.GetValue(batchOptions.Format));
|
|
var outputDirectory = parseResult.GetValue(batchOptions.Output);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleAdviseBatchAsync(
|
|
services,
|
|
AdvisoryAiTaskType.Summary,
|
|
advisoryKeys,
|
|
artifactId,
|
|
artifactPurl,
|
|
policyVersion,
|
|
profile,
|
|
sections,
|
|
forceRefresh,
|
|
timeoutSeconds,
|
|
outputFormat,
|
|
outputDirectory,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
advise.Add(run);
|
|
advise.Add(summarize);
|
|
advise.Add(explain);
|
|
advise.Add(remediate);
|
|
advise.Add(batch);
|
|
return advise;
|
|
}
|
|
|
|
private static AdvisoryCommandOptions CreateAdvisoryOptions()
|
|
{
|
|
var advisoryKey = new Option<string>("--advisory-key")
|
|
{
|
|
Description = "Advisory identifier to summarise (required).",
|
|
Required = true
|
|
};
|
|
|
|
var artifactId = new Option<string?>("--artifact-id")
|
|
{
|
|
Description = "Optional artifact identifier to scope SBOM context."
|
|
};
|
|
|
|
var artifactPurl = new Option<string?>("--artifact-purl")
|
|
{
|
|
Description = "Optional package URL to scope dependency context."
|
|
};
|
|
|
|
var policyVersion = new Option<string?>("--policy-version")
|
|
{
|
|
Description = "Policy revision to evaluate (defaults to current)."
|
|
};
|
|
|
|
var profile = new Option<string?>("--profile")
|
|
{
|
|
Description = "Advisory AI execution profile (default, fips-local, etc.)."
|
|
};
|
|
|
|
var sections = new Option<string[]>("--section")
|
|
{
|
|
Description = "Preferred context sections to emphasise (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
sections.AllowMultipleArgumentsPerToken = true;
|
|
|
|
var forceRefresh = new Option<bool>("--force-refresh")
|
|
{
|
|
Description = "Bypass cached plan/output and recompute."
|
|
};
|
|
|
|
var timeoutSeconds = new Option<int?>("--timeout")
|
|
{
|
|
Description = "Seconds to wait for generated output before timing out (0 = single attempt)."
|
|
};
|
|
timeoutSeconds.Arity = ArgumentArity.ZeroOrOne;
|
|
|
|
var format = new Option<string?>("--format")
|
|
{
|
|
Description = "Output format: table (default), json, or markdown."
|
|
};
|
|
|
|
var output = new Option<string?>("--output")
|
|
{
|
|
Description = "File path to write advisory output when using json/markdown formats."
|
|
};
|
|
|
|
return new AdvisoryCommandOptions(
|
|
advisoryKey,
|
|
artifactId,
|
|
artifactPurl,
|
|
policyVersion,
|
|
profile,
|
|
sections,
|
|
forceRefresh,
|
|
timeoutSeconds,
|
|
format,
|
|
output);
|
|
}
|
|
|
|
private static void AddAdvisoryOptions(Command command, AdvisoryCommandOptions options)
|
|
{
|
|
command.Add(options.AdvisoryKey);
|
|
command.Add(options.ArtifactId);
|
|
command.Add(options.ArtifactPurl);
|
|
command.Add(options.PolicyVersion);
|
|
command.Add(options.Profile);
|
|
command.Add(options.Sections);
|
|
command.Add(options.ForceRefresh);
|
|
command.Add(options.TimeoutSeconds);
|
|
command.Add(options.Format);
|
|
command.Add(options.Output);
|
|
}
|
|
|
|
private static AdvisoryOutputFormat ParseAdvisoryOutputFormat(string? formatValue)
|
|
{
|
|
var normalized = string.IsNullOrWhiteSpace(formatValue)
|
|
? "table"
|
|
: formatValue!.Trim().ToLowerInvariant();
|
|
|
|
return normalized switch
|
|
{
|
|
"json" => AdvisoryOutputFormat.Json,
|
|
"markdown" => AdvisoryOutputFormat.Markdown,
|
|
"md" => AdvisoryOutputFormat.Markdown,
|
|
_ => AdvisoryOutputFormat.Table
|
|
};
|
|
}
|
|
|
|
private sealed record AdvisoryCommandOptions(
|
|
Option<string> AdvisoryKey,
|
|
Option<string?> ArtifactId,
|
|
Option<string?> ArtifactPurl,
|
|
Option<string?> PolicyVersion,
|
|
Option<string?> Profile,
|
|
Option<string[]> Sections,
|
|
Option<bool> ForceRefresh,
|
|
Option<int?> TimeoutSeconds,
|
|
Option<string?> Format,
|
|
Option<string?> Output);
|
|
|
|
private static Command BuildVulnCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var vuln = new Command("vuln", "Explore vulnerability observations and overlays.");
|
|
|
|
var observations = new Command("observations", "List raw advisory observations for overlay consumers.");
|
|
|
|
var tenantOption = new Option<string>("--tenant")
|
|
{
|
|
Description = "Tenant identifier.",
|
|
Required = true
|
|
};
|
|
var observationIdOption = new Option<string[]>("--observation-id")
|
|
{
|
|
Description = "Filter by observation identifier (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var aliasOption = new Option<string[]>("--alias")
|
|
{
|
|
Description = "Filter by vulnerability alias (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var purlOption = new Option<string[]>("--purl")
|
|
{
|
|
Description = "Filter by Package URL (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var cpeOption = new Option<string[]>("--cpe")
|
|
{
|
|
Description = "Filter by CPE value (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var jsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Emit raw JSON payload instead of a table."
|
|
};
|
|
var limitOption = new Option<int?>("--limit")
|
|
{
|
|
Description = "Maximum number of observations to return (default 200, max 500)."
|
|
};
|
|
var cursorOption = new Option<string?>("--cursor")
|
|
{
|
|
Description = "Opaque cursor token returned by a previous page."
|
|
};
|
|
|
|
observations.Add(tenantOption);
|
|
observations.Add(observationIdOption);
|
|
observations.Add(aliasOption);
|
|
observations.Add(purlOption);
|
|
observations.Add(cpeOption);
|
|
observations.Add(limitOption);
|
|
observations.Add(cursorOption);
|
|
observations.Add(jsonOption);
|
|
|
|
observations.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption) ?? string.Empty;
|
|
var observationIds = parseResult.GetValue(observationIdOption) ?? Array.Empty<string>();
|
|
var aliases = parseResult.GetValue(aliasOption) ?? Array.Empty<string>();
|
|
var purls = parseResult.GetValue(purlOption) ?? Array.Empty<string>();
|
|
var cpes = parseResult.GetValue(cpeOption) ?? Array.Empty<string>();
|
|
var limit = parseResult.GetValue(limitOption);
|
|
var cursor = parseResult.GetValue(cursorOption);
|
|
var emitJson = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleVulnObservationsAsync(
|
|
services,
|
|
tenant,
|
|
observationIds,
|
|
aliases,
|
|
purls,
|
|
cpes,
|
|
limit,
|
|
cursor,
|
|
emitJson,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
vuln.Add(observations);
|
|
|
|
// CLI-VULN-29-001: Vulnerability explorer list command
|
|
var list = new Command("list", "List vulnerabilities with grouping, filters, and pagination.");
|
|
|
|
var listVulnIdOption = new Option<string?>("--vuln-id")
|
|
{
|
|
Description = "Filter by vulnerability identifier (e.g., CVE-2024-1234)."
|
|
};
|
|
var listSeverityOption = new Option<string?>("--severity")
|
|
{
|
|
Description = "Filter by severity level (critical, high, medium, low)."
|
|
};
|
|
var listStatusOption = new Option<string?>("--status")
|
|
{
|
|
Description = "Filter by status (open, triaged, accepted, fixed, etc.)."
|
|
};
|
|
var listPurlOption = new Option<string?>("--purl")
|
|
{
|
|
Description = "Filter by Package URL."
|
|
};
|
|
var listCpeOption = new Option<string?>("--cpe")
|
|
{
|
|
Description = "Filter by CPE value."
|
|
};
|
|
var listSbomIdOption = new Option<string?>("--sbom-id")
|
|
{
|
|
Description = "Filter by SBOM identifier."
|
|
};
|
|
var listPolicyIdOption = new Option<string?>("--policy-id")
|
|
{
|
|
Description = "Filter by policy identifier."
|
|
};
|
|
var listPolicyVersionOption = new Option<int?>("--policy-version")
|
|
{
|
|
Description = "Filter by policy version."
|
|
};
|
|
var listGroupByOption = new Option<string?>("--group-by")
|
|
{
|
|
Description = "Group results by field (vuln, package, severity, status)."
|
|
};
|
|
var listLimitOption = new Option<int?>("--limit")
|
|
{
|
|
Description = "Maximum number of items to return (default 50, max 500)."
|
|
};
|
|
var listOffsetOption = new Option<int?>("--offset")
|
|
{
|
|
Description = "Number of items to skip for pagination."
|
|
};
|
|
var listCursorOption = new Option<string?>("--cursor")
|
|
{
|
|
Description = "Opaque cursor token returned by a previous page."
|
|
};
|
|
var listTenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Tenant identifier (overrides profile/environment)."
|
|
};
|
|
var listJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Emit raw JSON payload instead of a table."
|
|
};
|
|
var listCsvOption = new Option<bool>("--csv")
|
|
{
|
|
Description = "Emit CSV format instead of a table."
|
|
};
|
|
|
|
list.Add(listVulnIdOption);
|
|
list.Add(listSeverityOption);
|
|
list.Add(listStatusOption);
|
|
list.Add(listPurlOption);
|
|
list.Add(listCpeOption);
|
|
list.Add(listSbomIdOption);
|
|
list.Add(listPolicyIdOption);
|
|
list.Add(listPolicyVersionOption);
|
|
list.Add(listGroupByOption);
|
|
list.Add(listLimitOption);
|
|
list.Add(listOffsetOption);
|
|
list.Add(listCursorOption);
|
|
list.Add(listTenantOption);
|
|
list.Add(listJsonOption);
|
|
list.Add(listCsvOption);
|
|
list.Add(verboseOption);
|
|
|
|
list.SetAction((parseResult, _) =>
|
|
{
|
|
var vulnId = parseResult.GetValue(listVulnIdOption);
|
|
var severity = parseResult.GetValue(listSeverityOption);
|
|
var status = parseResult.GetValue(listStatusOption);
|
|
var purl = parseResult.GetValue(listPurlOption);
|
|
var cpe = parseResult.GetValue(listCpeOption);
|
|
var sbomId = parseResult.GetValue(listSbomIdOption);
|
|
var policyId = parseResult.GetValue(listPolicyIdOption);
|
|
var policyVersion = parseResult.GetValue(listPolicyVersionOption);
|
|
var groupBy = parseResult.GetValue(listGroupByOption);
|
|
var limit = parseResult.GetValue(listLimitOption);
|
|
var offset = parseResult.GetValue(listOffsetOption);
|
|
var cursor = parseResult.GetValue(listCursorOption);
|
|
var tenant = parseResult.GetValue(listTenantOption);
|
|
var emitJson = parseResult.GetValue(listJsonOption);
|
|
var emitCsv = parseResult.GetValue(listCsvOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleVulnListAsync(
|
|
services,
|
|
vulnId,
|
|
severity,
|
|
status,
|
|
purl,
|
|
cpe,
|
|
sbomId,
|
|
policyId,
|
|
policyVersion,
|
|
groupBy,
|
|
limit,
|
|
offset,
|
|
cursor,
|
|
tenant,
|
|
emitJson,
|
|
emitCsv,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
vuln.Add(list);
|
|
|
|
// CLI-VULN-29-002: Vulnerability show command
|
|
var show = new Command("show", "Display detailed vulnerability information including evidence, rationale, paths, and ledger.");
|
|
|
|
var showVulnIdArg = new Argument<string>("vulnerability-id")
|
|
{
|
|
Description = "Vulnerability identifier (e.g., CVE-2024-1234)."
|
|
};
|
|
var showTenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Tenant identifier (overrides profile/environment)."
|
|
};
|
|
var showJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Emit raw JSON payload instead of formatted output."
|
|
};
|
|
|
|
show.Add(showVulnIdArg);
|
|
show.Add(showTenantOption);
|
|
show.Add(showJsonOption);
|
|
show.Add(verboseOption);
|
|
|
|
show.SetAction((parseResult, _) =>
|
|
{
|
|
var vulnIdVal = parseResult.GetValue(showVulnIdArg) ?? string.Empty;
|
|
var tenantVal = parseResult.GetValue(showTenantOption);
|
|
var emitJson = parseResult.GetValue(showJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleVulnShowAsync(
|
|
services,
|
|
vulnIdVal,
|
|
tenantVal,
|
|
emitJson,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
vuln.Add(show);
|
|
|
|
// CLI-VULN-29-003: Workflow commands
|
|
// Common options for workflow commands
|
|
var wfVulnIdsOption = new Option<string[]>("--vuln-id")
|
|
{
|
|
Description = "Vulnerability IDs to operate on (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var wfFilterSeverityOption = new Option<string?>("--filter-severity")
|
|
{
|
|
Description = "Filter vulnerabilities by severity (critical, high, medium, low)."
|
|
};
|
|
var wfFilterStatusOption = new Option<string?>("--filter-status")
|
|
{
|
|
Description = "Filter vulnerabilities by current status."
|
|
};
|
|
var wfFilterPurlOption = new Option<string?>("--filter-purl")
|
|
{
|
|
Description = "Filter vulnerabilities by Package URL."
|
|
};
|
|
var wfFilterSbomOption = new Option<string?>("--filter-sbom")
|
|
{
|
|
Description = "Filter vulnerabilities by SBOM ID."
|
|
};
|
|
var wfTenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Tenant identifier (overrides profile/environment)."
|
|
};
|
|
var wfIdempotencyKeyOption = new Option<string?>("--idempotency-key")
|
|
{
|
|
Description = "Idempotency key for retry-safe operations."
|
|
};
|
|
var wfJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Emit raw JSON response."
|
|
};
|
|
|
|
// assign command
|
|
var assign = new Command("assign", "Assign vulnerabilities to a user.");
|
|
var assignAssigneeArg = new Argument<string>("assignee") { Description = "Username or email to assign to." };
|
|
assign.Add(assignAssigneeArg);
|
|
assign.Add(wfVulnIdsOption);
|
|
assign.Add(wfFilterSeverityOption);
|
|
assign.Add(wfFilterStatusOption);
|
|
assign.Add(wfFilterPurlOption);
|
|
assign.Add(wfFilterSbomOption);
|
|
assign.Add(wfTenantOption);
|
|
assign.Add(wfIdempotencyKeyOption);
|
|
assign.Add(wfJsonOption);
|
|
assign.Add(verboseOption);
|
|
assign.SetAction((parseResult, _) => CommandHandlers.HandleVulnWorkflowAsync(
|
|
services, "assign", parseResult.GetValue(wfVulnIdsOption) ?? Array.Empty<string>(),
|
|
parseResult.GetValue(wfFilterSeverityOption), parseResult.GetValue(wfFilterStatusOption),
|
|
parseResult.GetValue(wfFilterPurlOption), parseResult.GetValue(wfFilterSbomOption),
|
|
parseResult.GetValue(wfTenantOption), parseResult.GetValue(wfIdempotencyKeyOption),
|
|
parseResult.GetValue(wfJsonOption), parseResult.GetValue(verboseOption),
|
|
parseResult.GetValue(assignAssigneeArg), null, null, null, null, cancellationToken));
|
|
vuln.Add(assign);
|
|
|
|
// comment command
|
|
var comment = new Command("comment", "Add a comment to vulnerabilities.");
|
|
var commentTextArg = new Argument<string>("text") { Description = "Comment text to add." };
|
|
comment.Add(commentTextArg);
|
|
comment.Add(wfVulnIdsOption);
|
|
comment.Add(wfFilterSeverityOption);
|
|
comment.Add(wfFilterStatusOption);
|
|
comment.Add(wfFilterPurlOption);
|
|
comment.Add(wfFilterSbomOption);
|
|
comment.Add(wfTenantOption);
|
|
comment.Add(wfIdempotencyKeyOption);
|
|
comment.Add(wfJsonOption);
|
|
comment.Add(verboseOption);
|
|
comment.SetAction((parseResult, _) => CommandHandlers.HandleVulnWorkflowAsync(
|
|
services, "comment", parseResult.GetValue(wfVulnIdsOption) ?? Array.Empty<string>(),
|
|
parseResult.GetValue(wfFilterSeverityOption), parseResult.GetValue(wfFilterStatusOption),
|
|
parseResult.GetValue(wfFilterPurlOption), parseResult.GetValue(wfFilterSbomOption),
|
|
parseResult.GetValue(wfTenantOption), parseResult.GetValue(wfIdempotencyKeyOption),
|
|
parseResult.GetValue(wfJsonOption), parseResult.GetValue(verboseOption),
|
|
null, parseResult.GetValue(commentTextArg), null, null, null, cancellationToken));
|
|
vuln.Add(comment);
|
|
|
|
// accept-risk command
|
|
var acceptRisk = new Command("accept-risk", "Accept risk for vulnerabilities with justification.");
|
|
var acceptJustificationArg = new Argument<string>("justification") { Description = "Justification for accepting the risk." };
|
|
var acceptDueDateOption = new Option<string?>("--due-date") { Description = "Due date for risk review (ISO-8601)." };
|
|
acceptRisk.Add(acceptJustificationArg);
|
|
acceptRisk.Add(acceptDueDateOption);
|
|
acceptRisk.Add(wfVulnIdsOption);
|
|
acceptRisk.Add(wfFilterSeverityOption);
|
|
acceptRisk.Add(wfFilterStatusOption);
|
|
acceptRisk.Add(wfFilterPurlOption);
|
|
acceptRisk.Add(wfFilterSbomOption);
|
|
acceptRisk.Add(wfTenantOption);
|
|
acceptRisk.Add(wfIdempotencyKeyOption);
|
|
acceptRisk.Add(wfJsonOption);
|
|
acceptRisk.Add(verboseOption);
|
|
acceptRisk.SetAction((parseResult, _) => CommandHandlers.HandleVulnWorkflowAsync(
|
|
services, "accept_risk", parseResult.GetValue(wfVulnIdsOption) ?? Array.Empty<string>(),
|
|
parseResult.GetValue(wfFilterSeverityOption), parseResult.GetValue(wfFilterStatusOption),
|
|
parseResult.GetValue(wfFilterPurlOption), parseResult.GetValue(wfFilterSbomOption),
|
|
parseResult.GetValue(wfTenantOption), parseResult.GetValue(wfIdempotencyKeyOption),
|
|
parseResult.GetValue(wfJsonOption), parseResult.GetValue(verboseOption),
|
|
null, null, parseResult.GetValue(acceptJustificationArg), parseResult.GetValue(acceptDueDateOption), null, cancellationToken));
|
|
vuln.Add(acceptRisk);
|
|
|
|
// verify-fix command
|
|
var verifyFix = new Command("verify-fix", "Mark vulnerabilities as fixed and verified.");
|
|
var fixVersionOption = new Option<string?>("--fix-version") { Description = "Version where the fix was applied." };
|
|
var fixCommentOption = new Option<string?>("--comment") { Description = "Optional comment about the fix." };
|
|
verifyFix.Add(fixVersionOption);
|
|
verifyFix.Add(fixCommentOption);
|
|
verifyFix.Add(wfVulnIdsOption);
|
|
verifyFix.Add(wfFilterSeverityOption);
|
|
verifyFix.Add(wfFilterStatusOption);
|
|
verifyFix.Add(wfFilterPurlOption);
|
|
verifyFix.Add(wfFilterSbomOption);
|
|
verifyFix.Add(wfTenantOption);
|
|
verifyFix.Add(wfIdempotencyKeyOption);
|
|
verifyFix.Add(wfJsonOption);
|
|
verifyFix.Add(verboseOption);
|
|
verifyFix.SetAction((parseResult, _) => CommandHandlers.HandleVulnWorkflowAsync(
|
|
services, "verify_fix", parseResult.GetValue(wfVulnIdsOption) ?? Array.Empty<string>(),
|
|
parseResult.GetValue(wfFilterSeverityOption), parseResult.GetValue(wfFilterStatusOption),
|
|
parseResult.GetValue(wfFilterPurlOption), parseResult.GetValue(wfFilterSbomOption),
|
|
parseResult.GetValue(wfTenantOption), parseResult.GetValue(wfIdempotencyKeyOption),
|
|
parseResult.GetValue(wfJsonOption), parseResult.GetValue(verboseOption),
|
|
null, parseResult.GetValue(fixCommentOption), null, null, parseResult.GetValue(fixVersionOption), cancellationToken));
|
|
vuln.Add(verifyFix);
|
|
|
|
// target-fix command
|
|
var targetFix = new Command("target-fix", "Set a target fix date for vulnerabilities.");
|
|
var targetDueDateArg = new Argument<string>("due-date") { Description = "Target fix date (ISO-8601 format, e.g., 2024-12-31)." };
|
|
var targetCommentOption = new Option<string?>("--comment") { Description = "Optional comment about the target." };
|
|
targetFix.Add(targetDueDateArg);
|
|
targetFix.Add(targetCommentOption);
|
|
targetFix.Add(wfVulnIdsOption);
|
|
targetFix.Add(wfFilterSeverityOption);
|
|
targetFix.Add(wfFilterStatusOption);
|
|
targetFix.Add(wfFilterPurlOption);
|
|
targetFix.Add(wfFilterSbomOption);
|
|
targetFix.Add(wfTenantOption);
|
|
targetFix.Add(wfIdempotencyKeyOption);
|
|
targetFix.Add(wfJsonOption);
|
|
targetFix.Add(verboseOption);
|
|
targetFix.SetAction((parseResult, _) => CommandHandlers.HandleVulnWorkflowAsync(
|
|
services, "target_fix", parseResult.GetValue(wfVulnIdsOption) ?? Array.Empty<string>(),
|
|
parseResult.GetValue(wfFilterSeverityOption), parseResult.GetValue(wfFilterStatusOption),
|
|
parseResult.GetValue(wfFilterPurlOption), parseResult.GetValue(wfFilterSbomOption),
|
|
parseResult.GetValue(wfTenantOption), parseResult.GetValue(wfIdempotencyKeyOption),
|
|
parseResult.GetValue(wfJsonOption), parseResult.GetValue(verboseOption),
|
|
null, parseResult.GetValue(targetCommentOption), null, parseResult.GetValue(targetDueDateArg), null, cancellationToken));
|
|
vuln.Add(targetFix);
|
|
|
|
// reopen command
|
|
var reopen = new Command("reopen", "Reopen closed or accepted vulnerabilities.");
|
|
var reopenCommentOption = new Option<string?>("--comment") { Description = "Reason for reopening." };
|
|
reopen.Add(reopenCommentOption);
|
|
reopen.Add(wfVulnIdsOption);
|
|
reopen.Add(wfFilterSeverityOption);
|
|
reopen.Add(wfFilterStatusOption);
|
|
reopen.Add(wfFilterPurlOption);
|
|
reopen.Add(wfFilterSbomOption);
|
|
reopen.Add(wfTenantOption);
|
|
reopen.Add(wfIdempotencyKeyOption);
|
|
reopen.Add(wfJsonOption);
|
|
reopen.Add(verboseOption);
|
|
reopen.SetAction((parseResult, _) => CommandHandlers.HandleVulnWorkflowAsync(
|
|
services, "reopen", parseResult.GetValue(wfVulnIdsOption) ?? Array.Empty<string>(),
|
|
parseResult.GetValue(wfFilterSeverityOption), parseResult.GetValue(wfFilterStatusOption),
|
|
parseResult.GetValue(wfFilterPurlOption), parseResult.GetValue(wfFilterSbomOption),
|
|
parseResult.GetValue(wfTenantOption), parseResult.GetValue(wfIdempotencyKeyOption),
|
|
parseResult.GetValue(wfJsonOption), parseResult.GetValue(verboseOption),
|
|
null, parseResult.GetValue(reopenCommentOption), null, null, null, cancellationToken));
|
|
vuln.Add(reopen);
|
|
|
|
// CLI-VULN-29-004: simulate command
|
|
var simulate = new Command("simulate", "Simulate policy/VEX changes and show delta summaries.");
|
|
var simPolicyIdOption = new Option<string?>("--policy-id")
|
|
{
|
|
Description = "Policy ID to simulate (uses different version or a new policy)."
|
|
};
|
|
var simPolicyVersionOption = new Option<int?>("--policy-version")
|
|
{
|
|
Description = "Policy version to simulate against."
|
|
};
|
|
var simVexOverrideOption = new Option<string[]>("--vex-override")
|
|
{
|
|
Description = "VEX status overrides in format vulnId=status (e.g., CVE-2024-1234=not_affected).",
|
|
AllowMultipleArgumentsPerToken = true
|
|
};
|
|
var simSeverityThresholdOption = new Option<string?>("--severity-threshold")
|
|
{
|
|
Description = "Severity threshold for simulation (critical, high, medium, low)."
|
|
};
|
|
var simSbomIdsOption = new Option<string[]>("--sbom-id")
|
|
{
|
|
Description = "SBOM IDs to include in simulation scope.",
|
|
AllowMultipleArgumentsPerToken = true
|
|
};
|
|
var simOutputMarkdownOption = new Option<bool>("--markdown")
|
|
{
|
|
Description = "Include Markdown report suitable for CI pipelines."
|
|
};
|
|
var simChangedOnlyOption = new Option<bool>("--changed-only")
|
|
{
|
|
Description = "Only show items that changed."
|
|
};
|
|
var simTenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Tenant identifier for multi-tenant environments."
|
|
};
|
|
var simJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON for automation."
|
|
};
|
|
var simOutputFileOption = new Option<string?>("--output")
|
|
{
|
|
Description = "Write Markdown report to file instead of console."
|
|
};
|
|
simulate.Add(simPolicyIdOption);
|
|
simulate.Add(simPolicyVersionOption);
|
|
simulate.Add(simVexOverrideOption);
|
|
simulate.Add(simSeverityThresholdOption);
|
|
simulate.Add(simSbomIdsOption);
|
|
simulate.Add(simOutputMarkdownOption);
|
|
simulate.Add(simChangedOnlyOption);
|
|
simulate.Add(simTenantOption);
|
|
simulate.Add(simJsonOption);
|
|
simulate.Add(simOutputFileOption);
|
|
simulate.Add(verboseOption);
|
|
simulate.SetAction((parseResult, _) => CommandHandlers.HandleVulnSimulateAsync(
|
|
services,
|
|
parseResult.GetValue(simPolicyIdOption),
|
|
parseResult.GetValue(simPolicyVersionOption),
|
|
parseResult.GetValue(simVexOverrideOption) ?? Array.Empty<string>(),
|
|
parseResult.GetValue(simSeverityThresholdOption),
|
|
parseResult.GetValue(simSbomIdsOption) ?? Array.Empty<string>(),
|
|
parseResult.GetValue(simOutputMarkdownOption),
|
|
parseResult.GetValue(simChangedOnlyOption),
|
|
parseResult.GetValue(simTenantOption),
|
|
parseResult.GetValue(simJsonOption),
|
|
parseResult.GetValue(simOutputFileOption),
|
|
parseResult.GetValue(verboseOption),
|
|
cancellationToken));
|
|
vuln.Add(simulate);
|
|
|
|
// CLI-VULN-29-005: export command with verify subcommand
|
|
var export = new Command("export", "Export vulnerability evidence bundles.");
|
|
var expVulnIdsOption = new Option<string[]>("--vuln-id")
|
|
{
|
|
Description = "Vulnerability IDs to include in export.",
|
|
AllowMultipleArgumentsPerToken = true
|
|
};
|
|
var expSbomIdsOption = new Option<string[]>("--sbom-id")
|
|
{
|
|
Description = "SBOM IDs to include in export scope.",
|
|
AllowMultipleArgumentsPerToken = true
|
|
};
|
|
var expPolicyIdOption = new Option<string?>("--policy-id")
|
|
{
|
|
Description = "Policy ID for export filtering."
|
|
};
|
|
var expFormatOption = new Option<string>("--format")
|
|
{
|
|
Description = "Export format (ndjson, json).",
|
|
DefaultValueFactory = _ => "ndjson"
|
|
};
|
|
var expIncludeEvidenceOption = new Option<bool>("--include-evidence")
|
|
{
|
|
Description = "Include evidence data in export (default: true).",
|
|
DefaultValueFactory = _ => true
|
|
};
|
|
var expIncludeLedgerOption = new Option<bool>("--include-ledger")
|
|
{
|
|
Description = "Include workflow ledger in export (default: true).",
|
|
DefaultValueFactory = _ => true
|
|
};
|
|
var expSignedOption = new Option<bool>("--signed")
|
|
{
|
|
Description = "Request signed export bundle (default: true).",
|
|
DefaultValueFactory = _ => true
|
|
};
|
|
var expOutputOption = new Option<string>("--output")
|
|
{
|
|
Description = "Output file path for the export bundle.",
|
|
Required = true
|
|
};
|
|
var expTenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Tenant identifier for multi-tenant environments."
|
|
};
|
|
export.Add(expVulnIdsOption);
|
|
export.Add(expSbomIdsOption);
|
|
export.Add(expPolicyIdOption);
|
|
export.Add(expFormatOption);
|
|
export.Add(expIncludeEvidenceOption);
|
|
export.Add(expIncludeLedgerOption);
|
|
export.Add(expSignedOption);
|
|
export.Add(expOutputOption);
|
|
export.Add(expTenantOption);
|
|
export.Add(verboseOption);
|
|
export.SetAction((parseResult, _) => CommandHandlers.HandleVulnExportAsync(
|
|
services,
|
|
parseResult.GetValue(expVulnIdsOption) ?? Array.Empty<string>(),
|
|
parseResult.GetValue(expSbomIdsOption) ?? Array.Empty<string>(),
|
|
parseResult.GetValue(expPolicyIdOption),
|
|
parseResult.GetValue(expFormatOption) ?? "ndjson",
|
|
parseResult.GetValue(expIncludeEvidenceOption),
|
|
parseResult.GetValue(expIncludeLedgerOption),
|
|
parseResult.GetValue(expSignedOption),
|
|
parseResult.GetValue(expOutputOption) ?? "",
|
|
parseResult.GetValue(expTenantOption),
|
|
parseResult.GetValue(verboseOption),
|
|
cancellationToken));
|
|
|
|
// verify subcommand
|
|
var verify = new Command("verify", "Verify signature and digest of an exported vulnerability bundle.");
|
|
var verifyFileArg = new Argument<string>("file")
|
|
{
|
|
Description = "Path to the export bundle file to verify."
|
|
};
|
|
var verifyExpectedDigestOption = new Option<string?>("--expected-digest")
|
|
{
|
|
Description = "Expected digest to verify (sha256:hex format)."
|
|
};
|
|
var verifyPublicKeyOption = new Option<string?>("--public-key")
|
|
{
|
|
Description = "Path to public key file for signature verification."
|
|
};
|
|
verify.Add(verifyFileArg);
|
|
verify.Add(verifyExpectedDigestOption);
|
|
verify.Add(verifyPublicKeyOption);
|
|
verify.Add(verboseOption);
|
|
verify.SetAction((parseResult, _) => CommandHandlers.HandleVulnExportVerifyAsync(
|
|
services,
|
|
parseResult.GetValue(verifyFileArg) ?? "",
|
|
parseResult.GetValue(verifyExpectedDigestOption),
|
|
parseResult.GetValue(verifyPublicKeyOption),
|
|
parseResult.GetValue(verboseOption),
|
|
cancellationToken));
|
|
export.Add(verify);
|
|
|
|
vuln.Add(export);
|
|
|
|
return vuln;
|
|
}
|
|
|
|
// CLI-VEX-30-001: VEX consensus commands
|
|
private static Command BuildVexCommand(IServiceProvider services, StellaOpsCliOptions options, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var vex = new Command("vex", "Manage VEX (Vulnerability Exploitability eXchange) consensus data.");
|
|
|
|
var consensus = new Command("consensus", "Explore VEX consensus decisions.");
|
|
var list = new Command("list", "List VEX consensus decisions with filters and pagination.");
|
|
|
|
var vulnIdOption = new Option<string?>("--vuln-id")
|
|
{
|
|
Description = "Filter by vulnerability identifier (e.g., CVE-2024-1234)."
|
|
};
|
|
var productKeyOption = new Option<string?>("--product-key")
|
|
{
|
|
Description = "Filter by product key."
|
|
};
|
|
var purlOption = new Option<string?>("--purl")
|
|
{
|
|
Description = "Filter by Package URL."
|
|
};
|
|
var statusOption = new Option<string?>("--status")
|
|
{
|
|
Description = "Filter by VEX status (affected, not_affected, fixed, under_investigation)."
|
|
};
|
|
var policyVersionOption = new Option<string?>("--policy-version")
|
|
{
|
|
Description = "Filter by policy version."
|
|
};
|
|
var limitOption = new Option<int?>("--limit")
|
|
{
|
|
Description = "Maximum number of results (default 50)."
|
|
};
|
|
var offsetOption = new Option<int?>("--offset")
|
|
{
|
|
Description = "Number of results to skip for pagination."
|
|
};
|
|
var tenantOption = new Option<string?>("--tenant", new[] { "-t" })
|
|
{
|
|
Description = "Tenant identifier. Overrides profile and STELLAOPS_TENANT environment variable."
|
|
};
|
|
var jsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Emit raw JSON payload instead of a table."
|
|
};
|
|
var csvOption = new Option<bool>("--csv")
|
|
{
|
|
Description = "Emit CSV format instead of a table."
|
|
};
|
|
|
|
list.Add(vulnIdOption);
|
|
list.Add(productKeyOption);
|
|
list.Add(purlOption);
|
|
list.Add(statusOption);
|
|
list.Add(policyVersionOption);
|
|
list.Add(limitOption);
|
|
list.Add(offsetOption);
|
|
list.Add(tenantOption);
|
|
list.Add(jsonOption);
|
|
list.Add(csvOption);
|
|
|
|
list.SetAction((parseResult, _) =>
|
|
{
|
|
var vulnId = parseResult.GetValue(vulnIdOption);
|
|
var productKey = parseResult.GetValue(productKeyOption);
|
|
var purl = parseResult.GetValue(purlOption);
|
|
var status = parseResult.GetValue(statusOption);
|
|
var policyVersion = parseResult.GetValue(policyVersionOption);
|
|
var limit = parseResult.GetValue(limitOption);
|
|
var offset = parseResult.GetValue(offsetOption);
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var emitJson = parseResult.GetValue(jsonOption);
|
|
var emitCsv = parseResult.GetValue(csvOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleVexConsensusListAsync(
|
|
services,
|
|
vulnId,
|
|
productKey,
|
|
purl,
|
|
status,
|
|
policyVersion,
|
|
limit,
|
|
offset,
|
|
tenant,
|
|
emitJson,
|
|
emitCsv,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
// CLI-VEX-30-002: show subcommand
|
|
var show = new Command("show", "Display detailed VEX consensus including quorum, evidence, rationale, and signature status.");
|
|
|
|
var showVulnIdArg = new Argument<string>("vulnerability-id")
|
|
{
|
|
Description = "Vulnerability identifier (e.g., CVE-2024-1234)."
|
|
};
|
|
var showProductKeyArg = new Argument<string>("product-key")
|
|
{
|
|
Description = "Product key identifying the affected component."
|
|
};
|
|
var showTenantOption = new Option<string?>("--tenant", new[] { "-t" })
|
|
{
|
|
Description = "Tenant identifier. Overrides profile and STELLAOPS_TENANT environment variable."
|
|
};
|
|
var showJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Emit raw JSON payload instead of formatted output."
|
|
};
|
|
|
|
show.Add(showVulnIdArg);
|
|
show.Add(showProductKeyArg);
|
|
show.Add(showTenantOption);
|
|
show.Add(showJsonOption);
|
|
|
|
show.SetAction((parseResult, _) =>
|
|
{
|
|
var vulnId = parseResult.GetValue(showVulnIdArg) ?? string.Empty;
|
|
var productKey = parseResult.GetValue(showProductKeyArg) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(showTenantOption);
|
|
var emitJson = parseResult.GetValue(showJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleVexConsensusShowAsync(
|
|
services,
|
|
vulnId,
|
|
productKey,
|
|
tenant,
|
|
emitJson,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
consensus.Add(list);
|
|
consensus.Add(show);
|
|
vex.Add(consensus);
|
|
|
|
// CLI-VEX-30-003: simulate command
|
|
var simulate = new Command("simulate", "Simulate VEX consensus with trust/threshold overrides to preview changes.");
|
|
|
|
var simVulnIdOption = new Option<string?>("--vuln-id")
|
|
{
|
|
Description = "Filter by vulnerability identifier."
|
|
};
|
|
var simProductKeyOption = new Option<string?>("--product-key")
|
|
{
|
|
Description = "Filter by product key."
|
|
};
|
|
var simPurlOption = new Option<string?>("--purl")
|
|
{
|
|
Description = "Filter by Package URL."
|
|
};
|
|
var simThresholdOption = new Option<double?>("--threshold")
|
|
{
|
|
Description = "Override the weight threshold for consensus (0.0-1.0)."
|
|
};
|
|
var simQuorumOption = new Option<int?>("--quorum")
|
|
{
|
|
Description = "Override the minimum quorum requirement."
|
|
};
|
|
var simTrustOption = new Option<string[]>("--trust", new[] { "-w" })
|
|
{
|
|
Description = "Trust weight override in format provider=weight (repeatable). Example: --trust nvd=1.5 --trust vendor=2.0",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var simExcludeOption = new Option<string[]>("--exclude")
|
|
{
|
|
Description = "Exclude provider from simulation (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var simIncludeOnlyOption = new Option<string[]>("--include-only")
|
|
{
|
|
Description = "Include only these providers (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var simTenantOption = new Option<string?>("--tenant", new[] { "-t" })
|
|
{
|
|
Description = "Tenant identifier."
|
|
};
|
|
var simJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Emit raw JSON output with full diff details."
|
|
};
|
|
var simChangedOnlyOption = new Option<bool>("--changed-only")
|
|
{
|
|
Description = "Show only items where the status changed."
|
|
};
|
|
|
|
simulate.Add(simVulnIdOption);
|
|
simulate.Add(simProductKeyOption);
|
|
simulate.Add(simPurlOption);
|
|
simulate.Add(simThresholdOption);
|
|
simulate.Add(simQuorumOption);
|
|
simulate.Add(simTrustOption);
|
|
simulate.Add(simExcludeOption);
|
|
simulate.Add(simIncludeOnlyOption);
|
|
simulate.Add(simTenantOption);
|
|
simulate.Add(simJsonOption);
|
|
simulate.Add(simChangedOnlyOption);
|
|
|
|
simulate.SetAction((parseResult, _) =>
|
|
{
|
|
var vulnId = parseResult.GetValue(simVulnIdOption);
|
|
var productKey = parseResult.GetValue(simProductKeyOption);
|
|
var purl = parseResult.GetValue(simPurlOption);
|
|
var threshold = parseResult.GetValue(simThresholdOption);
|
|
var quorum = parseResult.GetValue(simQuorumOption);
|
|
var trustOverrides = parseResult.GetValue(simTrustOption) ?? Array.Empty<string>();
|
|
var exclude = parseResult.GetValue(simExcludeOption) ?? Array.Empty<string>();
|
|
var includeOnly = parseResult.GetValue(simIncludeOnlyOption) ?? Array.Empty<string>();
|
|
var tenant = parseResult.GetValue(simTenantOption);
|
|
var emitJson = parseResult.GetValue(simJsonOption);
|
|
var changedOnly = parseResult.GetValue(simChangedOnlyOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleVexSimulateAsync(
|
|
services,
|
|
vulnId,
|
|
productKey,
|
|
purl,
|
|
threshold,
|
|
quorum,
|
|
trustOverrides,
|
|
exclude,
|
|
includeOnly,
|
|
tenant,
|
|
emitJson,
|
|
changedOnly,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
vex.Add(simulate);
|
|
|
|
// CLI-VEX-30-004: export command
|
|
var export = new Command("export", "Export VEX consensus data as NDJSON bundle with optional signature.");
|
|
|
|
var expVulnIdsOption = new Option<string[]>("--vuln-id")
|
|
{
|
|
Description = "Filter by vulnerability identifiers (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var expProductKeysOption = new Option<string[]>("--product-key")
|
|
{
|
|
Description = "Filter by product keys (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var expPurlsOption = new Option<string[]>("--purl")
|
|
{
|
|
Description = "Filter by Package URLs (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var expStatusesOption = new Option<string[]>("--status")
|
|
{
|
|
Description = "Filter by VEX statuses (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var expPolicyVersionOption = new Option<string?>("--policy-version")
|
|
{
|
|
Description = "Filter by policy version."
|
|
};
|
|
var expOutputOption = new Option<string>("--output", new[] { "-o" })
|
|
{
|
|
Description = "Output file path for the NDJSON bundle.",
|
|
Required = true
|
|
};
|
|
var expUnsignedOption = new Option<bool>("--unsigned")
|
|
{
|
|
Description = "Generate unsigned export (default is signed)."
|
|
};
|
|
var expTenantOption = new Option<string?>("--tenant", new[] { "-t" })
|
|
{
|
|
Description = "Tenant identifier."
|
|
};
|
|
|
|
export.Add(expVulnIdsOption);
|
|
export.Add(expProductKeysOption);
|
|
export.Add(expPurlsOption);
|
|
export.Add(expStatusesOption);
|
|
export.Add(expPolicyVersionOption);
|
|
export.Add(expOutputOption);
|
|
export.Add(expUnsignedOption);
|
|
export.Add(expTenantOption);
|
|
|
|
export.SetAction((parseResult, _) =>
|
|
{
|
|
var vulnIds = parseResult.GetValue(expVulnIdsOption) ?? Array.Empty<string>();
|
|
var productKeys = parseResult.GetValue(expProductKeysOption) ?? Array.Empty<string>();
|
|
var purls = parseResult.GetValue(expPurlsOption) ?? Array.Empty<string>();
|
|
var statuses = parseResult.GetValue(expStatusesOption) ?? Array.Empty<string>();
|
|
var policyVersion = parseResult.GetValue(expPolicyVersionOption);
|
|
var output = parseResult.GetValue(expOutputOption) ?? string.Empty;
|
|
var unsigned = parseResult.GetValue(expUnsignedOption);
|
|
var tenant = parseResult.GetValue(expTenantOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleVexExportAsync(
|
|
services,
|
|
vulnIds,
|
|
productKeys,
|
|
purls,
|
|
statuses,
|
|
policyVersion,
|
|
output,
|
|
!unsigned,
|
|
tenant,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
// verify subcommand for signature verification
|
|
var verify = new Command("verify", "Verify signature and digest of a VEX export bundle.");
|
|
|
|
var verifyFileArg = new Argument<string>("file")
|
|
{
|
|
Description = "Path to the NDJSON export file to verify."
|
|
};
|
|
var verifyDigestOption = new Option<string?>("--digest")
|
|
{
|
|
Description = "Expected SHA-256 digest to verify."
|
|
};
|
|
var verifyKeyOption = new Option<string?>("--public-key")
|
|
{
|
|
Description = "Path to public key file for signature verification."
|
|
};
|
|
|
|
verify.Add(verifyFileArg);
|
|
verify.Add(verifyDigestOption);
|
|
verify.Add(verifyKeyOption);
|
|
|
|
verify.SetAction((parseResult, _) =>
|
|
{
|
|
var file = parseResult.GetValue(verifyFileArg) ?? string.Empty;
|
|
var digest = parseResult.GetValue(verifyDigestOption);
|
|
var publicKey = parseResult.GetValue(verifyKeyOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleVexVerifyAsync(
|
|
services,
|
|
file,
|
|
digest,
|
|
publicKey,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
export.Add(verify);
|
|
vex.Add(export);
|
|
|
|
// CLI-LNM-22-002: VEX observation commands
|
|
var obs = new Command("obs", "Query VEX observations (Link-Not-Merge architecture).");
|
|
|
|
// vex obs get
|
|
var obsGet = new Command("get", "Get VEX observations with filters.");
|
|
var obsGetTenantOption = new Option<string>("--tenant", new[] { "-t" })
|
|
{
|
|
Description = "Tenant identifier.",
|
|
Required = true
|
|
};
|
|
var obsGetVulnIdOption = new Option<string[]>("--vuln-id")
|
|
{
|
|
Description = "Filter by vulnerability IDs (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var obsGetProductKeyOption = new Option<string[]>("--product-key")
|
|
{
|
|
Description = "Filter by product keys (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var obsGetPurlOption = new Option<string[]>("--purl")
|
|
{
|
|
Description = "Filter by Package URLs (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var obsGetCpeOption = new Option<string[]>("--cpe")
|
|
{
|
|
Description = "Filter by CPEs (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var obsGetStatusOption = new Option<string[]>("--status")
|
|
{
|
|
Description = "Filter by status (affected, not_affected, fixed, under_investigation). Repeatable.",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var obsGetProviderOption = new Option<string[]>("--provider")
|
|
{
|
|
Description = "Filter by provider IDs (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var obsGetLimitOption = new Option<int?>("--limit", "-l")
|
|
{
|
|
Description = "Maximum number of results (default 50)."
|
|
};
|
|
var obsGetCursorOption = new Option<string?>("--cursor")
|
|
{
|
|
Description = "Pagination cursor from previous response."
|
|
};
|
|
var obsGetJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON for CI integration."
|
|
};
|
|
|
|
obsGet.Add(obsGetTenantOption);
|
|
obsGet.Add(obsGetVulnIdOption);
|
|
obsGet.Add(obsGetProductKeyOption);
|
|
obsGet.Add(obsGetPurlOption);
|
|
obsGet.Add(obsGetCpeOption);
|
|
obsGet.Add(obsGetStatusOption);
|
|
obsGet.Add(obsGetProviderOption);
|
|
obsGet.Add(obsGetLimitOption);
|
|
obsGet.Add(obsGetCursorOption);
|
|
obsGet.Add(obsGetJsonOption);
|
|
|
|
obsGet.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(obsGetTenantOption) ?? string.Empty;
|
|
var vulnIds = parseResult.GetValue(obsGetVulnIdOption) ?? Array.Empty<string>();
|
|
var productKeys = parseResult.GetValue(obsGetProductKeyOption) ?? Array.Empty<string>();
|
|
var purls = parseResult.GetValue(obsGetPurlOption) ?? Array.Empty<string>();
|
|
var cpes = parseResult.GetValue(obsGetCpeOption) ?? Array.Empty<string>();
|
|
var statuses = parseResult.GetValue(obsGetStatusOption) ?? Array.Empty<string>();
|
|
var providers = parseResult.GetValue(obsGetProviderOption) ?? Array.Empty<string>();
|
|
var limit = parseResult.GetValue(obsGetLimitOption);
|
|
var cursor = parseResult.GetValue(obsGetCursorOption);
|
|
var emitJson = parseResult.GetValue(obsGetJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleVexObsGetAsync(
|
|
services,
|
|
tenant,
|
|
vulnIds,
|
|
productKeys,
|
|
purls,
|
|
cpes,
|
|
statuses,
|
|
providers,
|
|
limit,
|
|
cursor,
|
|
emitJson,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
obs.Add(obsGet);
|
|
|
|
// vex linkset show
|
|
var linkset = new Command("linkset", "Explore VEX observation linksets.");
|
|
var linksetShow = new Command("show", "Show linked observations for a vulnerability.");
|
|
var linksetShowVulnIdArg = new Argument<string>("vulnerability-id")
|
|
{
|
|
Description = "Vulnerability identifier (e.g., CVE-2024-1234)."
|
|
};
|
|
var linksetShowTenantOption = new Option<string>("--tenant", new[] { "-t" })
|
|
{
|
|
Description = "Tenant identifier.",
|
|
Required = true
|
|
};
|
|
var linksetShowProductKeyOption = new Option<string[]>("--product-key")
|
|
{
|
|
Description = "Filter by product keys (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var linksetShowPurlOption = new Option<string[]>("--purl")
|
|
{
|
|
Description = "Filter by Package URLs (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var linksetShowStatusOption = new Option<string[]>("--status")
|
|
{
|
|
Description = "Filter by status (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var linksetShowJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON for CI integration."
|
|
};
|
|
|
|
linksetShow.Add(linksetShowVulnIdArg);
|
|
linksetShow.Add(linksetShowTenantOption);
|
|
linksetShow.Add(linksetShowProductKeyOption);
|
|
linksetShow.Add(linksetShowPurlOption);
|
|
linksetShow.Add(linksetShowStatusOption);
|
|
linksetShow.Add(linksetShowJsonOption);
|
|
|
|
linksetShow.SetAction((parseResult, _) =>
|
|
{
|
|
var vulnId = parseResult.GetValue(linksetShowVulnIdArg) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(linksetShowTenantOption) ?? string.Empty;
|
|
var productKeys = parseResult.GetValue(linksetShowProductKeyOption) ?? Array.Empty<string>();
|
|
var purls = parseResult.GetValue(linksetShowPurlOption) ?? Array.Empty<string>();
|
|
var statuses = parseResult.GetValue(linksetShowStatusOption) ?? Array.Empty<string>();
|
|
var emitJson = parseResult.GetValue(linksetShowJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleVexLinksetShowAsync(
|
|
services,
|
|
tenant,
|
|
vulnId,
|
|
productKeys,
|
|
purls,
|
|
statuses,
|
|
emitJson,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
linkset.Add(linksetShow);
|
|
obs.Add(linkset);
|
|
vex.Add(obs);
|
|
|
|
return vex;
|
|
}
|
|
|
|
private static Command BuildConfigCommand(StellaOpsCliOptions options)
|
|
{
|
|
var config = new Command("config", "Inspect CLI configuration state.");
|
|
var show = new Command("show", "Display resolved configuration values.");
|
|
|
|
show.SetAction((_, _) =>
|
|
{
|
|
var authority = options.Authority ?? new StellaOpsCliAuthorityOptions();
|
|
var lines = new[]
|
|
{
|
|
$"Backend URL: {MaskIfEmpty(options.BackendUrl)}",
|
|
$"Concelier URL: {MaskIfEmpty(options.ConcelierUrl)}",
|
|
$"API Key: {DescribeSecret(options.ApiKey)}",
|
|
$"Scanner Cache: {options.ScannerCacheDirectory}",
|
|
$"Results Directory: {options.ResultsDirectory}",
|
|
$"Default Runner: {options.DefaultRunner}",
|
|
$"Authority URL: {MaskIfEmpty(authority.Url)}",
|
|
$"Authority Client ID: {MaskIfEmpty(authority.ClientId)}",
|
|
$"Authority Client Secret: {DescribeSecret(authority.ClientSecret ?? string.Empty)}",
|
|
$"Authority Username: {MaskIfEmpty(authority.Username)}",
|
|
$"Authority Password: {DescribeSecret(authority.Password ?? string.Empty)}",
|
|
$"Authority Scope: {MaskIfEmpty(authority.Scope)}",
|
|
$"Authority Token Cache: {MaskIfEmpty(authority.TokenCacheDirectory ?? string.Empty)}"
|
|
};
|
|
|
|
foreach (var line in lines)
|
|
{
|
|
Console.WriteLine(line);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
});
|
|
|
|
config.Add(show);
|
|
return config;
|
|
}
|
|
|
|
private static string MaskIfEmpty(string value)
|
|
=> string.IsNullOrWhiteSpace(value) ? "<not configured>" : value;
|
|
|
|
private static string DescribeSecret(string value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return "<not configured>";
|
|
}
|
|
|
|
return value.Length switch
|
|
{
|
|
<= 4 => "****",
|
|
_ => $"{value[..2]}***{value[^2..]}"
|
|
};
|
|
}
|
|
|
|
private static Command BuildAttestCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var attest = new Command("attest", "Verify and inspect DSSE attestations.");
|
|
|
|
// attest verify
|
|
var verify = new Command("verify", "Verify a DSSE envelope offline against policy and trust roots.");
|
|
var envelopeOption = new Option<string>("--envelope", new[] { "-e" })
|
|
{
|
|
Description = "Path to the DSSE envelope file (JSON or sigstore bundle).",
|
|
Required = true
|
|
};
|
|
var policyOption = new Option<string?>("--policy")
|
|
{
|
|
Description = "Path to policy JSON file for verification rules."
|
|
};
|
|
var rootOption = new Option<string?>("--root")
|
|
{
|
|
Description = "Path to trusted root certificate (PEM format)."
|
|
};
|
|
var checkpointOption = new Option<string?>("--transparency-checkpoint")
|
|
{
|
|
Description = "Path to Rekor transparency checkpoint file."
|
|
};
|
|
var verifyOutputOption = new Option<string?>("--output", new[] { "-o" })
|
|
{
|
|
Description = "Output path for verification report."
|
|
};
|
|
var verifyFormatOption = new Option<string?>("--format", new[] { "-f" })
|
|
{
|
|
Description = "Output format: table (default), json."
|
|
};
|
|
var verifyExplainOption = new Option<bool>("--explain")
|
|
{
|
|
Description = "Include detailed explanations for each verification check."
|
|
};
|
|
|
|
verify.Add(envelopeOption);
|
|
verify.Add(policyOption);
|
|
verify.Add(rootOption);
|
|
verify.Add(checkpointOption);
|
|
verify.Add(verifyOutputOption);
|
|
verify.Add(verifyFormatOption);
|
|
verify.Add(verifyExplainOption);
|
|
|
|
verify.SetAction((parseResult, _) =>
|
|
{
|
|
var envelope = parseResult.GetValue(envelopeOption)!;
|
|
var policy = parseResult.GetValue(policyOption);
|
|
var root = parseResult.GetValue(rootOption);
|
|
var checkpoint = parseResult.GetValue(checkpointOption);
|
|
var output = parseResult.GetValue(verifyOutputOption);
|
|
var format = parseResult.GetValue(verifyFormatOption) ?? "table";
|
|
var explain = parseResult.GetValue(verifyExplainOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleAttestVerifyAsync(services, envelope, policy, root, checkpoint, output, format, explain, verbose, cancellationToken);
|
|
});
|
|
|
|
// attest list (CLI-ATTEST-74-001)
|
|
var list = new Command("list", "List attestations from local storage or backend.");
|
|
var listTenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Filter by tenant identifier."
|
|
};
|
|
var listIssuerOption = new Option<string?>("--issuer")
|
|
{
|
|
Description = "Filter by issuer identifier."
|
|
};
|
|
var listSubjectOption = new Option<string?>("--subject", new[] { "-s" })
|
|
{
|
|
Description = "Filter by subject (e.g., image digest, package PURL)."
|
|
};
|
|
var listTypeOption = new Option<string?>("--type", new[] { "-t" })
|
|
{
|
|
Description = "Filter by predicate type URI."
|
|
};
|
|
var listScopeOption = new Option<string?>("--scope")
|
|
{
|
|
Description = "Filter by scope (local, remote, all). Default: all."
|
|
};
|
|
var listFormatOption = new Option<string?>("--format", new[] { "-f" })
|
|
{
|
|
Description = "Output format (table, json). Default: table."
|
|
};
|
|
var listLimitOption = new Option<int?>("--limit", new[] { "-n" })
|
|
{
|
|
Description = "Maximum number of results to return. Default: 50."
|
|
};
|
|
var listOffsetOption = new Option<int?>("--offset")
|
|
{
|
|
Description = "Number of results to skip (for pagination). Default: 0."
|
|
};
|
|
|
|
list.Add(listTenantOption);
|
|
list.Add(listIssuerOption);
|
|
list.Add(listSubjectOption);
|
|
list.Add(listTypeOption);
|
|
list.Add(listScopeOption);
|
|
list.Add(listFormatOption);
|
|
list.Add(listLimitOption);
|
|
list.Add(listOffsetOption);
|
|
|
|
list.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(listTenantOption);
|
|
var issuer = parseResult.GetValue(listIssuerOption);
|
|
var subject = parseResult.GetValue(listSubjectOption);
|
|
var type = parseResult.GetValue(listTypeOption);
|
|
var scope = parseResult.GetValue(listScopeOption) ?? "all";
|
|
var format = parseResult.GetValue(listFormatOption) ?? "table";
|
|
var limit = parseResult.GetValue(listLimitOption);
|
|
var offset = parseResult.GetValue(listOffsetOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleAttestListAsync(services, tenant, issuer, subject, type, scope, format, limit, offset, verbose, cancellationToken);
|
|
});
|
|
|
|
// attest show
|
|
var show = new Command("show", "Display details for a specific attestation.");
|
|
var idOption = new Option<string>("--id")
|
|
{
|
|
Description = "Attestation identifier.",
|
|
Required = true
|
|
};
|
|
var showOutputOption = new Option<string?>("--output", new[] { "-o" })
|
|
{
|
|
Description = "Output format (json, table)."
|
|
};
|
|
var includeProofOption = new Option<bool>("--include-proof")
|
|
{
|
|
Description = "Include Rekor inclusion proof in output."
|
|
};
|
|
|
|
show.Add(idOption);
|
|
show.Add(showOutputOption);
|
|
show.Add(includeProofOption);
|
|
|
|
show.SetAction((parseResult, _) =>
|
|
{
|
|
var id = parseResult.GetValue(idOption)!;
|
|
var output = parseResult.GetValue(showOutputOption) ?? "json";
|
|
var includeProof = parseResult.GetValue(includeProofOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleAttestShowAsync(services, id, output, includeProof, verbose, cancellationToken);
|
|
});
|
|
|
|
// attest sign (CLI-ATTEST-73-001)
|
|
var sign = new Command("sign", "Create and sign a DSSE attestation envelope.");
|
|
var predicateFileOption = new Option<string>("--predicate", new[] { "-p" })
|
|
{
|
|
Description = "Path to the predicate JSON file.",
|
|
Required = true
|
|
};
|
|
var predicateTypeOption = new Option<string>("--predicate-type")
|
|
{
|
|
Description = "Predicate type URI (e.g., https://slsa.dev/provenance/v1).",
|
|
Required = true
|
|
};
|
|
var subjectNameOption = new Option<string>("--subject")
|
|
{
|
|
Description = "Subject name or URI to attest.",
|
|
Required = true
|
|
};
|
|
var subjectDigestOption = new Option<string>("--digest")
|
|
{
|
|
Description = "Subject digest in format algorithm:hex (e.g., sha256:abc123...).",
|
|
Required = true
|
|
};
|
|
var signKeyOption = new Option<string?>("--key", new[] { "-k" })
|
|
{
|
|
Description = "Key identifier or path for signing."
|
|
};
|
|
var keylessOption = new Option<bool>("--keyless")
|
|
{
|
|
Description = "Use keyless (OIDC) signing via Sigstore Fulcio."
|
|
};
|
|
var transparencyLogOption = new Option<bool>("--rekor")
|
|
{
|
|
Description = "Submit attestation to Rekor transparency log (default: false)."
|
|
};
|
|
var noRekorOption = new Option<bool>("--no-rekor")
|
|
{
|
|
Description = "Explicitly skip Rekor submission."
|
|
};
|
|
var signOutputOption = new Option<string?>("--output", new[] { "-o" })
|
|
{
|
|
Description = "Output path for the signed DSSE envelope JSON."
|
|
};
|
|
var signFormatOption = new Option<string?>("--format", new[] { "-f" })
|
|
{
|
|
Description = "Output format: dsse (default), sigstore-bundle."
|
|
};
|
|
|
|
sign.Add(predicateFileOption);
|
|
sign.Add(predicateTypeOption);
|
|
sign.Add(subjectNameOption);
|
|
sign.Add(subjectDigestOption);
|
|
sign.Add(signKeyOption);
|
|
sign.Add(keylessOption);
|
|
sign.Add(transparencyLogOption);
|
|
sign.Add(noRekorOption);
|
|
sign.Add(signOutputOption);
|
|
sign.Add(signFormatOption);
|
|
|
|
sign.SetAction((parseResult, _) =>
|
|
{
|
|
var predicatePath = parseResult.GetValue(predicateFileOption)!;
|
|
var predicateType = parseResult.GetValue(predicateTypeOption)!;
|
|
var subjectName = parseResult.GetValue(subjectNameOption)!;
|
|
var digest = parseResult.GetValue(subjectDigestOption)!;
|
|
var keyId = parseResult.GetValue(signKeyOption);
|
|
var keyless = parseResult.GetValue(keylessOption);
|
|
var useRekor = parseResult.GetValue(transparencyLogOption);
|
|
var noRekor = parseResult.GetValue(noRekorOption);
|
|
var output = parseResult.GetValue(signOutputOption);
|
|
var format = parseResult.GetValue(signFormatOption) ?? "dsse";
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleAttestSignAsync(
|
|
services,
|
|
predicatePath,
|
|
predicateType,
|
|
subjectName,
|
|
digest,
|
|
keyId,
|
|
keyless,
|
|
useRekor && !noRekor,
|
|
output,
|
|
format,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
// attest fetch (CLI-ATTEST-74-002)
|
|
var fetch = new Command("fetch", "Download attestation envelopes and payloads to disk.");
|
|
var fetchIdOption = new Option<string?>("--id")
|
|
{
|
|
Description = "Attestation ID to fetch."
|
|
};
|
|
var fetchSubjectOption = new Option<string?>("--subject", new[] { "-s" })
|
|
{
|
|
Description = "Subject filter (e.g., image digest, package PURL)."
|
|
};
|
|
var fetchTypeOption = new Option<string?>("--type", new[] { "-t" })
|
|
{
|
|
Description = "Predicate type filter."
|
|
};
|
|
var fetchOutputDirOption = new Option<string>("--output-dir", new[] { "-o" })
|
|
{
|
|
Description = "Output directory for downloaded files.",
|
|
Required = true
|
|
};
|
|
var fetchIncludeOption = new Option<string?>("--include")
|
|
{
|
|
Description = "What to download: envelope, payload, both (default: both)."
|
|
};
|
|
var fetchScopeOption = new Option<string?>("--scope")
|
|
{
|
|
Description = "Source scope: local, remote, all (default: all)."
|
|
};
|
|
var fetchFormatOption = new Option<string?>("--format", new[] { "-f" })
|
|
{
|
|
Description = "Output format for payloads: json (default), raw."
|
|
};
|
|
var fetchOverwriteOption = new Option<bool>("--overwrite")
|
|
{
|
|
Description = "Overwrite existing files."
|
|
};
|
|
|
|
fetch.Add(fetchIdOption);
|
|
fetch.Add(fetchSubjectOption);
|
|
fetch.Add(fetchTypeOption);
|
|
fetch.Add(fetchOutputDirOption);
|
|
fetch.Add(fetchIncludeOption);
|
|
fetch.Add(fetchScopeOption);
|
|
fetch.Add(fetchFormatOption);
|
|
fetch.Add(fetchOverwriteOption);
|
|
|
|
fetch.SetAction((parseResult, _) =>
|
|
{
|
|
var id = parseResult.GetValue(fetchIdOption);
|
|
var subject = parseResult.GetValue(fetchSubjectOption);
|
|
var type = parseResult.GetValue(fetchTypeOption);
|
|
var outputDir = parseResult.GetValue(fetchOutputDirOption)!;
|
|
var include = parseResult.GetValue(fetchIncludeOption) ?? "both";
|
|
var scope = parseResult.GetValue(fetchScopeOption) ?? "all";
|
|
var format = parseResult.GetValue(fetchFormatOption) ?? "json";
|
|
var overwrite = parseResult.GetValue(fetchOverwriteOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleAttestFetchAsync(
|
|
services,
|
|
id,
|
|
subject,
|
|
type,
|
|
outputDir,
|
|
include,
|
|
scope,
|
|
format,
|
|
overwrite,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
// attest key (CLI-ATTEST-75-001)
|
|
var key = new Command("key", "Manage attestation signing keys.");
|
|
|
|
// attest key create
|
|
var keyCreate = new Command("create", "Create a new signing key for attestations.");
|
|
var keyNameOption = new Option<string>("--name", new[] { "-n" })
|
|
{
|
|
Description = "Key identifier/name.",
|
|
Required = true
|
|
};
|
|
var keyAlgorithmOption = new Option<string?>("--algorithm", new[] { "-a" })
|
|
{
|
|
Description = "Key algorithm: ECDSA-P256 (default), ECDSA-P384."
|
|
};
|
|
var keyPasswordOption = new Option<string?>("--password", new[] { "-p" })
|
|
{
|
|
Description = "Password to protect the key (required for file-based keys)."
|
|
};
|
|
var keyOutputOption = new Option<string?>("--output", new[] { "-o" })
|
|
{
|
|
Description = "Output path for the key directory (default: ~/.stellaops/keys)."
|
|
};
|
|
var keyFormatOption = new Option<string?>("--format", new[] { "-f" })
|
|
{
|
|
Description = "Output format: table (default), json."
|
|
};
|
|
var keyExportPublicOption = new Option<bool>("--export-public")
|
|
{
|
|
Description = "Export public key to file alongside key creation."
|
|
};
|
|
|
|
keyCreate.Add(keyNameOption);
|
|
keyCreate.Add(keyAlgorithmOption);
|
|
keyCreate.Add(keyPasswordOption);
|
|
keyCreate.Add(keyOutputOption);
|
|
keyCreate.Add(keyFormatOption);
|
|
keyCreate.Add(keyExportPublicOption);
|
|
|
|
keyCreate.SetAction((parseResult, _) =>
|
|
{
|
|
var name = parseResult.GetValue(keyNameOption)!;
|
|
var algorithm = parseResult.GetValue(keyAlgorithmOption) ?? "ECDSA-P256";
|
|
var password = parseResult.GetValue(keyPasswordOption);
|
|
var output = parseResult.GetValue(keyOutputOption);
|
|
var format = parseResult.GetValue(keyFormatOption) ?? "table";
|
|
var exportPublic = parseResult.GetValue(keyExportPublicOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleAttestKeyCreateAsync(
|
|
services,
|
|
name,
|
|
algorithm,
|
|
password,
|
|
output,
|
|
format,
|
|
exportPublic,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
key.Add(keyCreate);
|
|
|
|
// attest bundle (CLI-ATTEST-75-002)
|
|
var bundle = new Command("bundle", "Build and verify attestation bundles.");
|
|
|
|
// attest bundle build
|
|
var bundleBuild = new Command("build", "Build an audit bundle from artifacts (attestations, SBOMs, VEX, scans).");
|
|
var bundleSubjectNameOption = new Option<string>("--subject-name", new[] { "-s" })
|
|
{
|
|
Description = "Primary subject name (e.g., image reference).",
|
|
Required = true
|
|
};
|
|
var bundleSubjectDigestOption = new Option<string>("--subject-digest", new[] { "-d" })
|
|
{
|
|
Description = "Subject digest in algorithm:hex format (e.g., sha256:abc123...).",
|
|
Required = true
|
|
};
|
|
var bundleSubjectTypeOption = new Option<string?>("--subject-type")
|
|
{
|
|
Description = "Subject type: IMAGE (default), REPO, SBOM, OTHER."
|
|
};
|
|
var bundleInputDirOption = new Option<string>("--input", new[] { "-i" })
|
|
{
|
|
Description = "Input directory containing artifacts to bundle.",
|
|
Required = true
|
|
};
|
|
var bundleOutputOption = new Option<string>("--output", new[] { "-o" })
|
|
{
|
|
Description = "Output path for the bundle (directory or .tar.gz file).",
|
|
Required = true
|
|
};
|
|
var bundleFromOption = new Option<string?>("--from")
|
|
{
|
|
Description = "Start of time window for artifacts (ISO-8601)."
|
|
};
|
|
var bundleToOption = new Option<string?>("--to")
|
|
{
|
|
Description = "End of time window for artifacts (ISO-8601)."
|
|
};
|
|
var bundleIncludeOption = new Option<string?>("--include")
|
|
{
|
|
Description = "Artifact types to include: attestations,sboms,vex,scans,policy,all (default: all)."
|
|
};
|
|
var bundleCompressOption = new Option<bool>("--compress")
|
|
{
|
|
Description = "Compress output as tar.gz."
|
|
};
|
|
var bundleCreatorIdOption = new Option<string?>("--creator-id")
|
|
{
|
|
Description = "Creator user ID (default: current user)."
|
|
};
|
|
var bundleCreatorNameOption = new Option<string?>("--creator-name")
|
|
{
|
|
Description = "Creator display name (default: current user)."
|
|
};
|
|
var bundleFormatOption = new Option<string?>("--format", new[] { "-f" })
|
|
{
|
|
Description = "Output format: table (default), json."
|
|
};
|
|
|
|
bundleBuild.Add(bundleSubjectNameOption);
|
|
bundleBuild.Add(bundleSubjectDigestOption);
|
|
bundleBuild.Add(bundleSubjectTypeOption);
|
|
bundleBuild.Add(bundleInputDirOption);
|
|
bundleBuild.Add(bundleOutputOption);
|
|
bundleBuild.Add(bundleFromOption);
|
|
bundleBuild.Add(bundleToOption);
|
|
bundleBuild.Add(bundleIncludeOption);
|
|
bundleBuild.Add(bundleCompressOption);
|
|
bundleBuild.Add(bundleCreatorIdOption);
|
|
bundleBuild.Add(bundleCreatorNameOption);
|
|
bundleBuild.Add(bundleFormatOption);
|
|
|
|
bundleBuild.SetAction((parseResult, _) =>
|
|
{
|
|
var subjectName = parseResult.GetValue(bundleSubjectNameOption)!;
|
|
var subjectDigest = parseResult.GetValue(bundleSubjectDigestOption)!;
|
|
var subjectType = parseResult.GetValue(bundleSubjectTypeOption) ?? "IMAGE";
|
|
var inputDir = parseResult.GetValue(bundleInputDirOption)!;
|
|
var output = parseResult.GetValue(bundleOutputOption)!;
|
|
var from = parseResult.GetValue(bundleFromOption);
|
|
var to = parseResult.GetValue(bundleToOption);
|
|
var include = parseResult.GetValue(bundleIncludeOption) ?? "all";
|
|
var compress = parseResult.GetValue(bundleCompressOption);
|
|
var creatorId = parseResult.GetValue(bundleCreatorIdOption);
|
|
var creatorName = parseResult.GetValue(bundleCreatorNameOption);
|
|
var format = parseResult.GetValue(bundleFormatOption) ?? "table";
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleAttestBundleBuildAsync(
|
|
services,
|
|
subjectName,
|
|
subjectDigest,
|
|
subjectType,
|
|
inputDir,
|
|
output,
|
|
from,
|
|
to,
|
|
include,
|
|
compress,
|
|
creatorId,
|
|
creatorName,
|
|
format,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
// attest bundle verify
|
|
var bundleVerify = new Command("verify", "Verify an attestation bundle's integrity and signatures.");
|
|
var bundleVerifyInputOption = new Option<string>("--input", new[] { "-i" })
|
|
{
|
|
Description = "Input bundle path (directory or .tar.gz file).",
|
|
Required = true
|
|
};
|
|
var bundleVerifyPolicyOption = new Option<string?>("--policy")
|
|
{
|
|
Description = "Policy file for attestation verification (JSON with requiredPredicateTypes, minimumSignatures, etc.)."
|
|
};
|
|
var bundleVerifyRootOption = new Option<string?>("--root")
|
|
{
|
|
Description = "Trust root file (PEM certificate or public key) for signature verification."
|
|
};
|
|
var bundleVerifyOutputOption = new Option<string?>("--output", new[] { "-o" })
|
|
{
|
|
Description = "Write verification report to file (JSON format)."
|
|
};
|
|
var bundleVerifyFormatOption = new Option<string?>("--format", new[] { "-f" })
|
|
{
|
|
Description = "Output format: table (default), json."
|
|
};
|
|
var bundleVerifyStrictOption = new Option<bool>("--strict")
|
|
{
|
|
Description = "Treat warnings as errors (exit code 1 on any issue)."
|
|
};
|
|
|
|
bundleVerify.Add(bundleVerifyInputOption);
|
|
bundleVerify.Add(bundleVerifyPolicyOption);
|
|
bundleVerify.Add(bundleVerifyRootOption);
|
|
bundleVerify.Add(bundleVerifyOutputOption);
|
|
bundleVerify.Add(bundleVerifyFormatOption);
|
|
bundleVerify.Add(bundleVerifyStrictOption);
|
|
|
|
bundleVerify.SetAction((parseResult, _) =>
|
|
{
|
|
var input = parseResult.GetValue(bundleVerifyInputOption)!;
|
|
var policy = parseResult.GetValue(bundleVerifyPolicyOption);
|
|
var root = parseResult.GetValue(bundleVerifyRootOption);
|
|
var output = parseResult.GetValue(bundleVerifyOutputOption);
|
|
var format = parseResult.GetValue(bundleVerifyFormatOption) ?? "table";
|
|
var strict = parseResult.GetValue(bundleVerifyStrictOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleAttestBundleVerifyAsync(
|
|
services,
|
|
input,
|
|
policy,
|
|
root,
|
|
output,
|
|
format,
|
|
strict,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
bundle.Add(bundleBuild);
|
|
bundle.Add(bundleVerify);
|
|
|
|
attest.Add(sign);
|
|
attest.Add(verify);
|
|
attest.Add(list);
|
|
attest.Add(show);
|
|
attest.Add(fetch);
|
|
attest.Add(key);
|
|
attest.Add(bundle);
|
|
|
|
return attest;
|
|
}
|
|
|
|
private static Command BuildRiskProfileCommand(Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
_ = cancellationToken;
|
|
var riskProfile = new Command("risk-profile", "Manage risk profile schemas and validation.");
|
|
|
|
var validate = new Command("validate", "Validate a risk profile JSON file against the schema.");
|
|
var inputOption = new Option<string>("--input", new[] { "-i" })
|
|
{
|
|
Description = "Path to the risk profile JSON file to validate.",
|
|
Required = true
|
|
};
|
|
var formatOption = new Option<string?>("--format")
|
|
{
|
|
Description = "Output format: table (default) or json."
|
|
};
|
|
var outputOption = new Option<string?>("--output")
|
|
{
|
|
Description = "Write validation report to the specified file path."
|
|
};
|
|
var strictOption = new Option<bool>("--strict")
|
|
{
|
|
Description = "Treat warnings as errors (exit code 1 on any issue)."
|
|
};
|
|
|
|
validate.Add(inputOption);
|
|
validate.Add(formatOption);
|
|
validate.Add(outputOption);
|
|
validate.Add(strictOption);
|
|
|
|
validate.SetAction((parseResult, _) =>
|
|
{
|
|
var input = parseResult.GetValue(inputOption) ?? string.Empty;
|
|
var format = parseResult.GetValue(formatOption) ?? "table";
|
|
var output = parseResult.GetValue(outputOption);
|
|
var strict = parseResult.GetValue(strictOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleRiskProfileValidateAsync(input, format, output, strict, verbose);
|
|
});
|
|
|
|
var schema = new Command("schema", "Display or export the risk profile JSON schema.");
|
|
var schemaOutputOption = new Option<string?>("--output")
|
|
{
|
|
Description = "Write the schema to the specified file path."
|
|
};
|
|
schema.Add(schemaOutputOption);
|
|
|
|
schema.SetAction((parseResult, _) =>
|
|
{
|
|
var output = parseResult.GetValue(schemaOutputOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleRiskProfileSchemaAsync(output, verbose);
|
|
});
|
|
|
|
riskProfile.Add(validate);
|
|
riskProfile.Add(schema);
|
|
return riskProfile;
|
|
}
|
|
|
|
// CLI-LNM-22-001: Advisory command group
|
|
private static Command BuildAdvisoryCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var advisory = new Command("advisory", "Explore advisory observations, linksets, and exports (Link-Not-Merge).");
|
|
|
|
// Common options
|
|
var tenantOption = new Option<string>("--tenant", "-t")
|
|
{
|
|
Description = "Tenant identifier.",
|
|
Required = true
|
|
};
|
|
var aliasOption = new Option<string[]>("--alias", "-a")
|
|
{
|
|
Description = "Filter by vulnerability alias (CVE, GHSA, etc.). Repeatable.",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var purlOption = new Option<string[]>("--purl")
|
|
{
|
|
Description = "Filter by Package URL. Repeatable.",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var cpeOption = new Option<string[]>("--cpe")
|
|
{
|
|
Description = "Filter by CPE value. Repeatable.",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var sourceOption = new Option<string[]>("--source", "-s")
|
|
{
|
|
Description = "Filter by source vendor (e.g., nvd, redhat, ubuntu). Repeatable.",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var severityOption = new Option<string?>("--severity")
|
|
{
|
|
Description = "Filter by severity (critical, high, medium, low)."
|
|
};
|
|
var kevOption = new Option<bool>("--kev-only")
|
|
{
|
|
Description = "Only show advisories listed in KEV (Known Exploited Vulnerabilities)."
|
|
};
|
|
var hasFixOption = new Option<bool?>("--has-fix")
|
|
{
|
|
Description = "Filter by fix availability (true/false)."
|
|
};
|
|
var limitOption = new Option<int?>("--limit", "-l")
|
|
{
|
|
Description = "Maximum number of results (default 200, max 500)."
|
|
};
|
|
var cursorOption = new Option<string?>("--cursor")
|
|
{
|
|
Description = "Pagination cursor from previous response."
|
|
};
|
|
|
|
// stella advisory obs get
|
|
var obsGet = new Command("obs", "Get raw advisory observations.");
|
|
var obsIdOption = new Option<string[]>("--observation-id", "-i")
|
|
{
|
|
Description = "Filter by observation identifier. Repeatable.",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var obsJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON."
|
|
};
|
|
var obsOsvOption = new Option<bool>("--osv")
|
|
{
|
|
Description = "Output in OSV (Open Source Vulnerability) format."
|
|
};
|
|
var obsShowConflictsOption = new Option<bool>("--show-conflicts")
|
|
{
|
|
Description = "Include conflict information in output."
|
|
};
|
|
|
|
obsGet.Add(tenantOption);
|
|
obsGet.Add(obsIdOption);
|
|
obsGet.Add(aliasOption);
|
|
obsGet.Add(purlOption);
|
|
obsGet.Add(cpeOption);
|
|
obsGet.Add(sourceOption);
|
|
obsGet.Add(severityOption);
|
|
obsGet.Add(kevOption);
|
|
obsGet.Add(hasFixOption);
|
|
obsGet.Add(limitOption);
|
|
obsGet.Add(cursorOption);
|
|
obsGet.Add(obsJsonOption);
|
|
obsGet.Add(obsOsvOption);
|
|
obsGet.Add(obsShowConflictsOption);
|
|
|
|
obsGet.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption) ?? string.Empty;
|
|
var observationIds = parseResult.GetValue(obsIdOption) ?? Array.Empty<string>();
|
|
var aliases = parseResult.GetValue(aliasOption) ?? Array.Empty<string>();
|
|
var purls = parseResult.GetValue(purlOption) ?? Array.Empty<string>();
|
|
var cpes = parseResult.GetValue(cpeOption) ?? Array.Empty<string>();
|
|
var sources = parseResult.GetValue(sourceOption) ?? Array.Empty<string>();
|
|
var severity = parseResult.GetValue(severityOption);
|
|
var kevOnly = parseResult.GetValue(kevOption);
|
|
var hasFix = parseResult.GetValue(hasFixOption);
|
|
var limit = parseResult.GetValue(limitOption);
|
|
var cursor = parseResult.GetValue(cursorOption);
|
|
var emitJson = parseResult.GetValue(obsJsonOption);
|
|
var emitOsv = parseResult.GetValue(obsOsvOption);
|
|
var showConflicts = parseResult.GetValue(obsShowConflictsOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleAdvisoryObsGetAsync(
|
|
services,
|
|
tenant,
|
|
observationIds,
|
|
aliases,
|
|
purls,
|
|
cpes,
|
|
sources,
|
|
severity,
|
|
kevOnly,
|
|
hasFix,
|
|
limit,
|
|
cursor,
|
|
emitJson,
|
|
emitOsv,
|
|
showConflicts,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
advisory.Add(obsGet);
|
|
|
|
// stella advisory linkset show
|
|
var linksetShow = new Command("linkset", "Show aggregated linkset with conflict summary.");
|
|
var linksetJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON."
|
|
};
|
|
|
|
linksetShow.Add(tenantOption);
|
|
linksetShow.Add(aliasOption);
|
|
linksetShow.Add(purlOption);
|
|
linksetShow.Add(cpeOption);
|
|
linksetShow.Add(sourceOption);
|
|
linksetShow.Add(linksetJsonOption);
|
|
|
|
linksetShow.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption) ?? string.Empty;
|
|
var aliases = parseResult.GetValue(aliasOption) ?? Array.Empty<string>();
|
|
var purls = parseResult.GetValue(purlOption) ?? Array.Empty<string>();
|
|
var cpes = parseResult.GetValue(cpeOption) ?? Array.Empty<string>();
|
|
var sources = parseResult.GetValue(sourceOption) ?? Array.Empty<string>();
|
|
var emitJson = parseResult.GetValue(linksetJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleAdvisoryLinksetShowAsync(
|
|
services,
|
|
tenant,
|
|
aliases,
|
|
purls,
|
|
cpes,
|
|
sources,
|
|
emitJson,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
advisory.Add(linksetShow);
|
|
|
|
// stella advisory export
|
|
var export = new Command("export", "Export advisory observations to various formats.");
|
|
var exportFormatOption = new Option<string>("--format", "-f")
|
|
{
|
|
Description = "Export format (json, osv, ndjson, csv). Default: json."
|
|
};
|
|
var exportOutputOption = new Option<string?>("--output", "-o")
|
|
{
|
|
Description = "Output file path. If not specified, writes to stdout."
|
|
};
|
|
var exportSignedOption = new Option<bool>("--signed")
|
|
{
|
|
Description = "Request signed export (if supported by backend)."
|
|
};
|
|
|
|
export.Add(tenantOption);
|
|
export.Add(aliasOption);
|
|
export.Add(purlOption);
|
|
export.Add(cpeOption);
|
|
export.Add(sourceOption);
|
|
export.Add(severityOption);
|
|
export.Add(kevOption);
|
|
export.Add(hasFixOption);
|
|
export.Add(limitOption);
|
|
export.Add(exportFormatOption);
|
|
export.Add(exportOutputOption);
|
|
export.Add(exportSignedOption);
|
|
|
|
export.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption) ?? string.Empty;
|
|
var aliases = parseResult.GetValue(aliasOption) ?? Array.Empty<string>();
|
|
var purls = parseResult.GetValue(purlOption) ?? Array.Empty<string>();
|
|
var cpes = parseResult.GetValue(cpeOption) ?? Array.Empty<string>();
|
|
var sources = parseResult.GetValue(sourceOption) ?? Array.Empty<string>();
|
|
var severity = parseResult.GetValue(severityOption);
|
|
var kevOnly = parseResult.GetValue(kevOption);
|
|
var hasFix = parseResult.GetValue(hasFixOption);
|
|
var limit = parseResult.GetValue(limitOption);
|
|
var format = parseResult.GetValue(exportFormatOption) ?? "json";
|
|
var output = parseResult.GetValue(exportOutputOption);
|
|
var signed = parseResult.GetValue(exportSignedOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleAdvisoryExportAsync(
|
|
services,
|
|
tenant,
|
|
aliases,
|
|
purls,
|
|
cpes,
|
|
sources,
|
|
severity,
|
|
kevOnly,
|
|
hasFix,
|
|
limit,
|
|
format,
|
|
output,
|
|
signed,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
advisory.Add(export);
|
|
|
|
return advisory;
|
|
}
|
|
|
|
// CLI-FORENSICS-53-001: Forensic snapshot command group
|
|
private static Command BuildForensicCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var forensic = new Command("forensic", "Manage forensic snapshots and evidence locker operations.");
|
|
|
|
// Common options
|
|
var tenantOption = new Option<string>("--tenant", "-t")
|
|
{
|
|
Description = "Tenant identifier.",
|
|
Required = true
|
|
};
|
|
|
|
// stella forensic snapshot create --case
|
|
var snapshotCreate = new Command("snapshot", "Create a forensic snapshot for evidence preservation.");
|
|
var createCaseOption = new Option<string>("--case", "-c")
|
|
{
|
|
Description = "Case identifier to associate with the snapshot.",
|
|
Required = true
|
|
};
|
|
var createDescOption = new Option<string?>("--description", "-d")
|
|
{
|
|
Description = "Description of the snapshot purpose."
|
|
};
|
|
var createTagsOption = new Option<string[]>("--tag")
|
|
{
|
|
Description = "Tags to attach to the snapshot. Repeatable.",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var createSbomOption = new Option<string[]>("--sbom-id")
|
|
{
|
|
Description = "SBOM IDs to include in the snapshot scope. Repeatable.",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var createScanOption = new Option<string[]>("--scan-id")
|
|
{
|
|
Description = "Scan IDs to include in the snapshot scope. Repeatable.",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var createPolicyOption = new Option<string[]>("--policy-id")
|
|
{
|
|
Description = "Policy IDs to include in the snapshot scope. Repeatable.",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var createVulnOption = new Option<string[]>("--vuln-id")
|
|
{
|
|
Description = "Vulnerability IDs to include in the snapshot scope. Repeatable.",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var createRetentionOption = new Option<int?>("--retention-days")
|
|
{
|
|
Description = "Retention period in days (default: per tenant policy)."
|
|
};
|
|
var createJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON."
|
|
};
|
|
|
|
snapshotCreate.Add(tenantOption);
|
|
snapshotCreate.Add(createCaseOption);
|
|
snapshotCreate.Add(createDescOption);
|
|
snapshotCreate.Add(createTagsOption);
|
|
snapshotCreate.Add(createSbomOption);
|
|
snapshotCreate.Add(createScanOption);
|
|
snapshotCreate.Add(createPolicyOption);
|
|
snapshotCreate.Add(createVulnOption);
|
|
snapshotCreate.Add(createRetentionOption);
|
|
snapshotCreate.Add(createJsonOption);
|
|
|
|
snapshotCreate.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption) ?? string.Empty;
|
|
var caseId = parseResult.GetValue(createCaseOption) ?? string.Empty;
|
|
var description = parseResult.GetValue(createDescOption);
|
|
var tags = parseResult.GetValue(createTagsOption) ?? Array.Empty<string>();
|
|
var sbomIds = parseResult.GetValue(createSbomOption) ?? Array.Empty<string>();
|
|
var scanIds = parseResult.GetValue(createScanOption) ?? Array.Empty<string>();
|
|
var policyIds = parseResult.GetValue(createPolicyOption) ?? Array.Empty<string>();
|
|
var vulnIds = parseResult.GetValue(createVulnOption) ?? Array.Empty<string>();
|
|
var retentionDays = parseResult.GetValue(createRetentionOption);
|
|
var emitJson = parseResult.GetValue(createJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleForensicSnapshotCreateAsync(
|
|
services,
|
|
tenant,
|
|
caseId,
|
|
description,
|
|
tags,
|
|
sbomIds,
|
|
scanIds,
|
|
policyIds,
|
|
vulnIds,
|
|
retentionDays,
|
|
emitJson,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
forensic.Add(snapshotCreate);
|
|
|
|
// stella forensic list
|
|
var snapshotList = new Command("list", "List forensic snapshots.");
|
|
var listCaseOption = new Option<string?>("--case", "-c")
|
|
{
|
|
Description = "Filter by case identifier."
|
|
};
|
|
var listStatusOption = new Option<string?>("--status")
|
|
{
|
|
Description = "Filter by status (pending, creating, ready, failed, expired, archived)."
|
|
};
|
|
var listTagsOption = new Option<string[]>("--tag")
|
|
{
|
|
Description = "Filter by tags. Repeatable.",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
var listLimitOption = new Option<int?>("--limit", "-l")
|
|
{
|
|
Description = "Maximum number of results (default 50)."
|
|
};
|
|
var listOffsetOption = new Option<int?>("--offset")
|
|
{
|
|
Description = "Number of results to skip for pagination."
|
|
};
|
|
var listJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON."
|
|
};
|
|
|
|
snapshotList.Add(tenantOption);
|
|
snapshotList.Add(listCaseOption);
|
|
snapshotList.Add(listStatusOption);
|
|
snapshotList.Add(listTagsOption);
|
|
snapshotList.Add(listLimitOption);
|
|
snapshotList.Add(listOffsetOption);
|
|
snapshotList.Add(listJsonOption);
|
|
|
|
snapshotList.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption) ?? string.Empty;
|
|
var caseId = parseResult.GetValue(listCaseOption);
|
|
var status = parseResult.GetValue(listStatusOption);
|
|
var tags = parseResult.GetValue(listTagsOption) ?? Array.Empty<string>();
|
|
var limit = parseResult.GetValue(listLimitOption);
|
|
var offset = parseResult.GetValue(listOffsetOption);
|
|
var emitJson = parseResult.GetValue(listJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleForensicSnapshotListAsync(
|
|
services,
|
|
tenant,
|
|
caseId,
|
|
status,
|
|
tags,
|
|
limit,
|
|
offset,
|
|
emitJson,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
forensic.Add(snapshotList);
|
|
|
|
// stella forensic show
|
|
var snapshotShow = new Command("show", "Show forensic snapshot details including manifest digests.");
|
|
var showSnapshotIdArg = new Argument<string>("snapshot-id")
|
|
{
|
|
Description = "Snapshot identifier to show."
|
|
};
|
|
var showJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON."
|
|
};
|
|
var showManifestOption = new Option<bool>("--manifest")
|
|
{
|
|
Description = "Include full manifest with artifact digests."
|
|
};
|
|
|
|
snapshotShow.Add(showSnapshotIdArg);
|
|
snapshotShow.Add(tenantOption);
|
|
snapshotShow.Add(showJsonOption);
|
|
snapshotShow.Add(showManifestOption);
|
|
|
|
snapshotShow.SetAction((parseResult, _) =>
|
|
{
|
|
var snapshotId = parseResult.GetValue(showSnapshotIdArg) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(tenantOption) ?? string.Empty;
|
|
var emitJson = parseResult.GetValue(showJsonOption);
|
|
var includeManifest = parseResult.GetValue(showManifestOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleForensicSnapshotShowAsync(
|
|
services,
|
|
tenant,
|
|
snapshotId,
|
|
emitJson,
|
|
includeManifest,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
forensic.Add(snapshotShow);
|
|
|
|
// CLI-FORENSICS-54-001: stella forensic verify <bundle>
|
|
var verifyCommand = new Command("verify", "Verify forensic bundle integrity, signatures, and chain-of-custody.");
|
|
var verifyBundleArg = new Argument<string>("bundle")
|
|
{
|
|
Description = "Path to forensic bundle directory or manifest file."
|
|
};
|
|
var verifyJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON for CI integration."
|
|
};
|
|
var verifyTrustRootOption = new Option<string?>("--trust-root", "-r")
|
|
{
|
|
Description = "Path to trust root JSON file containing public keys."
|
|
};
|
|
var verifySkipChecksumsOption = new Option<bool>("--skip-checksums")
|
|
{
|
|
Description = "Skip artifact checksum verification."
|
|
};
|
|
var verifySkipSignaturesOption = new Option<bool>("--skip-signatures")
|
|
{
|
|
Description = "Skip DSSE signature verification."
|
|
};
|
|
var verifySkipChainOption = new Option<bool>("--skip-chain")
|
|
{
|
|
Description = "Skip chain-of-custody verification."
|
|
};
|
|
var verifyStrictTimelineOption = new Option<bool>("--strict-timeline")
|
|
{
|
|
Description = "Enforce strict timeline continuity (fail on gaps > 24h)."
|
|
};
|
|
|
|
verifyCommand.Add(verifyBundleArg);
|
|
verifyCommand.Add(verifyJsonOption);
|
|
verifyCommand.Add(verifyTrustRootOption);
|
|
verifyCommand.Add(verifySkipChecksumsOption);
|
|
verifyCommand.Add(verifySkipSignaturesOption);
|
|
verifyCommand.Add(verifySkipChainOption);
|
|
verifyCommand.Add(verifyStrictTimelineOption);
|
|
|
|
verifyCommand.SetAction((parseResult, _) =>
|
|
{
|
|
var bundlePath = parseResult.GetValue(verifyBundleArg) ?? string.Empty;
|
|
var emitJson = parseResult.GetValue(verifyJsonOption);
|
|
var trustRootPath = parseResult.GetValue(verifyTrustRootOption);
|
|
var skipChecksums = parseResult.GetValue(verifySkipChecksumsOption);
|
|
var skipSignatures = parseResult.GetValue(verifySkipSignaturesOption);
|
|
var skipChain = parseResult.GetValue(verifySkipChainOption);
|
|
var strictTimeline = parseResult.GetValue(verifyStrictTimelineOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleForensicVerifyAsync(
|
|
services,
|
|
bundlePath,
|
|
emitJson,
|
|
trustRootPath,
|
|
!skipChecksums,
|
|
!skipSignatures,
|
|
!skipChain,
|
|
strictTimeline,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
forensic.Add(verifyCommand);
|
|
|
|
// CLI-FORENSICS-54-002: stella forensic attest show <artifact>
|
|
var attestCommand = new Command("attest", "Attestation operations for forensic artifacts.");
|
|
|
|
var attestShowCommand = new Command("show", "Show attestation details including signer, timestamp, and subjects.");
|
|
var attestArtifactArg = new Argument<string>("artifact")
|
|
{
|
|
Description = "Path to attestation file (DSSE envelope)."
|
|
};
|
|
var attestJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON for CI integration."
|
|
};
|
|
var attestTrustRootOption = new Option<string?>("--trust-root", "-r")
|
|
{
|
|
Description = "Path to trust root JSON file for signature verification."
|
|
};
|
|
var attestVerifyOption = new Option<bool>("--verify")
|
|
{
|
|
Description = "Verify signatures against trust roots."
|
|
};
|
|
|
|
attestShowCommand.Add(attestArtifactArg);
|
|
attestShowCommand.Add(attestJsonOption);
|
|
attestShowCommand.Add(attestTrustRootOption);
|
|
attestShowCommand.Add(attestVerifyOption);
|
|
|
|
attestShowCommand.SetAction((parseResult, _) =>
|
|
{
|
|
var artifactPath = parseResult.GetValue(attestArtifactArg) ?? string.Empty;
|
|
var emitJson = parseResult.GetValue(attestJsonOption);
|
|
var trustRootPath = parseResult.GetValue(attestTrustRootOption);
|
|
var verify = parseResult.GetValue(attestVerifyOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleForensicAttestShowAsync(
|
|
services,
|
|
artifactPath,
|
|
emitJson,
|
|
trustRootPath,
|
|
verify,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
attestCommand.Add(attestShowCommand);
|
|
forensic.Add(attestCommand);
|
|
|
|
return forensic;
|
|
}
|
|
|
|
// CLI-PROMO-70-001: Promotion commands
|
|
private static Command BuildPromotionCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var promotion = new Command("promotion", "Build and manage promotion attestations.");
|
|
|
|
// promotion assemble
|
|
var assemble = new Command("assemble", "Assemble promotion attestation resolving image digests, hashing SBOM/VEX, and emitting stella.ops/promotion@v1 JSON.");
|
|
|
|
var imageArg = new Argument<string>("image")
|
|
{
|
|
Description = "Container image reference (e.g., registry.example.com/app:v1.0)."
|
|
};
|
|
var sbomOption = new Option<string?>("--sbom", "-s")
|
|
{
|
|
Description = "Path to SBOM file (CycloneDX or SPDX)."
|
|
};
|
|
var vexOption = new Option<string?>("--vex", "-v")
|
|
{
|
|
Description = "Path to VEX file (OpenVEX or CSAF)."
|
|
};
|
|
var fromOption = new Option<string>("--from")
|
|
{
|
|
Description = "Source environment (default: staging)."
|
|
};
|
|
fromOption.SetDefaultValue("staging");
|
|
var toOption = new Option<string>("--to")
|
|
{
|
|
Description = "Target environment (default: prod)."
|
|
};
|
|
toOption.SetDefaultValue("prod");
|
|
var actorOption = new Option<string?>("--actor")
|
|
{
|
|
Description = "Actor performing the promotion (default: current user)."
|
|
};
|
|
var pipelineOption = new Option<string?>("--pipeline")
|
|
{
|
|
Description = "CI/CD pipeline URL."
|
|
};
|
|
var ticketOption = new Option<string?>("--ticket")
|
|
{
|
|
Description = "Issue tracker ticket reference (e.g., JIRA-1234)."
|
|
};
|
|
var notesOption = new Option<string?>("--notes")
|
|
{
|
|
Description = "Additional notes about the promotion."
|
|
};
|
|
var skipRekorOption = new Option<bool>("--skip-rekor")
|
|
{
|
|
Description = "Skip Rekor transparency log integration."
|
|
};
|
|
var outputOption = new Option<string?>("--output", "-o")
|
|
{
|
|
Description = "Output path for the attestation JSON file."
|
|
};
|
|
var jsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON for CI integration."
|
|
};
|
|
var tenantOption = new Option<string?>("--tenant", "-t")
|
|
{
|
|
Description = "Tenant identifier."
|
|
};
|
|
|
|
assemble.Add(imageArg);
|
|
assemble.Add(sbomOption);
|
|
assemble.Add(vexOption);
|
|
assemble.Add(fromOption);
|
|
assemble.Add(toOption);
|
|
assemble.Add(actorOption);
|
|
assemble.Add(pipelineOption);
|
|
assemble.Add(ticketOption);
|
|
assemble.Add(notesOption);
|
|
assemble.Add(skipRekorOption);
|
|
assemble.Add(outputOption);
|
|
assemble.Add(jsonOption);
|
|
assemble.Add(tenantOption);
|
|
|
|
assemble.SetAction((parseResult, _) =>
|
|
{
|
|
var image = parseResult.GetValue(imageArg) ?? string.Empty;
|
|
var sbom = parseResult.GetValue(sbomOption);
|
|
var vex = parseResult.GetValue(vexOption);
|
|
var from = parseResult.GetValue(fromOption) ?? "staging";
|
|
var to = parseResult.GetValue(toOption) ?? "prod";
|
|
var actor = parseResult.GetValue(actorOption);
|
|
var pipeline = parseResult.GetValue(pipelineOption);
|
|
var ticket = parseResult.GetValue(ticketOption);
|
|
var notes = parseResult.GetValue(notesOption);
|
|
var skipRekor = parseResult.GetValue(skipRekorOption);
|
|
var output = parseResult.GetValue(outputOption);
|
|
var emitJson = parseResult.GetValue(jsonOption);
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePromotionAssembleAsync(
|
|
services,
|
|
image,
|
|
sbom,
|
|
vex,
|
|
from,
|
|
to,
|
|
actor,
|
|
pipeline,
|
|
ticket,
|
|
notes,
|
|
skipRekor,
|
|
output,
|
|
emitJson,
|
|
tenant,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
promotion.Add(assemble);
|
|
|
|
// CLI-PROMO-70-002: promotion attest
|
|
var attest = new Command("attest", "Sign a promotion predicate and produce a DSSE bundle via Signer or cosign.");
|
|
|
|
var attestPredicateArg = new Argument<string>("predicate")
|
|
{
|
|
Description = "Path to the promotion predicate JSON file (output of 'promotion assemble')."
|
|
};
|
|
var attestKeyOption = new Option<string?>("--key", "-k")
|
|
{
|
|
Description = "Signing key path or KMS key ID."
|
|
};
|
|
var attestKeylessOption = new Option<bool>("--keyless")
|
|
{
|
|
Description = "Use keyless signing (Fulcio-based)."
|
|
};
|
|
var attestNoRekorOption = new Option<bool>("--no-rekor")
|
|
{
|
|
Description = "Skip uploading to Rekor transparency log."
|
|
};
|
|
var attestOutputOption = new Option<string?>("--output", "-o")
|
|
{
|
|
Description = "Output path for the DSSE bundle."
|
|
};
|
|
var attestJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON for CI integration."
|
|
};
|
|
var attestTenantOption = new Option<string?>("--tenant", "-t")
|
|
{
|
|
Description = "Tenant identifier for Signer API."
|
|
};
|
|
|
|
attest.Add(attestPredicateArg);
|
|
attest.Add(attestKeyOption);
|
|
attest.Add(attestKeylessOption);
|
|
attest.Add(attestNoRekorOption);
|
|
attest.Add(attestOutputOption);
|
|
attest.Add(attestJsonOption);
|
|
attest.Add(attestTenantOption);
|
|
|
|
attest.SetAction((parseResult, _) =>
|
|
{
|
|
var predicatePath = parseResult.GetValue(attestPredicateArg) ?? string.Empty;
|
|
var keyId = parseResult.GetValue(attestKeyOption);
|
|
var useKeyless = parseResult.GetValue(attestKeylessOption);
|
|
var noRekor = parseResult.GetValue(attestNoRekorOption);
|
|
var output = parseResult.GetValue(attestOutputOption);
|
|
var emitJson = parseResult.GetValue(attestJsonOption);
|
|
var tenant = parseResult.GetValue(attestTenantOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePromotionAttestAsync(
|
|
services,
|
|
predicatePath,
|
|
keyId,
|
|
useKeyless,
|
|
!noRekor,
|
|
output,
|
|
emitJson,
|
|
tenant,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
promotion.Add(attest);
|
|
|
|
// CLI-PROMO-70-002: promotion verify
|
|
var verify = new Command("verify", "Verify a promotion attestation bundle offline against trusted checkpoints.");
|
|
|
|
var verifyBundleArg = new Argument<string>("bundle")
|
|
{
|
|
Description = "Path to the DSSE bundle file."
|
|
};
|
|
var verifySbomOption = new Option<string?>("--sbom")
|
|
{
|
|
Description = "Path to SBOM file for material verification."
|
|
};
|
|
var verifyVexOption = new Option<string?>("--vex")
|
|
{
|
|
Description = "Path to VEX file for material verification."
|
|
};
|
|
var verifyTrustRootOption = new Option<string?>("--trust-root")
|
|
{
|
|
Description = "Path to trusted certificate chain."
|
|
};
|
|
var verifyCheckpointOption = new Option<string?>("--checkpoint")
|
|
{
|
|
Description = "Path to Rekor checkpoint for verification."
|
|
};
|
|
var verifySkipSigOption = new Option<bool>("--skip-signature")
|
|
{
|
|
Description = "Skip signature verification."
|
|
};
|
|
var verifySkipRekorOption = new Option<bool>("--skip-rekor")
|
|
{
|
|
Description = "Skip Rekor inclusion proof verification."
|
|
};
|
|
var verifyJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON for CI integration."
|
|
};
|
|
var verifyTenantOption = new Option<string?>("--tenant", "-t")
|
|
{
|
|
Description = "Tenant identifier."
|
|
};
|
|
|
|
verify.Add(verifyBundleArg);
|
|
verify.Add(verifySbomOption);
|
|
verify.Add(verifyVexOption);
|
|
verify.Add(verifyTrustRootOption);
|
|
verify.Add(verifyCheckpointOption);
|
|
verify.Add(verifySkipSigOption);
|
|
verify.Add(verifySkipRekorOption);
|
|
verify.Add(verifyJsonOption);
|
|
verify.Add(verifyTenantOption);
|
|
|
|
verify.SetAction((parseResult, _) =>
|
|
{
|
|
var bundlePath = parseResult.GetValue(verifyBundleArg) ?? string.Empty;
|
|
var sbom = parseResult.GetValue(verifySbomOption);
|
|
var vex = parseResult.GetValue(verifyVexOption);
|
|
var trustRoot = parseResult.GetValue(verifyTrustRootOption);
|
|
var checkpoint = parseResult.GetValue(verifyCheckpointOption);
|
|
var skipSig = parseResult.GetValue(verifySkipSigOption);
|
|
var skipRekor = parseResult.GetValue(verifySkipRekorOption);
|
|
var emitJson = parseResult.GetValue(verifyJsonOption);
|
|
var tenant = parseResult.GetValue(verifyTenantOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePromotionVerifyAsync(
|
|
services,
|
|
bundlePath,
|
|
sbom,
|
|
vex,
|
|
trustRoot,
|
|
checkpoint,
|
|
skipSig,
|
|
skipRekor,
|
|
emitJson,
|
|
tenant,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
promotion.Add(verify);
|
|
|
|
return promotion;
|
|
}
|
|
|
|
// CLI-DETER-70-003: Determinism score commands
|
|
private static Command BuildDetscoreCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var detscore = new Command("detscore", "Scanner determinism scoring harness for reproducibility testing.");
|
|
|
|
// detscore run
|
|
var run = new Command("run", "Run determinism harness with frozen clock, seeded RNG, and canonical hashes. Exits non-zero if score falls below threshold.");
|
|
|
|
var imagesOption = new Option<string[]>("--image", "-i")
|
|
{
|
|
Description = "Image digests to test (can be specified multiple times).",
|
|
AllowMultipleArgumentsPerToken = true
|
|
};
|
|
imagesOption.Required = true;
|
|
|
|
var scannerOption = new Option<string>("--scanner", "-s")
|
|
{
|
|
Description = "Scanner container image reference."
|
|
};
|
|
scannerOption.Required = true;
|
|
|
|
var policyBundleOption = new Option<string?>("--policy-bundle")
|
|
{
|
|
Description = "Path to policy bundle tarball."
|
|
};
|
|
|
|
var feedsBundleOption = new Option<string?>("--feeds-bundle")
|
|
{
|
|
Description = "Path to feeds bundle tarball."
|
|
};
|
|
|
|
var runsOption = new Option<int>("--runs", "-n")
|
|
{
|
|
Description = "Number of runs per image (default: 10)."
|
|
};
|
|
runsOption.SetDefaultValue(10);
|
|
|
|
var fixedClockOption = new Option<DateTimeOffset?>("--fixed-clock")
|
|
{
|
|
Description = "Fixed clock timestamp for deterministic execution (default: current UTC)."
|
|
};
|
|
|
|
var rngSeedOption = new Option<int>("--rng-seed")
|
|
{
|
|
Description = "RNG seed for deterministic execution (default: 1337)."
|
|
};
|
|
rngSeedOption.SetDefaultValue(1337);
|
|
|
|
var maxConcurrencyOption = new Option<int>("--max-concurrency")
|
|
{
|
|
Description = "Maximum concurrency for scanner (default: 1 for determinism)."
|
|
};
|
|
maxConcurrencyOption.SetDefaultValue(1);
|
|
|
|
var memoryLimitOption = new Option<string>("--memory")
|
|
{
|
|
Description = "Memory limit for container (default: 2G)."
|
|
};
|
|
memoryLimitOption.SetDefaultValue("2G");
|
|
|
|
var cpuSetOption = new Option<string>("--cpuset")
|
|
{
|
|
Description = "CPU set for container (default: 0)."
|
|
};
|
|
cpuSetOption.SetDefaultValue("0");
|
|
|
|
var platformOption = new Option<string>("--platform")
|
|
{
|
|
Description = "Platform (default: linux/amd64)."
|
|
};
|
|
platformOption.SetDefaultValue("linux/amd64");
|
|
|
|
var imageThresholdOption = new Option<double>("--image-threshold")
|
|
{
|
|
Description = "Minimum threshold for individual image scores (default: 0.90)."
|
|
};
|
|
imageThresholdOption.SetDefaultValue(0.90);
|
|
|
|
var overallThresholdOption = new Option<double>("--overall-threshold")
|
|
{
|
|
Description = "Minimum threshold for overall score (default: 0.95)."
|
|
};
|
|
overallThresholdOption.SetDefaultValue(0.95);
|
|
|
|
var outputDirOption = new Option<string?>("--output-dir", "-o")
|
|
{
|
|
Description = "Output directory for determinism.json and run artifacts."
|
|
};
|
|
|
|
var releaseOption = new Option<string?>("--release")
|
|
{
|
|
Description = "Release version string for the manifest."
|
|
};
|
|
|
|
var jsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON for CI integration."
|
|
};
|
|
|
|
run.Add(imagesOption);
|
|
run.Add(scannerOption);
|
|
run.Add(policyBundleOption);
|
|
run.Add(feedsBundleOption);
|
|
run.Add(runsOption);
|
|
run.Add(fixedClockOption);
|
|
run.Add(rngSeedOption);
|
|
run.Add(maxConcurrencyOption);
|
|
run.Add(memoryLimitOption);
|
|
run.Add(cpuSetOption);
|
|
run.Add(platformOption);
|
|
run.Add(imageThresholdOption);
|
|
run.Add(overallThresholdOption);
|
|
run.Add(outputDirOption);
|
|
run.Add(releaseOption);
|
|
run.Add(jsonOption);
|
|
run.Add(verboseOption);
|
|
|
|
run.SetAction((parseResult, _) =>
|
|
{
|
|
var images = parseResult.GetValue(imagesOption) ?? Array.Empty<string>();
|
|
var scanner = parseResult.GetValue(scannerOption) ?? string.Empty;
|
|
var policyBundle = parseResult.GetValue(policyBundleOption);
|
|
var feedsBundle = parseResult.GetValue(feedsBundleOption);
|
|
var runs = parseResult.GetValue(runsOption);
|
|
var fixedClock = parseResult.GetValue(fixedClockOption);
|
|
var rngSeed = parseResult.GetValue(rngSeedOption);
|
|
var maxConcurrency = parseResult.GetValue(maxConcurrencyOption);
|
|
var memoryLimit = parseResult.GetValue(memoryLimitOption) ?? "2G";
|
|
var cpuSet = parseResult.GetValue(cpuSetOption) ?? "0";
|
|
var platform = parseResult.GetValue(platformOption) ?? "linux/amd64";
|
|
var imageThreshold = parseResult.GetValue(imageThresholdOption);
|
|
var overallThreshold = parseResult.GetValue(overallThresholdOption);
|
|
var outputDir = parseResult.GetValue(outputDirOption);
|
|
var release = parseResult.GetValue(releaseOption);
|
|
var emitJson = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleDetscoreRunAsync(
|
|
services,
|
|
images,
|
|
scanner,
|
|
policyBundle,
|
|
feedsBundle,
|
|
runs,
|
|
fixedClock,
|
|
rngSeed,
|
|
maxConcurrency,
|
|
memoryLimit,
|
|
cpuSet,
|
|
platform,
|
|
imageThreshold,
|
|
overallThreshold,
|
|
outputDir,
|
|
release,
|
|
emitJson,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
detscore.Add(run);
|
|
|
|
// CLI-DETER-70-004: detscore report
|
|
var report = new Command("report", "Generate determinism score report from published determinism.json manifests for release notes and air-gap kits.");
|
|
|
|
var manifestsArg = new Argument<string[]>("manifests")
|
|
{
|
|
Description = "Paths to determinism.json manifest files.",
|
|
Arity = ArgumentArity.OneOrMore
|
|
};
|
|
|
|
var formatOption = new Option<string>("--format", "-f")
|
|
{
|
|
Description = "Output format: markdown, json, csv (default: markdown)."
|
|
};
|
|
formatOption.SetDefaultValue("markdown");
|
|
formatOption.FromAmong("markdown", "json", "csv");
|
|
|
|
var outputOption = new Option<string?>("--output", "-o")
|
|
{
|
|
Description = "Output file path. If omitted, writes to stdout."
|
|
};
|
|
|
|
var detailsOption = new Option<bool>("--details")
|
|
{
|
|
Description = "Include per-image matrix and run details in output."
|
|
};
|
|
|
|
var titleOption = new Option<string?>("--title")
|
|
{
|
|
Description = "Title for the report."
|
|
};
|
|
|
|
var reportJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Equivalent to --format json for CI integration."
|
|
};
|
|
|
|
report.Add(manifestsArg);
|
|
report.Add(formatOption);
|
|
report.Add(outputOption);
|
|
report.Add(detailsOption);
|
|
report.Add(titleOption);
|
|
report.Add(reportJsonOption);
|
|
report.Add(verboseOption);
|
|
|
|
report.SetAction((parseResult, _) =>
|
|
{
|
|
var manifests = parseResult.GetValue(manifestsArg) ?? Array.Empty<string>();
|
|
var format = parseResult.GetValue(formatOption) ?? "markdown";
|
|
var output = parseResult.GetValue(outputOption);
|
|
var details = parseResult.GetValue(detailsOption);
|
|
var title = parseResult.GetValue(titleOption);
|
|
var json = parseResult.GetValue(reportJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
// --json is shorthand for --format json
|
|
if (json)
|
|
{
|
|
format = "json";
|
|
}
|
|
|
|
return CommandHandlers.HandleDetscoreReportAsync(
|
|
services,
|
|
manifests,
|
|
format,
|
|
output,
|
|
details,
|
|
title,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
detscore.Add(report);
|
|
|
|
return detscore;
|
|
}
|
|
|
|
// CLI-OBS-51-001: Observability commands
|
|
private static Command BuildObsCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var obs = new Command("obs", "Platform observability: service health, SLOs, burn-rate alerts, and metrics.");
|
|
|
|
// obs top
|
|
var top = new Command("top", "Stream service health metrics, SLO status, and burn-rate alerts (like 'top' for your platform).");
|
|
|
|
var servicesOption = new Option<string[]>("--service", "-s")
|
|
{
|
|
Description = "Filter by service name (repeatable).",
|
|
AllowMultipleArgumentsPerToken = true
|
|
};
|
|
|
|
var tenantOption = new Option<string?>("--tenant", "-t")
|
|
{
|
|
Description = "Filter by tenant."
|
|
};
|
|
|
|
var refreshOption = new Option<int>("--refresh", "-r")
|
|
{
|
|
Description = "Refresh interval in seconds (0 = single fetch, default: 0)."
|
|
};
|
|
refreshOption.SetDefaultValue(0);
|
|
|
|
var includeQueuesOption = new Option<bool>("--queues")
|
|
{
|
|
Description = "Include queue health details (default: true)."
|
|
};
|
|
includeQueuesOption.SetDefaultValue(true);
|
|
|
|
var maxAlertsOption = new Option<int>("--max-alerts")
|
|
{
|
|
Description = "Maximum number of alerts to display (default: 20)."
|
|
};
|
|
maxAlertsOption.SetDefaultValue(20);
|
|
|
|
var outputOption = new Option<string>("--output", "-o")
|
|
{
|
|
Description = "Output format: table, json, ndjson (default: table)."
|
|
};
|
|
outputOption.SetDefaultValue("table");
|
|
outputOption.FromAmong("table", "json", "ndjson");
|
|
|
|
var jsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Equivalent to --output json for CI integration."
|
|
};
|
|
|
|
var offlineOption = new Option<bool>("--offline")
|
|
{
|
|
Description = "Operate on cached data only; exit with code 5 if network access required."
|
|
};
|
|
|
|
top.Add(servicesOption);
|
|
top.Add(tenantOption);
|
|
top.Add(refreshOption);
|
|
top.Add(includeQueuesOption);
|
|
top.Add(maxAlertsOption);
|
|
top.Add(outputOption);
|
|
top.Add(jsonOption);
|
|
top.Add(offlineOption);
|
|
top.Add(verboseOption);
|
|
|
|
top.SetAction((parseResult, _) =>
|
|
{
|
|
var serviceNames = parseResult.GetValue(servicesOption) ?? Array.Empty<string>();
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var refresh = parseResult.GetValue(refreshOption);
|
|
var includeQueues = parseResult.GetValue(includeQueuesOption);
|
|
var maxAlerts = parseResult.GetValue(maxAlertsOption);
|
|
var output = parseResult.GetValue(outputOption) ?? "table";
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var offline = parseResult.GetValue(offlineOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
// --json is shorthand for --output json
|
|
if (json)
|
|
{
|
|
output = "json";
|
|
}
|
|
|
|
return CommandHandlers.HandleObsTopAsync(
|
|
services,
|
|
serviceNames,
|
|
tenant,
|
|
refresh,
|
|
includeQueues,
|
|
maxAlerts,
|
|
output,
|
|
offline,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
obs.Add(top);
|
|
|
|
// CLI-OBS-52-001: obs trace
|
|
var trace = new Command("trace", "Fetch a distributed trace by ID with correlated spans and evidence links.");
|
|
|
|
var traceIdArg = new Argument<string>("trace_id")
|
|
{
|
|
Description = "The trace ID to fetch."
|
|
};
|
|
|
|
var traceTenantOption = new Option<string?>("--tenant", "-t")
|
|
{
|
|
Description = "Filter by tenant."
|
|
};
|
|
|
|
var includeEvidenceOption = new Option<bool>("--evidence")
|
|
{
|
|
Description = "Include evidence links (SBOM, VEX, attestations). Default: true."
|
|
};
|
|
includeEvidenceOption.SetDefaultValue(true);
|
|
|
|
var traceOutputOption = new Option<string>("--output", "-o")
|
|
{
|
|
Description = "Output format: table, json (default: table)."
|
|
};
|
|
traceOutputOption.SetDefaultValue("table");
|
|
traceOutputOption.FromAmong("table", "json");
|
|
|
|
var traceJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Equivalent to --output json."
|
|
};
|
|
|
|
var traceOfflineOption = new Option<bool>("--offline")
|
|
{
|
|
Description = "Operate on cached data only; exit with code 5 if network access required."
|
|
};
|
|
|
|
trace.Add(traceIdArg);
|
|
trace.Add(traceTenantOption);
|
|
trace.Add(includeEvidenceOption);
|
|
trace.Add(traceOutputOption);
|
|
trace.Add(traceJsonOption);
|
|
trace.Add(traceOfflineOption);
|
|
trace.Add(verboseOption);
|
|
|
|
trace.SetAction((parseResult, _) =>
|
|
{
|
|
var traceId = parseResult.GetValue(traceIdArg) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(traceTenantOption);
|
|
var includeEvidence = parseResult.GetValue(includeEvidenceOption);
|
|
var output = parseResult.GetValue(traceOutputOption) ?? "table";
|
|
var json = parseResult.GetValue(traceJsonOption);
|
|
var offline = parseResult.GetValue(traceOfflineOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
if (json)
|
|
{
|
|
output = "json";
|
|
}
|
|
|
|
return CommandHandlers.HandleObsTraceAsync(
|
|
services,
|
|
traceId,
|
|
tenant,
|
|
includeEvidence,
|
|
output,
|
|
offline,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
obs.Add(trace);
|
|
|
|
// CLI-OBS-52-001: obs logs
|
|
var logs = new Command("logs", "Fetch platform logs for a time window with pagination and filters.");
|
|
|
|
var fromOption = new Option<DateTimeOffset>("--from")
|
|
{
|
|
Description = "Start timestamp (ISO-8601). Required."
|
|
};
|
|
fromOption.Required = true;
|
|
|
|
var toOption = new Option<DateTimeOffset>("--to")
|
|
{
|
|
Description = "End timestamp (ISO-8601). Required."
|
|
};
|
|
toOption.Required = true;
|
|
|
|
var logsTenantOption = new Option<string?>("--tenant", "-t")
|
|
{
|
|
Description = "Filter by tenant."
|
|
};
|
|
|
|
var logsServicesOption = new Option<string[]>("--service", "-s")
|
|
{
|
|
Description = "Filter by service name (repeatable).",
|
|
AllowMultipleArgumentsPerToken = true
|
|
};
|
|
|
|
var logsLevelsOption = new Option<string[]>("--level", "-l")
|
|
{
|
|
Description = "Filter by log level: debug, info, warn, error (repeatable).",
|
|
AllowMultipleArgumentsPerToken = true
|
|
};
|
|
|
|
var logsQueryOption = new Option<string?>("--query", "-q")
|
|
{
|
|
Description = "Full-text search query."
|
|
};
|
|
|
|
var logsPageSizeOption = new Option<int>("--page-size")
|
|
{
|
|
Description = "Number of logs per page (default: 100, max: 500)."
|
|
};
|
|
logsPageSizeOption.SetDefaultValue(100);
|
|
|
|
var logsPageTokenOption = new Option<string?>("--page-token")
|
|
{
|
|
Description = "Pagination token for fetching next page."
|
|
};
|
|
|
|
var logsOutputOption = new Option<string>("--output", "-o")
|
|
{
|
|
Description = "Output format: table, json, ndjson (default: table)."
|
|
};
|
|
logsOutputOption.SetDefaultValue("table");
|
|
logsOutputOption.FromAmong("table", "json", "ndjson");
|
|
|
|
var logsJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Equivalent to --output json."
|
|
};
|
|
|
|
var logsOfflineOption = new Option<bool>("--offline")
|
|
{
|
|
Description = "Operate on cached data only; exit with code 5 if network access required."
|
|
};
|
|
|
|
logs.Add(fromOption);
|
|
logs.Add(toOption);
|
|
logs.Add(logsTenantOption);
|
|
logs.Add(logsServicesOption);
|
|
logs.Add(logsLevelsOption);
|
|
logs.Add(logsQueryOption);
|
|
logs.Add(logsPageSizeOption);
|
|
logs.Add(logsPageTokenOption);
|
|
logs.Add(logsOutputOption);
|
|
logs.Add(logsJsonOption);
|
|
logs.Add(logsOfflineOption);
|
|
logs.Add(verboseOption);
|
|
|
|
logs.SetAction((parseResult, _) =>
|
|
{
|
|
var from = parseResult.GetValue(fromOption);
|
|
var to = parseResult.GetValue(toOption);
|
|
var tenant = parseResult.GetValue(logsTenantOption);
|
|
var serviceNames = parseResult.GetValue(logsServicesOption) ?? Array.Empty<string>();
|
|
var levels = parseResult.GetValue(logsLevelsOption) ?? Array.Empty<string>();
|
|
var query = parseResult.GetValue(logsQueryOption);
|
|
var pageSize = parseResult.GetValue(logsPageSizeOption);
|
|
var pageToken = parseResult.GetValue(logsPageTokenOption);
|
|
var output = parseResult.GetValue(logsOutputOption) ?? "table";
|
|
var json = parseResult.GetValue(logsJsonOption);
|
|
var offline = parseResult.GetValue(logsOfflineOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
if (json)
|
|
{
|
|
output = "json";
|
|
}
|
|
|
|
return CommandHandlers.HandleObsLogsAsync(
|
|
services,
|
|
from,
|
|
to,
|
|
tenant,
|
|
serviceNames,
|
|
levels,
|
|
query,
|
|
pageSize,
|
|
pageToken,
|
|
output,
|
|
offline,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
obs.Add(logs);
|
|
|
|
// CLI-OBS-55-001: obs incident-mode
|
|
var incidentMode = new Command("incident-mode", "Manage incident mode for enhanced forensic fidelity and retention.");
|
|
|
|
// incident-mode enable
|
|
var incidentEnable = new Command("enable", "Enable incident mode with extended retention and debug artefacts.");
|
|
|
|
var enableTenantOption = new Option<string?>("--tenant", "-t")
|
|
{
|
|
Description = "Tenant scope for incident mode."
|
|
};
|
|
|
|
var enableTtlOption = new Option<int>("--ttl")
|
|
{
|
|
Description = "Time-to-live in minutes (default: 30). Mode auto-expires after TTL."
|
|
};
|
|
enableTtlOption.SetDefaultValue(30);
|
|
|
|
var enableRetentionOption = new Option<int>("--retention-days")
|
|
{
|
|
Description = "Extended retention period in days (default: 60)."
|
|
};
|
|
enableRetentionOption.SetDefaultValue(60);
|
|
|
|
var enableReasonOption = new Option<string?>("--reason")
|
|
{
|
|
Description = "Reason for enabling incident mode (appears in audit log)."
|
|
};
|
|
|
|
var enableJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON."
|
|
};
|
|
|
|
incidentEnable.Add(enableTenantOption);
|
|
incidentEnable.Add(enableTtlOption);
|
|
incidentEnable.Add(enableRetentionOption);
|
|
incidentEnable.Add(enableReasonOption);
|
|
incidentEnable.Add(enableJsonOption);
|
|
incidentEnable.Add(verboseOption);
|
|
|
|
incidentEnable.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(enableTenantOption);
|
|
var ttl = parseResult.GetValue(enableTtlOption);
|
|
var retention = parseResult.GetValue(enableRetentionOption);
|
|
var reason = parseResult.GetValue(enableReasonOption);
|
|
var json = parseResult.GetValue(enableJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleObsIncidentModeEnableAsync(
|
|
services,
|
|
tenant,
|
|
ttl,
|
|
retention,
|
|
reason,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
incidentMode.Add(incidentEnable);
|
|
|
|
// incident-mode disable
|
|
var incidentDisable = new Command("disable", "Disable incident mode and return to normal operation.");
|
|
|
|
var disableTenantOption = new Option<string?>("--tenant", "-t")
|
|
{
|
|
Description = "Tenant scope for incident mode."
|
|
};
|
|
|
|
var disableReasonOption = new Option<string?>("--reason")
|
|
{
|
|
Description = "Reason for disabling incident mode (appears in audit log)."
|
|
};
|
|
|
|
var disableJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON."
|
|
};
|
|
|
|
incidentDisable.Add(disableTenantOption);
|
|
incidentDisable.Add(disableReasonOption);
|
|
incidentDisable.Add(disableJsonOption);
|
|
incidentDisable.Add(verboseOption);
|
|
|
|
incidentDisable.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(disableTenantOption);
|
|
var reason = parseResult.GetValue(disableReasonOption);
|
|
var json = parseResult.GetValue(disableJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleObsIncidentModeDisableAsync(
|
|
services,
|
|
tenant,
|
|
reason,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
incidentMode.Add(incidentDisable);
|
|
|
|
// incident-mode status
|
|
var incidentStatus = new Command("status", "Show current incident mode status.");
|
|
|
|
var statusTenantOption = new Option<string?>("--tenant", "-t")
|
|
{
|
|
Description = "Tenant scope for incident mode."
|
|
};
|
|
|
|
var statusJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON."
|
|
};
|
|
|
|
incidentStatus.Add(statusTenantOption);
|
|
incidentStatus.Add(statusJsonOption);
|
|
incidentStatus.Add(verboseOption);
|
|
|
|
incidentStatus.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(statusTenantOption);
|
|
var json = parseResult.GetValue(statusJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleObsIncidentModeStatusAsync(
|
|
services,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
incidentMode.Add(incidentStatus);
|
|
|
|
obs.Add(incidentMode);
|
|
|
|
return obs;
|
|
}
|
|
|
|
// CLI-PACKS-42-001: Task Pack commands
|
|
private static Command BuildPackCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var pack = new Command("pack", "Task Pack operations: plan, run, push, pull, verify.");
|
|
|
|
// Common options
|
|
var tenantOption = new Option<string?>("--tenant", "-t")
|
|
{
|
|
Description = "Tenant scope for the operation."
|
|
};
|
|
|
|
var jsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON."
|
|
};
|
|
|
|
var offlineOption = new Option<bool>("--offline")
|
|
{
|
|
Description = "Offline mode - only use local cache (fails if not available)."
|
|
};
|
|
|
|
// pack plan
|
|
var plan = new Command("plan", "Plan a pack execution and validate inputs.");
|
|
|
|
var planPackIdArg = new Argument<string>("pack-id")
|
|
{
|
|
Description = "Pack identifier (e.g., stellaops/scanner-audit)."
|
|
};
|
|
|
|
var planVersionOption = new Option<string?>("--version", "-v")
|
|
{
|
|
Description = "Pack version (defaults to latest)."
|
|
};
|
|
|
|
var planInputsOption = new Option<string?>("--inputs", "-i")
|
|
{
|
|
Description = "Path to JSON file containing input values."
|
|
};
|
|
|
|
var planDryRunOption = new Option<bool>("--dry-run")
|
|
{
|
|
Description = "Validate only, do not prepare for execution."
|
|
};
|
|
|
|
var planOutputOption = new Option<string?>("--output", "-o")
|
|
{
|
|
Description = "Write plan to file."
|
|
};
|
|
|
|
plan.Add(planPackIdArg);
|
|
plan.Add(planVersionOption);
|
|
plan.Add(planInputsOption);
|
|
plan.Add(planDryRunOption);
|
|
plan.Add(planOutputOption);
|
|
plan.Add(tenantOption);
|
|
plan.Add(jsonOption);
|
|
plan.Add(offlineOption);
|
|
plan.Add(verboseOption);
|
|
|
|
plan.SetAction((parseResult, _) =>
|
|
{
|
|
var packId = parseResult.GetValue(planPackIdArg) ?? string.Empty;
|
|
var version = parseResult.GetValue(planVersionOption);
|
|
var inputsPath = parseResult.GetValue(planInputsOption);
|
|
var dryRun = parseResult.GetValue(planDryRunOption);
|
|
var output = parseResult.GetValue(planOutputOption);
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var offline = parseResult.GetValue(offlineOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePackPlanAsync(
|
|
services,
|
|
packId,
|
|
version,
|
|
inputsPath,
|
|
dryRun,
|
|
output,
|
|
tenant,
|
|
json,
|
|
offline,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
pack.Add(plan);
|
|
|
|
// pack run
|
|
var run = new Command("run", "Execute a pack with the specified inputs.");
|
|
|
|
var runPackIdArg = new Argument<string>("pack-id")
|
|
{
|
|
Description = "Pack identifier (e.g., stellaops/scanner-audit)."
|
|
};
|
|
|
|
var runVersionOption = new Option<string?>("--version", "-v")
|
|
{
|
|
Description = "Pack version (defaults to latest)."
|
|
};
|
|
|
|
var runInputsOption = new Option<string?>("--inputs", "-i")
|
|
{
|
|
Description = "Path to JSON file containing input values."
|
|
};
|
|
|
|
var runPlanIdOption = new Option<string?>("--plan-id")
|
|
{
|
|
Description = "Use a previously created plan instead of inputs."
|
|
};
|
|
|
|
var runWaitOption = new Option<bool>("--wait", "-w")
|
|
{
|
|
Description = "Wait for pack execution to complete."
|
|
};
|
|
|
|
var runTimeoutOption = new Option<int>("--timeout")
|
|
{
|
|
Description = "Timeout in minutes when waiting for completion (default: 60)."
|
|
};
|
|
runTimeoutOption.SetDefaultValue(60);
|
|
|
|
var runLabelsOption = new Option<string[]>("--label", "-l")
|
|
{
|
|
Description = "Labels to attach to the run (key=value format, repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
runLabelsOption.AllowMultipleArgumentsPerToken = true;
|
|
|
|
var runOutputOption = new Option<string?>("--output", "-o")
|
|
{
|
|
Description = "Write run result to file."
|
|
};
|
|
|
|
run.Add(runPackIdArg);
|
|
run.Add(runVersionOption);
|
|
run.Add(runInputsOption);
|
|
run.Add(runPlanIdOption);
|
|
run.Add(runWaitOption);
|
|
run.Add(runTimeoutOption);
|
|
run.Add(runLabelsOption);
|
|
run.Add(runOutputOption);
|
|
run.Add(tenantOption);
|
|
run.Add(jsonOption);
|
|
run.Add(offlineOption);
|
|
run.Add(verboseOption);
|
|
|
|
run.SetAction((parseResult, _) =>
|
|
{
|
|
var packId = parseResult.GetValue(runPackIdArg) ?? string.Empty;
|
|
var version = parseResult.GetValue(runVersionOption);
|
|
var inputsPath = parseResult.GetValue(runInputsOption);
|
|
var planId = parseResult.GetValue(runPlanIdOption);
|
|
var wait = parseResult.GetValue(runWaitOption);
|
|
var timeout = parseResult.GetValue(runTimeoutOption);
|
|
var labels = parseResult.GetValue(runLabelsOption) ?? Array.Empty<string>();
|
|
var output = parseResult.GetValue(runOutputOption);
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var offline = parseResult.GetValue(offlineOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePackRunAsync(
|
|
services,
|
|
packId,
|
|
version,
|
|
inputsPath,
|
|
planId,
|
|
wait,
|
|
timeout,
|
|
labels,
|
|
output,
|
|
tenant,
|
|
json,
|
|
offline,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
pack.Add(run);
|
|
|
|
// pack push
|
|
var push = new Command("push", "Push a pack to the registry.");
|
|
|
|
var pushPathArg = new Argument<string>("path")
|
|
{
|
|
Description = "Path to pack file (.tar.gz) or directory."
|
|
};
|
|
|
|
var pushNameOption = new Option<string?>("--name", "-n")
|
|
{
|
|
Description = "Pack name (overrides manifest)."
|
|
};
|
|
|
|
var pushVersionOption = new Option<string?>("--version", "-v")
|
|
{
|
|
Description = "Pack version (overrides manifest)."
|
|
};
|
|
|
|
var pushSignOption = new Option<bool>("--sign")
|
|
{
|
|
Description = "Sign the pack before pushing."
|
|
};
|
|
|
|
var pushKeyIdOption = new Option<string?>("--key-id")
|
|
{
|
|
Description = "Key ID to use for signing."
|
|
};
|
|
|
|
var pushForceOption = new Option<bool>("--force", "-f")
|
|
{
|
|
Description = "Overwrite existing version."
|
|
};
|
|
|
|
push.Add(pushPathArg);
|
|
push.Add(pushNameOption);
|
|
push.Add(pushVersionOption);
|
|
push.Add(pushSignOption);
|
|
push.Add(pushKeyIdOption);
|
|
push.Add(pushForceOption);
|
|
push.Add(tenantOption);
|
|
push.Add(jsonOption);
|
|
push.Add(verboseOption);
|
|
|
|
push.SetAction((parseResult, _) =>
|
|
{
|
|
var path = parseResult.GetValue(pushPathArg) ?? string.Empty;
|
|
var name = parseResult.GetValue(pushNameOption);
|
|
var version = parseResult.GetValue(pushVersionOption);
|
|
var sign = parseResult.GetValue(pushSignOption);
|
|
var keyId = parseResult.GetValue(pushKeyIdOption);
|
|
var force = parseResult.GetValue(pushForceOption);
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePackPushAsync(
|
|
services,
|
|
path,
|
|
name,
|
|
version,
|
|
sign,
|
|
keyId,
|
|
force,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
pack.Add(push);
|
|
|
|
// pack pull
|
|
var pull = new Command("pull", "Pull a pack from the registry.");
|
|
|
|
var pullPackIdArg = new Argument<string>("pack-id")
|
|
{
|
|
Description = "Pack identifier (e.g., stellaops/scanner-audit)."
|
|
};
|
|
|
|
var pullVersionOption = new Option<string?>("--version", "-v")
|
|
{
|
|
Description = "Pack version (defaults to latest)."
|
|
};
|
|
|
|
var pullOutputOption = new Option<string?>("--output", "-o")
|
|
{
|
|
Description = "Output path for downloaded pack."
|
|
};
|
|
|
|
var pullNoVerifyOption = new Option<bool>("--no-verify")
|
|
{
|
|
Description = "Skip signature verification."
|
|
};
|
|
|
|
pull.Add(pullPackIdArg);
|
|
pull.Add(pullVersionOption);
|
|
pull.Add(pullOutputOption);
|
|
pull.Add(pullNoVerifyOption);
|
|
pull.Add(tenantOption);
|
|
pull.Add(jsonOption);
|
|
pull.Add(verboseOption);
|
|
|
|
pull.SetAction((parseResult, _) =>
|
|
{
|
|
var packId = parseResult.GetValue(pullPackIdArg) ?? string.Empty;
|
|
var version = parseResult.GetValue(pullVersionOption);
|
|
var output = parseResult.GetValue(pullOutputOption);
|
|
var noVerify = parseResult.GetValue(pullNoVerifyOption);
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePackPullAsync(
|
|
services,
|
|
packId,
|
|
version,
|
|
output,
|
|
noVerify,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
pack.Add(pull);
|
|
|
|
// pack verify
|
|
var verify = new Command("verify", "Verify a pack's signature, digest, and schema.");
|
|
|
|
var verifyPathOption = new Option<string?>("--path", "-p")
|
|
{
|
|
Description = "Path to local pack file to verify."
|
|
};
|
|
|
|
var verifyPackIdOption = new Option<string?>("--pack-id")
|
|
{
|
|
Description = "Pack ID to verify from registry."
|
|
};
|
|
|
|
var verifyVersionOption = new Option<string?>("--version", "-v")
|
|
{
|
|
Description = "Pack version to verify."
|
|
};
|
|
|
|
var verifyDigestOption = new Option<string?>("--digest")
|
|
{
|
|
Description = "Expected digest to verify against."
|
|
};
|
|
|
|
var verifyNoRekorOption = new Option<bool>("--no-rekor")
|
|
{
|
|
Description = "Skip Rekor transparency log verification."
|
|
};
|
|
|
|
var verifyNoExpiryOption = new Option<bool>("--no-expiry")
|
|
{
|
|
Description = "Skip certificate expiry check."
|
|
};
|
|
|
|
verify.Add(verifyPathOption);
|
|
verify.Add(verifyPackIdOption);
|
|
verify.Add(verifyVersionOption);
|
|
verify.Add(verifyDigestOption);
|
|
verify.Add(verifyNoRekorOption);
|
|
verify.Add(verifyNoExpiryOption);
|
|
verify.Add(tenantOption);
|
|
verify.Add(jsonOption);
|
|
verify.Add(verboseOption);
|
|
|
|
verify.SetAction((parseResult, _) =>
|
|
{
|
|
var path = parseResult.GetValue(verifyPathOption);
|
|
var packId = parseResult.GetValue(verifyPackIdOption);
|
|
var version = parseResult.GetValue(verifyVersionOption);
|
|
var digest = parseResult.GetValue(verifyDigestOption);
|
|
var noRekor = parseResult.GetValue(verifyNoRekorOption);
|
|
var noExpiry = parseResult.GetValue(verifyNoExpiryOption);
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePackVerifyAsync(
|
|
services,
|
|
path,
|
|
packId,
|
|
version,
|
|
digest,
|
|
noRekor,
|
|
noExpiry,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
pack.Add(verify);
|
|
|
|
// CLI-PACKS-43-001: Advanced pack features
|
|
|
|
// pack runs list
|
|
var runs = new Command("runs", "Manage pack runs.");
|
|
|
|
var runsList = new Command("list", "List pack runs.");
|
|
|
|
var runsListPackOption = new Option<string?>("--pack")
|
|
{
|
|
Description = "Filter by pack ID."
|
|
};
|
|
|
|
var runsListStatusOption = new Option<string?>("--status", "-s")
|
|
{
|
|
Description = "Filter by status: pending, running, succeeded, failed, cancelled, waiting_approval."
|
|
};
|
|
|
|
var runsListActorOption = new Option<string?>("--actor")
|
|
{
|
|
Description = "Filter by actor (who started the run)."
|
|
};
|
|
|
|
var runsListSinceOption = new Option<DateTimeOffset?>("--since")
|
|
{
|
|
Description = "Filter by start time (ISO-8601)."
|
|
};
|
|
|
|
var runsListUntilOption = new Option<DateTimeOffset?>("--until")
|
|
{
|
|
Description = "Filter by end time (ISO-8601)."
|
|
};
|
|
|
|
var runsListPageSizeOption = new Option<int>("--page-size")
|
|
{
|
|
Description = "Page size (default: 20)."
|
|
};
|
|
runsListPageSizeOption.SetDefaultValue(20);
|
|
|
|
var runsListPageTokenOption = new Option<string?>("--page-token")
|
|
{
|
|
Description = "Page token for pagination."
|
|
};
|
|
|
|
runsList.Add(runsListPackOption);
|
|
runsList.Add(runsListStatusOption);
|
|
runsList.Add(runsListActorOption);
|
|
runsList.Add(runsListSinceOption);
|
|
runsList.Add(runsListUntilOption);
|
|
runsList.Add(runsListPageSizeOption);
|
|
runsList.Add(runsListPageTokenOption);
|
|
runsList.Add(tenantOption);
|
|
runsList.Add(jsonOption);
|
|
runsList.Add(verboseOption);
|
|
|
|
runsList.SetAction((parseResult, _) =>
|
|
{
|
|
var packId = parseResult.GetValue(runsListPackOption);
|
|
var status = parseResult.GetValue(runsListStatusOption);
|
|
var actor = parseResult.GetValue(runsListActorOption);
|
|
var since = parseResult.GetValue(runsListSinceOption);
|
|
var until = parseResult.GetValue(runsListUntilOption);
|
|
var pageSize = parseResult.GetValue(runsListPageSizeOption);
|
|
var pageToken = parseResult.GetValue(runsListPageTokenOption);
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePackRunsListAsync(
|
|
services,
|
|
packId,
|
|
status,
|
|
actor,
|
|
since,
|
|
until,
|
|
pageSize,
|
|
pageToken,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
runs.Add(runsList);
|
|
|
|
// pack runs show
|
|
var runsShow = new Command("show", "Show details of a pack run.");
|
|
|
|
var runsShowIdArg = new Argument<string>("run-id")
|
|
{
|
|
Description = "Run ID to show."
|
|
};
|
|
|
|
runsShow.Add(runsShowIdArg);
|
|
runsShow.Add(tenantOption);
|
|
runsShow.Add(jsonOption);
|
|
runsShow.Add(verboseOption);
|
|
|
|
runsShow.SetAction((parseResult, _) =>
|
|
{
|
|
var runId = parseResult.GetValue(runsShowIdArg) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePackRunsShowAsync(
|
|
services,
|
|
runId,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
runs.Add(runsShow);
|
|
|
|
// pack runs cancel
|
|
var runsCancel = new Command("cancel", "Cancel a running pack.");
|
|
|
|
var runsCancelIdArg = new Argument<string>("run-id")
|
|
{
|
|
Description = "Run ID to cancel."
|
|
};
|
|
|
|
var runsCancelReasonOption = new Option<string?>("--reason")
|
|
{
|
|
Description = "Reason for cancellation (appears in audit log)."
|
|
};
|
|
|
|
var runsCancelForceOption = new Option<bool>("--force")
|
|
{
|
|
Description = "Force cancel even if steps are running."
|
|
};
|
|
|
|
runsCancel.Add(runsCancelIdArg);
|
|
runsCancel.Add(runsCancelReasonOption);
|
|
runsCancel.Add(runsCancelForceOption);
|
|
runsCancel.Add(tenantOption);
|
|
runsCancel.Add(jsonOption);
|
|
runsCancel.Add(verboseOption);
|
|
|
|
runsCancel.SetAction((parseResult, _) =>
|
|
{
|
|
var runId = parseResult.GetValue(runsCancelIdArg) ?? string.Empty;
|
|
var reason = parseResult.GetValue(runsCancelReasonOption);
|
|
var force = parseResult.GetValue(runsCancelForceOption);
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePackRunsCancelAsync(
|
|
services,
|
|
runId,
|
|
reason,
|
|
force,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
runs.Add(runsCancel);
|
|
|
|
// pack runs pause
|
|
var runsPause = new Command("pause", "Pause a pack run for approval.");
|
|
|
|
var runsPauseIdArg = new Argument<string>("run-id")
|
|
{
|
|
Description = "Run ID to pause."
|
|
};
|
|
|
|
var runsPauseReasonOption = new Option<string?>("--reason")
|
|
{
|
|
Description = "Reason for pause (appears in audit log)."
|
|
};
|
|
|
|
var runsPauseStepOption = new Option<string?>("--step")
|
|
{
|
|
Description = "Specific step to pause at (next step if not specified)."
|
|
};
|
|
|
|
runsPause.Add(runsPauseIdArg);
|
|
runsPause.Add(runsPauseReasonOption);
|
|
runsPause.Add(runsPauseStepOption);
|
|
runsPause.Add(tenantOption);
|
|
runsPause.Add(jsonOption);
|
|
runsPause.Add(verboseOption);
|
|
|
|
runsPause.SetAction((parseResult, _) =>
|
|
{
|
|
var runId = parseResult.GetValue(runsPauseIdArg) ?? string.Empty;
|
|
var reason = parseResult.GetValue(runsPauseReasonOption);
|
|
var stepId = parseResult.GetValue(runsPauseStepOption);
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePackRunsPauseAsync(
|
|
services,
|
|
runId,
|
|
reason,
|
|
stepId,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
runs.Add(runsPause);
|
|
|
|
// pack runs resume
|
|
var runsResume = new Command("resume", "Resume a paused pack run.");
|
|
|
|
var runsResumeIdArg = new Argument<string>("run-id")
|
|
{
|
|
Description = "Run ID to resume."
|
|
};
|
|
|
|
var runsResumeApproveOption = new Option<bool>("--approve")
|
|
{
|
|
Description = "Approve the pending step (default: true)."
|
|
};
|
|
runsResumeApproveOption.SetDefaultValue(true);
|
|
|
|
var runsResumeReasonOption = new Option<string?>("--reason")
|
|
{
|
|
Description = "Reason for approval decision (appears in audit log)."
|
|
};
|
|
|
|
var runsResumeStepOption = new Option<string?>("--step")
|
|
{
|
|
Description = "Specific step to approve (current pending step if not specified)."
|
|
};
|
|
|
|
runsResume.Add(runsResumeIdArg);
|
|
runsResume.Add(runsResumeApproveOption);
|
|
runsResume.Add(runsResumeReasonOption);
|
|
runsResume.Add(runsResumeStepOption);
|
|
runsResume.Add(tenantOption);
|
|
runsResume.Add(jsonOption);
|
|
runsResume.Add(verboseOption);
|
|
|
|
runsResume.SetAction((parseResult, _) =>
|
|
{
|
|
var runId = parseResult.GetValue(runsResumeIdArg) ?? string.Empty;
|
|
var approve = parseResult.GetValue(runsResumeApproveOption);
|
|
var reason = parseResult.GetValue(runsResumeReasonOption);
|
|
var stepId = parseResult.GetValue(runsResumeStepOption);
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePackRunsResumeAsync(
|
|
services,
|
|
runId,
|
|
approve,
|
|
reason,
|
|
stepId,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
runs.Add(runsResume);
|
|
|
|
// pack runs logs
|
|
var runsLogs = new Command("logs", "Get logs for a pack run.");
|
|
|
|
var runsLogsIdArg = new Argument<string>("run-id")
|
|
{
|
|
Description = "Run ID to get logs for."
|
|
};
|
|
|
|
var runsLogsStepOption = new Option<string?>("--step")
|
|
{
|
|
Description = "Filter logs by step ID."
|
|
};
|
|
|
|
var runsLogsTailOption = new Option<int?>("--tail")
|
|
{
|
|
Description = "Show only the last N lines."
|
|
};
|
|
|
|
var runsLogsSinceOption = new Option<DateTimeOffset?>("--since")
|
|
{
|
|
Description = "Show logs since timestamp (ISO-8601)."
|
|
};
|
|
|
|
runsLogs.Add(runsLogsIdArg);
|
|
runsLogs.Add(runsLogsStepOption);
|
|
runsLogs.Add(runsLogsTailOption);
|
|
runsLogs.Add(runsLogsSinceOption);
|
|
runsLogs.Add(tenantOption);
|
|
runsLogs.Add(jsonOption);
|
|
runsLogs.Add(verboseOption);
|
|
|
|
runsLogs.SetAction((parseResult, _) =>
|
|
{
|
|
var runId = parseResult.GetValue(runsLogsIdArg) ?? string.Empty;
|
|
var stepId = parseResult.GetValue(runsLogsStepOption);
|
|
var tail = parseResult.GetValue(runsLogsTailOption);
|
|
var since = parseResult.GetValue(runsLogsSinceOption);
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePackRunsLogsAsync(
|
|
services,
|
|
runId,
|
|
stepId,
|
|
tail,
|
|
since,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
runs.Add(runsLogs);
|
|
|
|
pack.Add(runs);
|
|
|
|
// pack secrets inject
|
|
var secrets = new Command("secrets", "Secret injection for pack runs.");
|
|
|
|
var secretsInject = new Command("inject", "Inject a secret into a pack run.");
|
|
|
|
var secretsInjectRunIdArg = new Argument<string>("run-id")
|
|
{
|
|
Description = "Run ID to inject secret into."
|
|
};
|
|
|
|
var secretsInjectRefOption = new Option<string>("--secret-ref")
|
|
{
|
|
Description = "Secret reference (provider-specific path).",
|
|
Required = true
|
|
};
|
|
|
|
var secretsInjectProviderOption = new Option<string>("--provider")
|
|
{
|
|
Description = "Secret provider: vault, aws-ssm, azure-keyvault, k8s-secret."
|
|
};
|
|
secretsInjectProviderOption.SetDefaultValue("vault");
|
|
|
|
var secretsInjectEnvVarOption = new Option<string?>("--env-var")
|
|
{
|
|
Description = "Target environment variable name."
|
|
};
|
|
|
|
var secretsInjectPathOption = new Option<string?>("--path")
|
|
{
|
|
Description = "Target file path within the run container."
|
|
};
|
|
|
|
var secretsInjectStepOption = new Option<string?>("--step")
|
|
{
|
|
Description = "Inject for specific step only."
|
|
};
|
|
|
|
secretsInject.Add(secretsInjectRunIdArg);
|
|
secretsInject.Add(secretsInjectRefOption);
|
|
secretsInject.Add(secretsInjectProviderOption);
|
|
secretsInject.Add(secretsInjectEnvVarOption);
|
|
secretsInject.Add(secretsInjectPathOption);
|
|
secretsInject.Add(secretsInjectStepOption);
|
|
secretsInject.Add(tenantOption);
|
|
secretsInject.Add(jsonOption);
|
|
secretsInject.Add(verboseOption);
|
|
|
|
secretsInject.SetAction((parseResult, _) =>
|
|
{
|
|
var runId = parseResult.GetValue(secretsInjectRunIdArg) ?? string.Empty;
|
|
var secretRef = parseResult.GetValue(secretsInjectRefOption) ?? string.Empty;
|
|
var provider = parseResult.GetValue(secretsInjectProviderOption) ?? "vault";
|
|
var envVar = parseResult.GetValue(secretsInjectEnvVarOption);
|
|
var path = parseResult.GetValue(secretsInjectPathOption);
|
|
var stepId = parseResult.GetValue(secretsInjectStepOption);
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePackSecretsInjectAsync(
|
|
services,
|
|
runId,
|
|
secretRef,
|
|
provider,
|
|
envVar,
|
|
path,
|
|
stepId,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
secrets.Add(secretsInject);
|
|
|
|
pack.Add(secrets);
|
|
|
|
// pack cache
|
|
var cache = new Command("cache", "Manage offline pack cache.");
|
|
|
|
// pack cache list
|
|
var cacheList = new Command("list", "List cached packs.");
|
|
|
|
var cacheDirOption = new Option<string?>("--cache-dir")
|
|
{
|
|
Description = "Cache directory path (uses default if not specified)."
|
|
};
|
|
|
|
cacheList.Add(cacheDirOption);
|
|
cacheList.Add(jsonOption);
|
|
cacheList.Add(verboseOption);
|
|
|
|
cacheList.SetAction((parseResult, _) =>
|
|
{
|
|
var cacheDir = parseResult.GetValue(cacheDirOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePackCacheListAsync(
|
|
services,
|
|
cacheDir,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
cache.Add(cacheList);
|
|
|
|
// pack cache add
|
|
var cacheAdd = new Command("add", "Add a pack to the cache.");
|
|
|
|
var cacheAddPackIdArg = new Argument<string>("pack-id")
|
|
{
|
|
Description = "Pack ID to cache."
|
|
};
|
|
|
|
var cacheAddVersionOption = new Option<string?>("--version", "-v")
|
|
{
|
|
Description = "Pack version (defaults to latest)."
|
|
};
|
|
|
|
cacheAdd.Add(cacheAddPackIdArg);
|
|
cacheAdd.Add(cacheAddVersionOption);
|
|
cacheAdd.Add(cacheDirOption);
|
|
cacheAdd.Add(tenantOption);
|
|
cacheAdd.Add(jsonOption);
|
|
cacheAdd.Add(verboseOption);
|
|
|
|
cacheAdd.SetAction((parseResult, _) =>
|
|
{
|
|
var packId = parseResult.GetValue(cacheAddPackIdArg) ?? string.Empty;
|
|
var version = parseResult.GetValue(cacheAddVersionOption);
|
|
var cacheDir = parseResult.GetValue(cacheDirOption);
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePackCacheAddAsync(
|
|
services,
|
|
packId,
|
|
version,
|
|
cacheDir,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
cache.Add(cacheAdd);
|
|
|
|
// pack cache prune
|
|
var cachePrune = new Command("prune", "Remove old or unused packs from cache.");
|
|
|
|
var cachePruneMaxAgeOption = new Option<int?>("--max-age-days")
|
|
{
|
|
Description = "Remove packs older than N days."
|
|
};
|
|
|
|
var cachePruneMaxSizeOption = new Option<long?>("--max-size-mb")
|
|
{
|
|
Description = "Prune to keep cache under N megabytes."
|
|
};
|
|
|
|
var cachePruneDryRunOption = new Option<bool>("--dry-run")
|
|
{
|
|
Description = "Preview what would be removed without actually pruning."
|
|
};
|
|
|
|
cachePrune.Add(cachePruneMaxAgeOption);
|
|
cachePrune.Add(cachePruneMaxSizeOption);
|
|
cachePrune.Add(cachePruneDryRunOption);
|
|
cachePrune.Add(cacheDirOption);
|
|
cachePrune.Add(jsonOption);
|
|
cachePrune.Add(verboseOption);
|
|
|
|
cachePrune.SetAction((parseResult, _) =>
|
|
{
|
|
var maxAgeDays = parseResult.GetValue(cachePruneMaxAgeOption);
|
|
var maxSizeMb = parseResult.GetValue(cachePruneMaxSizeOption);
|
|
var dryRun = parseResult.GetValue(cachePruneDryRunOption);
|
|
var cacheDir = parseResult.GetValue(cacheDirOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandlePackCachePruneAsync(
|
|
services,
|
|
maxAgeDays,
|
|
maxSizeMb,
|
|
dryRun,
|
|
cacheDir,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
cache.Add(cachePrune);
|
|
|
|
pack.Add(cache);
|
|
|
|
return pack;
|
|
}
|
|
|
|
// CLI-EXC-25-001: Exception governance commands
|
|
private static Command BuildExceptionsCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var exceptions = new Command("exceptions", "Exception governance: list, show, create, promote, revoke, import, export.");
|
|
|
|
// Common options
|
|
var tenantOption = new Option<string?>("--tenant", "-t")
|
|
{
|
|
Description = "Tenant scope for the operation."
|
|
};
|
|
|
|
var jsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON."
|
|
};
|
|
|
|
// exceptions list
|
|
var list = new Command("list", "List exceptions with filters.");
|
|
|
|
var listVulnOption = new Option<string?>("--vuln")
|
|
{
|
|
Description = "Filter by vulnerability ID (CVE or alias)."
|
|
};
|
|
|
|
var listScopeTypeOption = new Option<string?>("--scope-type")
|
|
{
|
|
Description = "Filter by scope type: purl, image, component, tenant."
|
|
};
|
|
|
|
var listScopeValueOption = new Option<string?>("--scope-value")
|
|
{
|
|
Description = "Filter by scope value (e.g., purl string, image ref)."
|
|
};
|
|
|
|
var listStatusOption = new Option<string[]>("--status", "-s")
|
|
{
|
|
Description = "Filter by status (repeatable): draft, staged, active, expired, revoked.",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
listStatusOption.AllowMultipleArgumentsPerToken = true;
|
|
|
|
var listOwnerOption = new Option<string?>("--owner")
|
|
{
|
|
Description = "Filter by owner."
|
|
};
|
|
|
|
var listEffectOption = new Option<string?>("--effect")
|
|
{
|
|
Description = "Filter by effect type: suppress, defer, downgrade, requireControl."
|
|
};
|
|
|
|
var listExpiringOption = new Option<int?>("--expiring-within-days")
|
|
{
|
|
Description = "Show exceptions expiring within N days."
|
|
};
|
|
|
|
var listIncludeExpiredOption = new Option<bool>("--include-expired")
|
|
{
|
|
Description = "Include expired exceptions in results."
|
|
};
|
|
|
|
var listPageSizeOption = new Option<int>("--page-size")
|
|
{
|
|
Description = "Results per page (default: 50)."
|
|
};
|
|
listPageSizeOption.SetDefaultValue(50);
|
|
|
|
var listPageTokenOption = new Option<string?>("--page-token")
|
|
{
|
|
Description = "Pagination token for next page."
|
|
};
|
|
|
|
var listCsvOption = new Option<bool>("--csv")
|
|
{
|
|
Description = "Output as CSV."
|
|
};
|
|
|
|
list.Add(listVulnOption);
|
|
list.Add(listScopeTypeOption);
|
|
list.Add(listScopeValueOption);
|
|
list.Add(listStatusOption);
|
|
list.Add(listOwnerOption);
|
|
list.Add(listEffectOption);
|
|
list.Add(listExpiringOption);
|
|
list.Add(listIncludeExpiredOption);
|
|
list.Add(listPageSizeOption);
|
|
list.Add(listPageTokenOption);
|
|
list.Add(tenantOption);
|
|
list.Add(jsonOption);
|
|
list.Add(listCsvOption);
|
|
list.Add(verboseOption);
|
|
|
|
list.SetAction((parseResult, _) =>
|
|
{
|
|
var vuln = parseResult.GetValue(listVulnOption);
|
|
var scopeType = parseResult.GetValue(listScopeTypeOption);
|
|
var scopeValue = parseResult.GetValue(listScopeValueOption);
|
|
var statuses = parseResult.GetValue(listStatusOption) ?? Array.Empty<string>();
|
|
var owner = parseResult.GetValue(listOwnerOption);
|
|
var effect = parseResult.GetValue(listEffectOption);
|
|
var expiringDays = parseResult.GetValue(listExpiringOption);
|
|
var includeExpired = parseResult.GetValue(listIncludeExpiredOption);
|
|
var pageSize = parseResult.GetValue(listPageSizeOption);
|
|
var pageToken = parseResult.GetValue(listPageTokenOption);
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var csv = parseResult.GetValue(listCsvOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleExceptionsListAsync(
|
|
services,
|
|
tenant,
|
|
vuln,
|
|
scopeType,
|
|
scopeValue,
|
|
statuses,
|
|
owner,
|
|
effect,
|
|
expiringDays.HasValue ? DateTimeOffset.UtcNow.AddDays(expiringDays.Value) : null,
|
|
includeExpired,
|
|
pageSize,
|
|
pageToken,
|
|
json || csv,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
exceptions.Add(list);
|
|
|
|
// exceptions show
|
|
var show = new Command("show", "Show exception details.");
|
|
|
|
var showIdArg = new Argument<string>("exception-id")
|
|
{
|
|
Description = "Exception ID to show."
|
|
};
|
|
|
|
show.Add(showIdArg);
|
|
show.Add(tenantOption);
|
|
show.Add(jsonOption);
|
|
show.Add(verboseOption);
|
|
|
|
show.SetAction((parseResult, _) =>
|
|
{
|
|
var exceptionId = parseResult.GetValue(showIdArg) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleExceptionsShowAsync(
|
|
services,
|
|
exceptionId,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
exceptions.Add(show);
|
|
|
|
// exceptions create
|
|
var create = new Command("create", "Create a new exception.");
|
|
|
|
var createVulnOption = new Option<string>("--vuln")
|
|
{
|
|
Description = "Vulnerability ID (CVE or alias).",
|
|
Required = true
|
|
};
|
|
|
|
var createScopeTypeOption = new Option<string>("--scope-type")
|
|
{
|
|
Description = "Scope type: purl, image, component, tenant.",
|
|
Required = true
|
|
};
|
|
|
|
var createScopeValueOption = new Option<string>("--scope-value")
|
|
{
|
|
Description = "Scope value (e.g., purl string, image ref).",
|
|
Required = true
|
|
};
|
|
|
|
var createEffectOption = new Option<string>("--effect")
|
|
{
|
|
Description = "Effect ID to apply.",
|
|
Required = true
|
|
};
|
|
|
|
var createJustificationOption = new Option<string>("--justification")
|
|
{
|
|
Description = "Justification for the exception.",
|
|
Required = true
|
|
};
|
|
|
|
var createOwnerOption = new Option<string>("--owner")
|
|
{
|
|
Description = "Owner of the exception.",
|
|
Required = true
|
|
};
|
|
|
|
var createExpirationOption = new Option<string?>("--expiration")
|
|
{
|
|
Description = "Expiration date (ISO-8601) or relative (e.g., +30d, +90d)."
|
|
};
|
|
|
|
var createEvidenceOption = new Option<string[]>("--evidence")
|
|
{
|
|
Description = "Evidence reference (type:uri format, repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
createEvidenceOption.AllowMultipleArgumentsPerToken = true;
|
|
|
|
var createPolicyOption = new Option<string?>("--policy")
|
|
{
|
|
Description = "Policy binding (policy ID or version)."
|
|
};
|
|
|
|
var createStageOption = new Option<bool>("--stage")
|
|
{
|
|
Description = "Create as staged (skip draft status)."
|
|
};
|
|
|
|
create.Add(createVulnOption);
|
|
create.Add(createScopeTypeOption);
|
|
create.Add(createScopeValueOption);
|
|
create.Add(createEffectOption);
|
|
create.Add(createJustificationOption);
|
|
create.Add(createOwnerOption);
|
|
create.Add(createExpirationOption);
|
|
create.Add(createEvidenceOption);
|
|
create.Add(createPolicyOption);
|
|
create.Add(createStageOption);
|
|
create.Add(tenantOption);
|
|
create.Add(jsonOption);
|
|
create.Add(verboseOption);
|
|
|
|
create.SetAction((parseResult, _) =>
|
|
{
|
|
var vuln = parseResult.GetValue(createVulnOption) ?? string.Empty;
|
|
var scopeType = parseResult.GetValue(createScopeTypeOption) ?? string.Empty;
|
|
var scopeValue = parseResult.GetValue(createScopeValueOption) ?? string.Empty;
|
|
var effect = parseResult.GetValue(createEffectOption) ?? string.Empty;
|
|
var justification = parseResult.GetValue(createJustificationOption) ?? string.Empty;
|
|
var owner = parseResult.GetValue(createOwnerOption) ?? string.Empty;
|
|
var expirationStr = parseResult.GetValue(createExpirationOption);
|
|
var expiration = !string.IsNullOrWhiteSpace(expirationStr) && DateTimeOffset.TryParse(expirationStr, out var exp) ? exp : (DateTimeOffset?)null;
|
|
var evidence = parseResult.GetValue(createEvidenceOption) ?? Array.Empty<string>();
|
|
var policy = parseResult.GetValue(createPolicyOption);
|
|
var stage = parseResult.GetValue(createStageOption);
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleExceptionsCreateAsync(
|
|
services,
|
|
tenant ?? string.Empty,
|
|
vuln,
|
|
scopeType,
|
|
scopeValue,
|
|
effect,
|
|
justification,
|
|
owner ?? string.Empty,
|
|
expiration,
|
|
evidence,
|
|
policy,
|
|
stage,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
exceptions.Add(create);
|
|
|
|
// exceptions promote
|
|
var promote = new Command("promote", "Promote exception to next lifecycle stage.");
|
|
|
|
var promoteIdArg = new Argument<string>("exception-id")
|
|
{
|
|
Description = "Exception ID to promote."
|
|
};
|
|
|
|
var promoteTargetOption = new Option<string?>("--target")
|
|
{
|
|
Description = "Target status: staged or active (defaults to next stage)."
|
|
};
|
|
|
|
var promoteCommentOption = new Option<string?>("--comment")
|
|
{
|
|
Description = "Comment for the promotion (appears in audit log)."
|
|
};
|
|
|
|
promote.Add(promoteIdArg);
|
|
promote.Add(promoteTargetOption);
|
|
promote.Add(promoteCommentOption);
|
|
promote.Add(tenantOption);
|
|
promote.Add(jsonOption);
|
|
promote.Add(verboseOption);
|
|
|
|
promote.SetAction((parseResult, _) =>
|
|
{
|
|
var exceptionId = parseResult.GetValue(promoteIdArg) ?? string.Empty;
|
|
var target = parseResult.GetValue(promoteTargetOption);
|
|
var comment = parseResult.GetValue(promoteCommentOption);
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleExceptionsPromoteAsync(
|
|
services,
|
|
exceptionId,
|
|
tenant,
|
|
target ?? "active",
|
|
comment,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
exceptions.Add(promote);
|
|
|
|
// exceptions revoke
|
|
var revoke = new Command("revoke", "Revoke an active exception.");
|
|
|
|
var revokeIdArg = new Argument<string>("exception-id")
|
|
{
|
|
Description = "Exception ID to revoke."
|
|
};
|
|
|
|
var revokeReasonOption = new Option<string?>("--reason")
|
|
{
|
|
Description = "Reason for revocation (appears in audit log)."
|
|
};
|
|
|
|
revoke.Add(revokeIdArg);
|
|
revoke.Add(revokeReasonOption);
|
|
revoke.Add(tenantOption);
|
|
revoke.Add(jsonOption);
|
|
revoke.Add(verboseOption);
|
|
|
|
revoke.SetAction((parseResult, _) =>
|
|
{
|
|
var exceptionId = parseResult.GetValue(revokeIdArg) ?? string.Empty;
|
|
var reason = parseResult.GetValue(revokeReasonOption);
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleExceptionsRevokeAsync(
|
|
services,
|
|
exceptionId,
|
|
reason,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
exceptions.Add(revoke);
|
|
|
|
// exceptions import
|
|
var import = new Command("import", "Import exceptions from NDJSON file.");
|
|
|
|
var importFileArg = new Argument<string>("file")
|
|
{
|
|
Description = "Path to NDJSON file containing exceptions."
|
|
};
|
|
|
|
var importStageOption = new Option<bool>("--stage")
|
|
{
|
|
Description = "Import as staged (default: true)."
|
|
};
|
|
importStageOption.SetDefaultValue(true);
|
|
|
|
var importSourceOption = new Option<string?>("--source")
|
|
{
|
|
Description = "Source label for imported exceptions."
|
|
};
|
|
|
|
import.Add(importFileArg);
|
|
import.Add(importStageOption);
|
|
import.Add(importSourceOption);
|
|
import.Add(tenantOption);
|
|
import.Add(jsonOption);
|
|
import.Add(verboseOption);
|
|
|
|
import.SetAction((parseResult, _) =>
|
|
{
|
|
var file = parseResult.GetValue(importFileArg) ?? string.Empty;
|
|
var stage = parseResult.GetValue(importStageOption);
|
|
var source = parseResult.GetValue(importSourceOption);
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleExceptionsImportAsync(
|
|
services,
|
|
tenant ?? string.Empty,
|
|
file,
|
|
stage,
|
|
source,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
exceptions.Add(import);
|
|
|
|
// exceptions export
|
|
var export = new Command("export", "Export exceptions to file.");
|
|
|
|
var exportOutputOption = new Option<string>("--output", "-o")
|
|
{
|
|
Description = "Output file path.",
|
|
Required = true
|
|
};
|
|
|
|
var exportStatusOption = new Option<string[]>("--status", "-s")
|
|
{
|
|
Description = "Filter by status (repeatable).",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
exportStatusOption.AllowMultipleArgumentsPerToken = true;
|
|
|
|
var exportFormatOption = new Option<string>("--format")
|
|
{
|
|
Description = "Output format: ndjson or json (default: ndjson)."
|
|
};
|
|
exportFormatOption.SetDefaultValue("ndjson");
|
|
|
|
var exportSignedOption = new Option<bool>("--signed")
|
|
{
|
|
Description = "Request signed export with attestation."
|
|
};
|
|
|
|
export.Add(exportOutputOption);
|
|
export.Add(exportStatusOption);
|
|
export.Add(exportFormatOption);
|
|
export.Add(exportSignedOption);
|
|
export.Add(tenantOption);
|
|
export.Add(verboseOption);
|
|
|
|
export.SetAction((parseResult, _) =>
|
|
{
|
|
var output = parseResult.GetValue(exportOutputOption) ?? string.Empty;
|
|
var statuses = parseResult.GetValue(exportStatusOption) ?? Array.Empty<string>();
|
|
var format = parseResult.GetValue(exportFormatOption) ?? "ndjson";
|
|
var signed = parseResult.GetValue(exportSignedOption);
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleExceptionsExportAsync(
|
|
services,
|
|
tenant,
|
|
statuses,
|
|
format,
|
|
output,
|
|
false, // includeManifest
|
|
signed,
|
|
false, // json output
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
exceptions.Add(export);
|
|
|
|
return exceptions;
|
|
}
|
|
|
|
// CLI-ORCH-32-001: Orchestrator commands
|
|
private static Command BuildOrchCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var orch = new Command("orch", "Interact with Source & Job Orchestrator.");
|
|
|
|
// Common options
|
|
var tenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Tenant ID to scope the operation."
|
|
};
|
|
|
|
var jsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output results as JSON."
|
|
};
|
|
|
|
// sources subcommand group
|
|
var sources = new Command("sources", "Manage orchestrator data sources.");
|
|
|
|
// sources list
|
|
var sourcesList = new Command("list", "List orchestrator sources.");
|
|
|
|
var typeOption = new Option<string?>("--type")
|
|
{
|
|
Description = "Filter by source type (advisory, vex, sbom, package, registry, custom)."
|
|
};
|
|
|
|
var statusOption = new Option<string?>("--status")
|
|
{
|
|
Description = "Filter by status (active, paused, disabled, throttled, error)."
|
|
};
|
|
|
|
var enabledOption = new Option<bool?>("--enabled")
|
|
{
|
|
Description = "Filter by enabled state."
|
|
};
|
|
|
|
var hostOption = new Option<string?>("--host")
|
|
{
|
|
Description = "Filter by host name."
|
|
};
|
|
|
|
var tagOption = new Option<string?>("--tag")
|
|
{
|
|
Description = "Filter by tag."
|
|
};
|
|
|
|
var pageSizeOption = new Option<int>("--page-size")
|
|
{
|
|
Description = "Number of results per page (default 50)."
|
|
};
|
|
pageSizeOption.SetDefaultValue(50);
|
|
|
|
var pageTokenOption = new Option<string?>("--page-token")
|
|
{
|
|
Description = "Page token for pagination."
|
|
};
|
|
|
|
sourcesList.Add(tenantOption);
|
|
sourcesList.Add(typeOption);
|
|
sourcesList.Add(statusOption);
|
|
sourcesList.Add(enabledOption);
|
|
sourcesList.Add(hostOption);
|
|
sourcesList.Add(tagOption);
|
|
sourcesList.Add(pageSizeOption);
|
|
sourcesList.Add(pageTokenOption);
|
|
sourcesList.Add(jsonOption);
|
|
sourcesList.Add(verboseOption);
|
|
|
|
sourcesList.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var type = parseResult.GetValue(typeOption);
|
|
var status = parseResult.GetValue(statusOption);
|
|
var enabled = parseResult.GetValue(enabledOption);
|
|
var host = parseResult.GetValue(hostOption);
|
|
var tag = parseResult.GetValue(tagOption);
|
|
var pageSize = parseResult.GetValue(pageSizeOption);
|
|
var pageToken = parseResult.GetValue(pageTokenOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleOrchSourcesListAsync(
|
|
services,
|
|
tenant,
|
|
type,
|
|
status,
|
|
enabled,
|
|
host,
|
|
tag,
|
|
pageSize,
|
|
pageToken,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
sources.Add(sourcesList);
|
|
|
|
// sources show
|
|
var sourcesShow = new Command("show", "Show details for a specific source.");
|
|
|
|
var sourceIdArg = new Argument<string>("source-id")
|
|
{
|
|
Description = "Source ID to show."
|
|
};
|
|
|
|
sourcesShow.Add(sourceIdArg);
|
|
sourcesShow.Add(tenantOption);
|
|
sourcesShow.Add(jsonOption);
|
|
sourcesShow.Add(verboseOption);
|
|
|
|
sourcesShow.SetAction((parseResult, _) =>
|
|
{
|
|
var sourceId = parseResult.GetValue(sourceIdArg) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleOrchSourcesShowAsync(
|
|
services,
|
|
sourceId,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
sources.Add(sourcesShow);
|
|
|
|
// CLI-ORCH-33-001: sources test
|
|
var sourcesTest = new Command("test", "Test connectivity to a source.");
|
|
|
|
var testSourceIdArg = new Argument<string>("source-id")
|
|
{
|
|
Description = "Source ID to test."
|
|
};
|
|
|
|
var testTimeoutOption = new Option<int>("--timeout")
|
|
{
|
|
Description = "Timeout in seconds (default 30)."
|
|
};
|
|
testTimeoutOption.SetDefaultValue(30);
|
|
|
|
sourcesTest.Add(testSourceIdArg);
|
|
sourcesTest.Add(tenantOption);
|
|
sourcesTest.Add(testTimeoutOption);
|
|
sourcesTest.Add(jsonOption);
|
|
sourcesTest.Add(verboseOption);
|
|
|
|
sourcesTest.SetAction((parseResult, _) =>
|
|
{
|
|
var sourceId = parseResult.GetValue(testSourceIdArg) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var timeout = parseResult.GetValue(testTimeoutOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleOrchSourcesTestAsync(
|
|
services,
|
|
sourceId,
|
|
tenant,
|
|
timeout,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
sources.Add(sourcesTest);
|
|
|
|
// CLI-ORCH-33-001: sources pause
|
|
var sourcesPause = new Command("pause", "Pause a source (stops scheduled runs).");
|
|
|
|
var pauseSourceIdArg = new Argument<string>("source-id")
|
|
{
|
|
Description = "Source ID to pause."
|
|
};
|
|
|
|
var pauseReasonOption = new Option<string?>("--reason")
|
|
{
|
|
Description = "Reason for pausing (appears in audit log)."
|
|
};
|
|
|
|
var pauseDurationOption = new Option<int?>("--duration")
|
|
{
|
|
Description = "Duration in minutes before auto-resume (optional)."
|
|
};
|
|
|
|
sourcesPause.Add(pauseSourceIdArg);
|
|
sourcesPause.Add(tenantOption);
|
|
sourcesPause.Add(pauseReasonOption);
|
|
sourcesPause.Add(pauseDurationOption);
|
|
sourcesPause.Add(jsonOption);
|
|
sourcesPause.Add(verboseOption);
|
|
|
|
sourcesPause.SetAction((parseResult, _) =>
|
|
{
|
|
var sourceId = parseResult.GetValue(pauseSourceIdArg) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var reason = parseResult.GetValue(pauseReasonOption);
|
|
var duration = parseResult.GetValue(pauseDurationOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleOrchSourcesPauseAsync(
|
|
services,
|
|
sourceId,
|
|
tenant,
|
|
reason,
|
|
duration,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
sources.Add(sourcesPause);
|
|
|
|
// CLI-ORCH-33-001: sources resume
|
|
var sourcesResume = new Command("resume", "Resume a paused source.");
|
|
|
|
var resumeSourceIdArg = new Argument<string>("source-id")
|
|
{
|
|
Description = "Source ID to resume."
|
|
};
|
|
|
|
var resumeReasonOption = new Option<string?>("--reason")
|
|
{
|
|
Description = "Reason for resuming (appears in audit log)."
|
|
};
|
|
|
|
sourcesResume.Add(resumeSourceIdArg);
|
|
sourcesResume.Add(tenantOption);
|
|
sourcesResume.Add(resumeReasonOption);
|
|
sourcesResume.Add(jsonOption);
|
|
sourcesResume.Add(verboseOption);
|
|
|
|
sourcesResume.SetAction((parseResult, _) =>
|
|
{
|
|
var sourceId = parseResult.GetValue(resumeSourceIdArg) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var reason = parseResult.GetValue(resumeReasonOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleOrchSourcesResumeAsync(
|
|
services,
|
|
sourceId,
|
|
tenant,
|
|
reason,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
sources.Add(sourcesResume);
|
|
|
|
orch.Add(sources);
|
|
|
|
// CLI-ORCH-34-001: backfill command group
|
|
var backfill = new Command("backfill", "Manage backfill operations for data sources.");
|
|
|
|
// backfill start (wizard)
|
|
var backfillStart = new Command("start", "Start a backfill operation for a source.");
|
|
|
|
var backfillSourceIdArg = new Argument<string>("source-id")
|
|
{
|
|
Description = "Source ID to backfill."
|
|
};
|
|
|
|
var backfillFromOption = new Option<DateTimeOffset>("--from")
|
|
{
|
|
Description = "Start date/time for backfill (ISO 8601 format).",
|
|
Required = true
|
|
};
|
|
|
|
var backfillToOption = new Option<DateTimeOffset>("--to")
|
|
{
|
|
Description = "End date/time for backfill (ISO 8601 format).",
|
|
Required = true
|
|
};
|
|
|
|
var backfillDryRunOption = new Option<bool>("--dry-run")
|
|
{
|
|
Description = "Preview what would be backfilled without executing."
|
|
};
|
|
|
|
var backfillPriorityOption = new Option<int>("--priority")
|
|
{
|
|
Description = "Priority level 1-10 (default 5, higher = more resources)."
|
|
};
|
|
backfillPriorityOption.SetDefaultValue(5);
|
|
|
|
var backfillConcurrencyOption = new Option<int>("--concurrency")
|
|
{
|
|
Description = "Number of concurrent workers (default 1)."
|
|
};
|
|
backfillConcurrencyOption.SetDefaultValue(1);
|
|
|
|
var backfillBatchSizeOption = new Option<int>("--batch-size")
|
|
{
|
|
Description = "Items per batch (default 100)."
|
|
};
|
|
backfillBatchSizeOption.SetDefaultValue(100);
|
|
|
|
var backfillResumeOption = new Option<bool>("--resume")
|
|
{
|
|
Description = "Resume from last checkpoint if a previous backfill was interrupted."
|
|
};
|
|
|
|
var backfillFilterOption = new Option<string?>("--filter")
|
|
{
|
|
Description = "Filter expression to limit items (source-specific syntax)."
|
|
};
|
|
|
|
var backfillForceOption = new Option<bool>("--force")
|
|
{
|
|
Description = "Force backfill even if data already exists (overwrites)."
|
|
};
|
|
|
|
backfillStart.Add(backfillSourceIdArg);
|
|
backfillStart.Add(tenantOption);
|
|
backfillStart.Add(backfillFromOption);
|
|
backfillStart.Add(backfillToOption);
|
|
backfillStart.Add(backfillDryRunOption);
|
|
backfillStart.Add(backfillPriorityOption);
|
|
backfillStart.Add(backfillConcurrencyOption);
|
|
backfillStart.Add(backfillBatchSizeOption);
|
|
backfillStart.Add(backfillResumeOption);
|
|
backfillStart.Add(backfillFilterOption);
|
|
backfillStart.Add(backfillForceOption);
|
|
backfillStart.Add(jsonOption);
|
|
backfillStart.Add(verboseOption);
|
|
|
|
backfillStart.SetAction((parseResult, _) =>
|
|
{
|
|
var sourceId = parseResult.GetValue(backfillSourceIdArg) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var from = parseResult.GetValue(backfillFromOption);
|
|
var to = parseResult.GetValue(backfillToOption);
|
|
var dryRun = parseResult.GetValue(backfillDryRunOption);
|
|
var priority = parseResult.GetValue(backfillPriorityOption);
|
|
var concurrency = parseResult.GetValue(backfillConcurrencyOption);
|
|
var batchSize = parseResult.GetValue(backfillBatchSizeOption);
|
|
var resume = parseResult.GetValue(backfillResumeOption);
|
|
var filter = parseResult.GetValue(backfillFilterOption);
|
|
var force = parseResult.GetValue(backfillForceOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleOrchBackfillStartAsync(
|
|
services,
|
|
sourceId,
|
|
tenant,
|
|
from,
|
|
to,
|
|
dryRun,
|
|
priority,
|
|
concurrency,
|
|
batchSize,
|
|
resume,
|
|
filter,
|
|
force,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
backfill.Add(backfillStart);
|
|
|
|
// backfill status
|
|
var backfillStatus = new Command("status", "Show status of a backfill operation.");
|
|
|
|
var backfillIdArg = new Argument<string>("backfill-id")
|
|
{
|
|
Description = "Backfill operation ID."
|
|
};
|
|
|
|
backfillStatus.Add(backfillIdArg);
|
|
backfillStatus.Add(tenantOption);
|
|
backfillStatus.Add(jsonOption);
|
|
backfillStatus.Add(verboseOption);
|
|
|
|
backfillStatus.SetAction((parseResult, _) =>
|
|
{
|
|
var backfillId = parseResult.GetValue(backfillIdArg) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleOrchBackfillStatusAsync(
|
|
services,
|
|
backfillId,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
backfill.Add(backfillStatus);
|
|
|
|
// backfill list
|
|
var backfillList = new Command("list", "List backfill operations.");
|
|
|
|
var backfillSourceFilterOption = new Option<string?>("--source")
|
|
{
|
|
Description = "Filter by source ID."
|
|
};
|
|
|
|
var backfillStatusFilterOption = new Option<string?>("--status")
|
|
{
|
|
Description = "Filter by status (pending, running, completed, failed, cancelled)."
|
|
};
|
|
|
|
backfillList.Add(backfillSourceFilterOption);
|
|
backfillList.Add(backfillStatusFilterOption);
|
|
backfillList.Add(tenantOption);
|
|
backfillList.Add(pageSizeOption);
|
|
backfillList.Add(pageTokenOption);
|
|
backfillList.Add(jsonOption);
|
|
backfillList.Add(verboseOption);
|
|
|
|
backfillList.SetAction((parseResult, _) =>
|
|
{
|
|
var sourceId = parseResult.GetValue(backfillSourceFilterOption);
|
|
var status = parseResult.GetValue(backfillStatusFilterOption);
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var pageSize = parseResult.GetValue(pageSizeOption);
|
|
var pageToken = parseResult.GetValue(pageTokenOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleOrchBackfillListAsync(
|
|
services,
|
|
sourceId,
|
|
status,
|
|
tenant,
|
|
pageSize,
|
|
pageToken,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
backfill.Add(backfillList);
|
|
|
|
// backfill cancel
|
|
var backfillCancel = new Command("cancel", "Cancel a running backfill operation.");
|
|
|
|
var cancelBackfillIdArg = new Argument<string>("backfill-id")
|
|
{
|
|
Description = "Backfill operation ID to cancel."
|
|
};
|
|
|
|
var cancelReasonOption = new Option<string?>("--reason")
|
|
{
|
|
Description = "Reason for cancellation (appears in audit log)."
|
|
};
|
|
|
|
backfillCancel.Add(cancelBackfillIdArg);
|
|
backfillCancel.Add(tenantOption);
|
|
backfillCancel.Add(cancelReasonOption);
|
|
backfillCancel.Add(jsonOption);
|
|
backfillCancel.Add(verboseOption);
|
|
|
|
backfillCancel.SetAction((parseResult, _) =>
|
|
{
|
|
var backfillId = parseResult.GetValue(cancelBackfillIdArg) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var reason = parseResult.GetValue(cancelReasonOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleOrchBackfillCancelAsync(
|
|
services,
|
|
backfillId,
|
|
tenant,
|
|
reason,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
backfill.Add(backfillCancel);
|
|
|
|
orch.Add(backfill);
|
|
|
|
// CLI-ORCH-34-001: quotas command group
|
|
var quotas = new Command("quotas", "Manage resource quotas.");
|
|
|
|
// quotas get
|
|
var quotasGet = new Command("get", "Get current quota usage.");
|
|
|
|
var quotaSourceOption = new Option<string?>("--source")
|
|
{
|
|
Description = "Filter by source ID."
|
|
};
|
|
|
|
var quotaResourceTypeOption = new Option<string?>("--resource-type")
|
|
{
|
|
Description = "Filter by resource type (api_calls, data_ingested_bytes, items_processed, backfills, concurrent_jobs, storage_bytes)."
|
|
};
|
|
|
|
quotasGet.Add(tenantOption);
|
|
quotasGet.Add(quotaSourceOption);
|
|
quotasGet.Add(quotaResourceTypeOption);
|
|
quotasGet.Add(jsonOption);
|
|
quotasGet.Add(verboseOption);
|
|
|
|
quotasGet.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var sourceId = parseResult.GetValue(quotaSourceOption);
|
|
var resourceType = parseResult.GetValue(quotaResourceTypeOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleOrchQuotasGetAsync(
|
|
services,
|
|
tenant,
|
|
sourceId,
|
|
resourceType,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
quotas.Add(quotasGet);
|
|
|
|
// quotas set
|
|
var quotasSet = new Command("set", "Set a quota limit.");
|
|
|
|
var quotaSetTenantOption = new Option<string>("--tenant")
|
|
{
|
|
Description = "Tenant ID.",
|
|
Required = true
|
|
};
|
|
|
|
var quotaSetResourceTypeOption = new Option<string>("--resource-type")
|
|
{
|
|
Description = "Resource type (api_calls, data_ingested_bytes, items_processed, backfills, concurrent_jobs, storage_bytes).",
|
|
Required = true
|
|
};
|
|
|
|
var quotaSetLimitOption = new Option<long>("--limit")
|
|
{
|
|
Description = "Quota limit value.",
|
|
Required = true
|
|
};
|
|
|
|
var quotaSetPeriodOption = new Option<string>("--period")
|
|
{
|
|
Description = "Quota period (hourly, daily, weekly, monthly). Default: monthly."
|
|
};
|
|
quotaSetPeriodOption.SetDefaultValue("monthly");
|
|
|
|
var quotaSetWarningThresholdOption = new Option<double>("--warning-threshold")
|
|
{
|
|
Description = "Warning threshold as percentage (0.0-1.0). Default: 0.8."
|
|
};
|
|
quotaSetWarningThresholdOption.SetDefaultValue(0.8);
|
|
|
|
quotasSet.Add(quotaSetTenantOption);
|
|
quotasSet.Add(quotaSourceOption);
|
|
quotasSet.Add(quotaSetResourceTypeOption);
|
|
quotasSet.Add(quotaSetLimitOption);
|
|
quotasSet.Add(quotaSetPeriodOption);
|
|
quotasSet.Add(quotaSetWarningThresholdOption);
|
|
quotasSet.Add(jsonOption);
|
|
quotasSet.Add(verboseOption);
|
|
|
|
quotasSet.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(quotaSetTenantOption) ?? string.Empty;
|
|
var sourceId = parseResult.GetValue(quotaSourceOption);
|
|
var resourceType = parseResult.GetValue(quotaSetResourceTypeOption) ?? string.Empty;
|
|
var limit = parseResult.GetValue(quotaSetLimitOption);
|
|
var period = parseResult.GetValue(quotaSetPeriodOption) ?? "monthly";
|
|
var warningThreshold = parseResult.GetValue(quotaSetWarningThresholdOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleOrchQuotasSetAsync(
|
|
services,
|
|
tenant,
|
|
sourceId,
|
|
resourceType,
|
|
limit,
|
|
period,
|
|
warningThreshold,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
quotas.Add(quotasSet);
|
|
|
|
// quotas reset
|
|
var quotasReset = new Command("reset", "Reset a quota's usage counter.");
|
|
|
|
var quotaResetTenantOption = new Option<string>("--tenant")
|
|
{
|
|
Description = "Tenant ID.",
|
|
Required = true
|
|
};
|
|
|
|
var quotaResetResourceTypeOption = new Option<string>("--resource-type")
|
|
{
|
|
Description = "Resource type to reset.",
|
|
Required = true
|
|
};
|
|
|
|
var quotaResetReasonOption = new Option<string?>("--reason")
|
|
{
|
|
Description = "Reason for reset (appears in audit log)."
|
|
};
|
|
|
|
quotasReset.Add(quotaResetTenantOption);
|
|
quotasReset.Add(quotaSourceOption);
|
|
quotasReset.Add(quotaResetResourceTypeOption);
|
|
quotasReset.Add(quotaResetReasonOption);
|
|
quotasReset.Add(jsonOption);
|
|
quotasReset.Add(verboseOption);
|
|
|
|
quotasReset.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(quotaResetTenantOption) ?? string.Empty;
|
|
var sourceId = parseResult.GetValue(quotaSourceOption);
|
|
var resourceType = parseResult.GetValue(quotaResetResourceTypeOption) ?? string.Empty;
|
|
var reason = parseResult.GetValue(quotaResetReasonOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleOrchQuotasResetAsync(
|
|
services,
|
|
tenant,
|
|
sourceId,
|
|
resourceType,
|
|
reason,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
quotas.Add(quotasReset);
|
|
|
|
orch.Add(quotas);
|
|
|
|
return orch;
|
|
}
|
|
|
|
// CLI-PARITY-41-001: SBOM command group
|
|
private static Command BuildSbomCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var sbom = new Command("sbom", "Explore and manage Software Bill of Materials (SBOM) documents.");
|
|
|
|
// Common options
|
|
var tenantOption = new Option<string?>("--tenant", "-t")
|
|
{
|
|
Description = "Tenant identifier (overrides profile/environment)."
|
|
};
|
|
var jsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON for CI integration."
|
|
};
|
|
|
|
// sbom list
|
|
var list = new Command("list", "List SBOMs with filters and pagination.");
|
|
|
|
var listImageRefOption = new Option<string?>("--image")
|
|
{
|
|
Description = "Filter by image reference (e.g., myregistry.io/app:v1)."
|
|
};
|
|
var listDigestOption = new Option<string?>("--digest")
|
|
{
|
|
Description = "Filter by image digest (sha256:...)."
|
|
};
|
|
var listFormatOption = new Option<string?>("--format")
|
|
{
|
|
Description = "Filter by SBOM format (spdx, cyclonedx)."
|
|
};
|
|
var listCreatedAfterOption = new Option<DateTimeOffset?>("--created-after")
|
|
{
|
|
Description = "Filter by creation date (ISO 8601)."
|
|
};
|
|
var listCreatedBeforeOption = new Option<DateTimeOffset?>("--created-before")
|
|
{
|
|
Description = "Filter by creation date (ISO 8601)."
|
|
};
|
|
var listHasVulnsOption = new Option<bool?>("--has-vulnerabilities")
|
|
{
|
|
Description = "Filter by vulnerability presence."
|
|
};
|
|
var listLimitOption = new Option<int>("--limit")
|
|
{
|
|
Description = "Maximum results (default 50)."
|
|
};
|
|
listLimitOption.SetDefaultValue(50);
|
|
var listOffsetOption = new Option<int?>("--offset")
|
|
{
|
|
Description = "Skip N results for pagination."
|
|
};
|
|
var listCursorOption = new Option<string?>("--cursor")
|
|
{
|
|
Description = "Pagination cursor from previous response."
|
|
};
|
|
|
|
list.Add(tenantOption);
|
|
list.Add(listImageRefOption);
|
|
list.Add(listDigestOption);
|
|
list.Add(listFormatOption);
|
|
list.Add(listCreatedAfterOption);
|
|
list.Add(listCreatedBeforeOption);
|
|
list.Add(listHasVulnsOption);
|
|
list.Add(listLimitOption);
|
|
list.Add(listOffsetOption);
|
|
list.Add(listCursorOption);
|
|
list.Add(jsonOption);
|
|
list.Add(verboseOption);
|
|
|
|
list.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var imageRef = parseResult.GetValue(listImageRefOption);
|
|
var digest = parseResult.GetValue(listDigestOption);
|
|
var format = parseResult.GetValue(listFormatOption);
|
|
var createdAfter = parseResult.GetValue(listCreatedAfterOption);
|
|
var createdBefore = parseResult.GetValue(listCreatedBeforeOption);
|
|
var hasVulns = parseResult.GetValue(listHasVulnsOption);
|
|
var limit = parseResult.GetValue(listLimitOption);
|
|
var offset = parseResult.GetValue(listOffsetOption);
|
|
var cursor = parseResult.GetValue(listCursorOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleSbomListAsync(
|
|
services,
|
|
tenant,
|
|
imageRef,
|
|
digest,
|
|
format,
|
|
createdAfter,
|
|
createdBefore,
|
|
hasVulns,
|
|
limit,
|
|
offset,
|
|
cursor,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
sbom.Add(list);
|
|
|
|
// sbom show
|
|
var show = new Command("show", "Display detailed SBOM information including components, vulnerabilities, and licenses.");
|
|
|
|
var showSbomIdArg = new Argument<string>("sbom-id")
|
|
{
|
|
Description = "SBOM identifier."
|
|
};
|
|
var showComponentsOption = new Option<bool>("--components")
|
|
{
|
|
Description = "Include component list."
|
|
};
|
|
var showVulnsOption = new Option<bool>("--vulnerabilities")
|
|
{
|
|
Description = "Include vulnerability list."
|
|
};
|
|
var showLicensesOption = new Option<bool>("--licenses")
|
|
{
|
|
Description = "Include license breakdown."
|
|
};
|
|
var showExplainOption = new Option<bool>("--explain")
|
|
{
|
|
Description = "Include determinism factors and composition path."
|
|
};
|
|
|
|
show.Add(showSbomIdArg);
|
|
show.Add(tenantOption);
|
|
show.Add(showComponentsOption);
|
|
show.Add(showVulnsOption);
|
|
show.Add(showLicensesOption);
|
|
show.Add(showExplainOption);
|
|
show.Add(jsonOption);
|
|
show.Add(verboseOption);
|
|
|
|
show.SetAction((parseResult, _) =>
|
|
{
|
|
var sbomId = parseResult.GetValue(showSbomIdArg) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var includeComponents = parseResult.GetValue(showComponentsOption);
|
|
var includeVulns = parseResult.GetValue(showVulnsOption);
|
|
var includeLicenses = parseResult.GetValue(showLicensesOption);
|
|
var explain = parseResult.GetValue(showExplainOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleSbomShowAsync(
|
|
services,
|
|
sbomId,
|
|
tenant,
|
|
includeComponents,
|
|
includeVulns,
|
|
includeLicenses,
|
|
explain,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
sbom.Add(show);
|
|
|
|
// sbom compare
|
|
var compare = new Command("compare", "Compare two SBOMs to show component, vulnerability, and license differences.");
|
|
|
|
var compareBaseArg = new Argument<string>("base-sbom-id")
|
|
{
|
|
Description = "Base SBOM identifier (before)."
|
|
};
|
|
var compareTargetArg = new Argument<string>("target-sbom-id")
|
|
{
|
|
Description = "Target SBOM identifier (after)."
|
|
};
|
|
var compareUnchangedOption = new Option<bool>("--include-unchanged")
|
|
{
|
|
Description = "Include unchanged items in output."
|
|
};
|
|
|
|
compare.Add(compareBaseArg);
|
|
compare.Add(compareTargetArg);
|
|
compare.Add(tenantOption);
|
|
compare.Add(compareUnchangedOption);
|
|
compare.Add(jsonOption);
|
|
compare.Add(verboseOption);
|
|
|
|
compare.SetAction((parseResult, _) =>
|
|
{
|
|
var baseSbomId = parseResult.GetValue(compareBaseArg) ?? string.Empty;
|
|
var targetSbomId = parseResult.GetValue(compareTargetArg) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var includeUnchanged = parseResult.GetValue(compareUnchangedOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleSbomCompareAsync(
|
|
services,
|
|
baseSbomId,
|
|
targetSbomId,
|
|
tenant,
|
|
includeUnchanged,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
sbom.Add(compare);
|
|
|
|
// sbom export
|
|
var export = new Command("export", "Export an SBOM in SPDX or CycloneDX format.");
|
|
|
|
var exportSbomIdArg = new Argument<string>("sbom-id")
|
|
{
|
|
Description = "SBOM identifier to export."
|
|
};
|
|
var exportFormatOption = new Option<string>("--format")
|
|
{
|
|
Description = "Export format (spdx, cyclonedx). Default: spdx."
|
|
};
|
|
exportFormatOption.SetDefaultValue("spdx");
|
|
var exportVersionOption = new Option<string?>("--format-version")
|
|
{
|
|
Description = "Format version (e.g., 3.0.1 for SPDX, 1.6 for CycloneDX)."
|
|
};
|
|
var exportOutputOption = new Option<string?>("--output", "-o")
|
|
{
|
|
Description = "Output file path. If not specified, writes to stdout."
|
|
};
|
|
var exportSignedOption = new Option<bool>("--signed")
|
|
{
|
|
Description = "Request signed export with attestation."
|
|
};
|
|
var exportVexOption = new Option<bool>("--include-vex")
|
|
{
|
|
Description = "Embed VEX information in the export."
|
|
};
|
|
|
|
export.Add(exportSbomIdArg);
|
|
export.Add(tenantOption);
|
|
export.Add(exportFormatOption);
|
|
export.Add(exportVersionOption);
|
|
export.Add(exportOutputOption);
|
|
export.Add(exportSignedOption);
|
|
export.Add(exportVexOption);
|
|
export.Add(verboseOption);
|
|
|
|
export.SetAction((parseResult, _) =>
|
|
{
|
|
var sbomId = parseResult.GetValue(exportSbomIdArg) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var format = parseResult.GetValue(exportFormatOption) ?? "spdx";
|
|
var formatVersion = parseResult.GetValue(exportVersionOption);
|
|
var output = parseResult.GetValue(exportOutputOption);
|
|
var signed = parseResult.GetValue(exportSignedOption);
|
|
var includeVex = parseResult.GetValue(exportVexOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleSbomExportAsync(
|
|
services,
|
|
sbomId,
|
|
tenant,
|
|
format,
|
|
formatVersion,
|
|
output,
|
|
signed,
|
|
includeVex,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
sbom.Add(export);
|
|
|
|
// sbom parity-matrix
|
|
var parityMatrix = new Command("parity-matrix", "Show CLI command coverage and parity matrix.");
|
|
|
|
parityMatrix.Add(tenantOption);
|
|
parityMatrix.Add(jsonOption);
|
|
parityMatrix.Add(verboseOption);
|
|
|
|
parityMatrix.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleSbomParityMatrixAsync(
|
|
services,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
sbom.Add(parityMatrix);
|
|
|
|
return sbom;
|
|
}
|
|
|
|
// CLI-PARITY-41-002: Notify command group
|
|
private static Command BuildNotifyCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var notify = new Command("notify", "Manage notification channels, rules, and deliveries.");
|
|
|
|
// Common options
|
|
var tenantOption = new Option<string?>("--tenant", "-t")
|
|
{
|
|
Description = "Tenant identifier."
|
|
};
|
|
var jsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output in JSON format."
|
|
};
|
|
var limitOption = new Option<int?>("--limit", "-l")
|
|
{
|
|
Description = "Maximum number of items to return."
|
|
};
|
|
var cursorOption = new Option<string?>("--cursor")
|
|
{
|
|
Description = "Pagination cursor for next page."
|
|
};
|
|
|
|
// notify channels
|
|
var channels = new Command("channels", "Manage notification channels.");
|
|
|
|
// notify channels list
|
|
var channelsList = new Command("list", "List notification channels.");
|
|
|
|
var channelTypeOption = new Option<string?>("--type")
|
|
{
|
|
Description = "Filter by channel type (Slack, Teams, Email, Webhook, PagerDuty, OpsGenie)."
|
|
};
|
|
var channelEnabledOption = new Option<bool?>("--enabled")
|
|
{
|
|
Description = "Filter by enabled status."
|
|
};
|
|
|
|
channelsList.Add(tenantOption);
|
|
channelsList.Add(channelTypeOption);
|
|
channelsList.Add(channelEnabledOption);
|
|
channelsList.Add(limitOption);
|
|
channelsList.Add(cursorOption);
|
|
channelsList.Add(jsonOption);
|
|
channelsList.Add(verboseOption);
|
|
|
|
channelsList.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var channelType = parseResult.GetValue(channelTypeOption);
|
|
var enabled = parseResult.GetValue(channelEnabledOption);
|
|
var limit = parseResult.GetValue(limitOption);
|
|
var cursor = parseResult.GetValue(cursorOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleNotifyChannelsListAsync(
|
|
services,
|
|
tenant,
|
|
channelType,
|
|
enabled,
|
|
limit,
|
|
cursor,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
channels.Add(channelsList);
|
|
|
|
// notify channels show
|
|
var channelsShow = new Command("show", "Show notification channel details.");
|
|
|
|
var channelIdArg = new Argument<string>("channel-id")
|
|
{
|
|
Description = "Channel identifier."
|
|
};
|
|
|
|
channelsShow.Add(channelIdArg);
|
|
channelsShow.Add(tenantOption);
|
|
channelsShow.Add(jsonOption);
|
|
channelsShow.Add(verboseOption);
|
|
|
|
channelsShow.SetAction((parseResult, _) =>
|
|
{
|
|
var channelId = parseResult.GetValue(channelIdArg) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleNotifyChannelsShowAsync(
|
|
services,
|
|
channelId,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
channels.Add(channelsShow);
|
|
|
|
// notify channels test
|
|
var channelsTest = new Command("test", "Test a notification channel by sending a test message.");
|
|
|
|
var testMessageOption = new Option<string?>("--message", "-m")
|
|
{
|
|
Description = "Custom test message."
|
|
};
|
|
|
|
channelsTest.Add(channelIdArg);
|
|
channelsTest.Add(tenantOption);
|
|
channelsTest.Add(testMessageOption);
|
|
channelsTest.Add(jsonOption);
|
|
channelsTest.Add(verboseOption);
|
|
|
|
channelsTest.SetAction((parseResult, _) =>
|
|
{
|
|
var channelId = parseResult.GetValue(channelIdArg) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var message = parseResult.GetValue(testMessageOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleNotifyChannelsTestAsync(
|
|
services,
|
|
channelId,
|
|
tenant,
|
|
message,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
channels.Add(channelsTest);
|
|
|
|
notify.Add(channels);
|
|
|
|
// notify rules
|
|
var rules = new Command("rules", "Manage notification routing rules.");
|
|
|
|
// notify rules list
|
|
var rulesList = new Command("list", "List notification rules.");
|
|
|
|
var ruleEnabledOption = new Option<bool?>("--enabled")
|
|
{
|
|
Description = "Filter by enabled status."
|
|
};
|
|
var ruleEventTypeOption = new Option<string?>("--event-type")
|
|
{
|
|
Description = "Filter by event type."
|
|
};
|
|
var ruleChannelIdOption = new Option<string?>("--channel-id")
|
|
{
|
|
Description = "Filter by channel ID."
|
|
};
|
|
|
|
rulesList.Add(tenantOption);
|
|
rulesList.Add(ruleEnabledOption);
|
|
rulesList.Add(ruleEventTypeOption);
|
|
rulesList.Add(ruleChannelIdOption);
|
|
rulesList.Add(limitOption);
|
|
rulesList.Add(jsonOption);
|
|
rulesList.Add(verboseOption);
|
|
|
|
rulesList.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var enabled = parseResult.GetValue(ruleEnabledOption);
|
|
var eventType = parseResult.GetValue(ruleEventTypeOption);
|
|
var channelId = parseResult.GetValue(ruleChannelIdOption);
|
|
var limit = parseResult.GetValue(limitOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleNotifyRulesListAsync(
|
|
services,
|
|
tenant,
|
|
enabled,
|
|
eventType,
|
|
channelId,
|
|
limit,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
rules.Add(rulesList);
|
|
|
|
notify.Add(rules);
|
|
|
|
// notify deliveries
|
|
var deliveries = new Command("deliveries", "View and manage notification deliveries.");
|
|
|
|
// notify deliveries list
|
|
var deliveriesList = new Command("list", "List notification deliveries.");
|
|
|
|
var deliveryStatusOption = new Option<string?>("--status")
|
|
{
|
|
Description = "Filter by status (Pending, Sent, Failed, Throttled, Digested, Dropped)."
|
|
};
|
|
var deliveryEventTypeOption = new Option<string?>("--event-type")
|
|
{
|
|
Description = "Filter by event type."
|
|
};
|
|
var deliveryChannelIdOption = new Option<string?>("--channel-id")
|
|
{
|
|
Description = "Filter by channel ID."
|
|
};
|
|
var deliverySinceOption = new Option<DateTimeOffset?>("--since")
|
|
{
|
|
Description = "Filter deliveries since this time (ISO 8601 format)."
|
|
};
|
|
var deliveryUntilOption = new Option<DateTimeOffset?>("--until")
|
|
{
|
|
Description = "Filter deliveries until this time (ISO 8601 format)."
|
|
};
|
|
|
|
deliveriesList.Add(tenantOption);
|
|
deliveriesList.Add(deliveryStatusOption);
|
|
deliveriesList.Add(deliveryEventTypeOption);
|
|
deliveriesList.Add(deliveryChannelIdOption);
|
|
deliveriesList.Add(deliverySinceOption);
|
|
deliveriesList.Add(deliveryUntilOption);
|
|
deliveriesList.Add(limitOption);
|
|
deliveriesList.Add(cursorOption);
|
|
deliveriesList.Add(jsonOption);
|
|
deliveriesList.Add(verboseOption);
|
|
|
|
deliveriesList.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var status = parseResult.GetValue(deliveryStatusOption);
|
|
var eventType = parseResult.GetValue(deliveryEventTypeOption);
|
|
var channelId = parseResult.GetValue(deliveryChannelIdOption);
|
|
var since = parseResult.GetValue(deliverySinceOption);
|
|
var until = parseResult.GetValue(deliveryUntilOption);
|
|
var limit = parseResult.GetValue(limitOption);
|
|
var cursor = parseResult.GetValue(cursorOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleNotifyDeliveriesListAsync(
|
|
services,
|
|
tenant,
|
|
status,
|
|
eventType,
|
|
channelId,
|
|
since,
|
|
until,
|
|
limit,
|
|
cursor,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
deliveries.Add(deliveriesList);
|
|
|
|
// notify deliveries show
|
|
var deliveriesShow = new Command("show", "Show notification delivery details.");
|
|
|
|
var deliveryIdArg = new Argument<string>("delivery-id")
|
|
{
|
|
Description = "Delivery identifier."
|
|
};
|
|
|
|
deliveriesShow.Add(deliveryIdArg);
|
|
deliveriesShow.Add(tenantOption);
|
|
deliveriesShow.Add(jsonOption);
|
|
deliveriesShow.Add(verboseOption);
|
|
|
|
deliveriesShow.SetAction((parseResult, _) =>
|
|
{
|
|
var deliveryId = parseResult.GetValue(deliveryIdArg) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleNotifyDeliveriesShowAsync(
|
|
services,
|
|
deliveryId,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
deliveries.Add(deliveriesShow);
|
|
|
|
// notify deliveries retry
|
|
var deliveriesRetry = new Command("retry", "Retry a failed notification delivery.");
|
|
|
|
var idempotencyKeyOption = new Option<string?>("--idempotency-key")
|
|
{
|
|
Description = "Idempotency key to ensure retry is processed exactly once."
|
|
};
|
|
|
|
deliveriesRetry.Add(deliveryIdArg);
|
|
deliveriesRetry.Add(tenantOption);
|
|
deliveriesRetry.Add(idempotencyKeyOption);
|
|
deliveriesRetry.Add(jsonOption);
|
|
deliveriesRetry.Add(verboseOption);
|
|
|
|
deliveriesRetry.SetAction((parseResult, _) =>
|
|
{
|
|
var deliveryId = parseResult.GetValue(deliveryIdArg) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var idempotencyKey = parseResult.GetValue(idempotencyKeyOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleNotifyDeliveriesRetryAsync(
|
|
services,
|
|
deliveryId,
|
|
tenant,
|
|
idempotencyKey,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
deliveries.Add(deliveriesRetry);
|
|
|
|
notify.Add(deliveries);
|
|
|
|
// notify send
|
|
var send = new Command("send", "Send a notification.");
|
|
|
|
var eventTypeArg = new Argument<string>("event-type")
|
|
{
|
|
Description = "Event type for the notification."
|
|
};
|
|
var bodyArg = new Argument<string>("body")
|
|
{
|
|
Description = "Notification body/message."
|
|
};
|
|
var sendChannelIdOption = new Option<string?>("--channel-id")
|
|
{
|
|
Description = "Target channel ID (if not using routing rules)."
|
|
};
|
|
var sendSubjectOption = new Option<string?>("--subject", "-s")
|
|
{
|
|
Description = "Notification subject."
|
|
};
|
|
var sendSeverityOption = new Option<string?>("--severity")
|
|
{
|
|
Description = "Severity level (info, warning, error, critical)."
|
|
};
|
|
var sendMetadataOption = new Option<string[]?>("--metadata", "-m")
|
|
{
|
|
Description = "Additional metadata as key=value pairs.",
|
|
AllowMultipleArgumentsPerToken = true
|
|
};
|
|
var sendIdempotencyKeyOption = new Option<string?>("--idempotency-key")
|
|
{
|
|
Description = "Idempotency key to ensure notification is sent exactly once."
|
|
};
|
|
|
|
send.Add(eventTypeArg);
|
|
send.Add(bodyArg);
|
|
send.Add(tenantOption);
|
|
send.Add(sendChannelIdOption);
|
|
send.Add(sendSubjectOption);
|
|
send.Add(sendSeverityOption);
|
|
send.Add(sendMetadataOption);
|
|
send.Add(sendIdempotencyKeyOption);
|
|
send.Add(jsonOption);
|
|
send.Add(verboseOption);
|
|
|
|
send.SetAction((parseResult, _) =>
|
|
{
|
|
var eventType = parseResult.GetValue(eventTypeArg) ?? string.Empty;
|
|
var body = parseResult.GetValue(bodyArg) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var channelId = parseResult.GetValue(sendChannelIdOption);
|
|
var subject = parseResult.GetValue(sendSubjectOption);
|
|
var severity = parseResult.GetValue(sendSeverityOption);
|
|
var metadata = parseResult.GetValue(sendMetadataOption);
|
|
var idempotencyKey = parseResult.GetValue(sendIdempotencyKeyOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleNotifySendAsync(
|
|
services,
|
|
eventType,
|
|
body,
|
|
tenant,
|
|
channelId,
|
|
subject,
|
|
severity,
|
|
metadata,
|
|
idempotencyKey,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
notify.Add(send);
|
|
|
|
return notify;
|
|
}
|
|
|
|
// CLI-SBOM-60-001: Sbomer command group for layer/compose operations
|
|
private static Command BuildSbomerCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var sbomer = new Command("sbomer", "SBOM composition: layer fragments, canonical merge, and Merkle verification.");
|
|
|
|
// Common options
|
|
var tenantOption = new Option<string?>("--tenant", "-t")
|
|
{
|
|
Description = "Tenant identifier."
|
|
};
|
|
var jsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output in JSON format."
|
|
};
|
|
var scanIdOption = new Option<string?>("--scan-id")
|
|
{
|
|
Description = "Scan identifier."
|
|
};
|
|
var imageRefOption = new Option<string?>("--image-ref")
|
|
{
|
|
Description = "Container image reference."
|
|
};
|
|
var digestOption = new Option<string?>("--digest")
|
|
{
|
|
Description = "Container image digest."
|
|
};
|
|
var offlineOption = new Option<bool>("--offline")
|
|
{
|
|
Description = "Run in offline mode using local files only."
|
|
};
|
|
var verifiersPathOption = new Option<string?>("--verifiers-path")
|
|
{
|
|
Description = "Path to verifiers.json for DSSE signature verification."
|
|
};
|
|
|
|
// sbomer layer
|
|
var layer = new Command("layer", "Manage SBOM layer fragments.");
|
|
|
|
// sbomer layer list
|
|
var layerList = new Command("list", "List layer fragments for a scan.");
|
|
|
|
var limitOption = new Option<int?>("--limit", "-l")
|
|
{
|
|
Description = "Maximum number of items to return."
|
|
};
|
|
var cursorOption = new Option<string?>("--cursor")
|
|
{
|
|
Description = "Pagination cursor for next page."
|
|
};
|
|
|
|
layerList.Add(tenantOption);
|
|
layerList.Add(scanIdOption);
|
|
layerList.Add(imageRefOption);
|
|
layerList.Add(digestOption);
|
|
layerList.Add(limitOption);
|
|
layerList.Add(cursorOption);
|
|
layerList.Add(jsonOption);
|
|
layerList.Add(verboseOption);
|
|
|
|
layerList.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var scanId = parseResult.GetValue(scanIdOption);
|
|
var imageRef = parseResult.GetValue(imageRefOption);
|
|
var digest = parseResult.GetValue(digestOption);
|
|
var limit = parseResult.GetValue(limitOption);
|
|
var cursor = parseResult.GetValue(cursorOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleSbomerLayerListAsync(
|
|
services,
|
|
tenant,
|
|
scanId,
|
|
imageRef,
|
|
digest,
|
|
limit,
|
|
cursor,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
layer.Add(layerList);
|
|
|
|
// sbomer layer show
|
|
var layerShow = new Command("show", "Show layer fragment details.");
|
|
|
|
var layerDigestArg = new Argument<string>("layer-digest")
|
|
{
|
|
Description = "Layer digest (sha256:...)."
|
|
};
|
|
var includeComponentsOption = new Option<bool>("--components")
|
|
{
|
|
Description = "Include component list."
|
|
};
|
|
var includeDsseOption = new Option<bool>("--dsse")
|
|
{
|
|
Description = "Include DSSE envelope details."
|
|
};
|
|
|
|
layerShow.Add(layerDigestArg);
|
|
layerShow.Add(tenantOption);
|
|
layerShow.Add(scanIdOption);
|
|
layerShow.Add(includeComponentsOption);
|
|
layerShow.Add(includeDsseOption);
|
|
layerShow.Add(jsonOption);
|
|
layerShow.Add(verboseOption);
|
|
|
|
layerShow.SetAction((parseResult, _) =>
|
|
{
|
|
var layerDigest = parseResult.GetValue(layerDigestArg) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var scanId = parseResult.GetValue(scanIdOption);
|
|
var includeComponents = parseResult.GetValue(includeComponentsOption);
|
|
var includeDsse = parseResult.GetValue(includeDsseOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleSbomerLayerShowAsync(
|
|
services,
|
|
layerDigest,
|
|
tenant,
|
|
scanId,
|
|
includeComponents,
|
|
includeDsse,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
layer.Add(layerShow);
|
|
|
|
// sbomer layer verify
|
|
var layerVerify = new Command("verify", "Verify layer fragment DSSE signature and content hash.");
|
|
|
|
layerVerify.Add(layerDigestArg);
|
|
layerVerify.Add(tenantOption);
|
|
layerVerify.Add(scanIdOption);
|
|
layerVerify.Add(verifiersPathOption);
|
|
layerVerify.Add(offlineOption);
|
|
layerVerify.Add(jsonOption);
|
|
layerVerify.Add(verboseOption);
|
|
|
|
layerVerify.SetAction((parseResult, _) =>
|
|
{
|
|
var layerDigest = parseResult.GetValue(layerDigestArg) ?? string.Empty;
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var scanId = parseResult.GetValue(scanIdOption);
|
|
var verifiersPath = parseResult.GetValue(verifiersPathOption);
|
|
var offline = parseResult.GetValue(offlineOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleSbomerLayerVerifyAsync(
|
|
services,
|
|
layerDigest,
|
|
tenant,
|
|
scanId,
|
|
verifiersPath,
|
|
offline,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
layer.Add(layerVerify);
|
|
|
|
sbomer.Add(layer);
|
|
|
|
// sbomer compose
|
|
var compose = new Command("compose", "Compose SBOM from layer fragments with canonical ordering.");
|
|
|
|
var outputPathOption = new Option<string?>("--output", "-o")
|
|
{
|
|
Description = "Output file path for composed SBOM."
|
|
};
|
|
var formatOption = new Option<string?>("--format")
|
|
{
|
|
Description = "Output format (cyclonedx, spdx). Default: cyclonedx."
|
|
};
|
|
var verifyFragmentsOption = new Option<bool>("--verify")
|
|
{
|
|
Description = "Verify all fragment DSSE signatures before composing."
|
|
};
|
|
var emitManifestOption = new Option<bool>("--emit-manifest")
|
|
{
|
|
Description = "Emit _composition.json manifest. Default: true."
|
|
};
|
|
emitManifestOption.SetDefaultValue(true);
|
|
var emitMerkleOption = new Option<bool>("--emit-merkle")
|
|
{
|
|
Description = "Emit Merkle diagnostics file."
|
|
};
|
|
|
|
compose.Add(tenantOption);
|
|
compose.Add(scanIdOption);
|
|
compose.Add(imageRefOption);
|
|
compose.Add(digestOption);
|
|
compose.Add(outputPathOption);
|
|
compose.Add(formatOption);
|
|
compose.Add(verifyFragmentsOption);
|
|
compose.Add(verifiersPathOption);
|
|
compose.Add(offlineOption);
|
|
compose.Add(emitManifestOption);
|
|
compose.Add(emitMerkleOption);
|
|
compose.Add(jsonOption);
|
|
compose.Add(verboseOption);
|
|
|
|
compose.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var scanId = parseResult.GetValue(scanIdOption);
|
|
var imageRef = parseResult.GetValue(imageRefOption);
|
|
var digest = parseResult.GetValue(digestOption);
|
|
var outputPath = parseResult.GetValue(outputPathOption);
|
|
var format = parseResult.GetValue(formatOption);
|
|
var verifyFragments = parseResult.GetValue(verifyFragmentsOption);
|
|
var verifiersPath = parseResult.GetValue(verifiersPathOption);
|
|
var offline = parseResult.GetValue(offlineOption);
|
|
var emitManifest = parseResult.GetValue(emitManifestOption);
|
|
var emitMerkle = parseResult.GetValue(emitMerkleOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleSbomerComposeAsync(
|
|
services,
|
|
tenant,
|
|
scanId,
|
|
imageRef,
|
|
digest,
|
|
outputPath,
|
|
format,
|
|
verifyFragments,
|
|
verifiersPath,
|
|
offline,
|
|
emitManifest,
|
|
emitMerkle,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
sbomer.Add(compose);
|
|
|
|
// sbomer composition
|
|
var composition = new Command("composition", "View and verify composition manifests.");
|
|
|
|
// sbomer composition show
|
|
var compositionShow = new Command("show", "Show composition manifest details.");
|
|
|
|
var compositionPathOption = new Option<string?>("--path")
|
|
{
|
|
Description = "Path to local _composition.json file."
|
|
};
|
|
|
|
compositionShow.Add(tenantOption);
|
|
compositionShow.Add(scanIdOption);
|
|
compositionShow.Add(compositionPathOption);
|
|
compositionShow.Add(jsonOption);
|
|
compositionShow.Add(verboseOption);
|
|
|
|
compositionShow.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var scanId = parseResult.GetValue(scanIdOption);
|
|
var compositionPath = parseResult.GetValue(compositionPathOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleSbomerCompositionShowAsync(
|
|
services,
|
|
tenant,
|
|
scanId,
|
|
compositionPath,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
composition.Add(compositionShow);
|
|
|
|
// sbomer composition verify
|
|
var compositionVerify = new Command("verify", "Verify composition against manifest and recompute Merkle root.");
|
|
|
|
var sbomPathOption = new Option<string?>("--sbom-path")
|
|
{
|
|
Description = "Path to composed SBOM file to verify."
|
|
};
|
|
var recomposeOption = new Option<bool>("--recompose")
|
|
{
|
|
Description = "Re-run composition locally and compare hashes."
|
|
};
|
|
|
|
compositionVerify.Add(tenantOption);
|
|
compositionVerify.Add(scanIdOption);
|
|
compositionVerify.Add(compositionPathOption);
|
|
compositionVerify.Add(sbomPathOption);
|
|
compositionVerify.Add(verifiersPathOption);
|
|
compositionVerify.Add(offlineOption);
|
|
compositionVerify.Add(recomposeOption);
|
|
compositionVerify.Add(jsonOption);
|
|
compositionVerify.Add(verboseOption);
|
|
|
|
compositionVerify.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var scanId = parseResult.GetValue(scanIdOption);
|
|
var compositionPath = parseResult.GetValue(compositionPathOption);
|
|
var sbomPath = parseResult.GetValue(sbomPathOption);
|
|
var verifiersPath = parseResult.GetValue(verifiersPathOption);
|
|
var offline = parseResult.GetValue(offlineOption);
|
|
var recompose = parseResult.GetValue(recomposeOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleSbomerCompositionVerifyAsync(
|
|
services,
|
|
tenant,
|
|
scanId,
|
|
compositionPath,
|
|
sbomPath,
|
|
verifiersPath,
|
|
offline,
|
|
recompose,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
composition.Add(compositionVerify);
|
|
|
|
// sbomer composition merkle
|
|
var compositionMerkle = new Command("merkle", "Show Merkle tree diagnostics for a composition.");
|
|
|
|
compositionMerkle.Add(scanIdOption);
|
|
compositionMerkle.Add(tenantOption);
|
|
compositionMerkle.Add(jsonOption);
|
|
compositionMerkle.Add(verboseOption);
|
|
|
|
compositionMerkle.SetAction((parseResult, _) =>
|
|
{
|
|
var scanId = parseResult.GetValue(scanIdOption);
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleSbomerCompositionMerkleAsync(
|
|
services,
|
|
scanId ?? string.Empty,
|
|
tenant,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
composition.Add(compositionMerkle);
|
|
|
|
sbomer.Add(composition);
|
|
|
|
// CLI-SBOM-60-002: sbomer drift
|
|
var drift = new Command("drift", "Detect and explain determinism drift in SBOM composition.");
|
|
|
|
// sbomer drift (analyze)
|
|
var driftAnalyze = new Command("analyze", "Analyze drift between current SBOM and baseline, highlighting determinism breaks.")
|
|
{
|
|
Aliases = { "diff" }
|
|
};
|
|
|
|
var baselineScanIdOption = new Option<string?>("--baseline-scan-id")
|
|
{
|
|
Description = "Baseline scan ID to compare against."
|
|
};
|
|
var baselinePathOption = new Option<string?>("--baseline-path")
|
|
{
|
|
Description = "Path to baseline SBOM file."
|
|
};
|
|
var sbomPathOptionDrift = new Option<string?>("--sbom-path")
|
|
{
|
|
Description = "Path to current SBOM file."
|
|
};
|
|
var explainOption = new Option<bool>("--explain")
|
|
{
|
|
Description = "Provide detailed explanations for each drift, including root cause and remediation."
|
|
};
|
|
var offlineKitPathOption = new Option<string?>("--offline-kit")
|
|
{
|
|
Description = "Path to offline kit bundle for air-gapped verification."
|
|
};
|
|
|
|
driftAnalyze.Add(tenantOption);
|
|
driftAnalyze.Add(scanIdOption);
|
|
driftAnalyze.Add(baselineScanIdOption);
|
|
driftAnalyze.Add(sbomPathOptionDrift);
|
|
driftAnalyze.Add(baselinePathOption);
|
|
driftAnalyze.Add(compositionPathOption);
|
|
driftAnalyze.Add(explainOption);
|
|
driftAnalyze.Add(offlineOption);
|
|
driftAnalyze.Add(offlineKitPathOption);
|
|
driftAnalyze.Add(jsonOption);
|
|
driftAnalyze.Add(verboseOption);
|
|
|
|
driftAnalyze.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var scanId = parseResult.GetValue(scanIdOption);
|
|
var baselineScanId = parseResult.GetValue(baselineScanIdOption);
|
|
var sbomPath = parseResult.GetValue(sbomPathOptionDrift);
|
|
var baselinePath = parseResult.GetValue(baselinePathOption);
|
|
var compositionPath = parseResult.GetValue(compositionPathOption);
|
|
var explain = parseResult.GetValue(explainOption);
|
|
var offline = parseResult.GetValue(offlineOption);
|
|
var offlineKitPath = parseResult.GetValue(offlineKitPathOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleSbomerDriftAnalyzeAsync(
|
|
services,
|
|
tenant,
|
|
scanId,
|
|
baselineScanId,
|
|
sbomPath,
|
|
baselinePath,
|
|
compositionPath,
|
|
explain,
|
|
offline,
|
|
offlineKitPath,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
drift.Add(driftAnalyze);
|
|
|
|
// sbomer drift verify
|
|
var driftVerify = new Command("verify", "Verify SBOM with local recomposition and drift detection from offline kit.");
|
|
|
|
var recomposeLocallyOption = new Option<bool>("--recompose")
|
|
{
|
|
Description = "Re-run composition locally and compare hashes."
|
|
};
|
|
var validateFragmentsOption = new Option<bool>("--validate-fragments")
|
|
{
|
|
Description = "Validate all fragment DSSE signatures."
|
|
};
|
|
validateFragmentsOption.SetDefaultValue(true);
|
|
var checkMerkleOption = new Option<bool>("--check-merkle")
|
|
{
|
|
Description = "Verify Merkle proofs for all fragments."
|
|
};
|
|
checkMerkleOption.SetDefaultValue(true);
|
|
|
|
driftVerify.Add(tenantOption);
|
|
driftVerify.Add(scanIdOption);
|
|
driftVerify.Add(sbomPathOptionDrift);
|
|
driftVerify.Add(compositionPathOption);
|
|
driftVerify.Add(verifiersPathOption);
|
|
driftVerify.Add(offlineKitPathOption);
|
|
driftVerify.Add(recomposeLocallyOption);
|
|
driftVerify.Add(validateFragmentsOption);
|
|
driftVerify.Add(checkMerkleOption);
|
|
driftVerify.Add(jsonOption);
|
|
driftVerify.Add(verboseOption);
|
|
|
|
driftVerify.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var scanId = parseResult.GetValue(scanIdOption);
|
|
var sbomPath = parseResult.GetValue(sbomPathOptionDrift);
|
|
var compositionPath = parseResult.GetValue(compositionPathOption);
|
|
var verifiersPath = parseResult.GetValue(verifiersPathOption);
|
|
var offlineKitPath = parseResult.GetValue(offlineKitPathOption);
|
|
var recomposeLocally = parseResult.GetValue(recomposeLocallyOption);
|
|
var validateFragments = parseResult.GetValue(validateFragmentsOption);
|
|
var checkMerkle = parseResult.GetValue(checkMerkleOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleSbomerDriftVerifyAsync(
|
|
services,
|
|
tenant,
|
|
scanId,
|
|
sbomPath,
|
|
compositionPath,
|
|
verifiersPath,
|
|
offlineKitPath,
|
|
recomposeLocally,
|
|
validateFragments,
|
|
checkMerkle,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
drift.Add(driftVerify);
|
|
|
|
sbomer.Add(drift);
|
|
|
|
return sbomer;
|
|
}
|
|
|
|
// CLI-RISK-66-001 through CLI-RISK-68-001: Risk command group
|
|
private static Command BuildRiskCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var risk = new Command("risk", "Manage risk profiles, scoring, and bundle verification.");
|
|
|
|
// Common options
|
|
var tenantOption = new Option<string?>("--tenant", "-t")
|
|
{
|
|
Description = "Tenant identifier."
|
|
};
|
|
var jsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON."
|
|
};
|
|
|
|
// CLI-RISK-66-001: stella risk profile list
|
|
var profile = new Command("profile", "Manage risk profiles.");
|
|
|
|
var profileList = new Command("list", "List available risk profiles.");
|
|
var includeDisabledOption = new Option<bool>("--include-disabled")
|
|
{
|
|
Description = "Include disabled profiles in the listing."
|
|
};
|
|
var categoryOption = new Option<string?>("--category", "-c")
|
|
{
|
|
Description = "Filter by profile category."
|
|
};
|
|
var limitOption = new Option<int?>("--limit", "-l")
|
|
{
|
|
Description = "Maximum number of results (default 100)."
|
|
};
|
|
var offsetOption = new Option<int?>("--offset", "-o")
|
|
{
|
|
Description = "Pagination offset."
|
|
};
|
|
|
|
profileList.Add(tenantOption);
|
|
profileList.Add(includeDisabledOption);
|
|
profileList.Add(categoryOption);
|
|
profileList.Add(limitOption);
|
|
profileList.Add(offsetOption);
|
|
profileList.Add(jsonOption);
|
|
profileList.Add(verboseOption);
|
|
|
|
profileList.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var includeDisabled = parseResult.GetValue(includeDisabledOption);
|
|
var category = parseResult.GetValue(categoryOption);
|
|
var limit = parseResult.GetValue(limitOption);
|
|
var offset = parseResult.GetValue(offsetOption);
|
|
var emitJson = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleRiskProfileListAsync(
|
|
services,
|
|
tenant,
|
|
includeDisabled,
|
|
category,
|
|
limit,
|
|
offset,
|
|
emitJson,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
profile.Add(profileList);
|
|
risk.Add(profile);
|
|
|
|
// CLI-RISK-66-002: stella risk simulate
|
|
var simulate = new Command("simulate", "Simulate risk scoring against an SBOM or asset.");
|
|
var profileIdOption = new Option<string?>("--profile-id", "-p")
|
|
{
|
|
Description = "Risk profile identifier to use for simulation."
|
|
};
|
|
var sbomIdOption = new Option<string?>("--sbom-id")
|
|
{
|
|
Description = "SBOM identifier for risk evaluation."
|
|
};
|
|
var sbomPathOption = new Option<string?>("--sbom-path")
|
|
{
|
|
Description = "Local path to SBOM file for risk evaluation."
|
|
};
|
|
var assetIdOption = new Option<string?>("--asset-id", "-a")
|
|
{
|
|
Description = "Asset identifier for risk evaluation."
|
|
};
|
|
var diffModeOption = new Option<bool>("--diff")
|
|
{
|
|
Description = "Enable diff mode to compare with baseline."
|
|
};
|
|
var baselineProfileIdOption = new Option<string?>("--baseline-profile-id")
|
|
{
|
|
Description = "Baseline profile identifier for diff comparison."
|
|
};
|
|
var csvOption = new Option<bool>("--csv")
|
|
{
|
|
Description = "Output as CSV."
|
|
};
|
|
var outputOption = new Option<string?>("--output")
|
|
{
|
|
Description = "Write output to specified file path."
|
|
};
|
|
|
|
simulate.Add(tenantOption);
|
|
simulate.Add(profileIdOption);
|
|
simulate.Add(sbomIdOption);
|
|
simulate.Add(sbomPathOption);
|
|
simulate.Add(assetIdOption);
|
|
simulate.Add(diffModeOption);
|
|
simulate.Add(baselineProfileIdOption);
|
|
simulate.Add(jsonOption);
|
|
simulate.Add(csvOption);
|
|
simulate.Add(outputOption);
|
|
simulate.Add(verboseOption);
|
|
|
|
simulate.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var profileId = parseResult.GetValue(profileIdOption);
|
|
var sbomId = parseResult.GetValue(sbomIdOption);
|
|
var sbomPath = parseResult.GetValue(sbomPathOption);
|
|
var assetId = parseResult.GetValue(assetIdOption);
|
|
var diffMode = parseResult.GetValue(diffModeOption);
|
|
var baselineProfileId = parseResult.GetValue(baselineProfileIdOption);
|
|
var emitJson = parseResult.GetValue(jsonOption);
|
|
var emitCsv = parseResult.GetValue(csvOption);
|
|
var output = parseResult.GetValue(outputOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleRiskSimulateAsync(
|
|
services,
|
|
tenant,
|
|
profileId,
|
|
sbomId,
|
|
sbomPath,
|
|
assetId,
|
|
diffMode,
|
|
baselineProfileId,
|
|
emitJson,
|
|
emitCsv,
|
|
output,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
risk.Add(simulate);
|
|
|
|
// CLI-RISK-67-001: stella risk results
|
|
var results = new Command("results", "Get risk evaluation results.");
|
|
var resultsAssetIdOption = new Option<string?>("--asset-id", "-a")
|
|
{
|
|
Description = "Filter by asset identifier."
|
|
};
|
|
var resultsSbomIdOption = new Option<string?>("--sbom-id")
|
|
{
|
|
Description = "Filter by SBOM identifier."
|
|
};
|
|
var resultsProfileIdOption = new Option<string?>("--profile-id", "-p")
|
|
{
|
|
Description = "Filter by risk profile identifier."
|
|
};
|
|
var minSeverityOption = new Option<string?>("--min-severity")
|
|
{
|
|
Description = "Minimum severity threshold (critical, high, medium, low, info)."
|
|
};
|
|
var maxScoreOption = new Option<double?>("--max-score")
|
|
{
|
|
Description = "Maximum score threshold (0-100)."
|
|
};
|
|
var includeExplainOption = new Option<bool>("--explain")
|
|
{
|
|
Description = "Include explainability information in results."
|
|
};
|
|
|
|
results.Add(tenantOption);
|
|
results.Add(resultsAssetIdOption);
|
|
results.Add(resultsSbomIdOption);
|
|
results.Add(resultsProfileIdOption);
|
|
results.Add(minSeverityOption);
|
|
results.Add(maxScoreOption);
|
|
results.Add(includeExplainOption);
|
|
results.Add(limitOption);
|
|
results.Add(offsetOption);
|
|
results.Add(jsonOption);
|
|
results.Add(csvOption);
|
|
results.Add(verboseOption);
|
|
|
|
results.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var assetId = parseResult.GetValue(resultsAssetIdOption);
|
|
var sbomId = parseResult.GetValue(resultsSbomIdOption);
|
|
var profileId = parseResult.GetValue(resultsProfileIdOption);
|
|
var minSeverity = parseResult.GetValue(minSeverityOption);
|
|
var maxScore = parseResult.GetValue(maxScoreOption);
|
|
var includeExplain = parseResult.GetValue(includeExplainOption);
|
|
var limit = parseResult.GetValue(limitOption);
|
|
var offset = parseResult.GetValue(offsetOption);
|
|
var emitJson = parseResult.GetValue(jsonOption);
|
|
var emitCsv = parseResult.GetValue(csvOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleRiskResultsAsync(
|
|
services,
|
|
tenant,
|
|
assetId,
|
|
sbomId,
|
|
profileId,
|
|
minSeverity,
|
|
maxScore,
|
|
includeExplain,
|
|
limit,
|
|
offset,
|
|
emitJson,
|
|
emitCsv,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
risk.Add(results);
|
|
|
|
// CLI-RISK-68-001: stella risk bundle verify
|
|
var bundle = new Command("bundle", "Risk bundle operations.");
|
|
var bundleVerify = new Command("verify", "Verify a risk bundle for integrity and signatures.");
|
|
var bundlePathOption = new Option<string>("--bundle-path", "-b")
|
|
{
|
|
Description = "Path to the risk bundle file.",
|
|
Required = true
|
|
};
|
|
var signaturePathOption = new Option<string?>("--signature-path", "-s")
|
|
{
|
|
Description = "Path to detached signature file."
|
|
};
|
|
var checkRekorOption = new Option<bool>("--check-rekor")
|
|
{
|
|
Description = "Verify transparency log entry in Sigstore Rekor."
|
|
};
|
|
|
|
bundleVerify.Add(tenantOption);
|
|
bundleVerify.Add(bundlePathOption);
|
|
bundleVerify.Add(signaturePathOption);
|
|
bundleVerify.Add(checkRekorOption);
|
|
bundleVerify.Add(jsonOption);
|
|
bundleVerify.Add(verboseOption);
|
|
|
|
bundleVerify.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var bundlePath = parseResult.GetValue(bundlePathOption) ?? string.Empty;
|
|
var signaturePath = parseResult.GetValue(signaturePathOption);
|
|
var checkRekor = parseResult.GetValue(checkRekorOption);
|
|
var emitJson = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleRiskBundleVerifyAsync(
|
|
services,
|
|
tenant,
|
|
bundlePath,
|
|
signaturePath,
|
|
checkRekor,
|
|
emitJson,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
bundle.Add(bundleVerify);
|
|
risk.Add(bundle);
|
|
|
|
return risk;
|
|
}
|
|
|
|
// CLI-SIG-26-001: Reachability command group
|
|
private static Command BuildReachabilityCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var reachability = new Command("reachability", "Reachability analysis for vulnerability exploitability.");
|
|
|
|
// Common options
|
|
var tenantOption = new Option<string?>("--tenant", "-t")
|
|
{
|
|
Description = "Tenant identifier."
|
|
};
|
|
var jsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON."
|
|
};
|
|
|
|
// stella reachability upload-callgraph
|
|
var uploadCallGraph = new Command("upload-callgraph", "Upload a call graph for reachability analysis.");
|
|
var callGraphPathOption = new Option<string>("--path", "-p")
|
|
{
|
|
Description = "Path to the call graph file.",
|
|
Required = true
|
|
};
|
|
var scanIdOption = new Option<string?>("--scan-id")
|
|
{
|
|
Description = "Scan identifier to associate with the call graph."
|
|
};
|
|
var assetIdOption = new Option<string?>("--asset-id", "-a")
|
|
{
|
|
Description = "Asset identifier to associate with the call graph."
|
|
};
|
|
var formatOption = new Option<string?>("--format", "-f")
|
|
{
|
|
Description = "Call graph format (auto, json, proto, dot). Default: auto-detect."
|
|
};
|
|
|
|
uploadCallGraph.Add(tenantOption);
|
|
uploadCallGraph.Add(callGraphPathOption);
|
|
uploadCallGraph.Add(scanIdOption);
|
|
uploadCallGraph.Add(assetIdOption);
|
|
uploadCallGraph.Add(formatOption);
|
|
uploadCallGraph.Add(jsonOption);
|
|
uploadCallGraph.Add(verboseOption);
|
|
|
|
uploadCallGraph.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var callGraphPath = parseResult.GetValue(callGraphPathOption) ?? string.Empty;
|
|
var scanId = parseResult.GetValue(scanIdOption);
|
|
var assetId = parseResult.GetValue(assetIdOption);
|
|
var format = parseResult.GetValue(formatOption);
|
|
var emitJson = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleReachabilityUploadCallGraphAsync(
|
|
services,
|
|
tenant,
|
|
callGraphPath,
|
|
scanId,
|
|
assetId,
|
|
format,
|
|
emitJson,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
reachability.Add(uploadCallGraph);
|
|
|
|
// stella reachability list
|
|
var list = new Command("list", "List reachability analyses.");
|
|
var listScanIdOption = new Option<string?>("--scan-id")
|
|
{
|
|
Description = "Filter by scan identifier."
|
|
};
|
|
var listAssetIdOption = new Option<string?>("--asset-id", "-a")
|
|
{
|
|
Description = "Filter by asset identifier."
|
|
};
|
|
var statusOption = new Option<string?>("--status")
|
|
{
|
|
Description = "Filter by status (pending, processing, completed, failed)."
|
|
};
|
|
var limitOption = new Option<int?>("--limit", "-l")
|
|
{
|
|
Description = "Maximum number of results (default 100)."
|
|
};
|
|
var offsetOption = new Option<int?>("--offset", "-o")
|
|
{
|
|
Description = "Pagination offset."
|
|
};
|
|
|
|
list.Add(tenantOption);
|
|
list.Add(listScanIdOption);
|
|
list.Add(listAssetIdOption);
|
|
list.Add(statusOption);
|
|
list.Add(limitOption);
|
|
list.Add(offsetOption);
|
|
list.Add(jsonOption);
|
|
list.Add(verboseOption);
|
|
|
|
list.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var scanId = parseResult.GetValue(listScanIdOption);
|
|
var assetId = parseResult.GetValue(listAssetIdOption);
|
|
var status = parseResult.GetValue(statusOption);
|
|
var limit = parseResult.GetValue(limitOption);
|
|
var offset = parseResult.GetValue(offsetOption);
|
|
var emitJson = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleReachabilityListAsync(
|
|
services,
|
|
tenant,
|
|
scanId,
|
|
assetId,
|
|
status,
|
|
limit,
|
|
offset,
|
|
emitJson,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
reachability.Add(list);
|
|
|
|
// stella reachability explain
|
|
var explain = new Command("explain", "Explain reachability for a vulnerability or package.");
|
|
var analysisIdOption = new Option<string>("--analysis-id", "-i")
|
|
{
|
|
Description = "Analysis identifier.",
|
|
Required = true
|
|
};
|
|
var vulnerabilityIdOption = new Option<string?>("--vuln-id", "-v")
|
|
{
|
|
Description = "Vulnerability identifier to explain."
|
|
};
|
|
var packagePurlOption = new Option<string?>("--purl")
|
|
{
|
|
Description = "Package URL to explain."
|
|
};
|
|
var includeCallPathsOption = new Option<bool>("--call-paths")
|
|
{
|
|
Description = "Include detailed call paths in the explanation."
|
|
};
|
|
|
|
explain.Add(tenantOption);
|
|
explain.Add(analysisIdOption);
|
|
explain.Add(vulnerabilityIdOption);
|
|
explain.Add(packagePurlOption);
|
|
explain.Add(includeCallPathsOption);
|
|
explain.Add(jsonOption);
|
|
explain.Add(verboseOption);
|
|
|
|
explain.SetAction((parseResult, _) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var analysisId = parseResult.GetValue(analysisIdOption) ?? string.Empty;
|
|
var vulnerabilityId = parseResult.GetValue(vulnerabilityIdOption);
|
|
var packagePurl = parseResult.GetValue(packagePurlOption);
|
|
var includeCallPaths = parseResult.GetValue(includeCallPathsOption);
|
|
var emitJson = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleReachabilityExplainAsync(
|
|
services,
|
|
tenant,
|
|
analysisId,
|
|
vulnerabilityId,
|
|
packagePurl,
|
|
includeCallPaths,
|
|
emitJson,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
reachability.Add(explain);
|
|
|
|
return reachability;
|
|
}
|
|
|
|
// CLI-SDK-63-001: stella api command
|
|
private static Command BuildApiCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var api = new Command("api", "API management commands.");
|
|
|
|
// stella api spec
|
|
var spec = new Command("spec", "API specification operations.");
|
|
|
|
// stella api spec list
|
|
var list = new Command("list", "List available API specifications.");
|
|
|
|
var tenantOption = new Option<string?>("--tenant", "-t")
|
|
{
|
|
Description = "Tenant context for the operation."
|
|
};
|
|
|
|
var emitJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output in JSON format."
|
|
};
|
|
|
|
list.Add(tenantOption);
|
|
list.Add(emitJsonOption);
|
|
list.Add(verboseOption);
|
|
|
|
list.SetAction(async (parseResult, ct) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var emitJson = parseResult.GetValue(emitJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
await CommandHandlers.HandleApiSpecListAsync(
|
|
services,
|
|
tenant,
|
|
emitJson,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
spec.Add(list);
|
|
|
|
// stella api spec download
|
|
var download = new Command("download", "Download API specification.");
|
|
|
|
var outputOption = new Option<string>("--output", "-o")
|
|
{
|
|
Description = "Output path for the downloaded spec (file or directory).",
|
|
Required = true
|
|
};
|
|
|
|
var serviceOption = new Option<string?>("--service", "-s")
|
|
{
|
|
Description = "Service to download spec for (e.g., concelier, scanner, policy). Omit for aggregate spec."
|
|
};
|
|
|
|
var formatOption = new Option<string>("--format", "-f")
|
|
{
|
|
Description = "Output format: openapi-json (default) or openapi-yaml."
|
|
};
|
|
formatOption.SetDefaultValue("openapi-json");
|
|
|
|
var overwriteOption = new Option<bool>("--overwrite")
|
|
{
|
|
Description = "Overwrite existing file if present."
|
|
};
|
|
|
|
var etagOption = new Option<string?>("--etag")
|
|
{
|
|
Description = "Expected ETag for conditional download (If-None-Match)."
|
|
};
|
|
|
|
var checksumOption = new Option<string?>("--checksum")
|
|
{
|
|
Description = "Expected checksum for verification after download."
|
|
};
|
|
|
|
var checksumAlgoOption = new Option<string>("--checksum-algorithm")
|
|
{
|
|
Description = "Checksum algorithm: sha256 (default), sha384, sha512."
|
|
};
|
|
checksumAlgoOption.SetDefaultValue("sha256");
|
|
|
|
download.Add(tenantOption);
|
|
download.Add(outputOption);
|
|
download.Add(serviceOption);
|
|
download.Add(formatOption);
|
|
download.Add(overwriteOption);
|
|
download.Add(etagOption);
|
|
download.Add(checksumOption);
|
|
download.Add(checksumAlgoOption);
|
|
download.Add(emitJsonOption);
|
|
download.Add(verboseOption);
|
|
|
|
download.SetAction(async (parseResult, ct) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var output = parseResult.GetValue(outputOption)!;
|
|
var service = parseResult.GetValue(serviceOption);
|
|
var format = parseResult.GetValue(formatOption)!;
|
|
var overwrite = parseResult.GetValue(overwriteOption);
|
|
var etag = parseResult.GetValue(etagOption);
|
|
var checksum = parseResult.GetValue(checksumOption);
|
|
var checksumAlgo = parseResult.GetValue(checksumAlgoOption)!;
|
|
var emitJson = parseResult.GetValue(emitJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
await CommandHandlers.HandleApiSpecDownloadAsync(
|
|
services,
|
|
tenant,
|
|
output,
|
|
service,
|
|
format,
|
|
overwrite,
|
|
etag,
|
|
checksum,
|
|
checksumAlgo,
|
|
emitJson,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
spec.Add(download);
|
|
|
|
api.Add(spec);
|
|
return api;
|
|
}
|
|
|
|
// CLI-SDK-64-001: stella sdk command
|
|
private static Command BuildSdkCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var sdk = new Command("sdk", "SDK management commands.");
|
|
|
|
// stella sdk update
|
|
var update = new Command("update", "Check for SDK updates and fetch latest manifests/changelogs.");
|
|
|
|
var tenantOption = new Option<string?>("--tenant", "-t")
|
|
{
|
|
Description = "Tenant context for the operation."
|
|
};
|
|
|
|
var languageOption = new Option<string?>("--language", "-l")
|
|
{
|
|
Description = "SDK language filter (typescript, go, csharp, python, java). Omit for all."
|
|
};
|
|
|
|
var checkOnlyOption = new Option<bool>("--check-only")
|
|
{
|
|
Description = "Only check for updates, don't download."
|
|
};
|
|
|
|
var showChangelogOption = new Option<bool>("--changelog")
|
|
{
|
|
Description = "Show changelog for available updates."
|
|
};
|
|
|
|
var showDeprecationsOption = new Option<bool>("--deprecations")
|
|
{
|
|
Description = "Show deprecation notices."
|
|
};
|
|
|
|
var emitJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output in JSON format."
|
|
};
|
|
|
|
update.Add(tenantOption);
|
|
update.Add(languageOption);
|
|
update.Add(checkOnlyOption);
|
|
update.Add(showChangelogOption);
|
|
update.Add(showDeprecationsOption);
|
|
update.Add(emitJsonOption);
|
|
update.Add(verboseOption);
|
|
|
|
update.SetAction(async (parseResult, ct) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var language = parseResult.GetValue(languageOption);
|
|
var checkOnly = parseResult.GetValue(checkOnlyOption);
|
|
var showChangelog = parseResult.GetValue(showChangelogOption);
|
|
var showDeprecations = parseResult.GetValue(showDeprecationsOption);
|
|
var emitJson = parseResult.GetValue(emitJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
await CommandHandlers.HandleSdkUpdateAsync(
|
|
services,
|
|
tenant,
|
|
language,
|
|
checkOnly,
|
|
showChangelog,
|
|
showDeprecations,
|
|
emitJson,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
sdk.Add(update);
|
|
|
|
// stella sdk list
|
|
var list = new Command("list", "List installed SDK versions.");
|
|
|
|
list.Add(tenantOption);
|
|
list.Add(languageOption);
|
|
list.Add(emitJsonOption);
|
|
list.Add(verboseOption);
|
|
|
|
list.SetAction(async (parseResult, ct) =>
|
|
{
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var language = parseResult.GetValue(languageOption);
|
|
var emitJson = parseResult.GetValue(emitJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
await CommandHandlers.HandleSdkListAsync(
|
|
services,
|
|
tenant,
|
|
language,
|
|
emitJson,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
sdk.Add(list);
|
|
|
|
return sdk;
|
|
}
|
|
|
|
private static Command BuildMirrorCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var mirror = new Command("mirror", "Manage air-gap mirror bundles for offline distribution.");
|
|
|
|
// mirror create
|
|
var create = new Command("create", "Create an air-gap mirror bundle.");
|
|
|
|
var domainOption = new Option<string>("--domain", new[] { "-d" })
|
|
{
|
|
Description = "Domain identifier (e.g., vex-advisories, vulnerability-feeds, policy-packs).",
|
|
Required = true
|
|
};
|
|
|
|
var outputOption = new Option<string>("--output", new[] { "-o" })
|
|
{
|
|
Description = "Output directory for the bundle files.",
|
|
Required = true
|
|
};
|
|
|
|
var formatOption = new Option<string?>("--format", new[] { "-f" })
|
|
{
|
|
Description = "Export format filter (openvex, csaf, cyclonedx, spdx, ndjson, json)."
|
|
};
|
|
|
|
var tenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Tenant scope for the exports."
|
|
};
|
|
|
|
var displayNameOption = new Option<string?>("--display-name")
|
|
{
|
|
Description = "Human-readable display name for the bundle."
|
|
};
|
|
|
|
var targetRepoOption = new Option<string?>("--target-repository")
|
|
{
|
|
Description = "Target OCI repository URI for this bundle."
|
|
};
|
|
|
|
var providersOption = new Option<string[]?>("--provider", new[] { "-p" })
|
|
{
|
|
Description = "Provider filter for VEX exports (can be specified multiple times).",
|
|
AllowMultipleArgumentsPerToken = true
|
|
};
|
|
|
|
var signOption = new Option<bool>("--sign")
|
|
{
|
|
Description = "Include DSSE signatures in the bundle."
|
|
};
|
|
|
|
var attestOption = new Option<bool>("--attest")
|
|
{
|
|
Description = "Include attestation metadata in the bundle."
|
|
};
|
|
|
|
var jsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output result in JSON format."
|
|
};
|
|
|
|
create.Add(domainOption);
|
|
create.Add(outputOption);
|
|
create.Add(formatOption);
|
|
create.Add(tenantOption);
|
|
create.Add(displayNameOption);
|
|
create.Add(targetRepoOption);
|
|
create.Add(providersOption);
|
|
create.Add(signOption);
|
|
create.Add(attestOption);
|
|
create.Add(jsonOption);
|
|
|
|
create.SetAction((parseResult, _) =>
|
|
{
|
|
var domain = parseResult.GetValue(domainOption) ?? string.Empty;
|
|
var output = parseResult.GetValue(outputOption) ?? string.Empty;
|
|
var format = parseResult.GetValue(formatOption);
|
|
var tenant = parseResult.GetValue(tenantOption);
|
|
var displayName = parseResult.GetValue(displayNameOption);
|
|
var targetRepo = parseResult.GetValue(targetRepoOption);
|
|
var providers = parseResult.GetValue(providersOption);
|
|
var sign = parseResult.GetValue(signOption);
|
|
var attest = parseResult.GetValue(attestOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleMirrorCreateAsync(
|
|
services,
|
|
domain,
|
|
output,
|
|
format,
|
|
tenant,
|
|
displayName,
|
|
targetRepo,
|
|
providers?.ToList(),
|
|
sign,
|
|
attest,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
mirror.Add(create);
|
|
|
|
return mirror;
|
|
}
|
|
|
|
private static Command BuildAirgapCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
{
|
|
var airgap = new Command("airgap", "Manage air-gapped environment operations.");
|
|
|
|
// airgap import (CLI-AIRGAP-57-001)
|
|
var import = new Command("import", "Import an air-gap mirror bundle into the local data store.");
|
|
|
|
var bundlePathOption = new Option<string>("--bundle", new[] { "-b" })
|
|
{
|
|
Description = "Path to the bundle directory (contains manifest.json and artifacts).",
|
|
Required = true
|
|
};
|
|
|
|
var importTenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Import data under a specific tenant scope."
|
|
};
|
|
|
|
var globalOption = new Option<bool>("--global")
|
|
{
|
|
Description = "Import data to the global scope (requires elevated permissions)."
|
|
};
|
|
|
|
var dryRunOption = new Option<bool>("--dry-run")
|
|
{
|
|
Description = "Preview the import without making changes."
|
|
};
|
|
|
|
var forceOption = new Option<bool>("--force")
|
|
{
|
|
Description = "Force import even if checksums have been verified before."
|
|
};
|
|
|
|
var verifyOnlyOption = new Option<bool>("--verify-only")
|
|
{
|
|
Description = "Verify bundle integrity without importing."
|
|
};
|
|
|
|
var importJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output results in JSON format."
|
|
};
|
|
|
|
import.Add(bundlePathOption);
|
|
import.Add(importTenantOption);
|
|
import.Add(globalOption);
|
|
import.Add(dryRunOption);
|
|
import.Add(forceOption);
|
|
import.Add(verifyOnlyOption);
|
|
import.Add(importJsonOption);
|
|
|
|
import.SetAction((parseResult, _) =>
|
|
{
|
|
var bundlePath = parseResult.GetValue(bundlePathOption)!;
|
|
var tenant = parseResult.GetValue(importTenantOption);
|
|
var global = parseResult.GetValue(globalOption);
|
|
var dryRun = parseResult.GetValue(dryRunOption);
|
|
var force = parseResult.GetValue(forceOption);
|
|
var verifyOnly = parseResult.GetValue(verifyOnlyOption);
|
|
var json = parseResult.GetValue(importJsonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleAirgapImportAsync(
|
|
services,
|
|
bundlePath,
|
|
tenant,
|
|
global,
|
|
dryRun,
|
|
force,
|
|
verifyOnly,
|
|
json,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
airgap.Add(import);
|
|
|
|
// airgap seal (CLI-AIRGAP-57-002)
|
|
var seal = new Command("seal", "Seal the environment for air-gapped operation.");
|
|
|
|
var sealConfigDirOption = new Option<string?>("--config-dir", new[] { "-c" })
|
|
{
|
|
Description = "Path to the configuration directory (defaults to ~/.stellaops)."
|
|
};
|
|
|
|
var sealVerifyOption = new Option<bool>("--verify")
|
|
{
|
|
Description = "Verify imported bundles before sealing."
|
|
};
|
|
|
|
var sealForceOption = new Option<bool>("--force")
|
|
{
|
|
Description = "Force seal even if verification warnings exist."
|
|
};
|
|
|
|
var sealDryRunOption = new Option<bool>("--dry-run")
|
|
{
|
|
Description = "Preview the seal operation without making changes."
|
|
};
|
|
|
|
var sealJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output results in JSON format."
|
|
};
|
|
|
|
var sealReasonOption = new Option<string?>("--reason")
|
|
{
|
|
Description = "Reason for sealing (recorded in audit log)."
|
|
};
|
|
|
|
seal.Add(sealConfigDirOption);
|
|
seal.Add(sealVerifyOption);
|
|
seal.Add(sealForceOption);
|
|
seal.Add(sealDryRunOption);
|
|
seal.Add(sealJsonOption);
|
|
seal.Add(sealReasonOption);
|
|
|
|
seal.SetAction((parseResult, _) =>
|
|
{
|
|
var configDir = parseResult.GetValue(sealConfigDirOption);
|
|
var verify = parseResult.GetValue(sealVerifyOption);
|
|
var force = parseResult.GetValue(sealForceOption);
|
|
var dryRun = parseResult.GetValue(sealDryRunOption);
|
|
var json = parseResult.GetValue(sealJsonOption);
|
|
var reason = parseResult.GetValue(sealReasonOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleAirgapSealAsync(
|
|
services,
|
|
configDir,
|
|
verify,
|
|
force,
|
|
dryRun,
|
|
json,
|
|
reason,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
airgap.Add(seal);
|
|
|
|
// airgap export-evidence (CLI-AIRGAP-58-001)
|
|
var exportEvidence = new Command("export-evidence", "Export portable evidence packages for audit and compliance.");
|
|
|
|
var evidenceOutputOption = new Option<string>("--output", new[] { "-o" })
|
|
{
|
|
Description = "Output directory for the evidence package.",
|
|
Required = true
|
|
};
|
|
|
|
var evidenceIncludeOption = new Option<string[]>("--include", new[] { "-i" })
|
|
{
|
|
Description = "Evidence types to include: attestations, sboms, scans, vex, all (default: all).",
|
|
AllowMultipleArgumentsPerToken = true
|
|
};
|
|
|
|
var evidenceFromOption = new Option<DateTimeOffset?>("--from")
|
|
{
|
|
Description = "Include evidence from this date (UTC, ISO-8601)."
|
|
};
|
|
|
|
var evidenceToOption = new Option<DateTimeOffset?>("--to")
|
|
{
|
|
Description = "Include evidence up to this date (UTC, ISO-8601)."
|
|
};
|
|
|
|
var evidenceTenantOption = new Option<string?>("--tenant")
|
|
{
|
|
Description = "Export evidence for a specific tenant."
|
|
};
|
|
|
|
var evidenceSubjectOption = new Option<string?>("--subject")
|
|
{
|
|
Description = "Filter evidence by subject (e.g., image digest, package PURL)."
|
|
};
|
|
|
|
var evidenceCompressOption = new Option<bool>("--compress")
|
|
{
|
|
Description = "Compress the output package as a .tar.gz archive."
|
|
};
|
|
|
|
var evidenceJsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output results in JSON format."
|
|
};
|
|
|
|
var evidenceVerifyOption = new Option<bool>("--verify")
|
|
{
|
|
Description = "Verify evidence signatures before export."
|
|
};
|
|
|
|
exportEvidence.Add(evidenceOutputOption);
|
|
exportEvidence.Add(evidenceIncludeOption);
|
|
exportEvidence.Add(evidenceFromOption);
|
|
exportEvidence.Add(evidenceToOption);
|
|
exportEvidence.Add(evidenceTenantOption);
|
|
exportEvidence.Add(evidenceSubjectOption);
|
|
exportEvidence.Add(evidenceCompressOption);
|
|
exportEvidence.Add(evidenceJsonOption);
|
|
exportEvidence.Add(evidenceVerifyOption);
|
|
|
|
exportEvidence.SetAction((parseResult, _) =>
|
|
{
|
|
var output = parseResult.GetValue(evidenceOutputOption)!;
|
|
var include = parseResult.GetValue(evidenceIncludeOption) ?? Array.Empty<string>();
|
|
var from = parseResult.GetValue(evidenceFromOption);
|
|
var to = parseResult.GetValue(evidenceToOption);
|
|
var tenant = parseResult.GetValue(evidenceTenantOption);
|
|
var subject = parseResult.GetValue(evidenceSubjectOption);
|
|
var compress = parseResult.GetValue(evidenceCompressOption);
|
|
var json = parseResult.GetValue(evidenceJsonOption);
|
|
var verify = parseResult.GetValue(evidenceVerifyOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
return CommandHandlers.HandleAirgapExportEvidenceAsync(
|
|
services,
|
|
output,
|
|
include,
|
|
from,
|
|
to,
|
|
tenant,
|
|
subject,
|
|
compress,
|
|
json,
|
|
verify,
|
|
verbose,
|
|
cancellationToken);
|
|
});
|
|
|
|
airgap.Add(exportEvidence);
|
|
|
|
return airgap;
|
|
}
|
|
}
|