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:
232
src/Cli/StellaOps.Cli/Commands/SignCommandGroup.cs
Normal file
232
src/Cli/StellaOps.Cli/Commands/SignCommandGroup.cs
Normal file
@@ -0,0 +1,232 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_20251226_007_BE_determinism_gaps
|
||||
// Task: DET-GAP-08 - CLI command `stella sign --keyless --rekor` for CI pipelines
|
||||
|
||||
using System.CommandLine;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace StellaOps.Cli.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// CLI commands for Sigstore keyless signing operations.
|
||||
/// Supports self-hosted Sigstore (Fulcio + Rekor) for on-premise deployments.
|
||||
/// </summary>
|
||||
internal static class SignCommandGroup
|
||||
{
|
||||
/// <summary>
|
||||
/// Build the sign command with keyless/traditional subcommands.
|
||||
/// </summary>
|
||||
public static Command BuildSignCommand(
|
||||
IServiceProvider serviceProvider,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var command = new Command("sign", "Sign artifacts (keyless via Sigstore or traditional key-based)");
|
||||
|
||||
command.Add(BuildKeylessCommand(serviceProvider, verboseOption, cancellationToken));
|
||||
command.Add(BuildVerifyKeylessCommand(serviceProvider, verboseOption, cancellationToken));
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private static Command BuildKeylessCommand(
|
||||
IServiceProvider serviceProvider,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var command = new Command("keyless", "Sign artifact using Sigstore keyless signing (Fulcio + Rekor)");
|
||||
|
||||
var inputOption = new Option<string>("--input")
|
||||
{
|
||||
Description = "Path to file or artifact to sign",
|
||||
Required = true
|
||||
};
|
||||
command.Add(inputOption);
|
||||
|
||||
var outputOption = new Option<string?>("--output")
|
||||
{
|
||||
Description = "Output path for signature bundle (defaults to <input>.sigstore)"
|
||||
};
|
||||
command.Add(outputOption);
|
||||
|
||||
var identityTokenOption = new Option<string?>("--identity-token")
|
||||
{
|
||||
Description = "OIDC identity token (JWT). If not provided, attempts ambient credential detection."
|
||||
};
|
||||
command.Add(identityTokenOption);
|
||||
|
||||
var rekorOption = new Option<bool>("--rekor")
|
||||
{
|
||||
Description = "Upload signature to Rekor transparency log (default: true)",
|
||||
DefaultValue = true
|
||||
};
|
||||
command.Add(rekorOption);
|
||||
|
||||
var fulcioUrlOption = new Option<string?>("--fulcio-url")
|
||||
{
|
||||
Description = "Override Fulcio URL (for self-hosted Sigstore)"
|
||||
};
|
||||
command.Add(fulcioUrlOption);
|
||||
|
||||
var rekorUrlOption = new Option<string?>("--rekor-url")
|
||||
{
|
||||
Description = "Override Rekor URL (for self-hosted Sigstore)"
|
||||
};
|
||||
command.Add(rekorUrlOption);
|
||||
|
||||
var oidcIssuerOption = new Option<string?>("--oidc-issuer")
|
||||
{
|
||||
Description = "OIDC issuer URL for identity verification"
|
||||
};
|
||||
command.Add(oidcIssuerOption);
|
||||
|
||||
var bundleFormatOption = new Option<string>("--bundle-format")
|
||||
{
|
||||
Description = "Output bundle format: sigstore, cosign-bundle, dsse (default: sigstore)",
|
||||
DefaultValue = "sigstore"
|
||||
};
|
||||
command.Add(bundleFormatOption);
|
||||
|
||||
var caBundleOption = new Option<string?>("--ca-bundle")
|
||||
{
|
||||
Description = "Path to custom CA certificate bundle for self-hosted TLS"
|
||||
};
|
||||
command.Add(caBundleOption);
|
||||
|
||||
var insecureOption = new Option<bool>("--insecure-skip-verify")
|
||||
{
|
||||
Description = "Skip TLS verification (NOT for production)",
|
||||
DefaultValue = false
|
||||
};
|
||||
command.Add(insecureOption);
|
||||
|
||||
command.Add(verboseOption);
|
||||
|
||||
command.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var input = parseResult.GetValue(inputOption) ?? string.Empty;
|
||||
var output = parseResult.GetValue(outputOption);
|
||||
var identityToken = parseResult.GetValue(identityTokenOption);
|
||||
var useRekor = parseResult.GetValue(rekorOption);
|
||||
var fulcioUrl = parseResult.GetValue(fulcioUrlOption);
|
||||
var rekorUrl = parseResult.GetValue(rekorUrlOption);
|
||||
var oidcIssuer = parseResult.GetValue(oidcIssuerOption);
|
||||
var bundleFormat = parseResult.GetValue(bundleFormatOption) ?? "sigstore";
|
||||
var caBundle = parseResult.GetValue(caBundleOption);
|
||||
var insecure = parseResult.GetValue(insecureOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return await CommandHandlers.HandleSignKeylessAsync(
|
||||
serviceProvider,
|
||||
input,
|
||||
output,
|
||||
identityToken,
|
||||
useRekor,
|
||||
fulcioUrl,
|
||||
rekorUrl,
|
||||
oidcIssuer,
|
||||
bundleFormat,
|
||||
caBundle,
|
||||
insecure,
|
||||
verbose,
|
||||
ct);
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private static Command BuildVerifyKeylessCommand(
|
||||
IServiceProvider serviceProvider,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var command = new Command("verify-keyless", "Verify a keyless signature against Sigstore");
|
||||
|
||||
var inputOption = new Option<string>("--input")
|
||||
{
|
||||
Description = "Path to file or artifact to verify",
|
||||
Required = true
|
||||
};
|
||||
command.Add(inputOption);
|
||||
|
||||
var bundleOption = new Option<string?>("--bundle")
|
||||
{
|
||||
Description = "Path to Sigstore bundle file (defaults to <input>.sigstore)"
|
||||
};
|
||||
command.Add(bundleOption);
|
||||
|
||||
var certificateOption = new Option<string?>("--certificate")
|
||||
{
|
||||
Description = "Path to signing certificate (PEM format)"
|
||||
};
|
||||
command.Add(certificateOption);
|
||||
|
||||
var signatureOption = new Option<string?>("--signature")
|
||||
{
|
||||
Description = "Path to detached signature"
|
||||
};
|
||||
command.Add(signatureOption);
|
||||
|
||||
var rekorUuidOption = new Option<string?>("--rekor-uuid")
|
||||
{
|
||||
Description = "Rekor entry UUID for transparency verification"
|
||||
};
|
||||
command.Add(rekorUuidOption);
|
||||
|
||||
var rekorUrlOption = new Option<string?>("--rekor-url")
|
||||
{
|
||||
Description = "Override Rekor URL (for self-hosted Sigstore)"
|
||||
};
|
||||
command.Add(rekorUrlOption);
|
||||
|
||||
var issuerOption = new Option<string?>("--certificate-issuer")
|
||||
{
|
||||
Description = "Expected OIDC issuer in certificate"
|
||||
};
|
||||
command.Add(issuerOption);
|
||||
|
||||
var subjectOption = new Option<string?>("--certificate-subject")
|
||||
{
|
||||
Description = "Expected subject (email/identity) in certificate"
|
||||
};
|
||||
command.Add(subjectOption);
|
||||
|
||||
var caBundleOption = new Option<string?>("--ca-bundle")
|
||||
{
|
||||
Description = "Path to custom CA certificate bundle for self-hosted TLS"
|
||||
};
|
||||
command.Add(caBundleOption);
|
||||
|
||||
command.Add(verboseOption);
|
||||
|
||||
command.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var input = parseResult.GetValue(inputOption) ?? string.Empty;
|
||||
var bundle = parseResult.GetValue(bundleOption);
|
||||
var certificate = parseResult.GetValue(certificateOption);
|
||||
var signature = parseResult.GetValue(signatureOption);
|
||||
var rekorUuid = parseResult.GetValue(rekorUuidOption);
|
||||
var rekorUrl = parseResult.GetValue(rekorUrlOption);
|
||||
var issuer = parseResult.GetValue(issuerOption);
|
||||
var subject = parseResult.GetValue(subjectOption);
|
||||
var caBundle = parseResult.GetValue(caBundleOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return await CommandHandlers.HandleVerifyKeylessAsync(
|
||||
serviceProvider,
|
||||
input,
|
||||
bundle,
|
||||
certificate,
|
||||
signature,
|
||||
rekorUuid,
|
||||
rekorUrl,
|
||||
issuer,
|
||||
subject,
|
||||
caBundle,
|
||||
verbose,
|
||||
ct);
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user