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

View File

@@ -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);
}
}

View File

@@ -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"));
}
}