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
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:
@@ -0,0 +1,315 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ProofSpineAssemblyIntegrationTests.cs
|
||||
// Sprint: SPRINT_0501_0004_0001_proof_chain_spine_assembly
|
||||
// Tasks: #10, #11, #12
|
||||
// Description: Integration tests for proof spine assembly pipeline
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Text;
|
||||
using StellaOps.Attestor.ProofChain.Merkle;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.ProofChain.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for the full proof spine assembly pipeline.
|
||||
/// </summary>
|
||||
public class ProofSpineAssemblyIntegrationTests
|
||||
{
|
||||
private readonly IMerkleTreeBuilder _builder;
|
||||
|
||||
public ProofSpineAssemblyIntegrationTests()
|
||||
{
|
||||
_builder = new DeterministicMerkleTreeBuilder();
|
||||
}
|
||||
|
||||
#region Task #10: Merkle Tree Determinism Tests
|
||||
|
||||
[Fact]
|
||||
public void MerkleRoot_SameInputDifferentRuns_ProducesIdenticalRoot()
|
||||
{
|
||||
// Arrange - simulate a proof spine with SBOM, evidence, reasoning, VEX
|
||||
var sbomEntryId = "sha256:abc123...";
|
||||
var evidenceIds = new[] { "sha256:ev1...", "sha256:ev2...", "sha256:ev3..." };
|
||||
var reasoningId = "sha256:reason...";
|
||||
var vexVerdictId = "sha256:vex...";
|
||||
|
||||
// Act - compute root multiple times
|
||||
var root1 = ComputeProofSpineRoot(sbomEntryId, evidenceIds, reasoningId, vexVerdictId);
|
||||
var root2 = ComputeProofSpineRoot(sbomEntryId, evidenceIds, reasoningId, vexVerdictId);
|
||||
var root3 = ComputeProofSpineRoot(sbomEntryId, evidenceIds, reasoningId, vexVerdictId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(root1, root2);
|
||||
Assert.Equal(root2, root3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MerkleRoot_EvidenceOrderIsNormalized_ProducesSameRoot()
|
||||
{
|
||||
// Arrange
|
||||
var sbomEntryId = "sha256:abc123...";
|
||||
var evidenceIds1 = new[] { "sha256:b...", "sha256:a...", "sha256:c..." };
|
||||
var evidenceIds2 = new[] { "sha256:c...", "sha256:a...", "sha256:b..." };
|
||||
var reasoningId = "sha256:reason...";
|
||||
var vexVerdictId = "sha256:vex...";
|
||||
|
||||
// Act - evidence IDs should be sorted internally
|
||||
var root1 = ComputeProofSpineRoot(sbomEntryId, evidenceIds1, reasoningId, vexVerdictId);
|
||||
var root2 = ComputeProofSpineRoot(sbomEntryId, evidenceIds2, reasoningId, vexVerdictId);
|
||||
|
||||
// Assert - same root because evidence is sorted
|
||||
Assert.Equal(root1, root2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MerkleRoot_DifferentSbom_ProducesDifferentRoot()
|
||||
{
|
||||
// Arrange
|
||||
var evidenceIds = new[] { "sha256:ev1..." };
|
||||
var reasoningId = "sha256:reason...";
|
||||
var vexVerdictId = "sha256:vex...";
|
||||
|
||||
// Act
|
||||
var root1 = ComputeProofSpineRoot("sha256:sbom1...", evidenceIds, reasoningId, vexVerdictId);
|
||||
var root2 = ComputeProofSpineRoot("sha256:sbom2...", evidenceIds, reasoningId, vexVerdictId);
|
||||
|
||||
// Assert
|
||||
Assert.NotEqual(root1, root2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Task #11: Full Pipeline Integration Tests
|
||||
|
||||
[Fact]
|
||||
public void Pipeline_CompleteProofSpine_AssemblesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var sbomEntryId = "sha256:0123456789abcdef...";
|
||||
var evidenceIds = new[]
|
||||
{
|
||||
"sha256:evidence-cve-2024-0001...",
|
||||
"sha256:evidence-reachability...",
|
||||
"sha256:evidence-sbom-component...",
|
||||
};
|
||||
var reasoningId = "sha256:reasoning-policy-match...";
|
||||
var vexVerdictId = "sha256:vex-not-affected...";
|
||||
|
||||
// Act
|
||||
var root = ComputeProofSpineRoot(sbomEntryId, evidenceIds, reasoningId, vexVerdictId);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(root);
|
||||
Assert.Equal(32, root.Length); // SHA-256
|
||||
Assert.StartsWith("sha256:", FormatAsId(root));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pipeline_EmptyEvidence_HandlesGracefully()
|
||||
{
|
||||
// Arrange - minimal proof spine with no evidence
|
||||
var sbomEntryId = "sha256:sbom...";
|
||||
var evidenceIds = Array.Empty<string>();
|
||||
var reasoningId = "sha256:reason...";
|
||||
var vexVerdictId = "sha256:vex...";
|
||||
|
||||
// Act
|
||||
var root = ComputeProofSpineRoot(sbomEntryId, evidenceIds, reasoningId, vexVerdictId);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(root);
|
||||
Assert.Equal(32, root.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pipeline_ManyEvidenceItems_ScalesEfficiently()
|
||||
{
|
||||
// Arrange - large number of evidence items
|
||||
var sbomEntryId = "sha256:sbom...";
|
||||
var evidenceIds = Enumerable.Range(0, 1000)
|
||||
.Select(i => $"sha256:evidence-{i:D4}...")
|
||||
.ToArray();
|
||||
var reasoningId = "sha256:reason...";
|
||||
var vexVerdictId = "sha256:vex...";
|
||||
|
||||
// Act
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var root = ComputeProofSpineRoot(sbomEntryId, evidenceIds, reasoningId, vexVerdictId);
|
||||
sw.Stop();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(root);
|
||||
Assert.True(sw.ElapsedMilliseconds < 1000, "Should complete within 1 second");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Task #12: Cross-Platform Verification Tests
|
||||
|
||||
[Fact]
|
||||
public void CrossPlatform_KnownVector_ProducesExpectedRoot()
|
||||
{
|
||||
// Arrange - known test vector for cross-platform verification
|
||||
// This allows other implementations (Go, Rust, TypeScript) to verify compatibility
|
||||
var sbomEntryId = "sha256:0000000000000000000000000000000000000000000000000000000000000001";
|
||||
var evidenceIds = new[]
|
||||
{
|
||||
"sha256:0000000000000000000000000000000000000000000000000000000000000002",
|
||||
"sha256:0000000000000000000000000000000000000000000000000000000000000003",
|
||||
};
|
||||
var reasoningId = "sha256:0000000000000000000000000000000000000000000000000000000000000004";
|
||||
var vexVerdictId = "sha256:0000000000000000000000000000000000000000000000000000000000000005";
|
||||
|
||||
// Act
|
||||
var root = ComputeProofSpineRoot(sbomEntryId, evidenceIds, reasoningId, vexVerdictId);
|
||||
|
||||
// Assert - root should be deterministic and verifiable by other implementations
|
||||
Assert.NotNull(root);
|
||||
Assert.Equal(32, root.Length);
|
||||
|
||||
// The actual expected root hash would be computed once and verified across platforms
|
||||
// For now, we just verify determinism
|
||||
var root2 = ComputeProofSpineRoot(sbomEntryId, evidenceIds, reasoningId, vexVerdictId);
|
||||
Assert.Equal(root, root2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CrossPlatform_Utf8Encoding_HandlesBinaryCorrectly()
|
||||
{
|
||||
// Arrange - IDs with special characters (should be UTF-8 encoded)
|
||||
var sbomEntryId = "sha256:café"; // Non-ASCII
|
||||
var evidenceIds = new[] { "sha256:日本語" }; // Japanese
|
||||
var reasoningId = "sha256:émoji🎉"; // Emoji
|
||||
var vexVerdictId = "sha256:Ω"; // Greek
|
||||
|
||||
// Act
|
||||
var root = ComputeProofSpineRoot(sbomEntryId, evidenceIds, reasoningId, vexVerdictId);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(root);
|
||||
Assert.Equal(32, root.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CrossPlatform_BinaryDigests_HandleRawBytes()
|
||||
{
|
||||
// Arrange - actual SHA-256 digests (64 hex chars)
|
||||
var sbomEntryId = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
||||
var evidenceIds = new[]
|
||||
{
|
||||
"sha256:d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592",
|
||||
};
|
||||
var reasoningId = "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08";
|
||||
var vexVerdictId = "sha256:a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e";
|
||||
|
||||
// Act
|
||||
var root = ComputeProofSpineRoot(sbomEntryId, evidenceIds, reasoningId, vexVerdictId);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(root);
|
||||
var rootHex = Convert.ToHexString(root).ToLowerInvariant();
|
||||
Assert.Equal(64, rootHex.Length);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Computes the proof spine merkle root following the deterministic algorithm.
|
||||
/// </summary>
|
||||
private byte[] ComputeProofSpineRoot(
|
||||
string sbomEntryId,
|
||||
string[] evidenceIds,
|
||||
string reasoningId,
|
||||
string vexVerdictId)
|
||||
{
|
||||
// Step 1: Prepare leaves in deterministic order
|
||||
var leaves = new List<ReadOnlyMemory<byte>>();
|
||||
|
||||
// SBOM entry is always first
|
||||
leaves.Add(Encoding.UTF8.GetBytes(sbomEntryId));
|
||||
|
||||
// Evidence IDs sorted lexicographically
|
||||
var sortedEvidence = evidenceIds.OrderBy(x => x, StringComparer.Ordinal).ToArray();
|
||||
foreach (var evidenceId in sortedEvidence)
|
||||
{
|
||||
leaves.Add(Encoding.UTF8.GetBytes(evidenceId));
|
||||
}
|
||||
|
||||
// Reasoning ID
|
||||
leaves.Add(Encoding.UTF8.GetBytes(reasoningId));
|
||||
|
||||
// VEX verdict ID last
|
||||
leaves.Add(Encoding.UTF8.GetBytes(vexVerdictId));
|
||||
|
||||
// Build merkle tree
|
||||
return _builder.ComputeMerkleRoot(leaves.ToArray());
|
||||
}
|
||||
|
||||
private static string FormatAsId(byte[] hash)
|
||||
{
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for merkle tree building.
|
||||
/// </summary>
|
||||
public interface IMerkleTreeBuilder
|
||||
{
|
||||
byte[] ComputeMerkleRoot(ReadOnlyMemory<byte>[] leaves);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic merkle tree builder using SHA-256.
|
||||
/// </summary>
|
||||
public class DeterministicMerkleTreeBuilder : IMerkleTreeBuilder
|
||||
{
|
||||
public byte[] ComputeMerkleRoot(ReadOnlyMemory<byte>[] leaves)
|
||||
{
|
||||
if (leaves.Length == 0)
|
||||
{
|
||||
return new byte[32]; // Zero hash for empty tree
|
||||
}
|
||||
|
||||
// Hash all leaves
|
||||
var currentLevel = new List<byte[]>();
|
||||
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
||||
|
||||
foreach (var leaf in leaves)
|
||||
{
|
||||
currentLevel.Add(sha256.ComputeHash(leaf.ToArray()));
|
||||
}
|
||||
|
||||
// Pad to power of 2 by duplicating last leaf
|
||||
while (!IsPowerOfTwo(currentLevel.Count))
|
||||
{
|
||||
currentLevel.Add(currentLevel[^1]);
|
||||
}
|
||||
|
||||
// Build tree bottom-up
|
||||
while (currentLevel.Count > 1)
|
||||
{
|
||||
var nextLevel = new List<byte[]>();
|
||||
|
||||
for (int i = 0; i < currentLevel.Count; i += 2)
|
||||
{
|
||||
var left = currentLevel[i];
|
||||
var right = currentLevel[i + 1];
|
||||
|
||||
// Concatenate and hash
|
||||
var combined = new byte[left.Length + right.Length];
|
||||
Buffer.BlockCopy(left, 0, combined, 0, left.Length);
|
||||
Buffer.BlockCopy(right, 0, combined, left.Length, right.Length);
|
||||
|
||||
nextLevel.Add(sha256.ComputeHash(combined));
|
||||
}
|
||||
|
||||
currentLevel = nextLevel;
|
||||
}
|
||||
|
||||
return currentLevel[0];
|
||||
}
|
||||
|
||||
private static bool IsPowerOfTwo(int n) => n > 0 && (n & (n - 1)) == 0;
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) StellaOps Contributors
|
||||
|
||||
using System.Text.Json;
|
||||
using StellaOps.Attestor.ProofChain.Builders;
|
||||
using StellaOps.Attestor.ProofChain.Statements;
|
||||
|
||||
namespace StellaOps.Attestor.ProofChain.Tests.Statements;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for all DSSE statement types (Task PROOF-PRED-0012).
|
||||
/// </summary>
|
||||
public class StatementBuilderTests
|
||||
{
|
||||
private readonly StatementBuilder _builder = new();
|
||||
private readonly DateTimeOffset _fixedTime = new(2025, 12, 16, 10, 0, 0, TimeSpan.Zero);
|
||||
|
||||
[Fact]
|
||||
public void BuildEvidenceStatement_SetsPredicateType()
|
||||
{
|
||||
var statement = _builder.BuildEvidenceStatement(
|
||||
subject: new InTotoSubject { Name = "test-artifact", Digest = new() { ["sha256"] = "abc123" } },
|
||||
source: "trivy",
|
||||
sourceVersion: "0.50.0",
|
||||
collectionTime: _fixedTime,
|
||||
sbomEntryId: "sbom-123");
|
||||
|
||||
Assert.Equal("evidence.stella/v1", statement.PredicateType);
|
||||
Assert.Equal("https://in-toto.io/Statement/v1", statement.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildEvidenceStatement_PopulatesPredicate()
|
||||
{
|
||||
var statement = _builder.BuildEvidenceStatement(
|
||||
subject: new InTotoSubject { Name = "test-artifact", Digest = new() { ["sha256"] = "abc123" } },
|
||||
source: "trivy",
|
||||
sourceVersion: "0.50.0",
|
||||
collectionTime: _fixedTime,
|
||||
sbomEntryId: "sbom-123",
|
||||
vulnerabilityId: "CVE-2025-1234");
|
||||
|
||||
Assert.Equal("trivy", statement.Predicate.Source);
|
||||
Assert.Equal("0.50.0", statement.Predicate.SourceVersion);
|
||||
Assert.Equal(_fixedTime, statement.Predicate.CollectionTime);
|
||||
Assert.Equal("sbom-123", statement.Predicate.SbomEntryId);
|
||||
Assert.Equal("CVE-2025-1234", statement.Predicate.VulnerabilityId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildProofSpineStatement_SetsPredicateType()
|
||||
{
|
||||
var statement = _builder.BuildProofSpineStatement(
|
||||
subject: new InTotoSubject { Name = "image:v1.0", Digest = new() { ["sha256"] = "abc123" } },
|
||||
spineAlgorithm: "sha256-merkle",
|
||||
rootHash: "root-hash",
|
||||
leafHashes: ["leaf1", "leaf2", "leaf3"]);
|
||||
|
||||
Assert.Equal("proofspine.stella/v1", statement.PredicateType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildProofSpineStatement_ContainsLeafHashes()
|
||||
{
|
||||
var leafHashes = new[] { "hash1", "hash2", "hash3", "hash4" };
|
||||
var statement = _builder.BuildProofSpineStatement(
|
||||
subject: new InTotoSubject { Name = "image:v1.0", Digest = new() { ["sha256"] = "abc123" } },
|
||||
spineAlgorithm: "sha256-merkle",
|
||||
rootHash: "merkle-root",
|
||||
leafHashes: leafHashes);
|
||||
|
||||
Assert.Equal("sha256-merkle", statement.Predicate.Algorithm);
|
||||
Assert.Equal("merkle-root", statement.Predicate.RootHash);
|
||||
Assert.Equal(4, statement.Predicate.LeafHashes.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildVexVerdictStatement_SetsPredicateType()
|
||||
{
|
||||
var statement = _builder.BuildVexVerdictStatement(
|
||||
subject: new InTotoSubject { Name = "pkg:npm/lodash@4.17.21", Digest = new() { ["sha256"] = "abc123" } },
|
||||
vulnerabilityId: "CVE-2025-1234",
|
||||
vexStatus: "not_affected",
|
||||
justification: "vulnerable_code_not_present",
|
||||
analysisTime: _fixedTime);
|
||||
|
||||
Assert.Equal("vexverdict.stella/v1", statement.PredicateType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildVexVerdictStatement_PopulatesVexDetails()
|
||||
{
|
||||
var statement = _builder.BuildVexVerdictStatement(
|
||||
subject: new InTotoSubject { Name = "pkg:npm/lodash@4.17.21", Digest = new() { ["sha256"] = "abc123" } },
|
||||
vulnerabilityId: "CVE-2025-1234",
|
||||
vexStatus: "not_affected",
|
||||
justification: "vulnerable_code_not_present",
|
||||
analysisTime: _fixedTime);
|
||||
|
||||
Assert.Equal("CVE-2025-1234", statement.Predicate.VulnerabilityId);
|
||||
Assert.Equal("not_affected", statement.Predicate.Status);
|
||||
Assert.Equal("vulnerable_code_not_present", statement.Predicate.Justification);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildReasoningStatement_SetsPredicateType()
|
||||
{
|
||||
var statement = _builder.BuildReasoningStatement(
|
||||
subject: new InTotoSubject { Name = "finding:123", Digest = new() { ["sha256"] = "abc123" } },
|
||||
reasoningType: "exploitability",
|
||||
conclusion: "not_exploitable",
|
||||
evidenceRefs: ["evidence1", "evidence2"]);
|
||||
|
||||
Assert.Equal("reasoning.stella/v1", statement.PredicateType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildVerdictReceiptStatement_SetsPredicateType()
|
||||
{
|
||||
var statement = _builder.BuildVerdictReceiptStatement(
|
||||
subject: new InTotoSubject { Name = "scan:456", Digest = new() { ["sha256"] = "abc123" } },
|
||||
verdictHash: "verdict-hash",
|
||||
verdictTime: _fixedTime,
|
||||
signatureAlgorithm: "ECDSA-P256");
|
||||
|
||||
Assert.Equal("verdictreceipt.stella/v1", statement.PredicateType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildSbomLinkageStatement_SetsPredicateType()
|
||||
{
|
||||
var statement = _builder.BuildSbomLinkageStatement(
|
||||
subject: new InTotoSubject { Name = "image:v1.0", Digest = new() { ["sha256"] = "abc123" } },
|
||||
sbomDigest: "sbom-digest",
|
||||
sbomFormat: "cyclonedx",
|
||||
sbomVersion: "1.6");
|
||||
|
||||
Assert.Equal("sbomlinkage.stella/v1", statement.PredicateType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllStatements_SerializeToValidJson()
|
||||
{
|
||||
var subject = new InTotoSubject { Name = "test", Digest = new() { ["sha256"] = "abc" } };
|
||||
|
||||
var evidence = _builder.BuildEvidenceStatement(subject, "trivy", "1.0", _fixedTime, "sbom1");
|
||||
var spine = _builder.BuildProofSpineStatement(subject, "sha256", "root", ["leaf1"]);
|
||||
var vex = _builder.BuildVexVerdictStatement(subject, "CVE-1", "fixed", null, _fixedTime);
|
||||
var reasoning = _builder.BuildReasoningStatement(subject, "exploitability", "safe", []);
|
||||
var receipt = _builder.BuildVerdictReceiptStatement(subject, "hash", _fixedTime, "ECDSA");
|
||||
var sbom = _builder.BuildSbomLinkageStatement(subject, "sbom-hash", "spdx", "3.0");
|
||||
|
||||
// All should serialize without throwing
|
||||
Assert.NotNull(JsonSerializer.Serialize(evidence));
|
||||
Assert.NotNull(JsonSerializer.Serialize(spine));
|
||||
Assert.NotNull(JsonSerializer.Serialize(vex));
|
||||
Assert.NotNull(JsonSerializer.Serialize(reasoning));
|
||||
Assert.NotNull(JsonSerializer.Serialize(receipt));
|
||||
Assert.NotNull(JsonSerializer.Serialize(sbom));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvidenceStatement_RoundTripsViaJson()
|
||||
{
|
||||
var original = _builder.BuildEvidenceStatement(
|
||||
subject: new InTotoSubject { Name: "artifact", Digest = new() { ["sha256"] = "hash123" } },
|
||||
source: "grype",
|
||||
sourceVersion: "0.80.0",
|
||||
collectionTime: _fixedTime,
|
||||
sbomEntryId: "entry-456",
|
||||
vulnerabilityId: "CVE-2025-9999");
|
||||
|
||||
var json = JsonSerializer.Serialize(original);
|
||||
var restored = JsonSerializer.Deserialize<EvidenceStatement>(json);
|
||||
|
||||
Assert.NotNull(restored);
|
||||
Assert.Equal(original.PredicateType, restored.PredicateType);
|
||||
Assert.Equal(original.Predicate.Source, restored.Predicate.Source);
|
||||
Assert.Equal(original.Predicate.VulnerabilityId, restored.Predicate.VulnerabilityId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProofSpineStatement_RoundTripsViaJson()
|
||||
{
|
||||
var original = _builder.BuildProofSpineStatement(
|
||||
subject: new InTotoSubject { Name = "image:latest", Digest = new() { ["sha256"] = "img-hash" } },
|
||||
spineAlgorithm: "sha256-merkle-v2",
|
||||
rootHash: "merkle-root-abc",
|
||||
leafHashes: ["a", "b", "c", "d"]);
|
||||
|
||||
var json = JsonSerializer.Serialize(original);
|
||||
var restored = JsonSerializer.Deserialize<ProofSpineStatement>(json);
|
||||
|
||||
Assert.NotNull(restored);
|
||||
Assert.Equal(original.Predicate.RootHash, restored.Predicate.RootHash);
|
||||
Assert.Equal(original.Predicate.LeafHashes.Length, restored.Predicate.LeafHashes.Length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) StellaOps Contributors
|
||||
|
||||
using System.Text.Json;
|
||||
using StellaOps.Attestor.ProofChain.Builders;
|
||||
using StellaOps.Attestor.ProofChain.Statements;
|
||||
using StellaOps.Attestor.ProofChain.Validation;
|
||||
|
||||
namespace StellaOps.Attestor.ProofChain.Tests.Statements;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for statement validation (Task PROOF-PRED-0015).
|
||||
/// </summary>
|
||||
public class StatementValidatorTests
|
||||
{
|
||||
private readonly StatementBuilder _builder = new();
|
||||
private readonly IStatementValidator _validator = new StatementValidator();
|
||||
private readonly DateTimeOffset _fixedTime = new(2025, 12, 16, 10, 0, 0, TimeSpan.Zero);
|
||||
|
||||
[Fact]
|
||||
public void Validate_ValidEvidenceStatement_ReturnsSuccess()
|
||||
{
|
||||
var statement = _builder.BuildEvidenceStatement(
|
||||
subject: new InTotoSubject { Name = "artifact", Digest = new() { ["sha256"] = "abc123" } },
|
||||
source: "trivy",
|
||||
sourceVersion: "0.50.0",
|
||||
collectionTime: _fixedTime,
|
||||
sbomEntryId: "sbom-123");
|
||||
|
||||
var result = _validator.Validate(statement);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Empty(result.Errors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_EvidenceStatementWithEmptySource_ReturnsError()
|
||||
{
|
||||
var statement = new EvidenceStatement
|
||||
{
|
||||
Subject = [new InTotoSubject { Name = "artifact", Digest = new() { ["sha256"] = "abc" } }],
|
||||
Predicate = new EvidencePayload
|
||||
{
|
||||
Source = "",
|
||||
SourceVersion = "1.0",
|
||||
CollectionTime = _fixedTime,
|
||||
SbomEntryId = "sbom-1"
|
||||
}
|
||||
};
|
||||
|
||||
var result = _validator.Validate(statement);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.Contains("Source"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_StatementWithEmptySubject_ReturnsError()
|
||||
{
|
||||
var statement = new EvidenceStatement
|
||||
{
|
||||
Subject = [],
|
||||
Predicate = new EvidencePayload
|
||||
{
|
||||
Source = "trivy",
|
||||
SourceVersion = "1.0",
|
||||
CollectionTime = _fixedTime,
|
||||
SbomEntryId = "sbom-1"
|
||||
}
|
||||
};
|
||||
|
||||
var result = _validator.Validate(statement);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.Contains("Subject"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ProofSpineWithEmptyLeafHashes_ReturnsError()
|
||||
{
|
||||
var statement = new ProofSpineStatement
|
||||
{
|
||||
Subject = [new InTotoSubject { Name = "image", Digest = new() { ["sha256"] = "hash" } }],
|
||||
Predicate = new ProofSpinePayload
|
||||
{
|
||||
Algorithm = "sha256-merkle",
|
||||
RootHash = "root",
|
||||
LeafHashes = []
|
||||
}
|
||||
};
|
||||
|
||||
var result = _validator.Validate(statement);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.Contains("LeafHashes"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_VexVerdictWithValidStatus_ReturnsSuccess()
|
||||
{
|
||||
var validStatuses = new[] { "not_affected", "affected", "fixed", "under_investigation" };
|
||||
|
||||
foreach (var status in validStatuses)
|
||||
{
|
||||
var statement = _builder.BuildVexVerdictStatement(
|
||||
subject: new InTotoSubject { Name = "pkg", Digest = new() { ["sha256"] = "abc" } },
|
||||
vulnerabilityId: "CVE-2025-1",
|
||||
vexStatus: status,
|
||||
justification: null,
|
||||
analysisTime: _fixedTime);
|
||||
|
||||
var result = _validator.Validate(statement);
|
||||
|
||||
Assert.True(result.IsValid, $"Status '{status}' should be valid");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_VexVerdictWithInvalidStatus_ReturnsError()
|
||||
{
|
||||
var statement = new VexVerdictStatement
|
||||
{
|
||||
Subject = [new InTotoSubject { Name = "pkg", Digest = new() { ["sha256"] = "abc" } }],
|
||||
Predicate = new VexVerdictPayload
|
||||
{
|
||||
VulnerabilityId = "CVE-2025-1",
|
||||
Status = "invalid_status",
|
||||
AnalysisTime = _fixedTime
|
||||
}
|
||||
};
|
||||
|
||||
var result = _validator.Validate(statement);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.Contains("Status"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ReasoningStatementWithEvidence_ReturnsSuccess()
|
||||
{
|
||||
var statement = _builder.BuildReasoningStatement(
|
||||
subject: new InTotoSubject { Name = "finding", Digest = new() { ["sha256"] = "abc" } },
|
||||
reasoningType: "exploitability",
|
||||
conclusion: "not_exploitable",
|
||||
evidenceRefs: ["evidence-1", "evidence-2"]);
|
||||
|
||||
var result = _validator.Validate(statement);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_SubjectWithMissingDigest_ReturnsError()
|
||||
{
|
||||
var statement = new EvidenceStatement
|
||||
{
|
||||
Subject = [new InTotoSubject { Name = "artifact", Digest = new() }],
|
||||
Predicate = new EvidencePayload
|
||||
{
|
||||
Source = "trivy",
|
||||
SourceVersion = "1.0",
|
||||
CollectionTime = _fixedTime,
|
||||
SbomEntryId = "sbom-1"
|
||||
}
|
||||
};
|
||||
|
||||
var result = _validator.Validate(statement);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.Contains("Digest"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user