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:
StellaOps Bot
2025-12-26 15:17:15 +02:00
parent 7792749bb4
commit 907783f625
354 changed files with 79727 additions and 1346 deletions

View 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;
}
}

View 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;
}