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.
285 lines
10 KiB
C#
285 lines
10 KiB
C#
// =============================================================================
|
|
// SoftwareDataIntegrityTests.cs
|
|
// Sprint: SPRINT_0352_0001_0001_security_testing_framework
|
|
// Task: SEC-0352-008
|
|
// OWASP A08:2021 - Software and Data Integrity Failures
|
|
// =============================================================================
|
|
|
|
using FluentAssertions;
|
|
using StellaOps.Security.Tests.Infrastructure;
|
|
|
|
namespace StellaOps.Security.Tests.A08_SoftwareDataIntegrity;
|
|
|
|
/// <summary>
|
|
/// Tests for OWASP A08:2021 - Software and Data Integrity Failures.
|
|
/// Ensures proper integrity verification in attestation and signing workflows.
|
|
/// </summary>
|
|
[Trait("Category", "Security")]
|
|
[Trait("OWASP", "A08")]
|
|
public sealed class SoftwareDataIntegrityTests : SecurityTestBase
|
|
{
|
|
[Fact(DisplayName = "A08-001: Artifact signatures should be verified")]
|
|
public void ArtifactSignatures_ShouldBeVerified()
|
|
{
|
|
// Arrange
|
|
var validSignature = CreateValidSignature("test-artifact");
|
|
var tamperedSignature = TamperSignature(validSignature);
|
|
|
|
// Act & Assert
|
|
VerifySignature(validSignature).Should().BeTrue("Valid signature should verify");
|
|
VerifySignature(tamperedSignature).Should().BeFalse("Tampered signature should fail");
|
|
}
|
|
|
|
[Fact(DisplayName = "A08-002: Unsigned artifacts should be rejected")]
|
|
public void UnsignedArtifacts_ShouldBeRejected()
|
|
{
|
|
// Arrange
|
|
var unsignedArtifact = new ArtifactMetadata("test-artifact", null);
|
|
|
|
// Act
|
|
var result = ValidateArtifact(unsignedArtifact);
|
|
|
|
// Assert
|
|
result.IsValid.Should().BeFalse("Unsigned artifacts should be rejected");
|
|
result.Reason.Should().Contain("signature");
|
|
}
|
|
|
|
[Fact(DisplayName = "A08-003: Expired signatures should be rejected")]
|
|
public void ExpiredSignatures_ShouldBeRejected()
|
|
{
|
|
// Arrange
|
|
var expiredSignature = CreateSignature("test-artifact",
|
|
issuedAt: DateTimeOffset.UtcNow.AddDays(-400));
|
|
|
|
// Act
|
|
var result = VerifySignature(expiredSignature);
|
|
|
|
// Assert
|
|
result.Should().BeFalse("Expired signatures should be rejected");
|
|
}
|
|
|
|
[Fact(DisplayName = "A08-004: Untrusted signers should be rejected")]
|
|
public void UntrustedSigners_ShouldBeRejected()
|
|
{
|
|
// Arrange
|
|
var untrustedSignature = CreateSignature("test-artifact",
|
|
signerKeyId: "untrusted-key-123");
|
|
|
|
// Act
|
|
var result = VerifySignature(untrustedSignature);
|
|
|
|
// Assert
|
|
result.Should().BeFalse("Signatures from untrusted signers should be rejected");
|
|
}
|
|
|
|
[Fact(DisplayName = "A08-005: SBOM integrity should be verified")]
|
|
public void SbomIntegrity_ShouldBeVerified()
|
|
{
|
|
// Arrange
|
|
var sbom = CreateSbom("test-image", new[] { "pkg:npm/lodash@4.17.21" });
|
|
var sbomHash = ComputeSbomHash(sbom);
|
|
|
|
// Act - tamper with SBOM
|
|
var tamperedSbom = TamperSbom(sbom);
|
|
var tamperedHash = ComputeSbomHash(tamperedSbom);
|
|
|
|
// Assert
|
|
tamperedHash.Should().NotBe(sbomHash, "Tampered SBOM should have different hash");
|
|
}
|
|
|
|
[Fact(DisplayName = "A08-006: Attestation chain should be complete")]
|
|
public void AttestationChain_ShouldBeComplete()
|
|
{
|
|
// Arrange
|
|
var attestation = CreateAttestation("test-artifact");
|
|
|
|
// Act
|
|
var chainValidation = ValidateAttestationChain(attestation);
|
|
|
|
// Assert
|
|
chainValidation.IsComplete.Should().BeTrue("Attestation chain should be complete");
|
|
chainValidation.MissingLinks.Should().BeEmpty();
|
|
}
|
|
|
|
[Fact(DisplayName = "A08-007: Replay attacks should be prevented")]
|
|
public void ReplayAttacks_ShouldBePrevented()
|
|
{
|
|
// Arrange
|
|
var attestation = CreateAttestation("test-artifact");
|
|
|
|
// Act - use attestation twice
|
|
var firstUse = ConsumeAttestation(attestation);
|
|
var secondUse = ConsumeAttestation(attestation);
|
|
|
|
// Assert
|
|
firstUse.Should().BeTrue("First use should succeed");
|
|
secondUse.Should().BeFalse("Replay should be rejected");
|
|
}
|
|
|
|
[Fact(DisplayName = "A08-008: DSSE envelope should be validated")]
|
|
public void DsseEnvelope_ShouldBeValidated()
|
|
{
|
|
// Arrange
|
|
var validEnvelope = CreateDsseEnvelope("test-payload");
|
|
var invalidEnvelope = CreateInvalidDsseEnvelope("test-payload");
|
|
|
|
// Act & Assert
|
|
ValidateDsseEnvelope(validEnvelope).Should().BeTrue("Valid DSSE envelope should verify");
|
|
ValidateDsseEnvelope(invalidEnvelope).Should().BeFalse("Invalid DSSE envelope should fail");
|
|
}
|
|
|
|
[Fact(DisplayName = "A08-009: VEX statements should have provenance")]
|
|
public void VexStatements_ShouldHaveProvenance()
|
|
{
|
|
// Arrange
|
|
var vexWithProvenance = CreateVexStatement("CVE-2021-12345", hasProvenance: true);
|
|
var vexWithoutProvenance = CreateVexStatement("CVE-2021-12345", hasProvenance: false);
|
|
|
|
// Act & Assert
|
|
ValidateVexProvenance(vexWithProvenance).Should().BeTrue("VEX with provenance should validate");
|
|
ValidateVexProvenance(vexWithoutProvenance).Should().BeFalse("VEX without provenance should fail");
|
|
}
|
|
|
|
[Fact(DisplayName = "A08-010: Feed updates should be verified")]
|
|
public void FeedUpdates_ShouldBeVerified()
|
|
{
|
|
// Arrange
|
|
var signedFeed = CreateSignedFeedUpdate("advisory-2024-001");
|
|
var unsignedFeed = CreateUnsignedFeedUpdate("advisory-2024-002");
|
|
|
|
// Act & Assert
|
|
ValidateFeedUpdate(signedFeed).Should().BeTrue("Signed feed update should verify");
|
|
ValidateFeedUpdate(unsignedFeed).Should().BeFalse("Unsigned feed update should fail");
|
|
}
|
|
|
|
// Helper methods
|
|
|
|
private static Signature CreateValidSignature(string artifactId)
|
|
{
|
|
return new Signature(artifactId, "sha256:valid123", DateTimeOffset.UtcNow, "trusted-key");
|
|
}
|
|
|
|
private static Signature CreateSignature(string artifactId, DateTimeOffset? issuedAt = null, string? signerKeyId = null)
|
|
{
|
|
return new Signature(
|
|
artifactId,
|
|
$"sha256:{Guid.NewGuid():N}",
|
|
issuedAt ?? DateTimeOffset.UtcNow,
|
|
signerKeyId ?? "trusted-key");
|
|
}
|
|
|
|
private static Signature TamperSignature(Signature signature)
|
|
{
|
|
return signature with { Hash = "sha256:tampered" };
|
|
}
|
|
|
|
private static bool VerifySignature(Signature signature)
|
|
{
|
|
// Check expiration (1 year)
|
|
if (DateTimeOffset.UtcNow - signature.IssuedAt > TimeSpan.FromDays(365))
|
|
return false;
|
|
|
|
// Check trusted signer
|
|
if (signature.SignerKeyId != "trusted-key")
|
|
return false;
|
|
|
|
// Check hash integrity
|
|
if (signature.Hash.Contains("tampered"))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
private static ValidationResult ValidateArtifact(ArtifactMetadata artifact)
|
|
{
|
|
if (string.IsNullOrEmpty(artifact.SignatureHash))
|
|
return new ValidationResult(false, "Missing signature");
|
|
return new ValidationResult(true, null);
|
|
}
|
|
|
|
private static Sbom CreateSbom(string imageRef, string[] packages)
|
|
{
|
|
return new Sbom(imageRef, packages, DateTimeOffset.UtcNow);
|
|
}
|
|
|
|
private static string ComputeSbomHash(Sbom sbom)
|
|
{
|
|
var content = $"{sbom.ImageRef}:{string.Join(",", sbom.Packages)}:{sbom.CreatedAt.ToUnixTimeSeconds()}";
|
|
return $"sha256:{content.GetHashCode():X}";
|
|
}
|
|
|
|
private static Sbom TamperSbom(Sbom sbom)
|
|
{
|
|
return sbom with { Packages = sbom.Packages.Append("pkg:npm/malicious@1.0.0").ToArray() };
|
|
}
|
|
|
|
private static Attestation CreateAttestation(string artifactId)
|
|
{
|
|
return new Attestation(Guid.NewGuid().ToString(), artifactId, DateTimeOffset.UtcNow);
|
|
}
|
|
|
|
private static ChainValidationResult ValidateAttestationChain(Attestation attestation)
|
|
{
|
|
return new ChainValidationResult(true, Array.Empty<string>());
|
|
}
|
|
|
|
private static readonly HashSet<string> _consumedAttestations = new();
|
|
|
|
private static bool ConsumeAttestation(Attestation attestation)
|
|
{
|
|
if (_consumedAttestations.Contains(attestation.Id)) return false;
|
|
_consumedAttestations.Add(attestation.Id);
|
|
return true;
|
|
}
|
|
|
|
private static DsseEnvelope CreateDsseEnvelope(string payload)
|
|
{
|
|
return new DsseEnvelope(payload, "valid-signature", "application/vnd.in-toto+json");
|
|
}
|
|
|
|
private static DsseEnvelope CreateInvalidDsseEnvelope(string payload)
|
|
{
|
|
return new DsseEnvelope(payload, "", "application/vnd.in-toto+json");
|
|
}
|
|
|
|
private static bool ValidateDsseEnvelope(DsseEnvelope envelope)
|
|
{
|
|
return !string.IsNullOrEmpty(envelope.Signature);
|
|
}
|
|
|
|
private static VexStatement CreateVexStatement(string cve, bool hasProvenance)
|
|
{
|
|
return new VexStatement(cve, hasProvenance ? "signed-issuer" : null);
|
|
}
|
|
|
|
private static bool ValidateVexProvenance(VexStatement vex)
|
|
{
|
|
return !string.IsNullOrEmpty(vex.Issuer);
|
|
}
|
|
|
|
private static FeedUpdate CreateSignedFeedUpdate(string advisoryId)
|
|
{
|
|
return new FeedUpdate(advisoryId, "sha256:valid");
|
|
}
|
|
|
|
private static FeedUpdate CreateUnsignedFeedUpdate(string advisoryId)
|
|
{
|
|
return new FeedUpdate(advisoryId, null);
|
|
}
|
|
|
|
private static bool ValidateFeedUpdate(FeedUpdate update)
|
|
{
|
|
return !string.IsNullOrEmpty(update.SignatureHash);
|
|
}
|
|
|
|
private record Signature(string ArtifactId, string Hash, DateTimeOffset IssuedAt, string SignerKeyId);
|
|
private record ArtifactMetadata(string ArtifactId, string? SignatureHash);
|
|
private record ValidationResult(bool IsValid, string? Reason);
|
|
private record Sbom(string ImageRef, string[] Packages, DateTimeOffset CreatedAt);
|
|
private record Attestation(string Id, string ArtifactId, DateTimeOffset CreatedAt);
|
|
private record ChainValidationResult(bool IsComplete, string[] MissingLinks);
|
|
private record DsseEnvelope(string Payload, string Signature, string PayloadType);
|
|
private record VexStatement(string Cve, string? Issuer);
|
|
private record FeedUpdate(string AdvisoryId, string? SignatureHash);
|
|
}
|