Restructure solution layout by module
This commit is contained in:
994
src/Cli/StellaOps.Cli/Commands/CommandFactory.cs
Normal file
994
src/Cli/StellaOps.Cli/Commands/CommandFactory.cs
Normal file
@@ -0,0 +1,994 @@
|
||||
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..]}"
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user