// Licensed to StellaOps under the AGPL-3.0-or-later license. using System.CommandLine; namespace StellaOps.Cli.Commands.ReachGraph; /// /// CLI command group for reachability graph operations. /// stella reachgraph [slice|replay|verify] /// public static class ReachGraphCommandGroup { public static Command Build() { var command = new Command("reachgraph", "Reachability graph operations"); command.Add(BuildSliceCommand()); command.Add(BuildReplayCommand()); command.Add(BuildVerifyCommand()); return command; } private static Command BuildSliceCommand() { var digestOption = new Option("--digest", "-d") { Description = "BLAKE3 digest of the graph", Required = true }; var cveOption = new Option("--cve") { Description = "CVE identifier to slice by" }; var purlOption = new Option("--purl", "-p") { Description = "Package PURL pattern to slice by" }; var entrypointOption = new Option("--entrypoint", "-e") { Description = "Entrypoint path or symbol pattern" }; var fileOption = new Option("--file", "-f") { Description = "File path pattern (glob) to slice by" }; var depthOption = new Option("--depth") { Description = "Max traversal depth" }.SetDefaultValue(3); var outputOption = new Option("--output", "-o") { Description = "Output format: json, table, or dot (GraphViz)" }.SetDefaultValue("table"); var apiUrlOption = new Option("--api-url") { Description = "ReachGraph Store API URL" }.SetDefaultValue("http://localhost:5000"); var command = new Command("slice", "Query a slice of a reachability graph") { digestOption, cveOption, purlOption, entrypointOption, fileOption, depthOption, outputOption, apiUrlOption }; command.SetAction(async (parseResult, ct) => { var digest = parseResult.GetValue(digestOption) ?? string.Empty; var cve = parseResult.GetValue(cveOption); var purl = parseResult.GetValue(purlOption); var entrypoint = parseResult.GetValue(entrypointOption); var file = parseResult.GetValue(fileOption); var depth = parseResult.GetValue(depthOption); var output = parseResult.GetValue(outputOption) ?? "table"; var apiUrl = parseResult.GetValue(apiUrlOption) ?? "http://localhost:5000"; await ReachGraphCommandHandlers.HandleSliceAsync( digest, cve, purl, entrypoint, file, depth, output, apiUrl); }); return command; } private static Command BuildReplayCommand() { var inputsOption = new Option("--inputs", "-i") { Description = "Comma-separated input files (sbom.json,vex.json,callgraph.json)", Required = true }; var expectedOption = new Option("--expected", "-e") { Description = "Expected BLAKE3 digest", Required = true }; var outputFileOption = new Option("--output-file", "-o") { Description = "Write computed graph to file" }; var verboseOption = new Option("--verbose", "-v") { Description = "Show verbose output" }; var apiUrlOption = new Option("--api-url") { Description = "ReachGraph Store API URL" }.SetDefaultValue("http://localhost:5000"); var command = new Command("replay", "Verify deterministic replay of a graph") { inputsOption, expectedOption, outputFileOption, verboseOption, apiUrlOption }; command.SetAction(async (parseResult, ct) => { var inputs = parseResult.GetValue(inputsOption) ?? string.Empty; var expected = parseResult.GetValue(expectedOption) ?? string.Empty; var outputFile = parseResult.GetValue(outputFileOption); var verbose = parseResult.GetValue(verboseOption); var apiUrl = parseResult.GetValue(apiUrlOption) ?? "http://localhost:5000"; await ReachGraphCommandHandlers.HandleReplayAsync( inputs, expected, outputFile, verbose, apiUrl); }); return command; } private static Command BuildVerifyCommand() { var digestOption = new Option("--digest", "-d") { Description = "BLAKE3 digest of the graph to verify", Required = true }; var apiUrlOption = new Option("--api-url") { Description = "ReachGraph Store API URL" }.SetDefaultValue("http://localhost:5000"); var command = new Command("verify", "Verify signatures on a reachability graph") { digestOption, apiUrlOption }; command.SetAction(async (parseResult, ct) => { var digest = parseResult.GetValue(digestOption) ?? string.Empty; var apiUrl = parseResult.GetValue(apiUrlOption) ?? "http://localhost:5000"; await ReachGraphCommandHandlers.HandleVerifyAsync(digest, apiUrl); }); return command; } }