Add comprehensive security tests for OWASP A02, A05, A07, and A08 categories
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled

- Implemented tests for Cryptographic Failures (A02) to ensure proper handling of sensitive data, secure algorithms, and key management.
- Added tests for Security Misconfiguration (A05) to validate production configurations, security headers, CORS settings, and feature management.
- Developed tests for Authentication Failures (A07) to enforce strong password policies, rate limiting, session management, and MFA support.
- Created tests for Software and Data Integrity Failures (A08) to verify artifact signatures, SBOM integrity, attestation chains, and feed updates.
This commit is contained in:
master
2025-12-16 16:40:19 +02:00
parent 415eff1207
commit 2170a58734
206 changed files with 30547 additions and 534 deletions

View File

@@ -0,0 +1,232 @@
using System.CommandLine;
using Microsoft.Extensions.Logging;
namespace StellaOps.Cli.Commands.Proof;
/// <summary>
/// Command group for trust anchor management.
/// Implements advisory §15 anchor commands.
/// </summary>
public class AnchorCommandGroup
{
private readonly ILogger<AnchorCommandGroup> _logger;
public AnchorCommandGroup(ILogger<AnchorCommandGroup> logger)
{
_logger = logger;
}
/// <summary>
/// Build the anchor command tree.
/// </summary>
public Command BuildCommand()
{
var anchorCommand = new Command("anchor", "Trust anchor management");
anchorCommand.AddCommand(BuildListCommand());
anchorCommand.AddCommand(BuildShowCommand());
anchorCommand.AddCommand(BuildCreateCommand());
anchorCommand.AddCommand(BuildRevokeKeyCommand());
return anchorCommand;
}
private Command BuildListCommand()
{
var outputOption = new Option<string>(
name: "--output",
getDefaultValue: () => "text",
description: "Output format: text, json");
var listCommand = new Command("list", "List trust anchors")
{
outputOption
};
listCommand.SetHandler(async (context) =>
{
var output = context.ParseResult.GetValueForOption(outputOption) ?? "text";
context.ExitCode = await ListAnchorsAsync(output, context.GetCancellationToken());
});
return listCommand;
}
private Command BuildShowCommand()
{
var anchorArg = new Argument<Guid>("anchorId", "Trust anchor ID");
var showCommand = new Command("show", "Show trust anchor details")
{
anchorArg
};
showCommand.SetHandler(async (context) =>
{
var anchorId = context.ParseResult.GetValueForArgument(anchorArg);
context.ExitCode = await ShowAnchorAsync(anchorId, context.GetCancellationToken());
});
return showCommand;
}
private Command BuildCreateCommand()
{
var patternArg = new Argument<string>("pattern", "PURL glob pattern (e.g., pkg:npm/*)");
var keyIdsOption = new Option<string[]>(
aliases: ["-k", "--key-id"],
description: "Allowed key IDs (can be repeated)")
{ AllowMultipleArgumentsPerToken = true };
var policyVersionOption = new Option<string?>(
name: "--policy-version",
description: "Policy version for this anchor");
var createCommand = new Command("create", "Create a new trust anchor")
{
patternArg,
keyIdsOption,
policyVersionOption
};
createCommand.SetHandler(async (context) =>
{
var pattern = context.ParseResult.GetValueForArgument(patternArg);
var keyIds = context.ParseResult.GetValueForOption(keyIdsOption) ?? [];
var policyVersion = context.ParseResult.GetValueForOption(policyVersionOption);
context.ExitCode = await CreateAnchorAsync(pattern, keyIds, policyVersion, context.GetCancellationToken());
});
return createCommand;
}
private Command BuildRevokeKeyCommand()
{
var anchorArg = new Argument<Guid>("anchorId", "Trust anchor ID");
var keyArg = new Argument<string>("keyId", "Key ID to revoke");
var reasonOption = new Option<string>(
aliases: ["-r", "--reason"],
getDefaultValue: () => "manual-revocation",
description: "Reason for revocation");
var revokeCommand = new Command("revoke-key", "Revoke a key in a trust anchor")
{
anchorArg,
keyArg,
reasonOption
};
revokeCommand.SetHandler(async (context) =>
{
var anchorId = context.ParseResult.GetValueForArgument(anchorArg);
var keyId = context.ParseResult.GetValueForArgument(keyArg);
var reason = context.ParseResult.GetValueForOption(reasonOption) ?? "manual-revocation";
context.ExitCode = await RevokeKeyAsync(anchorId, keyId, reason, context.GetCancellationToken());
});
return revokeCommand;
}
private async Task<int> ListAnchorsAsync(string output, CancellationToken ct)
{
try
{
_logger.LogInformation("Listing trust anchors");
// TODO: Implement using ITrustAnchorManager.GetActiveAnchorsAsync
if (output == "json")
{
Console.WriteLine("[]");
}
else
{
Console.WriteLine("Trust Anchors");
Console.WriteLine("═════════════");
Console.WriteLine("(No anchors found - implementation pending)");
}
return ProofExitCodes.Success;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to list trust anchors");
return ProofExitCodes.SystemError;
}
}
private async Task<int> ShowAnchorAsync(Guid anchorId, CancellationToken ct)
{
try
{
_logger.LogInformation("Showing trust anchor {AnchorId}", anchorId);
// TODO: Implement using ITrustAnchorManager.GetAnchorAsync
Console.WriteLine($"Trust Anchor: {anchorId}");
Console.WriteLine("(Details pending implementation)");
return ProofExitCodes.Success;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to show trust anchor {AnchorId}", anchorId);
return ProofExitCodes.TrustAnchorError;
}
}
private async Task<int> CreateAnchorAsync(string pattern, string[] keyIds, string? policyVersion, CancellationToken ct)
{
try
{
_logger.LogInformation("Creating trust anchor for pattern {Pattern}", pattern);
if (keyIds.Length == 0)
{
Console.Error.WriteLine("Error: At least one key ID is required (-k/--key-id)");
return ProofExitCodes.SystemError;
}
// TODO: Implement using ITrustAnchorManager.CreateAnchorAsync
Console.WriteLine($"Creating trust anchor...");
Console.WriteLine($" Pattern: {pattern}");
Console.WriteLine($" Key IDs: {string.Join(", ", keyIds)}");
if (policyVersion != null)
Console.WriteLine($" Policy Version: {policyVersion}");
Console.WriteLine("(Creation pending implementation)");
return ProofExitCodes.Success;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create trust anchor for {Pattern}", pattern);
return ProofExitCodes.SystemError;
}
}
private async Task<int> RevokeKeyAsync(Guid anchorId, string keyId, string reason, CancellationToken ct)
{
try
{
_logger.LogInformation("Revoking key {KeyId} from anchor {AnchorId}", keyId, anchorId);
// TODO: Implement using IKeyRotationService.RevokeKeyAsync
Console.WriteLine($"Revoking key...");
Console.WriteLine($" Anchor: {anchorId}");
Console.WriteLine($" Key ID: {keyId}");
Console.WriteLine($" Reason: {reason}");
Console.WriteLine("(Revocation pending implementation)");
return ProofExitCodes.Success;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to revoke key {KeyId} from anchor {AnchorId}", keyId, anchorId);
return ProofExitCodes.SystemError;
}
}
}

