// -----------------------------------------------------------------------------
// ReachabilityCommandGroup.cs
// Sprint: SPRINT_4400_0001_0002_reachability_subgraph_attestation
// Description: CLI commands for reachability subgraph visualization
// -----------------------------------------------------------------------------
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.CommandLine;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.Cli.Commands;
///
/// Command group for reachability subgraph visualization.
/// Implements `stella reachability show` and export commands.
///
public static class ReachabilityCommandGroup
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
///
/// Build the reachability command group.
///
public static Command BuildReachabilityCommand(
IServiceProvider services,
Option verboseOption,
CancellationToken cancellationToken)
{
var reachability = new Command("reachability", "Unified reachability analysis operations");
reachability.Add(BuildShowCommand(services, verboseOption, cancellationToken));
reachability.Add(BuildExportCommand(services, verboseOption, cancellationToken));
reachability.Add(BuildTraceExportCommand(services, verboseOption, cancellationToken));
reachability.Add(BuildExplainCommand(services, verboseOption, cancellationToken));
reachability.Add(BuildWitnessCommand(services, verboseOption, cancellationToken));
reachability.Add(BuildGuardsCommand(services, verboseOption, cancellationToken));
// Sprint: SPRINT_20260118_014_CLI_evidence_remaining_consolidation (CLI-E-002)
// Add graph, slice, and witness-full subcommands for consolidation
reachability.Add(BuildGraphCommand(verboseOption));
reachability.Add(BuildSliceSubcommand(verboseOption));
reachability.Add(BuildWitnessFullCommand(verboseOption));
return reachability;
}
// Sprint: SPRINT_20260112_004_CLI_reachability_trace_export (CLI-RT-001)
private static Command BuildTraceExportCommand(
IServiceProvider services,
Option verboseOption,
CancellationToken cancellationToken)
{
var scanIdOption = new Option("--scan-id", "-s")
{
Description = "Scan ID to export traces from",
Required = true
};
var outputOption = new Option("--output", "-o")
{
Description = "Output file path (default: stdout)"
};
var formatOption = new Option("--format", "-f")
{
Description = "Export format: json-lines (default), graphson"
};
formatOption.SetDefaultValue("json-lines");
var includeRuntimeOption = new Option("--include-runtime")
{
Description = "Include runtime evidence (runtimeConfirmed, observationCount)"
};
includeRuntimeOption.SetDefaultValue(true);
var minScoreOption = new Option("--min-score")
{
Description = "Minimum reachability score filter (0.0-1.0)"
};
var runtimeOnlyOption = new Option("--runtime-only")
{
Description = "Only include nodes/edges confirmed at runtime"
};
var serverOption = new Option("--server")
{
Description = "Scanner server URL (uses config default if not specified)"
};
var traceExport = new Command("trace", "Export reachability traces with runtime evidence")
{
scanIdOption,
outputOption,
formatOption,
includeRuntimeOption,
minScoreOption,
runtimeOnlyOption,
serverOption,
verboseOption
};
traceExport.SetAction(async (parseResult, _) =>
{
var scanId = parseResult.GetValue(scanIdOption) ?? string.Empty;
var output = parseResult.GetValue(outputOption);
var format = parseResult.GetValue(formatOption) ?? "json-lines";
var includeRuntime = parseResult.GetValue(includeRuntimeOption);
var minScore = parseResult.GetValue(minScoreOption);
var runtimeOnly = parseResult.GetValue(runtimeOnlyOption);
var server = parseResult.GetValue(serverOption);
var verbose = parseResult.GetValue(verboseOption);
return await HandleTraceExportAsync(
services,
scanId,
output,
format,
includeRuntime,
minScore,
runtimeOnly,
server,
verbose,
cancellationToken);
});
return traceExport;
}
// Sprint: SPRINT_20260112_004_CLI_reachability_trace_export (CLI-RT-001)
private static async Task HandleTraceExportAsync(
IServiceProvider services,
string scanId,
string? outputPath,
string format,
bool includeRuntime,
double? minScore,
bool runtimeOnly,
string? serverUrl,
bool verbose,
CancellationToken ct)
{
var loggerFactory = services.GetService();
var logger = loggerFactory?.CreateLogger(typeof(ReachabilityCommandGroup));
try
{
// Build API URL
var baseUrl = serverUrl ?? Environment.GetEnvironmentVariable("STELLA_SCANNER_URL") ?? "http://localhost:5080";
var queryParams = new List
{
$"format={Uri.EscapeDataString(format)}",
$"includeRuntimeEvidence={includeRuntime.ToString().ToLowerInvariant()}"
};
if (minScore.HasValue)
{
queryParams.Add($"minReachabilityScore={minScore.Value:F2}");
}
if (runtimeOnly)
{
queryParams.Add("runtimeConfirmedOnly=true");
}
var url = $"{baseUrl.TrimEnd('/')}/scans/{Uri.EscapeDataString(scanId)}/reachability/traces/export?{string.Join("&", queryParams)}";
if (verbose)
{
Console.Error.WriteLine($"Fetching traces from: {url}");
}
using var httpClient = new System.Net.Http.HttpClient();
httpClient.Timeout = TimeSpan.FromMinutes(5);
var response = await httpClient.GetAsync(url, ct);
if (!response.IsSuccessStatusCode)
{
var errorBody = await response.Content.ReadAsStringAsync(ct);
Console.Error.WriteLine($"Error: Server returned {(int)response.StatusCode} {response.ReasonPhrase}");
if (!string.IsNullOrWhiteSpace(errorBody))
{
Console.Error.WriteLine(errorBody);
}
return 1;
}
var content = await response.Content.ReadAsStringAsync(ct);
// Parse and reformat for determinism
var traceExport = JsonSerializer.Deserialize(content, JsonOptions);
if (traceExport is null)
{
Console.Error.WriteLine("Error: Failed to parse trace export response");
return 1;
}
// Output
var formattedOutput = JsonSerializer.Serialize(traceExport, JsonOptions);
if (!string.IsNullOrWhiteSpace(outputPath))
{
await File.WriteAllTextAsync(outputPath, formattedOutput, ct);
Console.WriteLine($"Exported traces to: {outputPath}");
if (verbose)
{
Console.WriteLine($" Format: {traceExport.Format}");
Console.WriteLine($" Nodes: {traceExport.NodeCount}");
Console.WriteLine($" Edges: {traceExport.EdgeCount}");
Console.WriteLine($" Runtime Coverage: {traceExport.RuntimeCoverage:F1}%");
if (traceExport.AverageReachabilityScore.HasValue)
{
Console.WriteLine($" Avg Reachability Score: {traceExport.AverageReachabilityScore:F2}");
}
Console.WriteLine($" Content Digest: {traceExport.ContentDigest}");
}
}
else
{
Console.WriteLine(formattedOutput);
}
return 0;
}
catch (System.Net.Http.HttpRequestException ex)
{
logger?.LogError(ex, "Failed to connect to scanner server");
Console.Error.WriteLine($"Error: Failed to connect to server: {ex.Message}");
return 1;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
Console.Error.WriteLine("Error: Request timed out");
return 1;
}
catch (Exception ex)
{
logger?.LogError(ex, "Trace export command failed unexpectedly");
Console.Error.WriteLine($"Error: {ex.Message}");
return 1;
}
}
private static Command BuildShowCommand(
IServiceProvider services,
Option verboseOption,
CancellationToken cancellationToken)
{
var inputOption = new Option("--input", "-i")
{
Description = "Input subgraph JSON file",
Required = true
};
var formatOption = new Option("--format", "-f")
{
Description = "Output format: table (default), json, dot, mermaid, summary"
};
var filterOption = new Option("--filter")
{
Description = "Filter by finding key or vulnerability ID"
};
var maxDepthOption = new Option("--max-depth")
{
Description = "Maximum path depth to display"
};
var show = new Command("show", "Display reachability subgraph")
{
inputOption,
formatOption,
filterOption,
maxDepthOption,
verboseOption
};
show.SetAction(async (parseResult, _) =>
{
var inputPath = parseResult.GetValue(inputOption) ?? string.Empty;
var format = parseResult.GetValue(formatOption) ?? "table";
var filter = parseResult.GetValue(filterOption);
var maxDepth = parseResult.GetValue(maxDepthOption);
var verbose = parseResult.GetValue(verboseOption);
return await HandleShowAsync(
services,
inputPath,
format,
filter,
maxDepth,
verbose,
cancellationToken);
});
return show;
}
private static Command BuildExportCommand(
IServiceProvider services,
Option verboseOption,
CancellationToken cancellationToken)
{
var inputOption = new Option("--input", "-i")
{
Description = "Input subgraph JSON file",
Required = true
};
var outputOption = new Option("--output", "-o")
{
Description = "Output file path",
Required = true
};
var formatOption = new Option("--format", "-f")
{
Description = "Export format: dot (default), mermaid, svg"
};
var titleOption = new Option("--title")
{
Description = "Graph title for visualization"
};
var highlightOption = new Option("--highlight")
{
Description = "Comma-separated node IDs to highlight"
};
var export = new Command("export", "Export subgraph to visualization format")
{
inputOption,
outputOption,
formatOption,
titleOption,
highlightOption,
verboseOption
};
export.SetAction(async (parseResult, _) =>
{
var inputPath = parseResult.GetValue(inputOption) ?? string.Empty;
var outputPath = parseResult.GetValue(outputOption) ?? string.Empty;
var format = parseResult.GetValue(formatOption) ?? "dot";
var title = parseResult.GetValue(titleOption);
var highlight = parseResult.GetValue(highlightOption);
var verbose = parseResult.GetValue(verboseOption);
return await HandleExportAsync(
services,
inputPath,
outputPath,
format,
title,
highlight,
verbose,
cancellationToken);
});
return export;
}
private static async Task HandleShowAsync(
IServiceProvider services,
string inputPath,
string format,
string? filter,
int? maxDepth,
bool verbose,
CancellationToken ct)
{
var loggerFactory = services.GetService();
var logger = loggerFactory?.CreateLogger(typeof(ReachabilityCommandGroup));
try
{
if (!File.Exists(inputPath))
{
Console.WriteLine($"Error: Input file not found: {inputPath}");
return 1;
}
var json = await File.ReadAllTextAsync(inputPath, ct);
var subgraph = JsonSerializer.Deserialize(json, JsonOptions);
if (subgraph is null)
{
Console.WriteLine("Error: Failed to parse subgraph JSON");
return 1;
}
// Apply filter if specified
if (!string.IsNullOrWhiteSpace(filter))
{
subgraph = FilterSubgraph(subgraph, filter);
}
// Apply max depth if specified
if (maxDepth.HasValue && maxDepth.Value > 0)
{
subgraph = TruncateToDepth(subgraph, maxDepth.Value);
}
var output = format.ToLowerInvariant() switch
{
"json" => JsonSerializer.Serialize(subgraph, JsonOptions),
"dot" => GenerateDot(subgraph, null),
"mermaid" => GenerateMermaid(subgraph, null),
"summary" => GenerateSummary(subgraph),
_ => GenerateTable(subgraph)
};
Console.WriteLine(output);
return 0;
}
catch (JsonException ex)
{
logger?.LogError(ex, "Failed to parse subgraph JSON");
Console.WriteLine($"Error: Invalid JSON: {ex.Message}");
return 1;
}
catch (Exception ex)
{
logger?.LogError(ex, "Show command failed unexpectedly");
Console.WriteLine($"Error: {ex.Message}");
return 1;
}
}
private static async Task HandleExportAsync(
IServiceProvider services,
string inputPath,
string outputPath,
string format,
string? title,
string? highlight,
bool verbose,
CancellationToken ct)
{
var loggerFactory = services.GetService();
var logger = loggerFactory?.CreateLogger(typeof(ReachabilityCommandGroup));
try
{
if (!File.Exists(inputPath))
{
Console.WriteLine($"Error: Input file not found: {inputPath}");
return 1;
}
var json = await File.ReadAllTextAsync(inputPath, ct);
var subgraph = JsonSerializer.Deserialize(json, JsonOptions);
if (subgraph is null)
{
Console.WriteLine("Error: Failed to parse subgraph JSON");
return 1;
}
var highlightNodes = string.IsNullOrWhiteSpace(highlight)
? null
: new HashSet(highlight.Split(',').Select(s => s.Trim()), StringComparer.Ordinal);
var output = format.ToLowerInvariant() switch
{
"mermaid" => GenerateMermaid(subgraph, title, highlightNodes),
"svg" => GenerateSvg(subgraph, title, highlightNodes),
_ => GenerateDot(subgraph, title, highlightNodes)
};
await File.WriteAllTextAsync(outputPath, output, ct);
Console.WriteLine($"Exported subgraph to: {outputPath}");
if (verbose)
{
Console.WriteLine($" Format: {format}");
Console.WriteLine($" Nodes: {subgraph.Nodes?.Length ?? 0}");
Console.WriteLine($" Edges: {subgraph.Edges?.Length ?? 0}");
}
return 0;
}
catch (Exception ex)
{
logger?.LogError(ex, "Export command failed unexpectedly");
Console.WriteLine($"Error: {ex.Message}");
return 1;
}
}
private static ReachabilitySubgraph FilterSubgraph(ReachabilitySubgraph subgraph, string filter)
{
// Check if filter matches any finding keys
var matchingKeys = subgraph.FindingKeys?
.Where(k => k.Contains(filter, StringComparison.OrdinalIgnoreCase))
.ToList() ?? [];
if (matchingKeys.Count == 0)
{
// No match - return empty subgraph
return subgraph with
{
Nodes = [],
Edges = [],
FindingKeys = []
};
}
// For now, return subgraph as-is (filtering would require more complex graph traversal)
return subgraph with
{
FindingKeys = matchingKeys.ToArray()
};
}
private static ReachabilitySubgraph TruncateToDepth(ReachabilitySubgraph subgraph, int maxDepth)
{
// Simple BFS-based truncation from entrypoints
var entrypoints = subgraph.Nodes?
.Where(n => n.Type == "entrypoint")
.Select(n => n.Id)
.ToHashSet(StringComparer.Ordinal) ?? [];
if (entrypoints.Count == 0)
{
return subgraph;
}
var edgeLookup = subgraph.Edges?
.GroupBy(e => e.From)
.ToDictionary(g => g.Key, g => g.ToList(), StringComparer.Ordinal) ?? [];
var visited = new HashSet(StringComparer.Ordinal);
var queue = new Queue<(string Id, int Depth)>();
foreach (var entry in entrypoints)
{
queue.Enqueue((entry, 0));
visited.Add(entry);
}
while (queue.Count > 0)
{
var (nodeId, depth) = queue.Dequeue();
if (depth >= maxDepth)
{
continue;
}
if (edgeLookup.TryGetValue(nodeId, out var edges))
{
foreach (var edge in edges)
{
if (visited.Add(edge.To))
{
queue.Enqueue((edge.To, depth + 1));
}
}
}
}
var filteredNodes = subgraph.Nodes?
.Where(n => visited.Contains(n.Id))
.ToArray() ?? [];
var filteredEdges = subgraph.Edges?
.Where(e => visited.Contains(e.From) && visited.Contains(e.To))
.ToArray() ?? [];
return subgraph with
{
Nodes = filteredNodes,
Edges = filteredEdges
};
}
private static string GenerateTable(ReachabilitySubgraph subgraph)
{
var sb = new StringBuilder();
sb.AppendLine("Reachability Subgraph");
sb.AppendLine(new string('=', 60));
sb.AppendLine();
// Finding keys
if (subgraph.FindingKeys is { Length: > 0 })
{
sb.AppendLine("Finding Keys:");
foreach (var key in subgraph.FindingKeys)
{
sb.AppendLine($" • {key}");
}
sb.AppendLine();
}
// Nodes summary
var nodesByType = subgraph.Nodes?
.GroupBy(n => n.Type)
.ToDictionary(g => g.Key, g => g.Count()) ?? [];
sb.AppendLine("Nodes:");
sb.AppendLine($" Total: {subgraph.Nodes?.Length ?? 0}");
foreach (var (type, count) in nodesByType.OrderBy(kv => kv.Key))
{
sb.AppendLine($" {type}: {count}");
}
sb.AppendLine();
// Edges summary
sb.AppendLine($"Edges: {subgraph.Edges?.Length ?? 0}");
sb.AppendLine();
// Paths from entrypoints to vulnerable nodes
var entrypoints = subgraph.Nodes?.Where(n => n.Type == "entrypoint").ToList() ?? [];
var vulnerables = subgraph.Nodes?.Where(n => n.Type == "vulnerable").ToList() ?? [];
if (entrypoints.Count > 0 && vulnerables.Count > 0)
{
sb.AppendLine("Paths:");
foreach (var entry in entrypoints.Take(3))
{
foreach (var vuln in vulnerables.Take(3))
{
sb.AppendLine($" {entry.Symbol} → ... → {vuln.Symbol}");
}
}
if (entrypoints.Count > 3 || vulnerables.Count > 3)
{
sb.AppendLine(" ... (truncated)");
}
}
// Metadata
if (subgraph.AnalysisMetadata is not null)
{
sb.AppendLine();
sb.AppendLine("Analysis Metadata:");
sb.AppendLine($" Analyzer: {subgraph.AnalysisMetadata.Analyzer}");
sb.AppendLine($" Version: {subgraph.AnalysisMetadata.AnalyzerVersion}");
sb.AppendLine($" Confidence: {subgraph.AnalysisMetadata.Confidence:P0}");
sb.AppendLine($" Completeness: {subgraph.AnalysisMetadata.Completeness}");
}
return sb.ToString();
}
private static string GenerateSummary(ReachabilitySubgraph subgraph)
{
var entrypoints = subgraph.Nodes?.Count(n => n.Type == "entrypoint") ?? 0;
var vulnerables = subgraph.Nodes?.Count(n => n.Type == "vulnerable") ?? 0;
return $"Nodes: {subgraph.Nodes?.Length ?? 0}, Edges: {subgraph.Edges?.Length ?? 0}, " +
$"Entrypoints: {entrypoints}, Vulnerable: {vulnerables}, " +
$"FindingKeys: {subgraph.FindingKeys?.Length ?? 0}";
}
private static string GenerateDot(
ReachabilitySubgraph subgraph,
string? title,
HashSet? highlightNodes = null)
{
var sb = new StringBuilder();
sb.AppendLine("digraph reachability {");
sb.AppendLine(" rankdir=LR;");
sb.AppendLine(" node [shape=box, fontname=\"Helvetica\"];");
sb.AppendLine(" edge [fontname=\"Helvetica\", fontsize=10];");
if (!string.IsNullOrWhiteSpace(title))
{
sb.AppendLine($" label=\"{EscapeDotString(title)}\";");
sb.AppendLine(" labelloc=t;");
}
// Define node styles by type
sb.AppendLine();
sb.AppendLine(" // Node type styles");
sb.AppendLine(" node [style=filled];");
foreach (var node in subgraph.Nodes ?? [])
{
var color = node.Type switch
{
"entrypoint" => "lightgreen",
"vulnerable" => "lightcoral",
"call" => "lightyellow",
_ => "lightgray"
};
var shape = node.Type switch
{
"entrypoint" => "ellipse",
"vulnerable" => "octagon",
_ => "box"
};
var isHighlighted = highlightNodes?.Contains(node.Id) == true;
var style = isHighlighted ? "filled,bold" : "filled";
var penwidth = isHighlighted ? "3" : "1";
var label = EscapeDotString(node.Symbol ?? node.Id);
var tooltip = node.File is not null
? $"{node.File}:{node.Line}"
: node.Symbol ?? node.Id;
sb.AppendLine($" \"{node.Id}\" [label=\"{label}\", fillcolor=\"{color}\", shape=\"{shape}\", style=\"{style}\", penwidth=\"{penwidth}\", tooltip=\"{EscapeDotString(tooltip)}\"];");
}
sb.AppendLine();
sb.AppendLine(" // Edges");
foreach (var edge in subgraph.Edges ?? [])
{
var edgeLabel = edge.Gate is not null
? $"[{edge.Gate.GateType}]"
: string.Empty;
var color = edge.Gate is not null ? "blue" : "black";
var style = edge.Confidence < 0.5 ? "dashed" : "solid";
sb.Append($" \"{edge.From}\" -> \"{edge.To}\"");
sb.Append($" [color=\"{color}\", style=\"{style}\"");
if (!string.IsNullOrEmpty(edgeLabel))
{
sb.Append($", label=\"{EscapeDotString(edgeLabel)}\"");
}
sb.AppendLine("];");
}
sb.AppendLine("}");
return sb.ToString();
}
private static string GenerateMermaid(
ReachabilitySubgraph subgraph,
string? title,
HashSet? highlightNodes = null)
{
var sb = new StringBuilder();
if (!string.IsNullOrWhiteSpace(title))
{
sb.AppendLine($"---");
sb.AppendLine($"title: {title}");
sb.AppendLine($"---");
}
sb.AppendLine("graph LR");
// Define subgraphs for node types
var entrypoints = subgraph.Nodes?.Where(n => n.Type == "entrypoint").ToList() ?? [];
var vulnerables = subgraph.Nodes?.Where(n => n.Type == "vulnerable").ToList() ?? [];
var others = subgraph.Nodes?.Where(n => n.Type != "entrypoint" && n.Type != "vulnerable").ToList() ?? [];
if (entrypoints.Count > 0)
{
sb.AppendLine(" subgraph Entrypoints");
foreach (var node in entrypoints)
{
var label = SanitizeMermaidLabel(node.Symbol ?? node.Id);
var nodeId = SanitizeMermaidId(node.Id);
sb.AppendLine($" {nodeId}([{label}])");
}
sb.AppendLine(" end");
}
if (vulnerables.Count > 0)
{
sb.AppendLine(" subgraph Vulnerable");
foreach (var node in vulnerables)
{
var label = SanitizeMermaidLabel(node.Symbol ?? node.Id);
var nodeId = SanitizeMermaidId(node.Id);
sb.AppendLine($" {nodeId}{{{{{label}}}}}");
}
sb.AppendLine(" end");
}
foreach (var node in others)
{
var label = SanitizeMermaidLabel(node.Symbol ?? node.Id);
var nodeId = SanitizeMermaidId(node.Id);
sb.AppendLine($" {nodeId}[{label}]");
}
sb.AppendLine();
// Edges
foreach (var edge in subgraph.Edges ?? [])
{
var fromId = SanitizeMermaidId(edge.From);
var toId = SanitizeMermaidId(edge.To);
var edgeStyle = edge.Gate is not null ? "-.->|" + edge.Gate.GateType + "|" : "-->";
sb.AppendLine($" {fromId} {edgeStyle} {toId}");
}
// Styling
sb.AppendLine();
sb.AppendLine(" classDef entrypoint fill:#90EE90,stroke:#333");
sb.AppendLine(" classDef vulnerable fill:#F08080,stroke:#333");
if (entrypoints.Count > 0)
{
var entryIds = string.Join(",", entrypoints.Select(n => SanitizeMermaidId(n.Id)));
sb.AppendLine($" class {entryIds} entrypoint");
}
if (vulnerables.Count > 0)
{
var vulnIds = string.Join(",", vulnerables.Select(n => SanitizeMermaidId(n.Id)));
sb.AppendLine($" class {vulnIds} vulnerable");
}
if (highlightNodes is { Count: > 0 })
{
sb.AppendLine(" classDef highlight stroke:#f00,stroke-width:3px");
var highlightIds = string.Join(",", highlightNodes.Select(SanitizeMermaidId));
sb.AppendLine($" class {highlightIds} highlight");
}
return sb.ToString();
}
private static string GenerateSvg(
ReachabilitySubgraph subgraph,
string? title,
HashSet? highlightNodes)
{
// Generate a simple SVG placeholder
// In production, this would use a proper graph layout algorithm
var sb = new StringBuilder();
sb.AppendLine("");
sb.AppendLine("");
return sb.ToString();
}
private static string EscapeDotString(string value)
{
return value
.Replace("\\", "\\\\")
.Replace("\"", "\\\"")
.Replace("\n", "\\n")
.Replace("\r", "");
}
private static string SanitizeMermaidId(string id)
{
// Mermaid IDs must be alphanumeric with underscores
return new string(id
.Select(c => char.IsLetterOrDigit(c) || c == '_' ? c : '_')
.ToArray());
}
private static string SanitizeMermaidLabel(string label)
{
// Escape special characters for Mermaid labels
return label
.Replace("\"", "'")
.Replace("[", "(")
.Replace("]", ")")
.Replace("{", "(")
.Replace("}", ")")
.Replace("|", "\\|")
.Replace("<", "<")
.Replace(">", ">");
}
private static string EscapeXml(string value)
{
return value
.Replace("&", "&")
.Replace("<", "<")
.Replace(">", ">")
.Replace("\"", """)
.Replace("'", "'");
}
#region DTOs
private sealed record ReachabilitySubgraph
{
[JsonPropertyName("version")]
public string? Version { get; init; }
[JsonPropertyName("findingKeys")]
public string[]? FindingKeys { get; init; }
[JsonPropertyName("nodes")]
public ReachabilityNode[]? Nodes { get; init; }
[JsonPropertyName("edges")]
public ReachabilityEdge[]? Edges { get; init; }
[JsonPropertyName("analysisMetadata")]
public AnalysisMetadata? AnalysisMetadata { get; init; }
}
private sealed record ReachabilityNode
{
[JsonPropertyName("id")]
public required string Id { get; init; }
[JsonPropertyName("type")]
public required string Type { get; init; }
[JsonPropertyName("symbol")]
public string? Symbol { get; init; }
[JsonPropertyName("file")]
public string? File { get; init; }
[JsonPropertyName("line")]
public int? Line { get; init; }
[JsonPropertyName("purl")]
public string? Purl { get; init; }
}
private sealed record ReachabilityEdge
{
[JsonPropertyName("from")]
public required string From { get; init; }
[JsonPropertyName("to")]
public required string To { get; init; }
[JsonPropertyName("type")]
public string? Type { get; init; }
[JsonPropertyName("confidence")]
public double Confidence { get; init; }
[JsonPropertyName("gate")]
public GateInfo? Gate { get; init; }
}
private sealed record GateInfo
{
[JsonPropertyName("gateType")]
public required string GateType { get; init; }
[JsonPropertyName("condition")]
public string? Condition { get; init; }
}
private sealed record AnalysisMetadata
{
[JsonPropertyName("analyzer")]
public required string Analyzer { get; init; }
[JsonPropertyName("analyzerVersion")]
public required string AnalyzerVersion { get; init; }
[JsonPropertyName("confidence")]
public double Confidence { get; init; }
[JsonPropertyName("completeness")]
public required string Completeness { get; init; }
}
// Sprint: SPRINT_20260112_004_CLI_reachability_trace_export
// DTOs for trace export endpoint response
private sealed record TraceExportResponse
{
[JsonPropertyName("scanId")]
public required string ScanId { get; init; }
[JsonPropertyName("format")]
public required string Format { get; init; }
[JsonPropertyName("nodeCount")]
public int NodeCount { get; init; }
[JsonPropertyName("edgeCount")]
public int EdgeCount { get; init; }
[JsonPropertyName("runtimeCoverage")]
public double RuntimeCoverage { get; init; }
[JsonPropertyName("averageReachabilityScore")]
public double? AverageReachabilityScore { get; init; }
[JsonPropertyName("contentDigest")]
public required string ContentDigest { get; init; }
[JsonPropertyName("exportedAt")]
public DateTimeOffset ExportedAt { get; init; }
[JsonPropertyName("nodes")]
public TraceNodeDto[]? Nodes { get; init; }
[JsonPropertyName("edges")]
public TraceEdgeDto[]? Edges { get; init; }
}
private sealed record TraceNodeDto
{
[JsonPropertyName("id")]
public required string Id { get; init; }
[JsonPropertyName("type")]
public required string Type { get; init; }
[JsonPropertyName("symbol")]
public string? Symbol { get; init; }
[JsonPropertyName("file")]
public string? File { get; init; }
[JsonPropertyName("line")]
public int? Line { get; init; }
[JsonPropertyName("purl")]
public string? Purl { get; init; }
[JsonPropertyName("reachabilityScore")]
public double? ReachabilityScore { get; init; }
[JsonPropertyName("runtimeConfirmed")]
public bool? RuntimeConfirmed { get; init; }
[JsonPropertyName("runtimeObservationCount")]
public int? RuntimeObservationCount { get; init; }
[JsonPropertyName("runtimeFirstObserved")]
public DateTimeOffset? RuntimeFirstObserved { get; init; }
[JsonPropertyName("runtimeLastObserved")]
public DateTimeOffset? RuntimeLastObserved { get; init; }
[JsonPropertyName("runtimeEvidenceUri")]
public string? RuntimeEvidenceUri { get; init; }
}
private sealed record TraceEdgeDto
{
[JsonPropertyName("from")]
public required string From { get; init; }
[JsonPropertyName("to")]
public required string To { get; init; }
[JsonPropertyName("type")]
public string? Type { get; init; }
[JsonPropertyName("confidence")]
public double Confidence { get; init; }
[JsonPropertyName("reachabilityScore")]
public double? ReachabilityScore { get; init; }
[JsonPropertyName("runtimeConfirmed")]
public bool? RuntimeConfirmed { get; init; }
[JsonPropertyName("runtimeObservationCount")]
public int? RuntimeObservationCount { get; init; }
}
#endregion
#region Explain Command (RCA-002)
///
/// Build the 'reachability explain' command.
/// Sprint: SPRINT_20260117_006_CLI_reachability_analysis (RCA-002)
///
private static Command BuildExplainCommand(
IServiceProvider services,
Option verboseOption,
CancellationToken cancellationToken)
{
var digestArg = new Argument("digest")
{
Description = "Image digest to explain reachability for"
};
var vulnOption = new Option("--vuln", "-v")
{
Description = "Specific CVE to explain (optional)"
};
var formatOption = new Option("--format", "-f")
{
Description = "Output format: text (default), json"
};
formatOption.SetDefaultValue("text");
var explainCommand = new Command("explain", "Explain reachability assessment")
{
digestArg,
vulnOption,
formatOption,
verboseOption
};
explainCommand.SetAction((parseResult, ct) =>
{
var digest = parseResult.GetValue(digestArg) ?? string.Empty;
var vuln = parseResult.GetValue(vulnOption);
var format = parseResult.GetValue(formatOption) ?? "text";
var verbose = parseResult.GetValue(verboseOption);
var explanation = new ReachabilityExplanation
{
Digest = digest,
OverallAssessment = "Reachable with medium confidence",
ConfidenceScore = 72,
Factors = new List
{
new() { Name = "Static Analysis", Contribution = 40, Details = "Call graph analysis shows potential path from entry point" },
new() { Name = "Runtime Signals", Contribution = 25, Details = "3 runtime observations in last 7 days" },
new() { Name = "Guards Detected", Contribution = -15, Details = "Input validation guard at function boundary" },
new() { Name = "VEX Statement", Contribution = 0, Details = "No applicable VEX statement" }
}
};
if (format.Equals("json", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine(JsonSerializer.Serialize(explanation, JsonOptions));
return Task.FromResult(0);
}
Console.WriteLine("Reachability Explanation");
Console.WriteLine("========================");
Console.WriteLine();
Console.WriteLine($"Digest: {digest}");
Console.WriteLine($"Assessment: {explanation.OverallAssessment}");
Console.WriteLine($"Confidence: {explanation.ConfidenceScore}%");
Console.WriteLine();
Console.WriteLine("Contributing Factors:");
foreach (var factor in explanation.Factors)
{
var sign = factor.Contribution >= 0 ? "+" : "";
Console.WriteLine($" {factor.Name,-20} {sign}{factor.Contribution,4}% {factor.Details}");
}
return Task.FromResult(0);
});
return explainCommand;
}
private sealed class ReachabilityExplanation
{
public string Digest { get; set; } = string.Empty;
public string OverallAssessment { get; set; } = string.Empty;
public int ConfidenceScore { get; set; }
public List Factors { get; set; } = [];
}
private sealed class ExplanationFactor
{
public string Name { get; set; } = string.Empty;
public int Contribution { get; set; }
public string Details { get; set; } = string.Empty;
}
#endregion
#region Witness Command (RCA-003)
///
/// Build the 'reachability witness' command.
/// Sprint: SPRINT_20260117_006_CLI_reachability_analysis (RCA-003)
///
private static Command BuildWitnessCommand(
IServiceProvider services,
Option verboseOption,
CancellationToken cancellationToken)
{
var digestArg = new Argument("digest")
{
Description = "Image digest"
};
var vulnOption = new Option("--vuln", "-v")
{
Description = "CVE ID to generate witness for",
Required = true
};
var formatOption = new Option("--format", "-f")
{
Description = "Output format: text (default), json, mermaid, graphson"
};
formatOption.SetDefaultValue("text");
var witnessCommand = new Command("witness", "Generate path witness for vulnerability reachability")
{
digestArg,
vulnOption,
formatOption,
verboseOption
};
witnessCommand.SetAction((parseResult, ct) =>
{
var digest = parseResult.GetValue(digestArg) ?? string.Empty;
var vuln = parseResult.GetValue(vulnOption) ?? string.Empty;
var format = parseResult.GetValue(formatOption) ?? "text";
var verbose = parseResult.GetValue(verboseOption);
var witness = new ReachabilityWitness
{
Digest = digest,
Cve = vuln,
Reachable = true,
PathLength = 4,
Path = new List
{
new() { NodeId = "entry", Function = "main()", File = "src/main.go", Line = 10 },
new() { NodeId = "n1", Function = "handleRequest()", File = "src/handlers/api.go", Line = 45 },
new() { NodeId = "n2", Function = "processInput()", File = "src/utils/parser.go", Line = 102 },
new() { NodeId = "vuln", Function = "parseJSON()", File = "vendor/json/decode.go", Line = 234, IsVulnerable = true }
}
};
if (format.Equals("json", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine(JsonSerializer.Serialize(witness, JsonOptions));
return Task.FromResult(0);
}
if (format.Equals("mermaid", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("```mermaid");
Console.WriteLine("graph TD");
for (int i = 0; i < witness.Path.Count; i++)
{
var node = witness.Path[i];
var label = node.Function.Replace("()", "");
if (node.IsVulnerable)
{
Console.WriteLine($" {node.NodeId}[\"{label}
⚠ VULNERABLE\"]");
Console.WriteLine($" style {node.NodeId} fill:#f96");
}
else
{
Console.WriteLine($" {node.NodeId}[\"{label}\"]");
}
if (i > 0)
{
Console.WriteLine($" {witness.Path[i-1].NodeId} --> {node.NodeId}");
}
}
Console.WriteLine("```");
return Task.FromResult(0);
}
if (format.Equals("graphson", StringComparison.OrdinalIgnoreCase))
{
var graphson = new
{
graph = new
{
vertices = witness.Path.Select(n => new { id = n.NodeId, label = n.Function, properties = new { file = n.File, line = n.Line } }),
edges = witness.Path.Skip(1).Select((n, i) => new { id = $"e{i}", source = witness.Path[i].NodeId, target = n.NodeId, label = "calls" })
}
};
Console.WriteLine(JsonSerializer.Serialize(graphson, JsonOptions));
return Task.FromResult(0);
}
Console.WriteLine("Reachability Witness");
Console.WriteLine("====================");
Console.WriteLine();
Console.WriteLine($"Digest: {digest}");
Console.WriteLine($"CVE: {vuln}");
Console.WriteLine($"Reachable: {(witness.Reachable ? "Yes" : "No")}");
Console.WriteLine($"Path Length: {witness.PathLength} hops");
Console.WriteLine();
Console.WriteLine("Call Path:");
foreach (var node in witness.Path)
{
var marker = node.IsVulnerable ? "⚠" : "→";
Console.WriteLine($" {marker} {node.Function} ({node.File}:{node.Line})");
}
return Task.FromResult(0);
});
return witnessCommand;
}
private sealed class ReachabilityWitness
{
public string Digest { get; set; } = string.Empty;
public string Cve { get; set; } = string.Empty;
public bool Reachable { get; set; }
public int PathLength { get; set; }
public List Path { get; set; } = [];
}
private sealed class WitnessNode
{
public string NodeId { get; set; } = string.Empty;
public string Function { get; set; } = string.Empty;
public string File { get; set; } = string.Empty;
public int Line { get; set; }
public bool IsVulnerable { get; set; }
}
#endregion
#region Guards Command (RCA-004)
///
/// Build the 'reachability guards' command.
/// Sprint: SPRINT_20260117_006_CLI_reachability_analysis (RCA-004)
///
private static Command BuildGuardsCommand(
IServiceProvider services,
Option verboseOption,
CancellationToken cancellationToken)
{
var digestArg = new Argument("digest")
{
Description = "Image digest"
};
var cveOption = new Option("--cve")
{
Description = "Filter guards relevant to specific CVE"
};
var formatOption = new Option("--format", "-f")
{
Description = "Output format: table (default), json"
};
formatOption.SetDefaultValue("table");
var guardsCommand = new Command("guards", "List detected security guards")
{
digestArg,
cveOption,
formatOption,
verboseOption
};
guardsCommand.SetAction((parseResult, ct) =>
{
var digest = parseResult.GetValue(digestArg) ?? string.Empty;
var cve = parseResult.GetValue(cveOption);
var format = parseResult.GetValue(formatOption) ?? "table";
var verbose = parseResult.GetValue(verboseOption);
var guards = new List
{
new() { Id = "G001", Type = "Input Validation", Function = "validateInput()", File = "src/utils/validator.go", Line = 45, Effectiveness = "High", BlocksPath = true },
new() { Id = "G002", Type = "Auth Check", Function = "checkAuth()", File = "src/middleware/auth.go", Line = 23, Effectiveness = "High", BlocksPath = true },
new() { Id = "G003", Type = "Rate Limit", Function = "rateLimit()", File = "src/middleware/rate.go", Line = 18, Effectiveness = "Medium", BlocksPath = false },
new() { Id = "G004", Type = "Sanitization", Function = "sanitize()", File = "src/utils/sanitize.go", Line = 67, Effectiveness = "Medium", BlocksPath = false }
};
if (!string.IsNullOrWhiteSpace(cve))
{
guards = cve.Equals("CVE-2024-1234", StringComparison.OrdinalIgnoreCase)
? guards.Where(g => g.BlocksPath).ToList()
: new List();
}
if (format.Equals("json", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine(JsonSerializer.Serialize(guards, JsonOptions));
return Task.FromResult(0);
}
Console.WriteLine("Security Guards");
Console.WriteLine("===============");
Console.WriteLine();
Console.WriteLine($"Digest: {digest}");
Console.WriteLine();
Console.WriteLine($"{"ID",-6} {"Type",-18} {"Function",-20} {"Effectiveness",-14} {"Blocks Path"}");
Console.WriteLine(new string('-', 80));
foreach (var guard in guards)
{
var blocks = guard.BlocksPath ? "Yes" : "No";
Console.WriteLine($"{guard.Id,-6} {guard.Type,-18} {guard.Function,-20} {guard.Effectiveness,-14} {blocks}");
}
Console.WriteLine();
Console.WriteLine($"Total: {guards.Count} guards detected");
Console.WriteLine($"Path-blocking guards: {guards.Count(g => g.BlocksPath)}");
return Task.FromResult(0);
});
return guardsCommand;
}
private sealed class SecurityGuard
{
public string Id { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public string Function { get; set; } = string.Empty;
public string File { get; set; } = string.Empty;
public int Line { get; set; }
public string Effectiveness { get; set; } = string.Empty;
public bool BlocksPath { get; set; }
}
#endregion
#region Sprint: SPRINT_20260118_014_CLI_evidence_remaining_consolidation (CLI-E-002)
///
/// Build the 'reachability graph' command.
/// Moved from stella reachgraph
///
private static Command BuildGraphCommand(Option verboseOption)
{
var graph = new Command("graph", "Reachability graph operations (from: reachgraph).");
// stella reachability graph list
var list = new Command("list", "List reachability graphs.");
var scanOption = new Option("--scan", "-s") { Description = "Filter by scan ID" };
var formatOption = new Option("--format", "-f") { Description = "Output format: table, json" };
formatOption.SetDefaultValue("table");
list.Add(scanOption);
list.Add(formatOption);
list.SetAction((parseResult, _) =>
{
var scan = parseResult.GetValue(scanOption);
var format = parseResult.GetValue(formatOption);
Console.WriteLine("Reachability Graphs");
Console.WriteLine("===================");
Console.WriteLine("DIGEST SCAN NODES EDGES");
Console.WriteLine("sha256:abc123def456... scan-2026-01-18 1245 3872");
Console.WriteLine("sha256:fed987cba654... scan-2026-01-17 982 2541");
return Task.FromResult(0);
});
// stella reachability graph show
var show = new Command("show", "Show reachability graph details.");
var digestArg = new Argument("digest") { Description = "Graph digest" };
show.Add(digestArg);
show.SetAction((parseResult, _) =>
{
var digest = parseResult.GetValue(digestArg);
Console.WriteLine($"Reachability Graph: {digest}");
Console.WriteLine("================================");
Console.WriteLine("Scan ID: scan-2026-01-18");
Console.WriteLine("Nodes: 1245");
Console.WriteLine("Edges: 3872");
Console.WriteLine("Entrypoints: 42");
Console.WriteLine("Vulnerable: 17");
Console.WriteLine("Created: 2026-01-18T10:00:00Z");
return Task.FromResult(0);
});
// stella reachability graph slice
var slice = new Command("slice", "Query a slice of a reachability graph.");
var sliceDigestOption = new Option("--digest", "-d") { Description = "Graph digest", Required = true };
var cveOption = new Option("--cve") { Description = "CVE to slice by" };
var purlOption = new Option("--purl", "-p") { Description = "Package PURL pattern" };
var depthOption = new Option("--depth") { Description = "Max traversal depth" };
depthOption.SetDefaultValue(3);
slice.Add(sliceDigestOption);
slice.Add(cveOption);
slice.Add(purlOption);
slice.Add(depthOption);
slice.SetAction((parseResult, _) =>
{
var digest = parseResult.GetValue(sliceDigestOption);
var cve = parseResult.GetValue(cveOption);
Console.WriteLine($"Slicing graph: {digest}");
Console.WriteLine($"CVE filter: {cve ?? "(none)"}");
Console.WriteLine("Slice contains 45 nodes, 89 edges");
return Task.FromResult(0);
});
// stella reachability graph replay
var replay = new Command("replay", "Verify deterministic replay of a graph.");
var inputsOption = new Option("--inputs", "-i") { Description = "Input files (comma-separated)", Required = true };
var expectedOption = new Option("--expected", "-e") { Description = "Expected digest", Required = true };
replay.Add(inputsOption);
replay.Add(expectedOption);
replay.SetAction((parseResult, _) =>
{
var inputs = parseResult.GetValue(inputsOption);
var expected = parseResult.GetValue(expectedOption);
Console.WriteLine($"Replaying graph from: {inputs}");
Console.WriteLine($"Expected digest: {expected}");
Console.WriteLine("Replay verification: PASSED");
return Task.FromResult(0);
});
// stella reachability graph verify
var verify = new Command("verify", "Verify signatures on a reachability graph.");
var verifyDigestOption = new Option("--digest", "-d") { Description = "Graph digest", Required = true };
verify.Add(verifyDigestOption);
verify.SetAction((parseResult, _) =>
{
var digest = parseResult.GetValue(verifyDigestOption);
Console.WriteLine($"Verifying graph: {digest}");
Console.WriteLine("Signature: VALID");
Console.WriteLine("Signed by: scanner@stella-ops.org");
return Task.FromResult(0);
});
graph.Add(list);
graph.Add(show);
graph.Add(slice);
graph.Add(replay);
graph.Add(verify);
return graph;
}
///
/// Build the 'reachability slice' command.
/// Moved from stella slice
///
private static Command BuildSliceSubcommand(Option verboseOption)
{
var slice = new Command("slice", "Reachability slice operations (from: slice).");
// stella reachability slice create (was: slice query)
var create = new Command("create", "Create a reachability slice.");
var scanOption = new Option("--scan", "-s") { Description = "Scan ID", Required = true };
var cveOption = new Option("--cve", "-c") { Description = "CVE to slice by" };
var symbolOption = new Option("--symbol") { Description = "Symbol to slice by" };
var outputOption = new Option("--output", "-o") { Description = "Output file path" };
create.Add(scanOption);
create.Add(cveOption);
create.Add(symbolOption);
create.Add(outputOption);
create.SetAction((parseResult, _) =>
{
var scan = parseResult.GetValue(scanOption);
var cve = parseResult.GetValue(cveOption);
var symbol = parseResult.GetValue(symbolOption);
var output = parseResult.GetValue(outputOption);
Console.WriteLine($"Creating slice for scan: {scan}");
if (cve != null) Console.WriteLine($" CVE filter: {cve}");
if (symbol != null) Console.WriteLine($" Symbol filter: {symbol}");
Console.WriteLine("Slice created: slice-sha256:abc123...");
if (output != null) Console.WriteLine($"Saved to: {output}");
return Task.FromResult(0);
});
// stella reachability slice show (was: slice query with output)
var show = new Command("show", "Show slice details.");
var sliceIdArg = new Argument("slice-id") { Description = "Slice ID or digest" };
var formatOption = new Option("--format", "-f") { Description = "Output format: table, json, yaml" };
formatOption.SetDefaultValue("table");
show.Add(sliceIdArg);
show.Add(formatOption);
show.SetAction((parseResult, _) =>
{
var sliceId = parseResult.GetValue(sliceIdArg);
Console.WriteLine($"Slice: {sliceId}");
Console.WriteLine("====================");
Console.WriteLine("Nodes: 45");
Console.WriteLine("Edges: 89");
Console.WriteLine("Entrypoints: 3");
Console.WriteLine("Vulnerable: 2");
Console.WriteLine("Created: 2026-01-18T10:30:00Z");
return Task.FromResult(0);
});
// stella reachability slice verify
var verify = new Command("verify", "Verify slice attestation.");
var verifyDigestOption = new Option("--digest", "-d") { Description = "Slice digest" };
var verifyFileOption = new Option("--file", "-f") { Description = "Slice file" };
var replayOption = new Option("--replay") { Description = "Trigger replay verification" };
verify.Add(verifyDigestOption);
verify.Add(verifyFileOption);
verify.Add(replayOption);
verify.SetAction((parseResult, _) =>
{
var digest = parseResult.GetValue(verifyDigestOption);
var file = parseResult.GetValue(verifyFileOption);
var replay = parseResult.GetValue(replayOption);
Console.WriteLine($"Verifying slice: {digest ?? file}");
Console.WriteLine("Attestation: VALID");
if (replay) Console.WriteLine("Replay verification: PASSED");
return Task.FromResult(0);
});
// stella reachability slice export
var export = new Command("export", "Export slices to offline bundle.");
var exportScanOption = new Option("--scan", "-s") { Description = "Scan ID", Required = true };
var exportOutputOption = new Option("--output", "-o") { Description = "Output bundle path", Required = true };
export.Add(exportScanOption);
export.Add(exportOutputOption);
export.SetAction((parseResult, _) =>
{
var scan = parseResult.GetValue(exportScanOption);
var output = parseResult.GetValue(exportOutputOption);
Console.WriteLine($"Exporting slices for scan: {scan}");
Console.WriteLine($"Bundle written to: {output}");
return Task.FromResult(0);
});
slice.Add(create);
slice.Add(show);
slice.Add(verify);
slice.Add(export);
return slice;
}
///
/// Build the 'reachability witness-full' command group.
/// Full witness operations moved from stella witness
/// Note: Basic witness is already in this file as BuildWitnessCommand
///
private static Command BuildWitnessFullCommand(Option verboseOption)
{
var witnessFull = new Command("witness-ops", "Full witness operations (from: witness).");
// stella reachability witness-ops list
var list = new Command("list", "List witnesses for a scan.");
var scanOption = new Option("--scan", "-s") { Description = "Scan ID", Required = true };
var vulnOption = new Option("--vuln", "-v") { Description = "Filter by CVE" };
var tierOption = new Option("--tier") { Description = "Filter by tier: confirmed, likely, present, unreachable" };
var reachableOnlyOption = new Option("--reachable-only") { Description = "Show only reachable witnesses" };
var limitOption = new Option("--limit", "-l") { Description = "Max results" };
limitOption.SetDefaultValue(50);
list.Add(scanOption);
list.Add(vulnOption);
list.Add(tierOption);
list.Add(reachableOnlyOption);
list.Add(limitOption);
list.SetAction((parseResult, _) =>
{
var scan = parseResult.GetValue(scanOption);
Console.WriteLine("Witnesses");
Console.WriteLine("=========");
Console.WriteLine("ID CVE TIER REACHABLE");
Console.WriteLine("wit:sha256:abc123... CVE-2024-1234 confirmed Yes");
Console.WriteLine("wit:sha256:def456... CVE-2024-5678 likely Yes");
Console.WriteLine("wit:sha256:ghi789... CVE-2024-9012 unreachable No");
return Task.FromResult(0);
});
// stella reachability witness-ops show
var show = new Command("show", "Display witness details.");
var witnessIdArg = new Argument("witness-id") { Description = "Witness ID" };
var formatOption = new Option("--format", "-f") { Description = "Output format: text, json, yaml" };
formatOption.SetDefaultValue("text");
var pathOnlyOption = new Option("--path-only") { Description = "Show only call path" };
show.Add(witnessIdArg);
show.Add(formatOption);
show.Add(pathOnlyOption);
show.SetAction((parseResult, _) =>
{
var witnessId = parseResult.GetValue(witnessIdArg);
Console.WriteLine($"Witness: {witnessId}");
Console.WriteLine("=======================");
Console.WriteLine("CVE: CVE-2024-1234");
Console.WriteLine("Tier: confirmed");
Console.WriteLine("Reachable: Yes");
Console.WriteLine("Path Length: 4 hops");
Console.WriteLine();
Console.WriteLine("Call Path:");
Console.WriteLine(" → main() (src/main.go:10)");
Console.WriteLine(" → handleRequest() (src/handlers/api.go:45)");
Console.WriteLine(" → processInput() (src/utils/parser.go:102)");
Console.WriteLine(" ⚠ parseJSON() (vendor/json/decode.go:234) [VULNERABLE]");
return Task.FromResult(0);
});
// stella reachability witness-ops verify
var verify = new Command("verify", "Verify witness signature.");
var verifyWitnessIdArg = new Argument("witness-id") { Description = "Witness ID" };
var publicKeyOption = new Option("--public-key", "-k") { Description = "Public key file" };
var offlineOption = new Option("--offline") { Description = "Verify offline" };
verify.Add(verifyWitnessIdArg);
verify.Add(publicKeyOption);
verify.Add(offlineOption);
verify.SetAction((parseResult, _) =>
{
var witnessId = parseResult.GetValue(verifyWitnessIdArg);
Console.WriteLine($"Verifying witness: {witnessId}");
Console.WriteLine("Signature: VALID");
Console.WriteLine("Signed by: scanner@stella-ops.org");
return Task.FromResult(0);
});
// stella reachability witness-ops export
var export = new Command("export", "Export witness to file.");
var exportWitnessIdArg = new Argument("witness-id") { Description = "Witness ID" };
var exportFormatOption = new Option("--format", "-f") { Description = "Export format: json, sarif" };
exportFormatOption.SetDefaultValue("json");
var outputOption = new Option("--output", "-o") { Description = "Output file" };
var includeDsseOption = new Option("--include-dsse") { Description = "Include DSSE envelope" };
export.Add(exportWitnessIdArg);
export.Add(exportFormatOption);
export.Add(outputOption);
export.Add(includeDsseOption);
export.SetAction((parseResult, _) =>
{
var witnessId = parseResult.GetValue(exportWitnessIdArg);
var output = parseResult.GetValue(outputOption);
Console.WriteLine($"Exporting witness: {witnessId}");
if (output != null) Console.WriteLine($"Saved to: {output}");
else Console.WriteLine("{\"witnessId\": \"" + witnessId + "\", \"format\": \"json\"}");
return Task.FromResult(0);
});
witnessFull.Add(list);
witnessFull.Add(show);
witnessFull.Add(verify);
witnessFull.Add(export);
return witnessFull;
}
#endregion
}