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:
master
2025-12-17 18:02:37 +02:00
parent 394b57f6bf
commit 8bbfe4d2d2
211 changed files with 47179 additions and 1590 deletions

View File

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