// ----------------------------------------------------------------------------- // SignalsCommandGroup.cs // Sprint: SPRINT_20260117_006_CLI_reachability_analysis // Tasks: RCA-006 - Add stella signals inspect command // Description: CLI commands for runtime signal inspection // ----------------------------------------------------------------------------- using System.CommandLine; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace StellaOps.Cli.Commands; /// /// Command group for runtime signal inspection. /// Implements `stella signals inspect` for viewing collected runtime signals. /// public static class SignalsCommandGroup { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) { WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; /// /// Build the 'signals' command group. /// public static Command BuildSignalsCommand( IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var signalsCommand = new Command("signals", "Runtime signal inspection and analysis"); signalsCommand.Add(BuildInspectCommand(services, verboseOption, cancellationToken)); signalsCommand.Add(BuildListCommand(services, verboseOption, cancellationToken)); signalsCommand.Add(BuildSummaryCommand(services, verboseOption, cancellationToken)); return signalsCommand; } #region Inspect Command (RCA-006) /// /// Build the 'signals inspect' command. /// Sprint: SPRINT_20260117_006_CLI_reachability_analysis (RCA-006) /// private static Command BuildInspectCommand( IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var targetArg = new Argument("target") { Description = "Digest (sha256:...) or run ID (run-...) to inspect signals for" }; var typeOption = new Option("--type", "-t") { Description = "Filter by signal type: call, memory, network, file, process" }; var fromOption = new Option("--from") { Description = "Start time filter (ISO 8601)" }; var toOption = new Option("--to") { Description = "End time filter (ISO 8601)" }; var limitOption = new Option("--limit", "-n") { Description = "Maximum number of signals to show" }; limitOption.SetDefaultValue(100); var formatOption = new Option("--format", "-f") { Description = "Output format: table (default), json" }; formatOption.SetDefaultValue("table"); var inspectCommand = new Command("inspect", "Inspect runtime signals for a digest or run") { targetArg, typeOption, fromOption, toOption, limitOption, formatOption, verboseOption }; inspectCommand.SetAction((parseResult, ct) => { var target = parseResult.GetValue(targetArg) ?? string.Empty; var type = parseResult.GetValue(typeOption); var from = parseResult.GetValue(fromOption); var to = parseResult.GetValue(toOption); var limit = parseResult.GetValue(limitOption); var format = parseResult.GetValue(formatOption) ?? "table"; var verbose = parseResult.GetValue(verboseOption); var signals = GetSignals(target).Take(limit).ToList(); if (!string.IsNullOrEmpty(type)) { signals = signals.Where(s => s.Type.Equals(type, StringComparison.OrdinalIgnoreCase)).ToList(); } if (format.Equals("json", StringComparison.OrdinalIgnoreCase)) { Console.WriteLine(JsonSerializer.Serialize(signals, JsonOptions)); return Task.FromResult(0); } Console.WriteLine("Runtime Signals"); Console.WriteLine("==============="); Console.WriteLine(); Console.WriteLine($"Target: {target}"); Console.WriteLine(); Console.WriteLine($"{"Timestamp",-22} {"Type",-10} {"Source",-20} {"Details"}"); Console.WriteLine(new string('-', 90)); foreach (var signal in signals) { Console.WriteLine($"{signal.Timestamp:yyyy-MM-dd HH:mm:ss,-22} {signal.Type,-10} {signal.Source,-20} {signal.Details}"); } Console.WriteLine(); Console.WriteLine($"Total: {signals.Count} signals"); if (verbose) { Console.WriteLine(); Console.WriteLine("Signal Types:"); var grouped = signals.GroupBy(s => s.Type); foreach (var group in grouped) { Console.WriteLine($" {group.Key}: {group.Count()}"); } } return Task.FromResult(0); }); return inspectCommand; } #endregion #region List Command /// /// Build the 'signals list' command. /// private static Command BuildListCommand( IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var limitOption = new Option("--limit", "-n") { Description = "Maximum number of signal collections to show" }; limitOption.SetDefaultValue(20); var formatOption = new Option("--format", "-f") { Description = "Output format: table (default), json" }; formatOption.SetDefaultValue("table"); var listCommand = new Command("list", "List signal collections") { limitOption, formatOption, verboseOption }; listCommand.SetAction((parseResult, ct) => { var limit = parseResult.GetValue(limitOption); var format = parseResult.GetValue(formatOption) ?? "table"; var verbose = parseResult.GetValue(verboseOption); var collections = GetSignalCollections().Take(limit).ToList(); if (format.Equals("json", StringComparison.OrdinalIgnoreCase)) { Console.WriteLine(JsonSerializer.Serialize(collections, JsonOptions)); return Task.FromResult(0); } Console.WriteLine("Signal Collections"); Console.WriteLine("=================="); Console.WriteLine(); Console.WriteLine($"{"Target",-25} {"Signals",-10} {"First Seen",-12} {"Last Seen",-12}"); Console.WriteLine(new string('-', 70)); foreach (var collection in collections) { var shortTarget = collection.Target.Length > 23 ? collection.Target[..23] + "..." : collection.Target; Console.WriteLine($"{shortTarget,-25} {collection.SignalCount,-10} {collection.FirstSeen:yyyy-MM-dd,-12} {collection.LastSeen:yyyy-MM-dd,-12}"); } Console.WriteLine(); Console.WriteLine($"Total: {collections.Count} collections"); return Task.FromResult(0); }); return listCommand; } #endregion #region Summary Command /// /// Build the 'signals summary' command. /// private static Command BuildSummaryCommand( IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var targetArg = new Argument("target") { Description = "Digest or run ID" }; var formatOption = new Option("--format", "-f") { Description = "Output format: text (default), json" }; formatOption.SetDefaultValue("text"); var summaryCommand = new Command("summary", "Show signal summary for a target") { targetArg, formatOption, verboseOption }; summaryCommand.SetAction((parseResult, ct) => { var target = parseResult.GetValue(targetArg) ?? string.Empty; var format = parseResult.GetValue(formatOption) ?? "text"; var verbose = parseResult.GetValue(verboseOption); var summary = new SignalSummary { Target = target, TotalSignals = 147, SignalsByType = new Dictionary { ["call"] = 89, ["memory"] = 23, ["network"] = 18, ["file"] = 12, ["process"] = 5 }, FirstObserved = DateTimeOffset.UtcNow.AddDays(-7), LastObserved = DateTimeOffset.UtcNow.AddMinutes(-15), UniqueEntryPoints = 12, ReachableVulnerabilities = 3 }; if (format.Equals("json", StringComparison.OrdinalIgnoreCase)) { Console.WriteLine(JsonSerializer.Serialize(summary, JsonOptions)); return Task.FromResult(0); } Console.WriteLine("Signal Summary"); Console.WriteLine("=============="); Console.WriteLine(); Console.WriteLine($"Target: {target}"); Console.WriteLine($"Total Signals: {summary.TotalSignals}"); Console.WriteLine($"First Observed: {summary.FirstObserved:u}"); Console.WriteLine($"Last Observed: {summary.LastObserved:u}"); Console.WriteLine($"Unique Entry Points: {summary.UniqueEntryPoints}"); Console.WriteLine($"Reachable Vulns: {summary.ReachableVulnerabilities}"); Console.WriteLine(); Console.WriteLine("Signals by Type:"); foreach (var (type, count) in summary.SignalsByType) { var bar = new string('█', Math.Min(count / 5, 20)); Console.WriteLine($" {type,-10} {count,4} {bar}"); } return Task.FromResult(0); }); return summaryCommand; } #endregion #region Sample Data private static List GetSignals(string target) { var now = DateTimeOffset.UtcNow; return [ new RuntimeSignal { Timestamp = now.AddMinutes(-5), Type = "call", Source = "main.go:handleRequest", Details = "Called vulnerable function parseJSON" }, new RuntimeSignal { Timestamp = now.AddMinutes(-10), Type = "call", Source = "api.go:processInput", Details = "Entry point invoked" }, new RuntimeSignal { Timestamp = now.AddMinutes(-12), Type = "network", Source = "http:8080", Details = "Incoming request from 10.0.0.5" }, new RuntimeSignal { Timestamp = now.AddMinutes(-15), Type = "memory", Source = "heap:0x7fff", Details = "Allocation in vulnerable path" }, new RuntimeSignal { Timestamp = now.AddMinutes(-20), Type = "file", Source = "/etc/config", Details = "Config file read" }, new RuntimeSignal { Timestamp = now.AddMinutes(-25), Type = "process", Source = "worker:3", Details = "Process spawned for request handling" } ]; } private static List GetSignalCollections() { var now = DateTimeOffset.UtcNow; return [ new SignalCollection { Target = "sha256:abc123def456...", SignalCount = 147, FirstSeen = now.AddDays(-7), LastSeen = now.AddMinutes(-15) }, new SignalCollection { Target = "sha256:def456ghi789...", SignalCount = 89, FirstSeen = now.AddDays(-5), LastSeen = now.AddHours(-2) }, new SignalCollection { Target = "run-20260116-001", SignalCount = 234, FirstSeen = now.AddDays(-1), LastSeen = now.AddMinutes(-45) } ]; } #endregion #region DTOs private sealed class RuntimeSignal { public DateTimeOffset Timestamp { get; set; } public string Type { get; set; } = string.Empty; public string Source { get; set; } = string.Empty; public string Details { get; set; } = string.Empty; } private sealed class SignalCollection { public string Target { get; set; } = string.Empty; public int SignalCount { get; set; } public DateTimeOffset FirstSeen { get; set; } public DateTimeOffset LastSeen { get; set; } } private sealed class SignalSummary { public string Target { get; set; } = string.Empty; public int TotalSignals { get; set; } public Dictionary SignalsByType { get; set; } = []; public DateTimeOffset FirstObserved { get; set; } public DateTimeOffset LastObserved { get; set; } public int UniqueEntryPoints { get; set; } public int ReachableVulnerabilities { get; set; } } #endregion }