save progress

This commit is contained in:
StellaOps Bot
2026-01-03 00:47:24 +02:00
parent 3f197814c5
commit ca578801fd
319 changed files with 32478 additions and 2202 deletions

View File

@@ -5,6 +5,7 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Cli.Commands.Admin;
using StellaOps.Cli.Commands.Budget;
using StellaOps.Cli.Commands.DeltaSig;
using StellaOps.Cli.Commands.Proof;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Extensions;
@@ -82,6 +83,7 @@ internal static class CommandFactory
root.Add(BuildRiskCommand(services, verboseOption, cancellationToken));
root.Add(BuildReachabilityCommand(services, verboseOption, cancellationToken));
root.Add(BuildGraphCommand(services, verboseOption, cancellationToken));
root.Add(DeltaSigCommandGroup.BuildDeltaSigCommand(services, verboseOption, cancellationToken)); // Sprint: SPRINT_20260102_001_BE - Delta signatures
root.Add(Binary.BinaryCommandGroup.BuildBinaryCommand(services, verboseOption, cancellationToken)); // Sprint: SPRINT_3850_0001_0001
root.Add(BuildApiCommand(services, verboseOption, cancellationToken));
root.Add(BuildSdkCommand(services, verboseOption, cancellationToken));

View File

@@ -0,0 +1,456 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under AGPL-3.0-or-later. See LICENSE in the project root.
// Sprint: SPRINT_20260102_001_BE - Tasks: DS-025 through DS-032
using System.CommandLine;
using System.CommandLine.Parsing;
namespace StellaOps.Cli.Commands.DeltaSig;
/// <summary>
/// CLI command group for binary delta signature operations.
/// Delta signatures enable cryptographic detection of backported security patches
/// in binaries where version strings don't reflect the actual fix status.
/// </summary>
internal static class DeltaSigCommandGroup
{
internal static Command BuildDeltaSigCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var deltasig = new Command("deltasig", "Binary delta signature operations for backport detection.");
deltasig.Add(BuildExtractCommand(services, verboseOption, cancellationToken));
deltasig.Add(BuildAuthorCommand(services, verboseOption, cancellationToken));
deltasig.Add(BuildSignCommand(services, verboseOption, cancellationToken));
deltasig.Add(BuildVerifyCommand(services, verboseOption, cancellationToken));
deltasig.Add(BuildMatchCommand(services, verboseOption, cancellationToken));
deltasig.Add(BuildPackCommand(services, verboseOption, cancellationToken));
deltasig.Add(BuildInspectCommand(services, verboseOption, cancellationToken));
return deltasig;
}
/// <summary>
/// stella deltasig extract: Extract normalized signatures from a binary.
/// </summary>
private static Command BuildExtractCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var binaryArg = new Argument<string>("binary")
{
Description = "Path to ELF/PE/Mach-O binary file."
};
var symbolsOption = new Option<string[]>("--symbols", ["-s"])
{
Description = "Symbol names to extract (comma-separated or multiple --symbols).",
AllowMultipleArgumentsPerToken = true,
Arity = ArgumentArity.OneOrMore
};
var archOption = new Option<string?>("--arch", ["-a"])
{
Description = "Architecture hint (x86_64, aarch64). Auto-detected if not specified."
};
var outputOption = new Option<string?>("--out", ["-o"])
{
Description = "Output JSON path. Writes to stdout if not specified."
};
var jsonOption = new Option<bool>("--json", ["-j"])
{
Description = "Machine-readable JSON output."
};
var command = new Command("extract", "Extract normalized delta signatures from a binary.")
{
binaryArg,
symbolsOption,
archOption,
outputOption,
jsonOption,
verboseOption
};
command.SetAction(parseResult =>
{
var binary = parseResult.GetValue(binaryArg)!;
var symbols = parseResult.GetValue(symbolsOption) ?? [];
var arch = parseResult.GetValue(archOption);
var output = parseResult.GetValue(outputOption);
var json = parseResult.GetValue(jsonOption);
var verbose = parseResult.GetValue(verboseOption);
return DeltaSigCommandHandlers.HandleExtractAsync(
services,
binary,
symbols,
arch,
output,
json,
verbose,
cancellationToken);
});
return command;
}
/// <summary>
/// stella deltasig author: Author signatures by comparing vulnerable and patched binaries.
/// </summary>
private static Command BuildAuthorCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var vulnOption = new Option<string>("--vuln", ["-v"])
{
Description = "Path to vulnerable binary.",
Arity = ArgumentArity.ExactlyOne
};
var patchedOption = new Option<string>("--patched", ["-p"])
{
Description = "Path to patched binary.",
Arity = ArgumentArity.ExactlyOne
};
var cveOption = new Option<string>("--cve", ["-c"])
{
Description = "CVE identifier (e.g., CVE-2024-12345).",
Arity = ArgumentArity.ExactlyOne
};
var packageOption = new Option<string>("--package")
{
Description = "Package name (e.g., openssl, libcurl).",
Arity = ArgumentArity.ExactlyOne
};
var sonameOption = new Option<string?>("--soname")
{
Description = "Shared object name (e.g., libssl.so.3)."
};
var archOption = new Option<string>("--arch", ["-a"])
{
Description = "Architecture (x86_64, aarch64).",
Arity = ArgumentArity.ExactlyOne
};
var abiOption = new Option<string>("--abi")
{
Description = "ABI (gnu, musl, android)."
}.SetDefaultValue("gnu");
var outputOption = new Option<string>("--out", ["-o"])
{
Description = "Output directory for signature payloads.",
Arity = ArgumentArity.ExactlyOne
};
var command = new Command("author", "Author delta signatures by comparing vulnerable and patched binaries.")
{
vulnOption,
patchedOption,
cveOption,
packageOption,
sonameOption,
archOption,
abiOption,
outputOption,
verboseOption
};
command.SetAction(parseResult =>
{
var vuln = parseResult.GetValue(vulnOption)!;
var patched = parseResult.GetValue(patchedOption)!;
var cve = parseResult.GetValue(cveOption)!;
var package = parseResult.GetValue(packageOption)!;
var soname = parseResult.GetValue(sonameOption);
var arch = parseResult.GetValue(archOption)!;
var abi = parseResult.GetValue(abiOption)!;
var output = parseResult.GetValue(outputOption)!;
var verbose = parseResult.GetValue(verboseOption);
return DeltaSigCommandHandlers.HandleAuthorAsync(
services,
vuln,
patched,
cve,
package,
soname,
arch,
abi,
output,
verbose,
cancellationToken);
});
return command;
}
/// <summary>
/// stella deltasig sign: Sign a signature payload with DSSE envelope.
/// </summary>
private static Command BuildSignCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var inputOption = new Option<string>("--in", ["-i"])
{
Description = "Input payload JSON file.",
Arity = ArgumentArity.ExactlyOne
};
var keyOption = new Option<string>("--key", ["-k"])
{
Description = "Private key PEM file for signing.",
Arity = ArgumentArity.ExactlyOne
};
var outputOption = new Option<string>("--out", ["-o"])
{
Description = "Output DSSE envelope path.",
Arity = ArgumentArity.ExactlyOne
};
var algOption = new Option<string>("--alg")
{
Description = "Signature algorithm (ecdsa-p256-sha256, rsa-pss-sha256, ed25519)."
}.SetDefaultValue("ecdsa-p256-sha256");
var command = new Command("sign", "Sign a delta signature payload with DSSE envelope.")
{
inputOption,
keyOption,
outputOption,
algOption,
verboseOption
};
command.SetAction(parseResult =>
{
var input = parseResult.GetValue(inputOption)!;
var key = parseResult.GetValue(keyOption)!;
var output = parseResult.GetValue(outputOption)!;
var alg = parseResult.GetValue(algOption)!;
var verbose = parseResult.GetValue(verboseOption);
return DeltaSigCommandHandlers.HandleSignAsync(
services,
input,
key,
output,
alg,
verbose,
cancellationToken);
});
return command;
}
/// <summary>
/// stella deltasig verify: Verify a DSSE-signed signature envelope.
/// </summary>
private static Command BuildVerifyCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var inputOption = new Option<string>("--in", ["-i"])
{
Description = "Input DSSE envelope file.",
Arity = ArgumentArity.ExactlyOne
};
var pubOption = new Option<string>("--pub", ["-p"])
{
Description = "Public key PEM file for verification.",
Arity = ArgumentArity.ExactlyOne
};
var command = new Command("verify", "Verify a DSSE-signed delta signature envelope.")
{
inputOption,
pubOption,
verboseOption
};
command.SetAction(parseResult =>
{
var input = parseResult.GetValue(inputOption)!;
var pub = parseResult.GetValue(pubOption)!;
var verbose = parseResult.GetValue(verboseOption);
return DeltaSigCommandHandlers.HandleVerifyAsync(
services,
input,
pub,
verbose,
cancellationToken);
});
return command;
}
/// <summary>
/// stella deltasig match: Match a binary against signature pack.
/// </summary>
private static Command BuildMatchCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var binaryArg = new Argument<string>("binary")
{
Description = "Path to binary file to check."
};
var sigpackOption = new Option<string>("--sigpack", ["-s"])
{
Description = "Signature pack (ZIP file) or directory containing signatures.",
Arity = ArgumentArity.ExactlyOne
};
var cveOption = new Option<string?>("--cve", ["-c"])
{
Description = "Filter to specific CVE."
};
var jsonOption = new Option<bool>("--json", ["-j"])
{
Description = "Machine-readable JSON output."
};
var command = new Command("match", "Match a binary against known vulnerable/patched signatures.")
{
binaryArg,
sigpackOption,
cveOption,
jsonOption,
verboseOption
};
command.SetAction(parseResult =>
{
var binary = parseResult.GetValue(binaryArg)!;
var sigpack = parseResult.GetValue(sigpackOption)!;
var cve = parseResult.GetValue(cveOption);
var json = parseResult.GetValue(jsonOption);
var verbose = parseResult.GetValue(verboseOption);
return DeltaSigCommandHandlers.HandleMatchAsync(
services,
binary,
sigpack,
cve,
json,
verbose,
cancellationToken);
});
return command;
}
/// <summary>
/// stella deltasig pack: Create a signature pack from individual signatures.
/// </summary>
private static Command BuildPackCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var inputOption = new Option<string>("--in-dir", ["-i"])
{
Description = "Directory containing *.dsse.json signature files.",
Arity = ArgumentArity.ExactlyOne
};
var outputOption = new Option<string>("--out", ["-o"])
{
Description = "Output ZIP path for the signature pack.",
Arity = ArgumentArity.ExactlyOne
};
var packIdOption = new Option<string?>("--pack-id")
{
Description = "Pack identifier (e.g., stellaops-deltasig-2026-01). Auto-generated if not specified."
};
var command = new Command("pack", "Create a signature pack from individual signature files.")
{
inputOption,
outputOption,
packIdOption,
verboseOption
};
command.SetAction(parseResult =>
{
var input = parseResult.GetValue(inputOption)!;
var output = parseResult.GetValue(outputOption)!;
var packId = parseResult.GetValue(packIdOption);
var verbose = parseResult.GetValue(verboseOption);
return DeltaSigCommandHandlers.HandlePackAsync(
services,
input,
output,
packId,
verbose,
cancellationToken);
});
return command;
}
/// <summary>
/// stella deltasig inspect: Inspect a signature payload or envelope.
/// </summary>
private static Command BuildInspectCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var inputArg = new Argument<string>("file")
{
Description = "Signature payload (.json) or envelope (.dsse.json) to inspect."
};
var jsonOption = new Option<bool>("--json", ["-j"])
{
Description = "Machine-readable JSON output."
};
var command = new Command("inspect", "Inspect a delta signature payload or DSSE envelope.")
{
inputArg,
jsonOption,
verboseOption
};
command.SetAction(parseResult =>
{
var input = parseResult.GetValue(inputArg)!;
var json = parseResult.GetValue(jsonOption);
var verbose = parseResult.GetValue(verboseOption);
return DeltaSigCommandHandlers.HandleInspectAsync(
services,
input,
json,
verbose,
cancellationToken);
});
return command;
}
}