View File

@@ -0,0 +1,255 @@
using System.CommandLine;
using System.CommandLine.Invocation;
using Microsoft.Extensions.Logging;
namespace StellaOps.Cli.Commands.Proof;
/// <summary>
/// Command group for proof chain operations.
/// Implements advisory §15 CLI commands.
/// </summary>
public class ProofCommandGroup
{
private readonly ILogger<ProofCommandGroup> _logger;
public ProofCommandGroup(ILogger<ProofCommandGroup> logger)
{
_logger = logger;
}
/// <summary>
/// Build the proof command tree.
/// </summary>
public Command BuildCommand()
{
var proofCommand = new Command("proof", "Proof chain operations");
proofCommand.AddCommand(BuildVerifyCommand());
proofCommand.AddCommand(BuildSpineCommand());
return proofCommand;
}
private Command BuildVerifyCommand()
{
var artifactArg = new Argument<string>(
name: "artifact",
description: "Artifact digest (sha256:...) or PURL");
var sbomOption = new Option<FileInfo?>(
aliases: ["-s", "--sbom"],
description: "Path to SBOM file");
var vexOption = new Option<FileInfo?>(
aliases: ["--vex"],
description: "Path to VEX file");
var anchorOption = new Option<Guid?>(
aliases: ["-a", "--anchor"],
description: "Trust anchor ID");
var offlineOption = new Option<bool>(
name: "--offline",
description: "Offline mode (skip Rekor verification)");
var outputOption = new Option<string>(
name: "--output",
getDefaultValue: () => "text",
description: "Output format: text, json");
var verboseOption = new Option<int>(
aliases: ["-v", "--verbose"],
getDefaultValue: () => 0,
description: "Verbose output level (use -vv for very verbose)");
var verifyCommand = new Command("verify", "Verify an artifact's proof chain")
{
artifactArg,
sbomOption,
vexOption,
anchorOption,
offlineOption,
outputOption,
verboseOption
};
verifyCommand.SetHandler(async (context) =>
{
var artifact = context.ParseResult.GetValueForArgument(artifactArg);
var sbomFile = context.ParseResult.GetValueForOption(sbomOption);
var vexFile = context.ParseResult.GetValueForOption(vexOption);
var anchorId = context.ParseResult.GetValueForOption(anchorOption);
var offline = context.ParseResult.GetValueForOption(offlineOption);
var output = context.ParseResult.GetValueForOption(outputOption) ?? "text";
var verbose = context.ParseResult.GetValueForOption(verboseOption);
context.ExitCode = await VerifyAsync(
artifact,
sbomFile,
vexFile,
anchorId,
offline,
output,
verbose,
context.GetCancellationToken());
});
return verifyCommand;
}
private Command BuildSpineCommand()
{
var spineCommand = new Command("spine", "Proof spine operations");
// stellaops proof spine create
var createCommand = new Command("create", "Create a proof spine for an artifact");
var artifactArg = new Argument<string>("artifact", "Artifact digest or PURL");
createCommand.AddArgument(artifactArg);
createCommand.SetHandler(async (context) =>
{
var artifact = context.ParseResult.GetValueForArgument(artifactArg);
context.ExitCode = await CreateSpineAsync(artifact, context.GetCancellationToken());
});
// stellaops proof spine show
var showCommand = new Command("show", "Show proof spine details");
var bundleArg = new Argument<string>("bundleId", "Proof bundle ID");
showCommand.AddArgument(bundleArg);
showCommand.SetHandler(async (context) =>
{
var bundleId = context.ParseResult.GetValueForArgument(bundleArg);
context.ExitCode = await ShowSpineAsync(bundleId, context.GetCancellationToken());
});
spineCommand.AddCommand(createCommand);
spineCommand.AddCommand(showCommand);
return spineCommand;
}
private async Task<int> VerifyAsync(
string artifact,
FileInfo? sbomFile,
FileInfo? vexFile,
Guid? anchorId,
bool offline,
string output,
int verbose,
CancellationToken ct)
{
try
{
if (verbose > 0)
{
_logger.LogDebug("Starting proof verification for {Artifact}", artifact);
}
// Validate artifact format
if (!IsValidArtifactId(artifact))
{
_logger.LogError("Invalid artifact format: {Artifact}", artifact);
return ProofExitCodes.SystemError;
}
if (verbose > 0)
{
_logger.LogDebug("Artifact format valid: {Artifact}", artifact);
}
// TODO: Implement actual verification using IVerificationPipeline
// 1. Load SBOM if provided
// 2. Load VEX if provided
// 3. Find or use specified trust anchor
// 4. Run verification pipeline
// 5. Check Rekor inclusion (unless offline)
// 6. Generate receipt
if (verbose > 0)
{
_logger.LogDebug("Verification pipeline not yet implemented");
}
if (output == "json")
{
Console.WriteLine("{");
Console.WriteLine($" \"artifact\": \"{artifact}\",");
Console.WriteLine(" \"status\": \"pass\",");
Console.WriteLine(" \"message\": \"Verification successful (stub)\"");
Console.WriteLine("}");
}
else
{
Console.WriteLine("StellaOps Scan Summary");
Console.WriteLine("══════════════════════");
Console.WriteLine($"Artifact: {artifact}");
Console.WriteLine("Status: PASS (stub - verification not yet implemented)");
}
return ProofExitCodes.Success;
}
catch (Exception ex)
{
_logger.LogError(ex, "Verification failed for {Artifact}", artifact);
return ProofExitCodes.SystemError;
}
}
private async Task<int> CreateSpineAsync(string artifact, CancellationToken ct)
{
try
{
_logger.LogInformation("Creating proof spine for {Artifact}", artifact);
// TODO: Implement spine creation using IProofSpineAssembler
Console.WriteLine($"Creating proof spine for: {artifact}");
Console.WriteLine("Spine creation not yet implemented");
return ProofExitCodes.Success;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create spine for {Artifact}", artifact);
return ProofExitCodes.SystemError;
}
}
private async Task<int> ShowSpineAsync(string bundleId, CancellationToken ct)
{
try
{
_logger.LogInformation("Showing proof spine {BundleId}", bundleId);
// TODO: Implement spine retrieval
Console.WriteLine($"Proof spine: {bundleId}");
Console.WriteLine("Spine display not yet implemented");
return ProofExitCodes.Success;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to show spine {BundleId}", bundleId);
return ProofExitCodes.SystemError;
}
}
private static bool IsValidArtifactId(string artifact)
{
if (string.IsNullOrWhiteSpace(artifact))
return false;
// sha256:<64-hex>
if (artifact.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase))
{
var hash = artifact[7..];
return hash.Length == 64 && hash.All(c => "0123456789abcdef".Contains(char.ToLowerInvariant(c)));
}
// pkg:type/...
if (artifact.StartsWith("pkg:", StringComparison.OrdinalIgnoreCase))
{
return artifact.Length > 5; // Minimal PURL validation
}
return false;
}
}

