// ----------------------------------------------------------------------------- // DeltaSigCliCommands.cs // Sprint: SPRINT_20260119_004_BinaryIndex_deltasig_extensions // Task: DSIG-006 - CLI Updates // Description: CLI commands for DeltaSig v2 predicate operations. // Uses System.CommandLine 2.0.1 API with SetAction pattern. // ----------------------------------------------------------------------------- using System.CommandLine; using System.Text.Json; using System.Text.Json.Serialization; using StellaOps.BinaryIndex.DeltaSig.Attestation; namespace StellaOps.Cli.Plugins.DeltaSig; /// /// CLI commands for DeltaSig operations. /// Provides 'stella deltasig inspect', 'stella deltasig convert', 'stella deltasig version'. /// public static class DeltaSigCliCommands { /// /// Builds the deltasig command group. /// /// Verbose output option. /// The deltasig command. public static Command BuildDeltaSigCommand(Option verboseOption) { var deltasig = new Command("deltasig", "DeltaSig predicate generation and verification."); deltasig.Aliases.Add("dsig"); // Add subcommands deltasig.Add(BuildInspectCommand(verboseOption)); deltasig.Add(BuildConvertCommand(verboseOption)); deltasig.Add(BuildVersionCommand()); return deltasig; } private static Command BuildInspectCommand(Option verboseOption) { var pathArg = new Argument("predicate-file") { Description = "Path to DeltaSig predicate JSON file" }; var formatOption = new Option("--format", "-f") { Description = "Output format: summary, json, detailed", DefaultValueFactory = _ => "summary" }; var showEvidenceOption = new Option("--show-evidence", "-e") { Description = "Show provenance and IR diff evidence details" }; var outputV2Option = new Option("--v2", "--output-v2") { Description = "Force v2 format output when using json format" }; var inspect = new Command("inspect", "Inspect a DeltaSig predicate file.") { pathArg, formatOption, showEvidenceOption, outputV2Option, verboseOption }; inspect.SetAction(async (parseResult, ct) => { var file = parseResult.GetValue(pathArg)!; var format = parseResult.GetValue(formatOption) ?? "summary"; var showEvidence = parseResult.GetValue(showEvidenceOption); var verbose = parseResult.GetValue(verboseOption); if (!file.Exists) { Console.Error.WriteLine($"File not found: {file.FullName}"); return 1; } try { var json = await File.ReadAllTextAsync(file.FullName, ct); var doc = JsonDocument.Parse(json); var root = doc.RootElement; // Detect version var predicateType = root.TryGetProperty("predicateType", out var pt) ? pt.GetString() : null; var isV2 = predicateType == DeltaSigPredicateV2.PredicateType; if (format == "json") { Console.WriteLine(json); return 0; } Console.WriteLine($"DeltaSig Predicate: {(isV2 ? "v2" : "v1")}"); Console.WriteLine(new string('-', 50)); if (isV2) { var v2 = JsonSerializer.Deserialize(json, JsonOptions); if (v2 != null) { PrintV2Summary(v2, format == "detailed", showEvidence); } } else { var v1 = JsonSerializer.Deserialize(json, JsonOptions); if (v1 != null) { PrintV1Summary(v1, format == "detailed"); } } return 0; } catch (JsonException ex) { Console.Error.WriteLine($"Failed to parse predicate: {ex.Message}"); return 1; } }); return inspect; } private static Command BuildConvertCommand(Option verboseOption) { var inputArg = new Argument("input-file") { Description = "Path to source predicate JSON file" }; var outputOption = new Option("--output", "-o") { Description = "Output file path (default: stdout)" }; var toV2Option = new Option("--to-v2") { Description = "Convert v1 predicate to v2 format" }; var toV1Option = new Option("--to-v1") { Description = "Convert v2 predicate to v1 format" }; var convert = new Command("convert", "Convert between v1 and v2 predicate formats.") { inputArg, outputOption, toV2Option, toV1Option, verboseOption }; convert.SetAction(async (parseResult, ct) => { var file = parseResult.GetValue(inputArg)!; var output = parseResult.GetValue(outputOption); var toV2 = parseResult.GetValue(toV2Option); var toV1 = parseResult.GetValue(toV1Option); var verbose = parseResult.GetValue(verboseOption); if (toV2 == toV1) { Console.Error.WriteLine("Specify exactly one of --to-v1 or --to-v2"); return 1; } if (!file.Exists) { Console.Error.WriteLine($"File not found: {file.FullName}"); return 1; } try { var json = await File.ReadAllTextAsync(file.FullName, ct); string resultJson; if (toV2) { var v1 = JsonSerializer.Deserialize(json, JsonOptions); if (v1 == null) { Console.Error.WriteLine("Failed to parse v1 predicate"); return 1; } var v2 = DeltaSigPredicateConverter.ToV2(v1); resultJson = JsonSerializer.Serialize(v2, JsonOptions); if (verbose) { Console.Error.WriteLine($"Converted v1 → v2: {v1.Delta?.Count ?? 0} deltas → {v2.FunctionMatches.Count} matches"); } } else { var v2 = JsonSerializer.Deserialize(json, JsonOptions); if (v2 == null) { Console.Error.WriteLine("Failed to parse v2 predicate"); return 1; } var v1 = DeltaSigPredicateConverter.ToV1(v2); resultJson = JsonSerializer.Serialize(v1, JsonOptions); if (verbose) { Console.Error.WriteLine($"Converted v2 → v1: {v2.FunctionMatches.Count} matches → {v1.Delta?.Count ?? 0} deltas"); } } if (output != null) { await File.WriteAllTextAsync(output.FullName, resultJson, ct); Console.WriteLine($"Written to {output.FullName}"); } else { Console.WriteLine(resultJson); } return 0; } catch (JsonException ex) { Console.Error.WriteLine($"Failed to parse predicate: {ex.Message}"); return 1; } }); return convert; } private static Command BuildVersionCommand() { var version = new Command("version", "Show DeltaSig schema version information."); version.SetAction((_, _) => { Console.WriteLine("DeltaSig Schema Versions:"); Console.WriteLine($" v1: {DeltaSigPredicate.PredicateType}"); Console.WriteLine($" v2: {DeltaSigPredicateV2.PredicateType}"); Console.WriteLine(); Console.WriteLine("Features in v2:"); Console.WriteLine(" - Symbol provenance from ground-truth corpus"); Console.WriteLine(" - IR diff references with CAS storage"); Console.WriteLine(" - Explicit verdict and confidence scores"); Console.WriteLine(" - Function-level match states (vulnerable/patched/modified)"); Console.WriteLine(" - Enhanced tooling metadata"); return Task.FromResult(0); }); return version; } private static void PrintV1Summary(DeltaSigPredicate v1, bool detailed) { Console.WriteLine($"Subject Count: {v1.Subject?.Count() ?? 0}"); Console.WriteLine($"Delta Count: {v1.Delta?.Count ?? 0}"); Console.WriteLine($"Computed At: {v1.ComputedAt:u}"); Console.WriteLine($"CVEs: {string.Join(", ", v1.CveIds ?? Array.Empty())}"); Console.WriteLine($"Package: {v1.PackageName}"); if (v1.Summary != null) { Console.WriteLine(); Console.WriteLine("Summary:"); Console.WriteLine($" Functions Added: {v1.Summary.FunctionsAdded}"); Console.WriteLine($" Functions Removed: {v1.Summary.FunctionsRemoved}"); Console.WriteLine($" Functions Modified: {v1.Summary.FunctionsModified}"); } if (detailed && v1.Delta != null) { Console.WriteLine(); Console.WriteLine("Deltas:"); foreach (var delta in v1.Delta.Take(10)) { Console.WriteLine($" {delta.FunctionId}: {delta.ChangeType}"); } if (v1.Delta.Count > 10) { Console.WriteLine($" ... and {v1.Delta.Count - 10} more"); } } } private static void PrintV2Summary(DeltaSigPredicateV2 v2, bool detailed, bool showEvidence) { Console.WriteLine($"PURL: {v2.Subject.Purl}"); Console.WriteLine($"Verdict: {v2.Verdict}"); Console.WriteLine($"Confidence: {v2.Confidence:P0}"); Console.WriteLine($"Function Count: {v2.FunctionMatches.Count}"); Console.WriteLine($"Computed At: {v2.ComputedAt:u}"); Console.WriteLine($"CVEs: {string.Join(", ", v2.CveIds ?? Array.Empty())}"); if (v2.Summary != null) { Console.WriteLine(); Console.WriteLine("Summary:"); Console.WriteLine($" Total Functions: {v2.Summary.TotalFunctions}"); Console.WriteLine($" Vulnerable Functions: {v2.Summary.VulnerableFunctions}"); Console.WriteLine($" Patched Functions: {v2.Summary.PatchedFunctions}"); Console.WriteLine($" With Provenance: {v2.Summary.FunctionsWithProvenance}"); Console.WriteLine($" With IR Diff: {v2.Summary.FunctionsWithIrDiff}"); Console.WriteLine($" Avg Match Score: {v2.Summary.AvgMatchScore:F2}"); } if (v2.Tooling != null) { Console.WriteLine(); Console.WriteLine("Tooling:"); Console.WriteLine($" Lifter: {v2.Tooling.Lifter} {v2.Tooling.LifterVersion}"); Console.WriteLine($" IR Format: {v2.Tooling.CanonicalIr}"); Console.WriteLine($" Match Algorithm:{v2.Tooling.MatchAlgorithm}"); } if (detailed || showEvidence) { Console.WriteLine(); Console.WriteLine("Function Matches:"); foreach (var match in v2.FunctionMatches.Take(10)) { var provenance = match.SymbolProvenance != null ? $"[{match.SymbolProvenance.SourceId}]" : ""; var irDiff = match.IrDiff != null ? "[IR]" : ""; Console.WriteLine($" {match.Name}: {match.MatchState} ({match.MatchScore:P0}) {provenance}{irDiff}"); if (showEvidence) { if (match.SymbolProvenance != null) { Console.WriteLine($" Provenance: {match.SymbolProvenance.SourceId} @ {match.SymbolProvenance.FetchedAt:u}"); Console.WriteLine($" Observation: {match.SymbolProvenance.ObservationId}"); } if (match.IrDiff != null) { Console.WriteLine($" IR Diff: {match.IrDiff.CasDigest}"); Console.WriteLine($" Changes: +{match.IrDiff.AddedBlocks} -{match.IrDiff.RemovedBlocks} ~{match.IrDiff.ChangedInstructions}"); } } } if (v2.FunctionMatches.Count > 10) { Console.WriteLine($" ... and {v2.FunctionMatches.Count - 10} more"); } } } private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; }