View File

@@ -0,0 +1,800 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under AGPL-3.0-or-later. See LICENSE in the project root.
// Sprint: SPRINT_20260102_001_BE - Tasks: DS-025 through DS-032
using System.Collections.Immutable;
using System.IO.Compression;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Spectre.Console;
using StellaOps.BinaryIndex.DeltaSig;
using StellaOps.BinaryIndex.Disassembly;
using StellaOps.BinaryIndex.Normalization;
namespace StellaOps.Cli.Commands.DeltaSig;
/// <summary>
/// Command handlers for delta signature CLI operations.
/// </summary>
internal static class DeltaSigCommandHandlers
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
/// <summary>
/// Handle extract command - extract normalized signatures from a binary.
/// </summary>
public static async Task<int> HandleExtractAsync(
IServiceProvider services,
string binaryPath,
string[] symbols,
string? arch,
string? outputPath,
bool json,
bool verbose,
CancellationToken ct)
{
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("deltasig-extract");
if (!File.Exists(binaryPath))
{
AnsiConsole.MarkupLine($"[red]Error:[/] Binary file not found: {binaryPath}");
return 1;
}
try
{
var disassemblyService = services.GetRequiredService<IDisassemblyService>();
var normalizationService = services.GetRequiredService<NormalizationService>();
var sigGenerator = services.GetRequiredService<IDeltaSignatureGenerator>();
if (verbose)
{
AnsiConsole.MarkupLine($"[dim]Loading binary: {binaryPath}[/]");
}
// Load binary
var binaryBytes = await File.ReadAllBytesAsync(binaryPath, ct);
var (binaryInfo, plugin) = disassemblyService.LoadBinary(binaryBytes.AsSpan());
if (verbose)
{
AnsiConsole.MarkupLine($"[dim]Architecture: {binaryInfo.Architecture}[/]");
AnsiConsole.MarkupLine($"[dim]Format: {binaryInfo.Format}[/]");
}
// Get symbols from plugin
var allSymbols = plugin.GetSymbols(binaryInfo).ToList();
// Filter to requested symbols or all if none specified
var targetSymbols = symbols.Length > 0
? allSymbols.Where(s => symbols.Contains(s.Name)).ToList()
: allSymbols.Where(s => s.Type == SymbolType.Function).ToList();
if (targetSymbols.Count == 0)
{
AnsiConsole.MarkupLine("[yellow]Warning:[/] No symbols found in binary.");
return 0;
}
if (verbose)
{
AnsiConsole.MarkupLine($"[dim]Extracting {targetSymbols.Count} symbols...[/]");
}
// Get normalization pipeline
var pipeline = normalizationService.GetPipeline(binaryInfo.Architecture);
var extractedSignatures = new List<SymbolSignature>();
foreach (var symbol in targetSymbols)
{
ct.ThrowIfCancellationRequested();
// Disassemble
var instructions = plugin.DisassembleSymbol(binaryInfo, symbol).ToList();
if (instructions.Count == 0)
{
if (verbose)
{
AnsiConsole.MarkupLine($"[yellow]Warning:[/] No instructions for: {symbol.Name}");
}
continue;
}
// Normalize
var normalized = pipeline.Normalize(instructions, binaryInfo.Architecture);
// Generate signature
var sig = sigGenerator.GenerateSymbolSignature(
normalized,
symbol.Name,
symbol.Section ?? ".text");
extractedSignatures.Add(sig);
if (verbose)
{
AnsiConsole.MarkupLine($"[green]OK[/] {symbol.Name}: {sig.HashHex[..16]}... ({sig.SizeBytes} bytes)");
}
}
// Build output
var result = new
{
binaryPath = Path.GetFileName(binaryPath),
architecture = binaryInfo.Architecture.ToString(),
normalization = new
{
recipeId = pipeline.RecipeId,
recipeVersion = pipeline.RecipeVersion
},
symbols = extractedSignatures
};
var jsonOutput = JsonSerializer.Serialize(result, JsonOptions);
if (outputPath != null)
{
await File.WriteAllTextAsync(outputPath, jsonOutput, ct);
AnsiConsole.MarkupLine($"[green]Signatures written to:[/] {outputPath}");
}
else if (json)
{
Console.WriteLine(jsonOutput);
}
else
{
// Human-readable output
var table = new Table();
table.AddColumn("Symbol");
table.AddColumn("Hash");
table.AddColumn("Size");
table.AddColumn("BB Count");
foreach (var sig in extractedSignatures)
{
table.AddRow(
sig.Name,
sig.HashHex[..16] + "...",
sig.SizeBytes.ToString(),
sig.CfgBbCount?.ToString() ?? "-");
}
AnsiConsole.Write(table);
}
return 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Error extracting signatures from {BinaryPath}", binaryPath);
AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}");
return 1;
}
}
/// <summary>
/// Handle author command - create signatures by comparing vulnerable and patched binaries.
/// </summary>
public static async Task<int> HandleAuthorAsync(
IServiceProvider services,
string vulnPath,
string patchedPath,
string cve,
string package,
string? soname,
string arch,
string abi,
string outputDir,
bool verbose,
CancellationToken ct)
{
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("deltasig-author");
if (!File.Exists(vulnPath))
{
AnsiConsole.MarkupLine($"[red]Error:[/] Vulnerable binary not found: {vulnPath}");
return 1;
}
if (!File.Exists(patchedPath))
{
AnsiConsole.MarkupLine($"[red]Error:[/] Patched binary not found: {patchedPath}");
return 1;
}
try
{
var sigGenerator = services.GetRequiredService<IDeltaSignatureGenerator>();
Directory.CreateDirectory(outputDir);
if (verbose)
{
AnsiConsole.MarkupLine($"[dim]Loading vulnerable binary: {vulnPath}[/]");
AnsiConsole.MarkupLine($"[dim]Loading patched binary: {patchedPath}[/]");
}
// Generate vulnerable signature
await using var vulnStream = File.OpenRead(vulnPath);
var vulnRequest = new DeltaSignatureRequest
{
Cve = cve,
Package = package,
Soname = soname,
Arch = arch,
Abi = abi,
TargetSymbols = [], // Will detect automatically
SignatureState = "vulnerable"
};
var vulnSig = await sigGenerator.GenerateSignaturesAsync(vulnStream, vulnRequest, ct);
if (verbose)
{
AnsiConsole.MarkupLine($"[green]Vulnerable signature:[/] {vulnSig.Symbols.Length} symbols");
}
// Generate patched signature
await using var patchedStream = File.OpenRead(patchedPath);
var patchedRequest = new DeltaSignatureRequest
{
Cve = cve,
Package = package,
Soname = soname,
Arch = arch,
Abi = abi,
TargetSymbols = vulnSig.Symbols.Select(s => s.Name).ToArray(),
SignatureState = "patched"
};
var patchedSig = await sigGenerator.GenerateSignaturesAsync(patchedStream, patchedRequest, ct);
if (verbose)
{
AnsiConsole.MarkupLine($"[green]Patched signature:[/] {patchedSig.Symbols.Length} symbols");
}
// Find differing symbols
var vulnHashes = vulnSig.Symbols.ToDictionary(s => s.Name, s => s.HashHex);
var differingSymbols = patchedSig.Symbols
.Where(s => vulnHashes.TryGetValue(s.Name, out var vulnHash) && vulnHash != s.HashHex)
.Select(s => s.Name)
.ToImmutableArray();
if (verbose)
{
AnsiConsole.MarkupLine($"[yellow]Differing symbols:[/] {differingSymbols.Length}");
foreach (var sym in differingSymbols)
{
AnsiConsole.MarkupLine($" - {sym}");
}
}
// Write output files
var vulnOutPath = Path.Combine(outputDir, $"{cve}_{arch}_vulnerable.json");
var patchedOutPath = Path.Combine(outputDir, $"{cve}_{arch}_patched.json");
await File.WriteAllTextAsync(vulnOutPath, JsonSerializer.Serialize(vulnSig, JsonOptions), ct);
await File.WriteAllTextAsync(patchedOutPath, JsonSerializer.Serialize(patchedSig, JsonOptions), ct);
AnsiConsole.MarkupLine($"[green]Signatures written:[/]");
AnsiConsole.MarkupLine($" Vulnerable: {vulnOutPath}");
AnsiConsole.MarkupLine($" Patched: {patchedOutPath}");
AnsiConsole.MarkupLine($" Differing symbols: {differingSymbols.Length}");
return 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Error authoring signatures for {Cve}", cve);
AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}");
return 1;
}
}
/// <summary>
/// Handle sign command - sign a signature payload with DSSE.
/// </summary>
public static async Task<int> HandleSignAsync(
IServiceProvider services,
string inputPath,
string keyPath,
string outputPath,
string algorithm,
bool verbose,
CancellationToken ct)
{
if (!File.Exists(inputPath))
{
AnsiConsole.MarkupLine($"[red]Error:[/] Input file not found: {inputPath}");
return 1;
}
if (!File.Exists(keyPath))
{
AnsiConsole.MarkupLine($"[red]Error:[/] Key file not found: {keyPath}");
return 1;
}
try
{
var payloadJson = await File.ReadAllTextAsync(inputPath, ct);
var payloadBytes = System.Text.Encoding.UTF8.GetBytes(payloadJson);
if (verbose)
{
AnsiConsole.MarkupLine($"[dim]Payload size: {payloadBytes.Length} bytes[/]");
AnsiConsole.MarkupLine($"[dim]Algorithm: {algorithm}[/]");
}
// TODO: Integrate with StellaOps.Attestor.Envelope for real DSSE signing
// For now, create a placeholder envelope structure
var envelope = new
{
payloadType = "application/vnd.stellaops.deltasig.v1+json",
payload = Convert.ToBase64String(payloadBytes),
signatures = new[]
{
new
{
keyid = "placeholder-key-id",
sig = Convert.ToBase64String(new byte[64]) // Placeholder signature
}
}
};
var envelopeJson = JsonSerializer.Serialize(envelope, JsonOptions);
await File.WriteAllTextAsync(outputPath, envelopeJson, ct);
AnsiConsole.MarkupLine($"[green]DSSE envelope written to:[/] {outputPath}");
AnsiConsole.MarkupLine($"[yellow]Note:[/] Using placeholder signature - integrate with StellaOps.Attestor for production.");
return 0;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}");
return 1;
}
}
/// <summary>
/// Handle verify command - verify a DSSE-signed envelope.
/// </summary>
public static async Task<int> HandleVerifyAsync(
IServiceProvider services,
string inputPath,
string pubKeyPath,
bool verbose,
CancellationToken ct)
{
if (!File.Exists(inputPath))
{
AnsiConsole.MarkupLine($"[red]Error:[/] Input file not found: {inputPath}");
return 1;
}
if (!File.Exists(pubKeyPath))
{
AnsiConsole.MarkupLine($"[red]Error:[/] Public key file not found: {pubKeyPath}");
return 1;
}
try
{
var envelopeJson = await File.ReadAllTextAsync(inputPath, ct);
if (verbose)
{
AnsiConsole.MarkupLine($"[dim]Envelope size: {envelopeJson.Length} bytes[/]");
}
// TODO: Integrate with StellaOps.Attestor.Envelope for real DSSE verification
// For now, just parse and report structure
var envelope = JsonSerializer.Deserialize<JsonElement>(envelopeJson);
var payloadType = envelope.GetProperty("payloadType").GetString();
var hasSignatures = envelope.TryGetProperty("signatures", out var sigs);
AnsiConsole.MarkupLine($"[green]Envelope structure:[/]");
AnsiConsole.MarkupLine($" Payload type: {payloadType}");
AnsiConsole.MarkupLine($" Signatures: {(hasSignatures ? sigs.GetArrayLength().ToString() : "0")}");
AnsiConsole.MarkupLine($"[yellow]Note:[/] Using placeholder verification - integrate with StellaOps.Attestor for production.");
return 0;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}");
return 1;
}
}
/// <summary>
/// Handle match command - match a binary against signature packs.
/// </summary>
public static async Task<int> HandleMatchAsync(
IServiceProvider services,
string binaryPath,
string sigpackPath,
string? cveFilter,
bool json,
bool verbose,
CancellationToken ct)
{
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("deltasig-match");
if (!File.Exists(binaryPath))
{
AnsiConsole.MarkupLine($"[red]Error:[/] Binary file not found: {binaryPath}");
return 1;
}
if (!File.Exists(sigpackPath) && !Directory.Exists(sigpackPath))
{
AnsiConsole.MarkupLine($"[red]Error:[/] Signature pack not found: {sigpackPath}");
return 1;
}
try
{
var matcher = services.GetRequiredService<IDeltaSignatureMatcher>();
if (verbose)
{
AnsiConsole.MarkupLine($"[dim]Loading binary: {binaryPath}[/]");
}
// Load binary
var binaryBytes = await File.ReadAllBytesAsync(binaryPath, ct);
// Load signatures
var signatures = await LoadSignaturesAsync(sigpackPath, cveFilter, ct);
if (verbose)
{
AnsiConsole.MarkupLine($"[dim]Loaded {signatures.Count} signatures[/]");
}
// Match
using var binaryStream = new MemoryStream(binaryBytes);
var results = await matcher.MatchAsync(binaryStream, signatures, cveFilter, ct);
// Output results
var matchedResults = results.Where(r => r.Matched).ToList();
if (json)
{
var output = new
{
binary = Path.GetFileName(binaryPath),
matches = matchedResults.Select(r => new
{
cve = r.Cve,
state = r.SignatureState,
confidence = r.Confidence,
explanation = r.Explanation
})
};
Console.WriteLine(JsonSerializer.Serialize(output, JsonOptions));
}
else
{
if (matchedResults.Count == 0)
{
AnsiConsole.MarkupLine("[green]No matches found.[/]");
}
else
{
var table = new Table();
table.AddColumn("CVE");
table.AddColumn("State");
table.AddColumn("Confidence");
table.AddColumn("Explanation");
foreach (var r in matchedResults)
{
var stateColor = r.SignatureState == "vulnerable" ? "red" : "green";
table.AddRow(
r.Cve ?? "-",
$"[{stateColor}]{r.SignatureState}[/]",
$"{r.Confidence:P0}",
r.Explanation ?? "-");
}
AnsiConsole.Write(table);
}
}
// Exit code: 2 if vulnerable matches found
return matchedResults.Any(r => r.SignatureState == "vulnerable") ? 2 : 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Error matching binary {BinaryPath}", binaryPath);
AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}");
return 1;
}
}
/// <summary>
/// Handle pack command - create a signature pack from individual files.
/// </summary>
public static async Task<int> HandlePackAsync(
IServiceProvider services,
string inputDir,
string outputPath,
string? packId,
bool verbose,
CancellationToken ct)
{
if (!Directory.Exists(inputDir))
{
AnsiConsole.MarkupLine($"[red]Error:[/] Input directory not found: {inputDir}");
return 1;
}
try
{
var sigFiles = Directory.GetFiles(inputDir, "*.json", SearchOption.AllDirectories);
if (sigFiles.Length == 0)
{
AnsiConsole.MarkupLine("[yellow]Warning:[/] No signature files found in directory.");
return 0;
}
if (verbose)
{
AnsiConsole.MarkupLine($"[dim]Found {sigFiles.Length} signature files[/]");
}
// Create ZIP archive
packId ??= $"stellaops-deltasig-{DateTime.UtcNow:yyyyMMdd-HHmmss}";
await using var zipStream = File.Create(outputPath);
using var archive = new ZipArchive(zipStream, ZipArchiveMode.Create);
// Add manifest
var manifest = new
{
packId,
createdAt = DateTime.UtcNow.ToString("o"),
version = "1.0.0",
signatureCount = sigFiles.Length
};
var manifestEntry = archive.CreateEntry("manifest.json");
await using (var manifestStream = manifestEntry.Open())
{
await JsonSerializer.SerializeAsync(manifestStream, manifest, JsonOptions, ct);
}
// Add signature files
foreach (var sigFile in sigFiles)
{
ct.ThrowIfCancellationRequested();
var relativePath = Path.GetRelativePath(inputDir, sigFile);
var entry = archive.CreateEntry(relativePath);
await using var sigStream = File.OpenRead(sigFile);
await using var entryStream = entry.Open();
await sigStream.CopyToAsync(entryStream, ct);
if (verbose)
{
AnsiConsole.MarkupLine($"[dim]Added: {relativePath}[/]");
}
}
AnsiConsole.MarkupLine($"[green]Signature pack created:[/] {outputPath}");
AnsiConsole.MarkupLine($" Pack ID: {packId}");
AnsiConsole.MarkupLine($" Signatures: {sigFiles.Length}");
return 0;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}");
return 1;
}
}
/// <summary>
/// Handle inspect command - inspect a signature file or envelope.
/// </summary>
public static async Task<int> HandleInspectAsync(
IServiceProvider services,
string inputPath,
bool json,
bool verbose,
CancellationToken ct)
{
if (!File.Exists(inputPath))
{
AnsiConsole.MarkupLine($"[red]Error:[/] File not found: {inputPath}");
return 1;
}
try
{
var content = await File.ReadAllTextAsync(inputPath, ct);
var doc = JsonSerializer.Deserialize<JsonElement>(content);
// Detect file type
var isDsse = doc.TryGetProperty("payloadType", out _);
var isDeltaSig = doc.TryGetProperty("schema", out var schema) &&
schema.GetString()?.Contains("deltasig") == true;
if (json)
{
// Just pretty-print the JSON
Console.WriteLine(JsonSerializer.Serialize(doc, JsonOptions));
}
else if (isDsse)
{
AnsiConsole.MarkupLine("[bold]DSSE Envelope[/]");
AnsiConsole.MarkupLine($" Payload Type: {doc.GetProperty("payloadType").GetString()}");
if (doc.TryGetProperty("signatures", out var sigs))
{
AnsiConsole.MarkupLine($" Signatures: {sigs.GetArrayLength()}");
foreach (var sig in sigs.EnumerateArray())
{
var keyid = sig.GetProperty("keyid").GetString();
AnsiConsole.MarkupLine($" - Key ID: {keyid}");
}
}
if (doc.TryGetProperty("payload", out var payloadB64))
{
var payloadBytes = Convert.FromBase64String(payloadB64.GetString()!);
var payload = JsonSerializer.Deserialize<JsonElement>(payloadBytes);
if (payload.TryGetProperty("cve", out var cve))
{
AnsiConsole.MarkupLine($" CVE: {cve.GetString()}");
}
if (payload.TryGetProperty("signatureState", out var state))
{
AnsiConsole.MarkupLine($" State: {state.GetString()}");
}
if (payload.TryGetProperty("symbols", out var symbols))
{
AnsiConsole.MarkupLine($" Symbols: {symbols.GetArrayLength()}");
}
}
}
else if (isDeltaSig)
{
var sig = JsonSerializer.Deserialize<DeltaSignature>(content, JsonOptions)!;
AnsiConsole.MarkupLine("[bold]Delta Signature[/]");
AnsiConsole.MarkupLine($" Schema: {sig.Schema} v{sig.SchemaVersion}");
AnsiConsole.MarkupLine($" CVE: {sig.Cve}");
AnsiConsole.MarkupLine($" Package: {sig.Package.Name}");
AnsiConsole.MarkupLine($" State: {sig.SignatureState}");
AnsiConsole.MarkupLine($" Target: {sig.Target.Arch}/{sig.Target.Abi}");
AnsiConsole.MarkupLine($" Symbols: {sig.Symbols.Length}");
if (verbose)
{
var table = new Table();
table.AddColumn("Symbol");
table.AddColumn("Hash");
table.AddColumn("Size");
table.AddColumn("BB Count");
foreach (var sym in sig.Symbols)
{
table.AddRow(
sym.Name,
sym.HashHex[..Math.Min(16, sym.HashHex.Length)] + "...",
sym.SizeBytes.ToString(),
sym.CfgBbCount?.ToString() ?? "-");
}
AnsiConsole.Write(table);
}
}
else
{
AnsiConsole.MarkupLine("[yellow]Unknown file format[/]");
Console.WriteLine(JsonSerializer.Serialize(doc, JsonOptions));
}
return 0;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}");
return 1;
}
}
/// <summary>
/// Load signatures from a file, directory, or ZIP archive.
/// </summary>
private static async Task<List<DeltaSignature>> LoadSignaturesAsync(
string path,
string? cveFilter,
CancellationToken ct)
{
var signatures = new List<DeltaSignature>();
if (Directory.Exists(path))
{
// Load from directory
foreach (var file in Directory.GetFiles(path, "*.json", SearchOption.AllDirectories))
{
ct.ThrowIfCancellationRequested();
var sig = await LoadSignatureFileAsync(file, ct);
if (sig != null && (cveFilter == null || sig.Cve == cveFilter))
{
signatures.Add(sig);
}
}
}
else if (path.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
{
// Load from ZIP
using var archive = ZipFile.OpenRead(path);
foreach (var entry in archive.Entries.Where(e => e.Name.EndsWith(".json")))
{
ct.ThrowIfCancellationRequested();
if (entry.Name == "manifest.json") continue;
await using var stream = entry.Open();
using var reader = new StreamReader(stream);
var content = await reader.ReadToEndAsync(ct);
try
{
var sig = JsonSerializer.Deserialize<DeltaSignature>(content, JsonOptions);
if (sig != null && (cveFilter == null || sig.Cve == cveFilter))
{
signatures.Add(sig);
}
}
catch
{
// Skip non-signature files
}
}
}
else
{
// Single file
var sig = await LoadSignatureFileAsync(path, ct);
if (sig != null && (cveFilter == null || sig.Cve == cveFilter))
{
signatures.Add(sig);
}
}
return signatures;
}
private static async Task<DeltaSignature?> LoadSignatureFileAsync(string path, CancellationToken ct)
{
try
{
var content = await File.ReadAllTextAsync(path, ct);
return JsonSerializer.Deserialize<DeltaSignature>(content, JsonOptions);
}
catch
{
return null;
}
}
}

View File

@@ -85,6 +85,10 @@
<ProjectReference Include="../../__Libraries/StellaOps.AuditPack/StellaOps.AuditPack.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Provcache/StellaOps.Provcache.csproj" />
<ProjectReference Include="../../Signer/StellaOps.Signer/StellaOps.Signer.Infrastructure/StellaOps.Signer.Infrastructure.csproj" />
<!-- Binary Delta Signatures (SPRINT_20260102_001_BE) -->
<ProjectReference Include="../../BinaryIndex/__Libraries/StellaOps.BinaryIndex.Disassembly/StellaOps.BinaryIndex.Disassembly.csproj" />
<ProjectReference Include="../../BinaryIndex/__Libraries/StellaOps.BinaryIndex.Normalization/StellaOps.BinaryIndex.Normalization.csproj" />
<ProjectReference Include="../../BinaryIndex/__Libraries/StellaOps.BinaryIndex.DeltaSig/StellaOps.BinaryIndex.DeltaSig.csproj" />
</ItemGroup>
<!-- GOST Crypto Plugins (Russia distribution) -->