new two advisories and sprints work on them
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
669
src/Cli/StellaOps.Cli/Commands/Binary/DeltaSigCommandGroup.cs
Normal file
669
src/Cli/StellaOps.Cli/Commands/Binary/DeltaSigCommandGroup.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user