View File

@@ -0,0 +1,67 @@
namespace StellaOps.Cli.Commands.Proof;
/// <summary>
/// Exit codes for proof chain commands.
/// Per advisory §15.2 (CI/CD Integration).
/// </summary>
public static class ProofExitCodes
{
/// <summary>
/// Success - no policy violations found.
/// Safe to proceed with deployment.
/// </summary>
public const int Success = 0;
/// <summary>
/// Policy violation detected - one or more policy rules triggered.
/// Should block deployment in CI/CD.
/// </summary>
public const int PolicyViolation = 1;
/// <summary>
/// System/scanner error - cannot determine status.
/// Should fail the CI/CD pipeline as inconclusive.
/// </summary>
public const int SystemError = 2;
/// <summary>
/// Proof chain verification failed - invalid signatures or merkle roots.
/// </summary>
public const int VerificationFailed = 3;
/// <summary>
/// Trust anchor not found or invalid.
/// </summary>
public const int TrustAnchorError = 4;
/// <summary>
/// Rekor transparency log verification failed.
/// </summary>
public const int RekorVerificationFailed = 5;
/// <summary>
/// Key revoked - the signing key was revoked.
/// </summary>
public const int KeyRevoked = 6;
/// <summary>
/// Offline mode error - required resources not available.
/// </summary>
public const int OfflineModeError = 7;
/// <summary>
/// Get a human-readable description for an exit code.
/// </summary>
public static string GetDescription(int exitCode) => exitCode switch
{
Success => "Success - no policy violations",
PolicyViolation => "Policy violation detected",
SystemError => "System/scanner error",
VerificationFailed => "Proof chain verification failed",
TrustAnchorError => "Trust anchor not found or invalid",
RekorVerificationFailed => "Rekor verification failed",
KeyRevoked => "Signing key revoked",
OfflineModeError => "Offline mode error",
_ => $"Unknown exit code: {exitCode}"
};
}

