Files
git.stella-ops.org/src/Cli/StellaOps.Cli/Commands/CommandFactory.cs
StellaOps Bot 69651212ec
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
feat: Implement CVSS receipt management client and models
2025-12-07 01:14:28 +02:00

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;
}
}