Add property-based tests for SBOM/VEX document ordering and Unicode normalization determinism
- Implement `SbomVexOrderingDeterminismProperties` for testing component list and vulnerability metadata hash consistency. - Create `UnicodeNormalizationDeterminismProperties` to validate NFC normalization and Unicode string handling. - Add project file for `StellaOps.Testing.Determinism.Properties` with necessary dependencies. - Introduce CI/CD template validation tests including YAML syntax checks and documentation content verification. - Create validation script for CI/CD templates ensuring all required files and structures are present.
This commit is contained in:
289
src/Cli/StellaOps.Cli/Commands/Proof/FuncProofCommandGroup.cs
Normal file
289
src/Cli/StellaOps.Cli/Commands/Proof/FuncProofCommandGroup.cs
Normal file
@@ -0,0 +1,289 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// FuncProofCommandGroup.cs
|
||||
// Sprint: SPRINT_20251226_009_SCANNER_funcproof
|
||||
// Tasks: FUNC-16, FUNC-17
|
||||
// Description: CLI commands for function-level proof generation and verification.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.CommandLine;
|
||||
using StellaOps.Cli.Extensions;
|
||||
|
||||
namespace StellaOps.Cli.Commands.Proof;
|
||||
|
||||
/// <summary>
|
||||
/// CLI command group for function-level proof operations.
|
||||
/// Enables binary composition attestation and auditor replay verification.
|
||||
/// </summary>
|
||||
internal static class FuncProofCommandGroup
|
||||
{
|
||||
internal static Command BuildFuncProofCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var funcproof = new Command("funcproof", "Function-level proof operations for binary reachability evidence.");
|
||||
|
||||
funcproof.Add(BuildGenerateCommand(services, verboseOption, cancellationToken));
|
||||
funcproof.Add(BuildVerifyCommand(services, verboseOption, cancellationToken));
|
||||
funcproof.Add(BuildInfoCommand(services, verboseOption, cancellationToken));
|
||||
funcproof.Add(BuildExportCommand(services, verboseOption, cancellationToken));
|
||||
|
||||
return funcproof;
|
||||
}
|
||||
|
||||
private static Command BuildGenerateCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var binaryOption = new Option<string>("--binary", new[] { "-b" })
|
||||
{
|
||||
Description = "Path to binary file for function analysis.",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var buildIdOption = new Option<string?>("--build-id")
|
||||
{
|
||||
Description = "Build identifier (e.g., git commit SHA). Auto-detected from ELF if not specified."
|
||||
};
|
||||
|
||||
var signOption = new Option<bool>("--sign")
|
||||
{
|
||||
Description = "Sign the FuncProof with DSSE envelope."
|
||||
};
|
||||
|
||||
var transparencyOption = new Option<bool>("--transparency")
|
||||
{
|
||||
Description = "Submit signed FuncProof to Rekor transparency log."
|
||||
};
|
||||
|
||||
var registryOption = new Option<string?>("--registry", new[] { "-r" })
|
||||
{
|
||||
Description = "OCI registry to push FuncProof as referrer artifact (e.g., ghcr.io/myorg/proofs)."
|
||||
};
|
||||
|
||||
var subjectOption = new Option<string?>("--subject")
|
||||
{
|
||||
Description = "Subject digest for OCI referrer relationship (sha256:...)."
|
||||
};
|
||||
|
||||
var outputOption = new Option<string?>("--output", new[] { "-o" })
|
||||
{
|
||||
Description = "Output path for the generated FuncProof JSON. Defaults to stdout."
|
||||
};
|
||||
|
||||
var formatOption = new Option<string>("--format", new[] { "-f" })
|
||||
{
|
||||
Description = "Output format: json (default), summary."
|
||||
}.SetDefaultValue("json").FromAmong("json", "summary");
|
||||
|
||||
var detectMethodOption = new Option<string>("--detect-method")
|
||||
{
|
||||
Description = "Function detection method: auto (default), dwarf, symbols, heuristic."
|
||||
}.SetDefaultValue("auto").FromAmong("auto", "dwarf", "symbols", "heuristic");
|
||||
|
||||
var command = new Command("generate", "Generate function-level proof from a binary.")
|
||||
{
|
||||
binaryOption,
|
||||
buildIdOption,
|
||||
signOption,
|
||||
transparencyOption,
|
||||
registryOption,
|
||||
subjectOption,
|
||||
outputOption,
|
||||
formatOption,
|
||||
detectMethodOption,
|
||||
verboseOption
|
||||
};
|
||||
|
||||
command.SetAction(parseResult =>
|
||||
{
|
||||
var binaryPath = parseResult.GetValue(binaryOption) ?? string.Empty;
|
||||
var buildId = parseResult.GetValue(buildIdOption);
|
||||
var sign = parseResult.GetValue(signOption);
|
||||
var transparency = parseResult.GetValue(transparencyOption);
|
||||
var registry = parseResult.GetValue(registryOption);
|
||||
var subject = parseResult.GetValue(subjectOption);
|
||||
var output = parseResult.GetValue(outputOption);
|
||||
var format = parseResult.GetValue(formatOption) ?? "json";
|
||||
var detectMethod = parseResult.GetValue(detectMethodOption) ?? "auto";
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return FuncProofCommandHandlers.HandleGenerateAsync(
|
||||
services,
|
||||
binaryPath,
|
||||
buildId,
|
||||
sign,
|
||||
transparency,
|
||||
registry,
|
||||
subject,
|
||||
output,
|
||||
format,
|
||||
detectMethod,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private static Command BuildVerifyCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var proofOption = new Option<string>("--proof", new[] { "-p" })
|
||||
{
|
||||
Description = "Path to FuncProof JSON file or DSSE envelope.",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var binaryOption = new Option<string?>("--binary", new[] { "-b" })
|
||||
{
|
||||
Description = "Path to binary file for replay verification (optional, enables full replay)."
|
||||
};
|
||||
|
||||
var offlineOption = new Option<bool>("--offline")
|
||||
{
|
||||
Description = "Offline mode (skip transparency log verification)."
|
||||
};
|
||||
|
||||
var strictOption = new Option<bool>("--strict")
|
||||
{
|
||||
Description = "Strict mode (fail on any untrusted signature or missing evidence)."
|
||||
};
|
||||
|
||||
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 function-level proof and optionally replay against binary.")
|
||||
{
|
||||
proofOption,
|
||||
binaryOption,
|
||||
offlineOption,
|
||||
strictOption,
|
||||
formatOption,
|
||||
verboseOption
|
||||
};
|
||||
|
||||
command.SetAction(parseResult =>
|
||||
{
|
||||
var proofPath = parseResult.GetValue(proofOption) ?? string.Empty;
|
||||
var binaryPath = parseResult.GetValue(binaryOption);
|
||||
var offline = parseResult.GetValue(offlineOption);
|
||||
var strict = parseResult.GetValue(strictOption);
|
||||
var format = parseResult.GetValue(formatOption) ?? "text";
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return FuncProofCommandHandlers.HandleVerifyAsync(
|
||||
services,
|
||||
proofPath,
|
||||
binaryPath,
|
||||
offline,
|
||||
strict,
|
||||
format,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private static Command BuildInfoCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var proofArg = new Argument<string>("proof")
|
||||
{
|
||||
Description = "FuncProof ID, file path, or OCI reference."
|
||||
};
|
||||
|
||||
var formatOption = new Option<string>("--format", new[] { "-f" })
|
||||
{
|
||||
Description = "Output format: text (default), json."
|
||||
}.SetDefaultValue("text").FromAmong("text", "json");
|
||||
|
||||
var command = new Command("info", "Display FuncProof information and statistics.")
|
||||
{
|
||||
proofArg,
|
||||
formatOption,
|
||||
verboseOption
|
||||
};
|
||||
|
||||
command.SetAction(parseResult =>
|
||||
{
|
||||
var proof = parseResult.GetValue(proofArg)!;
|
||||
var format = parseResult.GetValue(formatOption)!;
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return FuncProofCommandHandlers.HandleInfoAsync(
|
||||
services,
|
||||
proof,
|
||||
format,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private static Command BuildExportCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var proofArg = new Argument<string>("proof")
|
||||
{
|
||||
Description = "FuncProof ID, file path, or OCI reference."
|
||||
};
|
||||
|
||||
var outputOption = new Option<string>("--output", new[] { "-o" })
|
||||
{
|
||||
Description = "Output directory for exported artifacts.",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var formatOption = new Option<string>("--format", new[] { "-f" })
|
||||
{
|
||||
Description = "Export format: bundle (default), evidence-locker."
|
||||
}.SetDefaultValue("bundle").FromAmong("bundle", "evidence-locker");
|
||||
|
||||
var includeOption = new Option<string[]>("--include", new[] { "-i" })
|
||||
{
|
||||
Description = "Include additional artifacts: dsse, tlog-receipt, raw-proof.",
|
||||
AllowMultipleArgumentsPerToken = true
|
||||
};
|
||||
|
||||
var command = new Command("export", "Export FuncProof and related artifacts.")
|
||||
{
|
||||
proofArg,
|
||||
outputOption,
|
||||
formatOption,
|
||||
includeOption,
|
||||
verboseOption
|
||||
};
|
||||
|
||||
command.SetAction(parseResult =>
|
||||
{
|
||||
var proof = parseResult.GetValue(proofArg)!;
|
||||
var output = parseResult.GetValue(outputOption)!;
|
||||
var format = parseResult.GetValue(formatOption)!;
|
||||
var include = parseResult.GetValue(includeOption) ?? Array.Empty<string>();
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return FuncProofCommandHandlers.HandleExportAsync(
|
||||
services,
|
||||
proof,
|
||||
output,
|
||||
format,
|
||||
include,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
}
|
||||
570
src/Cli/StellaOps.Cli/Commands/Proof/FuncProofCommandHandlers.cs
Normal file
570
src/Cli/StellaOps.Cli/Commands/Proof/FuncProofCommandHandlers.cs
Normal file
@@ -0,0 +1,570 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// FuncProofCommandHandlers.cs
|
||||
// Sprint: SPRINT_20251226_009_SCANNER_funcproof
|
||||
// Tasks: FUNC-16, FUNC-17
|
||||
// Description: CLI command handlers for function-level proof operations.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Cli.Commands.Proof;
|
||||
|
||||
/// <summary>
|
||||
/// Command handlers for FuncProof CLI operations.
|
||||
/// </summary>
|
||||
internal static class FuncProofCommandHandlers
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Generate a FuncProof from a binary file.
|
||||
/// </summary>
|
||||
public static async Task<int> HandleGenerateAsync(
|
||||
IServiceProvider services,
|
||||
string binaryPath,
|
||||
string? buildId,
|
||||
bool sign,
|
||||
bool transparency,
|
||||
string? registry,
|
||||
string? subject,
|
||||
string? output,
|
||||
string format,
|
||||
string detectMethod,
|
||||
bool verbose,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var logger = services.GetRequiredService<ILogger<FuncProofCommandGroup>>();
|
||||
|
||||
if (!File.Exists(binaryPath))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Binary file not found: {binaryPath}");
|
||||
return FuncProofExitCodes.FileNotFound;
|
||||
}
|
||||
|
||||
logger.LogInformation("Generating FuncProof for {BinaryPath}", binaryPath);
|
||||
|
||||
try
|
||||
{
|
||||
// Read binary and compute file hash
|
||||
var binaryBytes = await File.ReadAllBytesAsync(binaryPath, ct);
|
||||
var fileSha256 = ComputeSha256(binaryBytes);
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
Console.WriteLine($"Binary: {binaryPath}");
|
||||
Console.WriteLine($"Size: {binaryBytes.Length:N0} bytes");
|
||||
Console.WriteLine($"SHA-256: {fileSha256}");
|
||||
}
|
||||
|
||||
// TODO: Integrate with FunctionBoundaryDetector and FuncProofBuilder
|
||||
// For now, create a placeholder proof structure
|
||||
var proof = new FuncProofOutput
|
||||
{
|
||||
SchemaVersion = "1.0.0",
|
||||
ProofId = $"funcproof-{fileSha256[..16]}",
|
||||
BuildId = buildId ?? ExtractBuildId(binaryBytes) ?? "unknown",
|
||||
FileSha256 = fileSha256,
|
||||
FileSize = binaryBytes.Length,
|
||||
FunctionCount = 0, // Placeholder
|
||||
Metadata = new FuncProofMetadataOutput
|
||||
{
|
||||
CreatedAt = DateTimeOffset.UtcNow.ToString("O"),
|
||||
Tool = "stella-cli",
|
||||
ToolVersion = "0.1.0",
|
||||
DetectionMethod = detectMethod
|
||||
}
|
||||
};
|
||||
|
||||
if (format == "summary")
|
||||
{
|
||||
WriteSummary(proof);
|
||||
}
|
||||
else
|
||||
{
|
||||
var json = JsonSerializer.Serialize(proof, JsonOptions);
|
||||
if (string.IsNullOrEmpty(output))
|
||||
{
|
||||
Console.WriteLine(json);
|
||||
}
|
||||
else
|
||||
{
|
||||
await File.WriteAllTextAsync(output, json, ct);
|
||||
Console.WriteLine($"FuncProof written to: {output}");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle signing
|
||||
if (sign)
|
||||
{
|
||||
logger.LogInformation("Signing FuncProof with DSSE envelope");
|
||||
// TODO: Integrate with FuncProofDsseService
|
||||
Console.WriteLine("DSSE signing: enabled (integration pending)");
|
||||
}
|
||||
|
||||
// Handle transparency log submission
|
||||
if (transparency)
|
||||
{
|
||||
if (!sign)
|
||||
{
|
||||
Console.Error.WriteLine("Error: --transparency requires --sign");
|
||||
return FuncProofExitCodes.InvalidArguments;
|
||||
}
|
||||
logger.LogInformation("Submitting to transparency log");
|
||||
// TODO: Integrate with FuncProofTransparencyService
|
||||
Console.WriteLine("Transparency log: submission pending");
|
||||
}
|
||||
|
||||
// Handle OCI registry push
|
||||
if (!string.IsNullOrEmpty(registry))
|
||||
{
|
||||
if (string.IsNullOrEmpty(subject))
|
||||
{
|
||||
Console.Error.WriteLine("Error: --registry requires --subject for referrer relationship");
|
||||
return FuncProofExitCodes.InvalidArguments;
|
||||
}
|
||||
logger.LogInformation("Pushing FuncProof to OCI registry {Registry}", registry);
|
||||
// TODO: Integrate with FuncProofOciPublisher
|
||||
Console.WriteLine($"OCI push: to {registry} (integration pending)");
|
||||
}
|
||||
|
||||
return FuncProofExitCodes.Success;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "FuncProof generation failed");
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return FuncProofExitCodes.GenerationFailed;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify a FuncProof document.
|
||||
/// </summary>
|
||||
public static async Task<int> HandleVerifyAsync(
|
||||
IServiceProvider services,
|
||||
string proofPath,
|
||||
string? binaryPath,
|
||||
bool offline,
|
||||
bool strict,
|
||||
string format,
|
||||
bool verbose,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var logger = services.GetRequiredService<ILogger<FuncProofCommandGroup>>();
|
||||
|
||||
if (!File.Exists(proofPath))
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Proof file not found: {proofPath}");
|
||||
return FuncProofExitCodes.FileNotFound;
|
||||
}
|
||||
|
||||
logger.LogInformation("Verifying FuncProof: {ProofPath}", proofPath);
|
||||
|
||||
try
|
||||
{
|
||||
var proofJson = await File.ReadAllTextAsync(proofPath, ct);
|
||||
var proof = JsonSerializer.Deserialize<FuncProofOutput>(proofJson, JsonOptions);
|
||||
|
||||
if (proof is null)
|
||||
{
|
||||
Console.Error.WriteLine("Error: Invalid FuncProof JSON");
|
||||
return FuncProofExitCodes.InvalidProof;
|
||||
}
|
||||
|
||||
var result = new VerificationResult
|
||||
{
|
||||
ProofId = proof.ProofId ?? "unknown",
|
||||
IsValid = true,
|
||||
Checks = new List<VerificationCheck>()
|
||||
};
|
||||
|
||||
// Schema validation
|
||||
result.Checks.Add(new VerificationCheck
|
||||
{
|
||||
Name = "schema",
|
||||
Status = !string.IsNullOrEmpty(proof.SchemaVersion) ? "pass" : "fail",
|
||||
Details = $"Schema version: {proof.SchemaVersion ?? "missing"}"
|
||||
});
|
||||
|
||||
// Proof ID validation
|
||||
result.Checks.Add(new VerificationCheck
|
||||
{
|
||||
Name = "proof_id",
|
||||
Status = !string.IsNullOrEmpty(proof.ProofId) ? "pass" : "fail",
|
||||
Details = $"Proof ID: {proof.ProofId ?? "missing"}"
|
||||
});
|
||||
|
||||
// File hash validation (if binary provided)
|
||||
if (!string.IsNullOrEmpty(binaryPath))
|
||||
{
|
||||
if (File.Exists(binaryPath))
|
||||
{
|
||||
var binaryBytes = await File.ReadAllBytesAsync(binaryPath, ct);
|
||||
var computedHash = ComputeSha256(binaryBytes);
|
||||
var hashMatch = string.Equals(computedHash, proof.FileSha256, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
result.Checks.Add(new VerificationCheck
|
||||
{
|
||||
Name = "file_hash",
|
||||
Status = hashMatch ? "pass" : "fail",
|
||||
Details = hashMatch
|
||||
? $"File hash matches: {computedHash[..16]}..."
|
||||
: $"Hash mismatch: expected {proof.FileSha256?[..16]}..., got {computedHash[..16]}..."
|
||||
});
|
||||
|
||||
if (!hashMatch)
|
||||
{
|
||||
result.IsValid = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Checks.Add(new VerificationCheck
|
||||
{
|
||||
Name = "file_hash",
|
||||
Status = "skip",
|
||||
Details = "Binary file not found for replay verification"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Signature validation
|
||||
// TODO: Integrate with FuncProofDsseService
|
||||
result.Checks.Add(new VerificationCheck
|
||||
{
|
||||
Name = "signature",
|
||||
Status = "skip",
|
||||
Details = "DSSE signature verification: integration pending"
|
||||
});
|
||||
|
||||
// Transparency log validation
|
||||
if (!offline)
|
||||
{
|
||||
// TODO: Integrate with FuncProofTransparencyService
|
||||
result.Checks.Add(new VerificationCheck
|
||||
{
|
||||
Name = "transparency",
|
||||
Status = "skip",
|
||||
Details = "Transparency log verification: integration pending"
|
||||
});
|
||||
}
|
||||
|
||||
// Output results
|
||||
if (format == "json")
|
||||
{
|
||||
Console.WriteLine(JsonSerializer.Serialize(result, JsonOptions));
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteVerificationText(result, verbose);
|
||||
}
|
||||
|
||||
// Determine exit code
|
||||
if (!result.IsValid)
|
||||
{
|
||||
return FuncProofExitCodes.VerificationFailed;
|
||||
}
|
||||
|
||||
if (strict && result.Checks.Any(c => c.Status == "skip"))
|
||||
{
|
||||
Console.Error.WriteLine("Warning: Some checks were skipped (strict mode)");
|
||||
return FuncProofExitCodes.StrictChecksFailed;
|
||||
}
|
||||
|
||||
return FuncProofExitCodes.Success;
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Invalid JSON in proof file: {ex.Message}");
|
||||
return FuncProofExitCodes.InvalidProof;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "FuncProof verification failed");
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return FuncProofExitCodes.VerificationFailed;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display FuncProof information.
|
||||
/// </summary>
|
||||
public static async Task<int> HandleInfoAsync(
|
||||
IServiceProvider services,
|
||||
string proof,
|
||||
string format,
|
||||
bool verbose,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var logger = services.GetRequiredService<ILogger<FuncProofCommandGroup>>();
|
||||
|
||||
try
|
||||
{
|
||||
FuncProofOutput? proofData = null;
|
||||
|
||||
// Try to load from file
|
||||
if (File.Exists(proof))
|
||||
{
|
||||
var json = await File.ReadAllTextAsync(proof, ct);
|
||||
proofData = JsonSerializer.Deserialize<FuncProofOutput>(json, JsonOptions);
|
||||
}
|
||||
// TODO: Add support for loading by ID from database or OCI registry
|
||||
|
||||
if (proofData is null)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Could not load FuncProof: {proof}");
|
||||
return FuncProofExitCodes.FileNotFound;
|
||||
}
|
||||
|
||||
if (format == "json")
|
||||
{
|
||||
Console.WriteLine(JsonSerializer.Serialize(proofData, JsonOptions));
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteInfo(proofData, verbose);
|
||||
}
|
||||
|
||||
return FuncProofExitCodes.Success;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to load FuncProof info");
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return FuncProofExitCodes.GenerationFailed;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Export FuncProof and related artifacts.
|
||||
/// </summary>
|
||||
public static async Task<int> HandleExportAsync(
|
||||
IServiceProvider services,
|
||||
string proof,
|
||||
string output,
|
||||
string format,
|
||||
string[] include,
|
||||
bool verbose,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var logger = services.GetRequiredService<ILogger<FuncProofCommandGroup>>();
|
||||
|
||||
try
|
||||
{
|
||||
FuncProofOutput? proofData = null;
|
||||
|
||||
// Try to load from file
|
||||
if (File.Exists(proof))
|
||||
{
|
||||
var json = await File.ReadAllTextAsync(proof, ct);
|
||||
proofData = JsonSerializer.Deserialize<FuncProofOutput>(json, JsonOptions);
|
||||
}
|
||||
|
||||
if (proofData is null)
|
||||
{
|
||||
Console.Error.WriteLine($"Error: Could not load FuncProof: {proof}");
|
||||
return FuncProofExitCodes.FileNotFound;
|
||||
}
|
||||
|
||||
// Create output directory
|
||||
Directory.CreateDirectory(output);
|
||||
|
||||
// Write main proof file
|
||||
var proofPath = Path.Combine(output, $"{proofData.ProofId ?? "funcproof"}.json");
|
||||
await File.WriteAllTextAsync(proofPath, JsonSerializer.Serialize(proofData, JsonOptions), ct);
|
||||
Console.WriteLine($"Exported: {proofPath}");
|
||||
|
||||
// Handle additional includes
|
||||
foreach (var inc in include)
|
||||
{
|
||||
switch (inc.ToLowerInvariant())
|
||||
{
|
||||
case "dsse":
|
||||
// TODO: Export DSSE envelope
|
||||
Console.WriteLine("DSSE envelope export: integration pending");
|
||||
break;
|
||||
case "tlog-receipt":
|
||||
// TODO: Export transparency log receipt
|
||||
Console.WriteLine("Transparency log receipt export: integration pending");
|
||||
break;
|
||||
case "raw-proof":
|
||||
// Raw proof is the main export
|
||||
break;
|
||||
default:
|
||||
Console.Error.WriteLine($"Warning: Unknown include option: {inc}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Write manifest
|
||||
var manifest = new ExportManifest
|
||||
{
|
||||
ExportedAt = DateTimeOffset.UtcNow.ToString("O"),
|
||||
Format = format,
|
||||
ProofId = proofData.ProofId,
|
||||
Files = new List<string> { Path.GetFileName(proofPath) }
|
||||
};
|
||||
var manifestPath = Path.Combine(output, "manifest.json");
|
||||
await File.WriteAllTextAsync(manifestPath, JsonSerializer.Serialize(manifest, JsonOptions), ct);
|
||||
|
||||
Console.WriteLine($"Export complete: {output}");
|
||||
return FuncProofExitCodes.Success;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "FuncProof export failed");
|
||||
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||
return FuncProofExitCodes.GenerationFailed;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ComputeSha256(byte[] data)
|
||||
{
|
||||
var hash = System.Security.Cryptography.SHA256.HashData(data);
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string? ExtractBuildId(byte[] binary)
|
||||
{
|
||||
// Simple ELF build-id extraction (looks for .note.gnu.build-id section)
|
||||
// Full implementation in BinaryIdentity.cs
|
||||
if (binary.Length < 16)
|
||||
return null;
|
||||
|
||||
// Check ELF magic
|
||||
if (binary[0] == 0x7f && binary[1] == 'E' && binary[2] == 'L' && binary[3] == 'F')
|
||||
{
|
||||
// ELF file - would need full section parsing for build-id
|
||||
return null; // Placeholder
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void WriteSummary(FuncProofOutput proof)
|
||||
{
|
||||
Console.WriteLine("FuncProof Summary");
|
||||
Console.WriteLine(new string('=', 50));
|
||||
Console.WriteLine($" Proof ID: {proof.ProofId ?? "N/A"}");
|
||||
Console.WriteLine($" Build ID: {proof.BuildId ?? "N/A"}");
|
||||
Console.WriteLine($" File SHA-256: {proof.FileSha256?[..16]}...");
|
||||
Console.WriteLine($" File Size: {proof.FileSize:N0} bytes");
|
||||
Console.WriteLine($" Functions: {proof.FunctionCount:N0}");
|
||||
Console.WriteLine($" Created: {proof.Metadata?.CreatedAt ?? "N/A"}");
|
||||
Console.WriteLine($" Tool: {proof.Metadata?.Tool ?? "N/A"} {proof.Metadata?.ToolVersion ?? ""}");
|
||||
}
|
||||
|
||||
private static void WriteInfo(FuncProofOutput proof, bool verbose)
|
||||
{
|
||||
Console.WriteLine("FuncProof Information");
|
||||
Console.WriteLine(new string('=', 50));
|
||||
Console.WriteLine($" Proof ID: {proof.ProofId ?? "N/A"}");
|
||||
Console.WriteLine($" Schema Version: {proof.SchemaVersion ?? "N/A"}");
|
||||
Console.WriteLine($" Build ID: {proof.BuildId ?? "N/A"}");
|
||||
Console.WriteLine($" File SHA-256: {proof.FileSha256 ?? "N/A"}");
|
||||
Console.WriteLine($" File Size: {proof.FileSize:N0} bytes");
|
||||
Console.WriteLine($" Functions: {proof.FunctionCount:N0}");
|
||||
|
||||
if (verbose && proof.Metadata is not null)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Metadata:");
|
||||
Console.WriteLine($" Created: {proof.Metadata.CreatedAt ?? "N/A"}");
|
||||
Console.WriteLine($" Tool: {proof.Metadata.Tool ?? "N/A"}");
|
||||
Console.WriteLine($" Tool Version: {proof.Metadata.ToolVersion ?? "N/A"}");
|
||||
Console.WriteLine($" Detection: {proof.Metadata.DetectionMethod ?? "N/A"}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteVerificationText(VerificationResult result, bool verbose)
|
||||
{
|
||||
var statusSymbol = result.IsValid ? "✓" : "✗";
|
||||
Console.WriteLine($"FuncProof Verification: {statusSymbol} {(result.IsValid ? "PASSED" : "FAILED")}");
|
||||
Console.WriteLine(new string('=', 50));
|
||||
Console.WriteLine($" Proof ID: {result.ProofId}");
|
||||
Console.WriteLine();
|
||||
|
||||
foreach (var check in result.Checks)
|
||||
{
|
||||
var checkSymbol = check.Status switch
|
||||
{
|
||||
"pass" => "✓",
|
||||
"fail" => "✗",
|
||||
"skip" => "○",
|
||||
_ => "?"
|
||||
};
|
||||
Console.WriteLine($" {checkSymbol} {check.Name}: {check.Status}");
|
||||
if (verbose && !string.IsNullOrEmpty(check.Details))
|
||||
{
|
||||
Console.WriteLine($" {check.Details}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region DTOs
|
||||
|
||||
private sealed class FuncProofOutput
|
||||
{
|
||||
public string? SchemaVersion { get; set; }
|
||||
public string? ProofId { get; set; }
|
||||
public string? BuildId { get; set; }
|
||||
public string? FileSha256 { get; set; }
|
||||
public long FileSize { get; set; }
|
||||
public int FunctionCount { get; set; }
|
||||
public FuncProofMetadataOutput? Metadata { get; set; }
|
||||
}
|
||||
|
||||
private sealed class FuncProofMetadataOutput
|
||||
{
|
||||
public string? CreatedAt { get; set; }
|
||||
public string? Tool { get; set; }
|
||||
public string? ToolVersion { get; set; }
|
||||
public string? DetectionMethod { get; set; }
|
||||
}
|
||||
|
||||
private sealed class VerificationResult
|
||||
{
|
||||
public string ProofId { get; set; } = string.Empty;
|
||||
public bool IsValid { get; set; }
|
||||
public List<VerificationCheck> Checks { get; set; } = new();
|
||||
}
|
||||
|
||||
private sealed class VerificationCheck
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Status { get; set; } = string.Empty;
|
||||
public string? Details { get; set; }
|
||||
}
|
||||
|
||||
private sealed class ExportManifest
|
||||
{
|
||||
public string? ExportedAt { get; set; }
|
||||
public string? Format { get; set; }
|
||||
public string? ProofId { get; set; }
|
||||
public List<string>? Files { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exit codes for FuncProof CLI commands.
|
||||
/// </summary>
|
||||
internal static class FuncProofExitCodes
|
||||
{
|
||||
public const int Success = 0;
|
||||
public const int FileNotFound = 1;
|
||||
public const int InvalidArguments = 2;
|
||||
public const int GenerationFailed = 3;
|
||||
public const int InvalidProof = 4;
|
||||
public const int VerificationFailed = 5;
|
||||
public const int StrictChecksFailed = 6;
|
||||
}
|
||||
Reference in New Issue
Block a user