new two advisories and sprints work on them

This commit is contained in:
master
2026-01-16 18:39:36 +02:00
parent 9daf619954
commit c3a6269d55
72 changed files with 15540 additions and 18 deletions

View File

@@ -39,6 +39,9 @@ internal static class BinaryCommandGroup
// Sprint: SPRINT_20260112_006_CLI - BinaryIndex ops commands
binary.Add(BinaryIndexOpsCommandGroup.BuildOpsCommand(services, verboseOption, cancellationToken));
// Sprint: SPRINT_20260117_003_BINDEX - Delta-sig predicate operations
binary.Add(DeltaSigCommandGroup.BuildDeltaSigCommand(services, verboseOption, cancellationToken));
return binary;
}

View File

@@ -0,0 +1,669 @@
// -----------------------------------------------------------------------------
// DeltaSigCommandGroup.cs
// Sprint: SPRINT_20260117_003_BINDEX_delta_sig_predicate
// Task: DSP-007 - Add CLI commands for delta-sig operations
// Description: CLI commands for delta-sig diff, attest, verify, and gate operations
// -----------------------------------------------------------------------------
using System.CommandLine;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.BinaryIndex.DeltaSig;
using StellaOps.BinaryIndex.DeltaSig.Attestation;
using StellaOps.BinaryIndex.DeltaSig.Policy;
using StellaOps.Cli.Extensions;
namespace StellaOps.Cli.Commands.Binary;
/// <summary>
/// CLI command group for delta-sig binary diff operations.
/// </summary>
internal static class DeltaSigCommandGroup
{
/// <summary>
/// Builds the delta-sig command group.
/// </summary>
internal static Command BuildDeltaSigCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var deltaSig = new Command("delta-sig", "Binary delta signature operations for patch verification.");
deltaSig.Add(BuildDiffCommand(services, verboseOption, cancellationToken));
deltaSig.Add(BuildAttestCommand(services, verboseOption, cancellationToken));
deltaSig.Add(BuildVerifyCommand(services, verboseOption, cancellationToken));
deltaSig.Add(BuildGateCommand(services, verboseOption, cancellationToken));
return deltaSig;
}
/// <summary>
/// stella binary delta-sig diff - Generate delta-sig predicate from two binaries.
/// </summary>
private static Command BuildDiffCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var oldFileArg = new Argument<string>("old-file")
{
Description = "Path to the original (vulnerable) binary."
};
var newFileArg = new Argument<string>("new-file")
{
Description = "Path to the patched binary."
};
var outputOption = new Option<string?>("--output", new[] { "-o" })
{
Description = "Output file path (default: stdout)."
};
var archOption = new Option<string?>("--arch", new[] { "-a" })
{
Description = "Architecture hint (e.g., linux-amd64, linux-arm64)."
};
var cveOption = new Option<string[]>("--cve")
{
Description = "CVE IDs associated with the patch."
}.SetDefaultValue(Array.Empty<string>());
var packageOption = new Option<string?>("--package", new[] { "-p" })
{
Description = "Package name."
};
var oldVersionOption = new Option<string?>("--old-version")
{
Description = "Version of the old binary."
};
var newVersionOption = new Option<string?>("--new-version")
{
Description = "Version of the new binary."
};
var lifterOption = new Option<string>("--lifter")
{
Description = "Preferred binary lifter (b2r2, ghidra)."
}.SetDefaultValue("b2r2").FromAmong("b2r2", "ghidra");
var semanticOption = new Option<bool>("--semantic")
{
Description = "Compute semantic similarity using BSim."
};
var formatOption = new Option<string>("--format", new[] { "-f" })
{
Description = "Output format: json (default), yaml."
}.SetDefaultValue("json").FromAmong("json", "yaml");
var command = new Command("diff", "Generate a delta-sig predicate from two binaries.")
{
oldFileArg,
newFileArg,
outputOption,
archOption,
cveOption,
packageOption,
oldVersionOption,
newVersionOption,
lifterOption,
semanticOption,
formatOption,
verboseOption
};
command.SetAction(async parseResult =>
{
var oldFile = parseResult.GetValue(oldFileArg)!;
var newFile = parseResult.GetValue(newFileArg)!;
var output = parseResult.GetValue(outputOption);
var arch = parseResult.GetValue(archOption);
var cves = parseResult.GetValue(cveOption) ?? [];
var package = parseResult.GetValue(packageOption);
var oldVersion = parseResult.GetValue(oldVersionOption);
var newVersion = parseResult.GetValue(newVersionOption);
var lifter = parseResult.GetValue(lifterOption)!;
var semantic = parseResult.GetValue(semanticOption);
var format = parseResult.GetValue(formatOption)!;
var verbose = parseResult.GetValue(verboseOption);
await HandleDiffAsync(
services,
oldFile,
newFile,
output,
arch,
cves.ToList(),
package,
oldVersion,
newVersion,
lifter,
semantic,
format,
verbose,
cancellationToken);
});
return command;
}
/// <summary>
/// stella binary delta-sig attest - Sign and submit delta-sig to Rekor.
/// </summary>
private static Command BuildAttestCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var predicateFileArg = new Argument<string>("predicate-file")
{
Description = "Path to delta-sig predicate JSON file."
};
var keyOption = new Option<string?>("--key", new[] { "-k" })
{
Description = "Signing key identifier (uses default if not specified)."
};
var rekorOption = new Option<string?>("--rekor-url")
{
Description = "Rekor server URL (default: https://rekor.sigstore.dev)."
};
var outputOption = new Option<string?>("--output", new[] { "-o" })
{
Description = "Output file for DSSE envelope."
};
var dryRunOption = new Option<bool>("--dry-run")
{
Description = "Create envelope without submitting to Rekor."
};
var command = new Command("attest", "Sign and submit a delta-sig predicate to Rekor.")
{
predicateFileArg,
keyOption,
rekorOption,
outputOption,
dryRunOption,
verboseOption
};
command.SetAction(async parseResult =>
{
var predicateFile = parseResult.GetValue(predicateFileArg)!;
var key = parseResult.GetValue(keyOption);
var rekorUrl = parseResult.GetValue(rekorOption);
var output = parseResult.GetValue(outputOption);
var dryRun = parseResult.GetValue(dryRunOption);
var verbose = parseResult.GetValue(verboseOption);
await HandleAttestAsync(
services,
predicateFile,
key,
rekorUrl,
output,
dryRun,
verbose,
cancellationToken);
});
return command;
}
/// <summary>
/// stella binary delta-sig verify - Verify a binary against a delta-sig predicate.
/// </summary>
private static Command BuildVerifyCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var predicateArg = new Argument<string>("predicate")
{
Description = "Path to delta-sig predicate or Rekor entry UUID."
};
var binaryArg = new Argument<string>("binary")
{
Description = "Path to binary file to verify."
};
var rekorOption = new Option<string?>("--rekor-url")
{
Description = "Rekor server URL for fetching remote predicates."
};
var formatOption = new Option<string>("--format", new[] { "-f" })
{
Description = "Output format: text (default), json."
}.SetDefaultValue("text").FromAmong("text", "json");
var command = new Command("verify", "Verify a binary against a delta-sig predicate.")
{
predicateArg,
binaryArg,
rekorOption,
formatOption,
verboseOption
};
command.SetAction(async parseResult =>
{
var predicate = parseResult.GetValue(predicateArg)!;
var binary = parseResult.GetValue(binaryArg)!;
var rekorUrl = parseResult.GetValue(rekorOption);
var format = parseResult.GetValue(formatOption)!;
var verbose = parseResult.GetValue(verboseOption);
await HandleVerifyAsync(
services,
predicate,
binary,
rekorUrl,
format,
verbose,
cancellationToken);
});
return command;
}
/// <summary>
/// stella binary delta-sig gate - Evaluate delta-sig against policy constraints.
/// </summary>
private static Command BuildGateCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var predicateArg = new Argument<string>("predicate")
{
Description = "Path to delta-sig predicate JSON file."
};
var maxModifiedOption = new Option<int?>("--max-modified")
{
Description = "Maximum modified functions allowed."
};
var maxAddedOption = new Option<int?>("--max-added")
{
Description = "Maximum added functions allowed."
};
var maxRemovedOption = new Option<int?>("--max-removed")
{
Description = "Maximum removed functions allowed."
};
var maxBytesOption = new Option<long?>("--max-bytes")
{
Description = "Maximum bytes changed allowed."
};
var minSimilarityOption = new Option<double?>("--min-similarity")
{
Description = "Minimum semantic similarity (0.0-1.0)."
};
var formatOption = new Option<string>("--format", new[] { "-f" })
{
Description = "Output format: text (default), json."
}.SetDefaultValue("text").FromAmong("text", "json");
var command = new Command("gate", "Evaluate a delta-sig against policy constraints.")
{
predicateArg,
maxModifiedOption,
maxAddedOption,
maxRemovedOption,
maxBytesOption,
minSimilarityOption,
formatOption,
verboseOption
};
command.SetAction(async parseResult =>
{
var predicate = parseResult.GetValue(predicateArg)!;
var maxModified = parseResult.GetValue(maxModifiedOption);
var maxAdded = parseResult.GetValue(maxAddedOption);
var maxRemoved = parseResult.GetValue(maxRemovedOption);
var maxBytes = parseResult.GetValue(maxBytesOption);
var minSimilarity = parseResult.GetValue(minSimilarityOption);
var format = parseResult.GetValue(formatOption)!;
var verbose = parseResult.GetValue(verboseOption);
await HandleGateAsync(
services,
predicate,
maxModified,
maxAdded,
maxRemoved,
maxBytes,
minSimilarity,
format,
verbose,
cancellationToken);
});
return command;
}
// Handler implementations
private static async Task HandleDiffAsync(
IServiceProvider services,
string oldFile,
string newFile,
string? output,
string? arch,
IReadOnlyList<string> cves,
string? package,
string? oldVersion,
string? newVersion,
string lifter,
bool semantic,
string format,
bool verbose,
CancellationToken ct)
{
var deltaSigService = services.GetRequiredService<IDeltaSigService>();
var console = Console.Out;
if (verbose)
{
await console.WriteLineAsync($"Generating delta-sig: {oldFile} -> {newFile}");
}
// Open binary streams
await using var oldStream = File.OpenRead(oldFile);
await using var newStream = File.OpenRead(newFile);
var oldFileInfo = new FileInfo(oldFile);
var newFileInfo = new FileInfo(newFile);
// Compute digests
using var sha256 = System.Security.Cryptography.SHA256.Create();
var oldDigest = Convert.ToHexString(await sha256.ComputeHashAsync(oldStream, ct)).ToLowerInvariant();
oldStream.Position = 0;
var newDigest = Convert.ToHexString(await sha256.ComputeHashAsync(newStream, ct)).ToLowerInvariant();
newStream.Position = 0;
var request = new DeltaSigRequest
{
OldBinary = new BinaryReference
{
Uri = $"file://{oldFile}",
Digest = new Dictionary<string, string> { ["sha256"] = oldDigest },
Content = oldStream,
Filename = oldFileInfo.Name,
Size = oldFileInfo.Length
},
NewBinary = new BinaryReference
{
Uri = $"file://{newFile}",
Digest = new Dictionary<string, string> { ["sha256"] = newDigest },
Content = newStream,
Filename = newFileInfo.Name,
Size = newFileInfo.Length
},
Architecture = arch ?? "unknown",
CveIds = cves,
PackageName = package,
OldVersion = oldVersion,
NewVersion = newVersion,
PreferredLifter = lifter,
ComputeSemanticSimilarity = semantic
};
var predicate = await deltaSigService.GenerateAsync(request, ct);
// Serialize output
var json = System.Text.Json.JsonSerializer.Serialize(predicate, new System.Text.Json.JsonSerializerOptions
{
WriteIndented = true,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
});
if (!string.IsNullOrEmpty(output))
{
await File.WriteAllTextAsync(output, json, ct);
await console.WriteLineAsync($"Delta-sig written to: {output}");
}
else
{
await console.WriteLineAsync(json);
}
if (verbose)
{
await console.WriteLineAsync($"Summary: {predicate.Summary.FunctionsModified} modified, " +
$"{predicate.Summary.FunctionsAdded} added, " +
$"{predicate.Summary.FunctionsRemoved} removed");
}
}
private static async Task HandleAttestAsync(
IServiceProvider services,
string predicateFile,
string? key,
string? rekorUrl,
string? output,
bool dryRun,
bool verbose,
CancellationToken ct)
{
var console = Console.Out;
// Read predicate
var json = await File.ReadAllTextAsync(predicateFile, ct);
var predicate = System.Text.Json.JsonSerializer.Deserialize<DeltaSigPredicate>(json);
if (predicate is null)
{
Console.Error.WriteLine("Failed to parse predicate file.");
Environment.ExitCode = 1;
return;
}
if (verbose)
{
await console.WriteLineAsync($"Loaded predicate with {predicate.Delta.Count} function deltas");
}
// Build envelope
var builder = new DeltaSigEnvelopeBuilder();
var (payloadType, payload, pae) = builder.PrepareForSigning(predicate);
if (dryRun)
{
await console.WriteLineAsync("Dry run - envelope prepared but not submitted.");
await console.WriteLineAsync($"Payload type: {payloadType}");
await console.WriteLineAsync($"Payload size: {payload.Length} bytes");
return;
}
// In real implementation, we would:
// 1. Sign the PAE using the configured key
// 2. Create the DSSE envelope
// 3. Submit to Rekor
// For now, output a placeholder
await console.WriteLineAsync("Attestation not yet implemented - requires signing key configuration.");
Environment.ExitCode = 1;
}
private static async Task HandleVerifyAsync(
IServiceProvider services,
string predicateArg,
string binary,
string? rekorUrl,
string format,
bool verbose,
CancellationToken ct)
{
var deltaSigService = services.GetRequiredService<IDeltaSigService>();
var console = Console.Out;
// Load predicate
DeltaSigPredicate predicate;
if (File.Exists(predicateArg))
{
var json = await File.ReadAllTextAsync(predicateArg, ct);
predicate = System.Text.Json.JsonSerializer.Deserialize<DeltaSigPredicate>(json)!;
}
else
{
// Assume it's a Rekor entry ID - fetch from Rekor
Console.Error.WriteLine("Fetching from Rekor not yet implemented.");
Environment.ExitCode = 1;
return;
}
if (verbose)
{
await console.WriteLineAsync($"Verifying {binary} against predicate");
}
await using var binaryStream = File.OpenRead(binary);
var result = await deltaSigService.VerifyAsync(predicate, binaryStream, ct);
if (format == "json")
{
var json = System.Text.Json.JsonSerializer.Serialize(result, new System.Text.Json.JsonSerializerOptions
{
WriteIndented = true
});
await console.WriteLineAsync(json);
}
else
{
if (result.IsValid)
{
await console.WriteLineAsync("✓ Verification PASSED");
}
else
{
await console.WriteLineAsync($"✗ Verification FAILED: {result.FailureReason}");
Environment.ExitCode = 1;
}
}
}
private static async Task HandleGateAsync(
IServiceProvider services,
string predicateFile,
int? maxModified,
int? maxAdded,
int? maxRemoved,
long? maxBytes,
double? minSimilarity,
string format,
bool verbose,
CancellationToken ct)
{
var gate = services.GetService<IDeltaScopePolicyGate>();
var console = Console.Out;
// Read predicate
var json = await File.ReadAllTextAsync(predicateFile, ct);
var predicate = System.Text.Json.JsonSerializer.Deserialize<DeltaSigPredicate>(json);
if (predicate is null)
{
Console.Error.WriteLine("Failed to parse predicate file.");
Environment.ExitCode = 1;
return;
}
// Build options
var options = new DeltaScopeGateOptions
{
MaxModifiedFunctions = maxModified ?? 10,
MaxAddedFunctions = maxAdded ?? 5,
MaxRemovedFunctions = maxRemoved ?? 2,
MaxBytesChanged = maxBytes ?? 10_000,
MinSemanticSimilarity = minSimilarity ?? 0.8
};
if (gate is null)
{
// Use inline evaluation
var violations = new List<string>();
if (predicate.Summary.FunctionsModified > options.MaxModifiedFunctions)
{
violations.Add($"Modified {predicate.Summary.FunctionsModified} functions; max {options.MaxModifiedFunctions}");
}
if (predicate.Summary.FunctionsAdded > options.MaxAddedFunctions)
{
violations.Add($"Added {predicate.Summary.FunctionsAdded} functions; max {options.MaxAddedFunctions}");
}
if (predicate.Summary.FunctionsRemoved > options.MaxRemovedFunctions)
{
violations.Add($"Removed {predicate.Summary.FunctionsRemoved} functions; max {options.MaxRemovedFunctions}");
}
if (predicate.Summary.TotalBytesChanged > options.MaxBytesChanged)
{
violations.Add($"Changed {predicate.Summary.TotalBytesChanged} bytes; max {options.MaxBytesChanged}");
}
if (predicate.Summary.MinSemanticSimilarity < options.MinSemanticSimilarity)
{
violations.Add($"Min similarity {predicate.Summary.MinSemanticSimilarity:P0}; required {options.MinSemanticSimilarity:P0}");
}
if (format == "json")
{
var result = new { passed = violations.Count == 0, violations };
var resultJson = System.Text.Json.JsonSerializer.Serialize(result, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
await console.WriteLineAsync(resultJson);
}
else
{
if (violations.Count == 0)
{
await console.WriteLineAsync("✓ Gate PASSED");
}
else
{
await console.WriteLineAsync("✗ Gate FAILED");
foreach (var v in violations)
{
await console.WriteLineAsync($" - {v}");
}
Environment.ExitCode = 1;
}
}
}
else
{
var result = await gate.EvaluateAsync(predicate, options, ct);
if (format == "json")
{
var resultJson = System.Text.Json.JsonSerializer.Serialize(result, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
await console.WriteLineAsync(resultJson);
}
else
{
if (result.Passed)
{
await console.WriteLineAsync("✓ Gate PASSED");
}
else
{
await console.WriteLineAsync($"✗ Gate FAILED: {result.Reason}");
Environment.ExitCode = 1;
}
}
}
}
}