feat(rate-limiting): Implement core rate limiting functionality with configuration, decision-making, metrics, middleware, and service registration
- Add RateLimitConfig for configuration management with YAML binding support. - Introduce RateLimitDecision to encapsulate the result of rate limit checks. - Implement RateLimitMetrics for OpenTelemetry metrics tracking. - Create RateLimitMiddleware for enforcing rate limits on incoming requests. - Develop RateLimitService to orchestrate instance and environment rate limit checks. - Add RateLimitServiceCollectionExtensions for dependency injection registration.
This commit is contained in:
@@ -0,0 +1,364 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ProofLedgerDeterminismTests.cs
|
||||
// Sprint: SPRINT_3401_0002_0001_score_replay_proof_bundle
|
||||
// Task: SCORE-REPLAY-012 - Unit tests for ProofLedger determinism
|
||||
// Description: Verifies that proof ledger produces identical hashes across runs
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Policy.Scoring;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Scoring.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for ProofLedger determinism and hash stability.
|
||||
/// </summary>
|
||||
public sealed class ProofLedgerDeterminismTests
|
||||
{
|
||||
private static readonly byte[] TestSeed = new byte[32];
|
||||
private static readonly DateTimeOffset FixedTimestamp = new(2025, 12, 17, 12, 0, 0, TimeSpan.Zero);
|
||||
|
||||
[Fact]
|
||||
public void RootHash_SameNodesInSameOrder_ProducesIdenticalHash()
|
||||
{
|
||||
// Arrange
|
||||
var nodes = CreateTestNodes(count: 5);
|
||||
|
||||
var ledger1 = new ProofLedger();
|
||||
var ledger2 = new ProofLedger();
|
||||
|
||||
// Act
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
ledger1.Append(node);
|
||||
ledger2.Append(node);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Equal(ledger1.RootHash(), ledger2.RootHash());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RootHash_MultipleCallsOnSameLedger_ReturnsSameHash()
|
||||
{
|
||||
// Arrange
|
||||
var ledger = new ProofLedger();
|
||||
foreach (var node in CreateTestNodes(count: 3))
|
||||
{
|
||||
ledger.Append(node);
|
||||
}
|
||||
|
||||
// Act
|
||||
var hash1 = ledger.RootHash();
|
||||
var hash2 = ledger.RootHash();
|
||||
var hash3 = ledger.RootHash();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(hash1, hash2);
|
||||
Assert.Equal(hash2, hash3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RootHash_DifferentNodeOrder_ProducesDifferentHash()
|
||||
{
|
||||
// Arrange
|
||||
var node1 = ProofNode.Create("id-1", ProofNodeKind.Input, "rule-1", "actor", FixedTimestamp, TestSeed, delta: 0.1, total: 0.1);
|
||||
var node2 = ProofNode.Create("id-2", ProofNodeKind.Transform, "rule-2", "actor", FixedTimestamp, TestSeed, delta: 0.2, total: 0.3);
|
||||
|
||||
var ledger1 = new ProofLedger();
|
||||
ledger1.Append(node1);
|
||||
ledger1.Append(node2);
|
||||
|
||||
var ledger2 = new ProofLedger();
|
||||
ledger2.Append(node2);
|
||||
ledger2.Append(node1);
|
||||
|
||||
// Act
|
||||
var hash1 = ledger1.RootHash();
|
||||
var hash2 = ledger2.RootHash();
|
||||
|
||||
// Assert
|
||||
Assert.NotEqual(hash1, hash2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RootHash_DifferentNodeContent_ProducesDifferentHash()
|
||||
{
|
||||
// Arrange
|
||||
var node1a = ProofNode.Create("id-1", ProofNodeKind.Input, "rule-1", "actor", FixedTimestamp, TestSeed, delta: 0.1, total: 0.1);
|
||||
var node1b = ProofNode.Create("id-1", ProofNodeKind.Input, "rule-1", "actor", FixedTimestamp, TestSeed, delta: 0.2, total: 0.2); // Different delta
|
||||
|
||||
var ledger1 = new ProofLedger();
|
||||
ledger1.Append(node1a);
|
||||
|
||||
var ledger2 = new ProofLedger();
|
||||
ledger2.Append(node1b);
|
||||
|
||||
// Act
|
||||
var hash1 = ledger1.RootHash();
|
||||
var hash2 = ledger2.RootHash();
|
||||
|
||||
// Assert
|
||||
Assert.NotEqual(hash1, hash2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppendRange_ProducesSameHashAsIndividualAppends()
|
||||
{
|
||||
// Arrange
|
||||
var nodes = CreateTestNodes(count: 4);
|
||||
|
||||
var ledger1 = new ProofLedger();
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
ledger1.Append(node);
|
||||
}
|
||||
|
||||
var ledger2 = new ProofLedger();
|
||||
ledger2.AppendRange(nodes);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Equal(ledger1.RootHash(), ledger2.RootHash());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyIntegrity_ValidLedger_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var ledger = new ProofLedger();
|
||||
foreach (var node in CreateTestNodes(count: 3))
|
||||
{
|
||||
ledger.Append(node);
|
||||
}
|
||||
|
||||
// Act & Assert
|
||||
Assert.True(ledger.VerifyIntegrity());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToImmutableSnapshot_ReturnsCorrectNodes()
|
||||
{
|
||||
// Arrange
|
||||
var nodes = CreateTestNodes(count: 3);
|
||||
var ledger = new ProofLedger();
|
||||
ledger.AppendRange(nodes);
|
||||
|
||||
// Act
|
||||
var snapshot = ledger.ToImmutableSnapshot();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(nodes.Length, snapshot.Count);
|
||||
for (int i = 0; i < nodes.Length; i++)
|
||||
{
|
||||
Assert.Equal(nodes[i].Id, snapshot[i].Id);
|
||||
Assert.Equal(nodes[i].Kind, snapshot[i].Kind);
|
||||
Assert.Equal(nodes[i].Delta, snapshot[i].Delta);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToJson_ProducesValidJson()
|
||||
{
|
||||
// Arrange
|
||||
var ledger = new ProofLedger();
|
||||
foreach (var node in CreateTestNodes(count: 2))
|
||||
{
|
||||
ledger.Append(node);
|
||||
}
|
||||
|
||||
// Act
|
||||
var json = ledger.ToJson();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(json);
|
||||
Assert.Contains("nodes", json);
|
||||
Assert.Contains("rootHash", json);
|
||||
Assert.Contains("sha256:", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromJson_RoundTrip_PreservesIntegrity()
|
||||
{
|
||||
// Arrange
|
||||
var ledger = new ProofLedger();
|
||||
foreach (var node in CreateTestNodes(count: 3))
|
||||
{
|
||||
ledger.Append(node);
|
||||
}
|
||||
var originalHash = ledger.RootHash();
|
||||
|
||||
// Act
|
||||
var json = ledger.ToJson();
|
||||
var restored = ProofLedger.FromJson(json);
|
||||
|
||||
// Assert
|
||||
Assert.True(restored.VerifyIntegrity());
|
||||
Assert.Equal(originalHash, restored.RootHash());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RootHash_EmptyLedger_ProducesConsistentHash()
|
||||
{
|
||||
// Arrange
|
||||
var ledger1 = new ProofLedger();
|
||||
var ledger2 = new ProofLedger();
|
||||
|
||||
// Act
|
||||
var hash1 = ledger1.RootHash();
|
||||
var hash2 = ledger2.RootHash();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(hash1, hash2);
|
||||
Assert.StartsWith("sha256:", hash1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NodeHash_SameNodeRecreated_ProducesSameHash()
|
||||
{
|
||||
// Arrange
|
||||
var node1 = ProofNode.Create(
|
||||
id: "test-id",
|
||||
kind: ProofNodeKind.Delta,
|
||||
ruleId: "rule-x",
|
||||
actor: "scorer",
|
||||
tsUtc: FixedTimestamp,
|
||||
seed: TestSeed,
|
||||
delta: 0.15,
|
||||
total: 0.45,
|
||||
parentIds: ["parent-1", "parent-2"],
|
||||
evidenceRefs: ["sha256:abc123"]);
|
||||
|
||||
var node2 = ProofNode.Create(
|
||||
id: "test-id",
|
||||
kind: ProofNodeKind.Delta,
|
||||
ruleId: "rule-x",
|
||||
actor: "scorer",
|
||||
tsUtc: FixedTimestamp,
|
||||
seed: TestSeed,
|
||||
delta: 0.15,
|
||||
total: 0.45,
|
||||
parentIds: ["parent-1", "parent-2"],
|
||||
evidenceRefs: ["sha256:abc123"]);
|
||||
|
||||
// Act
|
||||
var hashedNode1 = ProofHashing.WithHash(node1);
|
||||
var hashedNode2 = ProofHashing.WithHash(node2);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(hashedNode1.NodeHash, hashedNode2.NodeHash);
|
||||
Assert.StartsWith("sha256:", hashedNode1.NodeHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NodeHash_DifferentTimestamp_ProducesDifferentHash()
|
||||
{
|
||||
// Arrange
|
||||
var node1 = ProofNode.Create("id-1", ProofNodeKind.Input, "rule-1", "actor", FixedTimestamp, TestSeed);
|
||||
var node2 = ProofNode.Create("id-1", ProofNodeKind.Input, "rule-1", "actor", FixedTimestamp.AddSeconds(1), TestSeed);
|
||||
|
||||
// Act
|
||||
var hashedNode1 = ProofHashing.WithHash(node1);
|
||||
var hashedNode2 = ProofHashing.WithHash(node2);
|
||||
|
||||
// Assert
|
||||
Assert.NotEqual(hashedNode1.NodeHash, hashedNode2.NodeHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyNodeHash_ValidHash_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var node = ProofNode.Create("id-1", ProofNodeKind.Input, "rule-1", "actor", FixedTimestamp, TestSeed);
|
||||
var hashedNode = ProofHashing.WithHash(node);
|
||||
|
||||
// Act & Assert
|
||||
Assert.True(ProofHashing.VerifyNodeHash(hashedNode));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyRootHash_ValidHash_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var ledger = new ProofLedger();
|
||||
foreach (var node in CreateTestNodes(count: 3))
|
||||
{
|
||||
ledger.Append(node);
|
||||
}
|
||||
var rootHash = ledger.RootHash();
|
||||
|
||||
// Act & Assert
|
||||
Assert.True(ProofHashing.VerifyRootHash(ledger.Nodes, rootHash));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyRootHash_TamperedHash_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var ledger = new ProofLedger();
|
||||
foreach (var node in CreateTestNodes(count: 3))
|
||||
{
|
||||
ledger.Append(node);
|
||||
}
|
||||
var tamperedHash = "sha256:0000000000000000000000000000000000000000000000000000000000000000";
|
||||
|
||||
// Act & Assert
|
||||
Assert.False(ProofHashing.VerifyRootHash(ledger.Nodes, tamperedHash));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConcurrentAppends_ProduceDeterministicOrder()
|
||||
{
|
||||
// Arrange - run same sequence multiple times
|
||||
var results = new List<string>();
|
||||
|
||||
for (int run = 0; run < 10; run++)
|
||||
{
|
||||
var ledger = new ProofLedger();
|
||||
var nodes = CreateTestNodes(count: 10);
|
||||
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
ledger.Append(node);
|
||||
}
|
||||
|
||||
results.Add(ledger.RootHash());
|
||||
}
|
||||
|
||||
// Assert - all runs should produce identical hash
|
||||
Assert.True(results.All(h => h == results[0]));
|
||||
}
|
||||
|
||||
private static ProofNode[] CreateTestNodes(int count)
|
||||
{
|
||||
var nodes = new ProofNode[count];
|
||||
double runningTotal = 0;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var delta = 0.1 * (i + 1);
|
||||
runningTotal += delta;
|
||||
|
||||
var kind = i switch
|
||||
{
|
||||
0 => ProofNodeKind.Input,
|
||||
_ when i == count - 1 => ProofNodeKind.Score,
|
||||
_ when i % 2 == 0 => ProofNodeKind.Transform,
|
||||
_ => ProofNodeKind.Delta
|
||||
};
|
||||
|
||||
nodes[i] = ProofNode.Create(
|
||||
id: $"node-{i:D3}",
|
||||
kind: kind,
|
||||
ruleId: $"rule-{i}",
|
||||
actor: "test-scorer",
|
||||
tsUtc: FixedTimestamp.AddMilliseconds(i * 100),
|
||||
seed: TestSeed,
|
||||
delta: delta,
|
||||
total: runningTotal,
|
||||
parentIds: i > 0 ? [$"node-{i - 1:D3}"] : null,
|
||||
evidenceRefs: [$"sha256:evidence{i:D3}"]);
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ProofLedgerTests.cs
|
||||
// Sprint: SPRINT_3401_0002_0001_score_replay_proof_bundle
|
||||
// Task: SCORE-REPLAY-012 - Unit tests for ProofLedger determinism
|
||||
// Description: Tests for proof ledger hash consistency and determinism
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Scoring;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Tests.Scoring;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for ProofLedger determinism.
|
||||
/// Validates that same inputs produce identical hashes across runs.
|
||||
/// </summary>
|
||||
public class ProofLedgerTests
|
||||
{
|
||||
private static readonly byte[] TestSeed = Enumerable.Repeat((byte)0x42, 32).ToArray();
|
||||
private static readonly DateTimeOffset FixedTimestamp = new(2025, 12, 17, 12, 0, 0, TimeSpan.Zero);
|
||||
|
||||
#region ProofNode Hash Tests
|
||||
|
||||
[Fact]
|
||||
public void ProofHashing_WithHash_ComputesConsistentHash()
|
||||
{
|
||||
// Arrange
|
||||
var node = ProofNode.Create(
|
||||
id: "node-001",
|
||||
kind: ProofNodeKind.Input,
|
||||
ruleId: "CVSS_BASE",
|
||||
actor: "scorer",
|
||||
tsUtc: FixedTimestamp,
|
||||
seed: TestSeed,
|
||||
total: 9.0);
|
||||
|
||||
// Act
|
||||
var hashed1 = ProofHashing.WithHash(node);
|
||||
var hashed2 = ProofHashing.WithHash(node);
|
||||
var hashed3 = ProofHashing.WithHash(node);
|
||||
|
||||
// Assert - all hashes should be identical
|
||||
hashed1.NodeHash.Should().StartWith("sha256:");
|
||||
hashed1.NodeHash.Should().Be(hashed2.NodeHash);
|
||||
hashed2.NodeHash.Should().Be(hashed3.NodeHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProofHashing_WithHash_DifferentInputsProduceDifferentHashes()
|
||||
{
|
||||
// Arrange
|
||||
var node1 = ProofNode.Create(
|
||||
id: "node-001",
|
||||
kind: ProofNodeKind.Input,
|
||||
ruleId: "CVSS_BASE",
|
||||
actor: "scorer",
|
||||
tsUtc: FixedTimestamp,
|
||||
seed: TestSeed,
|
||||
total: 9.0);
|
||||
|
||||
var node2 = ProofNode.Create(
|
||||
id: "node-002", // Different ID
|
||||
kind: ProofNodeKind.Input,
|
||||
ruleId: "CVSS_BASE",
|
||||
actor: "scorer",
|
||||
tsUtc: FixedTimestamp,
|
||||
seed: TestSeed,
|
||||
total: 9.0);
|
||||
|
||||
// Act
|
||||
var hashed1 = ProofHashing.WithHash(node1);
|
||||
var hashed2 = ProofHashing.WithHash(node2);
|
||||
|
||||
// Assert - different inputs = different hashes
|
||||
hashed1.NodeHash.Should().NotBe(hashed2.NodeHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProofHashing_VerifyNodeHash_ReturnsTrueForValidHash()
|
||||
{
|
||||
// Arrange
|
||||
var node = ProofNode.Create(
|
||||
id: "node-001",
|
||||
kind: ProofNodeKind.Input,
|
||||
ruleId: "CVSS_BASE",
|
||||
actor: "scorer",
|
||||
tsUtc: FixedTimestamp,
|
||||
seed: TestSeed,
|
||||
total: 9.0);
|
||||
|
||||
var hashed = ProofHashing.WithHash(node);
|
||||
|
||||
// Act
|
||||
var isValid = ProofHashing.VerifyNodeHash(hashed);
|
||||
|
||||
// Assert
|
||||
isValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProofHashing_VerifyNodeHash_ReturnsFalseForTamperedHash()
|
||||
{
|
||||
// Arrange
|
||||
var node = ProofNode.Create(
|
||||
id: "node-001",
|
||||
kind: ProofNodeKind.Input,
|
||||
ruleId: "CVSS_BASE",
|
||||
actor: "scorer",
|
||||
tsUtc: FixedTimestamp,
|
||||
seed: TestSeed,
|
||||
total: 9.0);
|
||||
|
||||
var hashed = ProofHashing.WithHash(node);
|
||||
var tampered = hashed with { Total = 8.0 }; // Tamper with the total
|
||||
|
||||
// Act
|
||||
var isValid = ProofHashing.VerifyNodeHash(tampered);
|
||||
|
||||
// Assert
|
||||
isValid.Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ProofLedger Determinism Tests
|
||||
|
||||
[Fact]
|
||||
public void ProofLedger_RootHash_IsDeterministic()
|
||||
{
|
||||
// Arrange - create identical ledgers
|
||||
var nodes = CreateTestNodes();
|
||||
|
||||
var ledger1 = new ProofLedger();
|
||||
var ledger2 = new ProofLedger();
|
||||
var ledger3 = new ProofLedger();
|
||||
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
ledger1.Append(node);
|
||||
ledger2.Append(node);
|
||||
ledger3.Append(node);
|
||||
}
|
||||
|
||||
// Act
|
||||
var hash1 = ledger1.RootHash();
|
||||
var hash2 = ledger2.RootHash();
|
||||
var hash3 = ledger3.RootHash();
|
||||
|
||||
// Assert - all root hashes should be identical
|
||||
hash1.Should().StartWith("sha256:");
|
||||
hash1.Should().Be(hash2);
|
||||
hash2.Should().Be(hash3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProofLedger_RootHash_DependsOnNodeOrder()
|
||||
{
|
||||
// Arrange - same nodes, different order
|
||||
var nodes = CreateTestNodes();
|
||||
var reversedNodes = nodes.Reverse().ToList();
|
||||
|
||||
var ledger1 = ProofLedger.FromNodes(nodes);
|
||||
var ledger2 = ProofLedger.FromNodes(reversedNodes);
|
||||
|
||||
// Act
|
||||
var hash1 = ledger1.RootHash();
|
||||
var hash2 = ledger2.RootHash();
|
||||
|
||||
// Assert - different order = different hash
|
||||
hash1.Should().NotBe(hash2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProofLedger_RootHash_ChangesWhenNodeAdded()
|
||||
{
|
||||
// Arrange
|
||||
var nodes = CreateTestNodes();
|
||||
var ledger = ProofLedger.FromNodes(nodes);
|
||||
var hash1 = ledger.RootHash();
|
||||
|
||||
// Act - add another node
|
||||
ledger.Append(ProofNode.Create(
|
||||
id: "node-extra",
|
||||
kind: ProofNodeKind.Score,
|
||||
ruleId: "FINAL",
|
||||
actor: "scorer",
|
||||
tsUtc: FixedTimestamp,
|
||||
seed: TestSeed,
|
||||
total: 0.73));
|
||||
|
||||
var hash2 = ledger.RootHash();
|
||||
|
||||
// Assert
|
||||
hash2.Should().NotBe(hash1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProofLedger_VerifyIntegrity_ReturnsTrueForValidLedger()
|
||||
{
|
||||
// Arrange
|
||||
var nodes = CreateTestNodes();
|
||||
var ledger = ProofLedger.FromNodes(nodes);
|
||||
|
||||
// Act
|
||||
var isValid = ledger.VerifyIntegrity();
|
||||
|
||||
// Assert
|
||||
isValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Serialization Tests
|
||||
|
||||
[Fact]
|
||||
public void ProofLedger_ToJson_FromJson_RoundTrips()
|
||||
{
|
||||
// Arrange
|
||||
var nodes = CreateTestNodes();
|
||||
var ledger = ProofLedger.FromNodes(nodes);
|
||||
var originalRootHash = ledger.RootHash();
|
||||
|
||||
// Act
|
||||
var json = ledger.ToJson();
|
||||
var restored = ProofLedger.FromJson(json);
|
||||
|
||||
// Assert
|
||||
restored.Count.Should().Be(ledger.Count);
|
||||
restored.RootHash().Should().Be(originalRootHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProofLedger_FromJson_ThrowsOnTamperedData()
|
||||
{
|
||||
// Arrange
|
||||
var nodes = CreateTestNodes();
|
||||
var ledger = ProofLedger.FromNodes(nodes);
|
||||
var json = ledger.ToJson();
|
||||
|
||||
// Tamper with the JSON
|
||||
var tampered = json.Replace("\"total\":9.0", "\"total\":8.0");
|
||||
|
||||
// Act & Assert
|
||||
var act = () => ProofLedger.FromJson(tampered);
|
||||
act.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("*integrity*");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Score Replay Invariant Tests
|
||||
|
||||
[Fact]
|
||||
public void ScoreReplay_SameInputs_ProducesIdenticalRootHash()
|
||||
{
|
||||
// Arrange - simulate score replay scenario
|
||||
// Same manifest + same seed + same timestamp = identical rootHash
|
||||
|
||||
var seed = Enumerable.Repeat((byte)7, 32).ToArray();
|
||||
var timestamp = new DateTimeOffset(2025, 12, 17, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
// First scoring run
|
||||
var ledger1 = SimulateScoring(seed, timestamp, cvssBase: 9.0, epss: 0.50);
|
||||
|
||||
// Second scoring run (replay)
|
||||
var ledger2 = SimulateScoring(seed, timestamp, cvssBase: 9.0, epss: 0.50);
|
||||
|
||||
// Third scoring run (replay again)
|
||||
var ledger3 = SimulateScoring(seed, timestamp, cvssBase: 9.0, epss: 0.50);
|
||||
|
||||
// Assert - all root hashes should be bit-identical
|
||||
ledger1.RootHash().Should().Be(ledger2.RootHash());
|
||||
ledger2.RootHash().Should().Be(ledger3.RootHash());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScoreReplay_DifferentSeed_ProducesDifferentRootHash()
|
||||
{
|
||||
// Arrange
|
||||
var seed1 = Enumerable.Repeat((byte)7, 32).ToArray();
|
||||
var seed2 = Enumerable.Repeat((byte)8, 32).ToArray();
|
||||
var timestamp = new DateTimeOffset(2025, 12, 17, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
// Act
|
||||
var ledger1 = SimulateScoring(seed1, timestamp, cvssBase: 9.0, epss: 0.50);
|
||||
var ledger2 = SimulateScoring(seed2, timestamp, cvssBase: 9.0, epss: 0.50);
|
||||
|
||||
// Assert
|
||||
ledger1.RootHash().Should().NotBe(ledger2.RootHash());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScoreReplay_DifferentInputs_ProducesDifferentRootHash()
|
||||
{
|
||||
// Arrange
|
||||
var seed = Enumerable.Repeat((byte)7, 32).ToArray();
|
||||
var timestamp = new DateTimeOffset(2025, 12, 17, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
// Act
|
||||
var ledger1 = SimulateScoring(seed, timestamp, cvssBase: 9.0, epss: 0.50);
|
||||
var ledger2 = SimulateScoring(seed, timestamp, cvssBase: 8.0, epss: 0.50);
|
||||
|
||||
// Assert
|
||||
ledger1.RootHash().Should().NotBe(ledger2.RootHash());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static List<ProofNode> CreateTestNodes()
|
||||
{
|
||||
return
|
||||
[
|
||||
ProofNode.CreateInput(
|
||||
id: "node-001",
|
||||
ruleId: "CVSS_BASE",
|
||||
actor: "scorer",
|
||||
tsUtc: FixedTimestamp,
|
||||
seed: TestSeed,
|
||||
initialValue: 9.0,
|
||||
evidenceRefs: ["sha256:vuln001"]),
|
||||
|
||||
ProofNode.CreateDelta(
|
||||
id: "node-002",
|
||||
ruleId: "EPSS_ADJUST",
|
||||
actor: "scorer",
|
||||
tsUtc: FixedTimestamp.AddMilliseconds(1),
|
||||
seed: TestSeed,
|
||||
delta: -0.5,
|
||||
newTotal: 8.5,
|
||||
parentIds: ["node-001"],
|
||||
evidenceRefs: ["sha256:epss001"]),
|
||||
|
||||
ProofNode.CreateScore(
|
||||
id: "node-003",
|
||||
ruleId: "FINAL_SCORE",
|
||||
actor: "scorer",
|
||||
tsUtc: FixedTimestamp.AddMilliseconds(2),
|
||||
seed: TestSeed,
|
||||
finalScore: 0.85,
|
||||
parentIds: ["node-002"])
|
||||
];
|
||||
}
|
||||
|
||||
private static ProofLedger SimulateScoring(byte[] seed, DateTimeOffset timestamp, double cvssBase, double epss)
|
||||
{
|
||||
var ledger = new ProofLedger();
|
||||
|
||||
// Input node - CVSS base score
|
||||
ledger.Append(ProofNode.CreateInput(
|
||||
id: "input-cvss",
|
||||
ruleId: "CVSS_BASE",
|
||||
actor: "scorer",
|
||||
tsUtc: timestamp,
|
||||
seed: seed,
|
||||
initialValue: cvssBase));
|
||||
|
||||
// Input node - EPSS score
|
||||
ledger.Append(ProofNode.CreateInput(
|
||||
id: "input-epss",
|
||||
ruleId: "EPSS_SCORE",
|
||||
actor: "scorer",
|
||||
tsUtc: timestamp.AddMilliseconds(1),
|
||||
seed: seed,
|
||||
initialValue: epss));
|
||||
|
||||
// Delta node - apply EPSS modifier
|
||||
var epssWeight = 0.3;
|
||||
var delta = epss * epssWeight;
|
||||
var total = (cvssBase / 10.0) * (1 - epssWeight) + delta;
|
||||
|
||||
ledger.Append(ProofNode.CreateDelta(
|
||||
id: "delta-epss",
|
||||
ruleId: "EPSS_WEIGHT",
|
||||
actor: "scorer",
|
||||
tsUtc: timestamp.AddMilliseconds(2),
|
||||
seed: seed,
|
||||
delta: delta,
|
||||
newTotal: total,
|
||||
parentIds: ["input-cvss", "input-epss"]));
|
||||
|
||||
// Final score node
|
||||
ledger.Append(ProofNode.CreateScore(
|
||||
id: "score-final",
|
||||
ruleId: "FINAL",
|
||||
actor: "scorer",
|
||||
tsUtc: timestamp.AddMilliseconds(3),
|
||||
seed: seed,
|
||||
finalScore: Math.Round(total, 2),
|
||||
parentIds: ["delta-epss"]));
|
||||
|
||||
return ledger;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user