Files
git.stella-ops.org/src/Cli/StellaOps.Cli/Commands/CommandFactory.cs
2025-10-28 15:10:40 +02:00

995 lines
37 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.Plugins;
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 root = new RootCommand("StellaOps command-line interface")
{
TreatUnmatchedTokensAsErrors = true
};
root.Add(verboseOption);
root.Add(BuildScannerCommand(services, verboseOption, cancellationToken));
root.Add(BuildScanCommand(services, options, 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(BuildPolicyCommand(services, options, verboseOption, cancellationToken));
root.Add(BuildFindingsCommand(services, verboseOption, cancellationToken));
root.Add(BuildConfigCommand(options));
root.Add(BuildVulnCommand(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 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);
});
scan.Add(run);
scan.Add(upload);
return scan;
}
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 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);
auth.Add(login);
auth.Add(logout);
auth.Add(status);
auth.Add(whoami);
auth.Add(revoke);
return auth;
}
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 or json."
};
var outputOption = new Option<string?>("--output")
{
Description = "Write JSON 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."
};
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.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 verbose = parseResult.GetValue(verboseOption);
return CommandHandlers.HandlePolicySimulateAsync(
services,
policyId,
baseVersion,
candidateVersion,
sbomSet,
environment,
format,
output,
explain,
failOnDiff,
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."
};
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);
return policy;
}
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 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);
return vuln;
}
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..]}"
};
}
}