todays product advirories implemented
This commit is contained in:
366
src/Cli/StellaOps.Cli/Commands/SignalsCommandGroup.cs
Normal file
366
src/Cli/StellaOps.Cli/Commands/SignalsCommandGroup.cs
Normal file
@@ -0,0 +1,366 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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;
|
||||
|
||||
/// <summary>
|
||||
/// Command group for runtime signal inspection.
|
||||
/// Implements `stella signals inspect` for viewing collected runtime signals.
|
||||
/// </summary>
|
||||
public static class SignalsCommandGroup
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Build the 'signals' command group.
|
||||
/// </summary>
|
||||
public static Command BuildSignalsCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> 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)
|
||||
|
||||
/// <summary>
|
||||
/// Build the 'signals inspect' command.
|
||||
/// Sprint: SPRINT_20260117_006_CLI_reachability_analysis (RCA-006)
|
||||
/// </summary>
|
||||
private static Command BuildInspectCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var targetArg = new Argument<string>("target")
|
||||
{
|
||||
Description = "Digest (sha256:...) or run ID (run-...) to inspect signals for"
|
||||
};
|
||||
|
||||
var typeOption = new Option<string?>("--type", "-t")
|
||||
{
|
||||
Description = "Filter by signal type: call, memory, network, file, process"
|
||||
};
|
||||
|
||||
var fromOption = new Option<string?>("--from")
|
||||
{
|
||||
Description = "Start time filter (ISO 8601)"
|
||||
};
|
||||
|
||||
var toOption = new Option<string?>("--to")
|
||||
{
|
||||
Description = "End time filter (ISO 8601)"
|
||||
};
|
||||
|
||||
var limitOption = new Option<int>("--limit", "-n")
|
||||
{
|
||||
Description = "Maximum number of signals to show"
|
||||
};
|
||||
limitOption.SetDefaultValue(100);
|
||||
|
||||
var formatOption = new Option<string>("--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
|
||||
|
||||
/// <summary>
|
||||
/// Build the 'signals list' command.
|
||||
/// </summary>
|
||||
private static Command BuildListCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var limitOption = new Option<int>("--limit", "-n")
|
||||
{
|
||||
Description = "Maximum number of signal collections to show"
|
||||
};
|
||||
limitOption.SetDefaultValue(20);
|
||||
|
||||
var formatOption = new Option<string>("--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
|
||||
|
||||
/// <summary>
|
||||
/// Build the 'signals summary' command.
|
||||
/// </summary>
|
||||
private static Command BuildSummaryCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var targetArg = new Argument<string>("target")
|
||||
{
|
||||
Description = "Digest or run ID"
|
||||
};
|
||||
|
||||
var formatOption = new Option<string>("--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<string, int>
|
||||
{
|
||||
["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<RuntimeSignal> 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<SignalCollection> 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<string, int> SignalsByType { get; set; } = [];
|
||||
public DateTimeOffset FirstObserved { get; set; }
|
||||
public DateTimeOffset LastObserved { get; set; }
|
||||
public int UniqueEntryPoints { get; set; }
|
||||
public int ReachableVulnerabilities { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user