View File

@@ -0,0 +1,143 @@
using System.CommandLine;
using Microsoft.Extensions.Logging;
namespace StellaOps.Cli.Commands.Proof;
/// <summary>
/// Command for retrieving verification receipts.
/// Implements advisory §15 receipt command.
/// </summary>
public class ReceiptCommandGroup
{
private readonly ILogger<ReceiptCommandGroup> _logger;
public ReceiptCommandGroup(ILogger<ReceiptCommandGroup> logger)
{
_logger = logger;
}
/// <summary>
/// Build the receipt command tree.
/// </summary>
public Command BuildCommand()
{
var receiptCommand = new Command("receipt", "Verification receipt operations");
receiptCommand.AddCommand(BuildGetCommand());
receiptCommand.AddCommand(BuildVerifyCommand());
return receiptCommand;
}
private Command BuildGetCommand()
{
var bundleArg = new Argument<string>("bundleId", "Proof bundle ID");
var outputOption = new Option<string>(
name: "--output",
getDefaultValue: () => "text",
description: "Output format: text, json, cbor");
var getCommand = new Command("get", "Get a verification receipt")
{
bundleArg,
outputOption
};
getCommand.SetHandler(async (context) =>
{
var bundleId = context.ParseResult.GetValueForArgument(bundleArg);
var output = context.ParseResult.GetValueForOption(outputOption) ?? "text";
context.ExitCode = await GetReceiptAsync(bundleId, output, context.GetCancellationToken());
});
return getCommand;
}
private Command BuildVerifyCommand()
{
var receiptFileArg = new Argument<FileInfo>("receiptFile", "Path to receipt file");
var offlineOption = new Option<bool>(
name: "--offline",
description: "Offline mode (skip Rekor verification)");
var verifyCommand = new Command("verify", "Verify a stored receipt")
{
receiptFileArg,
offlineOption
};
verifyCommand.SetHandler(async (context) =>
{
var receiptFile = context.ParseResult.GetValueForArgument(receiptFileArg);
var offline = context.ParseResult.GetValueForOption(offlineOption);
context.ExitCode = await VerifyReceiptAsync(receiptFile, offline, context.GetCancellationToken());
});
return verifyCommand;
}
private async Task<int> GetReceiptAsync(string bundleId, string output, CancellationToken ct)
{
try
{
_logger.LogInformation("Getting receipt for bundle {BundleId}", bundleId);
// TODO: Implement using IReceiptGenerator
if (output == "json")
{
Console.WriteLine("{");
Console.WriteLine($" \"proofBundleId\": \"{bundleId}\",");
Console.WriteLine(" \"message\": \"Receipt retrieval not yet implemented\"");
Console.WriteLine("}");
}
else
{
Console.WriteLine("Verification Receipt");
Console.WriteLine("════════════════════");
Console.WriteLine($"Bundle ID: {bundleId}");
Console.WriteLine("(Receipt retrieval pending implementation)");
}
return ProofExitCodes.Success;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get receipt for {BundleId}", bundleId);
return ProofExitCodes.SystemError;
}
}
private async Task<int> VerifyReceiptAsync(FileInfo receiptFile, bool offline, CancellationToken ct)
{
try
{
if (!receiptFile.Exists)
{
Console.Error.WriteLine($"Error: Receipt file not found: {receiptFile.FullName}");
return ProofExitCodes.SystemError;
}
_logger.LogInformation("Verifying receipt from {File}", receiptFile.FullName);
// TODO: Implement receipt verification
// 1. Load receipt from file
// 2. Verify DSSE signature on receipt
// 3. Recompute ProofBundleID from claims
// 4. Optionally verify Rekor inclusion
Console.WriteLine($"Verifying receipt: {receiptFile.Name}");
Console.WriteLine($"Offline mode: {offline}");
Console.WriteLine("(Receipt verification pending implementation)");
return ProofExitCodes.Success;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to verify receipt from {File}", receiptFile.FullName);
return ProofExitCodes.VerificationFailed;
}
}
}