371 lines
14 KiB
C#
371 lines
14 KiB
C#
// -----------------------------------------------------------------------------
|
|
// 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;
|
|
|
|
/// <summary>
|
|
/// CLI commands for DeltaSig operations.
|
|
/// Provides 'stella deltasig inspect', 'stella deltasig convert', 'stella deltasig version'.
|
|
/// </summary>
|
|
public static class DeltaSigCliCommands
|
|
{
|
|
/// <summary>
|
|
/// Builds the deltasig command group.
|
|
/// </summary>
|
|
/// <param name="verboseOption">Verbose output option.</param>
|
|
/// <returns>The deltasig command.</returns>
|
|
public static Command BuildDeltaSigCommand(Option<bool> 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<bool> verboseOption)
|
|
{
|
|
var pathArg = new Argument<FileInfo>("predicate-file")
|
|
{
|
|
Description = "Path to DeltaSig predicate JSON file"
|
|
};
|
|
|
|
var formatOption = new Option<string>("--format", "-f")
|
|
{
|
|
Description = "Output format: summary, json, detailed",
|
|
DefaultValueFactory = _ => "summary"
|
|
};
|
|
|
|
var showEvidenceOption = new Option<bool>("--show-evidence", "-e")
|
|
{
|
|
Description = "Show provenance and IR diff evidence details"
|
|
};
|
|
|
|
var outputV2Option = new Option<bool>("--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<DeltaSigPredicateV2>(json, JsonOptions);
|
|
if (v2 != null)
|
|
{
|
|
PrintV2Summary(v2, format == "detailed", showEvidence);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var v1 = JsonSerializer.Deserialize<DeltaSigPredicate>(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<bool> verboseOption)
|
|
{
|
|
var inputArg = new Argument<FileInfo>("input-file")
|
|
{
|
|
Description = "Path to source predicate JSON file"
|
|
};
|
|
|
|
var outputOption = new Option<FileInfo?>("--output", "-o")
|
|
{
|
|
Description = "Output file path (default: stdout)"
|
|
};
|
|
|
|
var toV2Option = new Option<bool>("--to-v2")
|
|
{
|
|
Description = "Convert v1 predicate to v2 format"
|
|
};
|
|
|
|
var toV1Option = new Option<bool>("--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<DeltaSigPredicate>(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<DeltaSigPredicateV2>(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<string>())}");
|
|
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<string>())}");
|
|
|
|
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
|
|
};
|
|
}
|