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,266 @@
// -----------------------------------------------------------------------------
// ProofAwareScoringEngine.cs
// Sprint: SPRINT_3401_0002_0001_score_replay_proof_bundle
// Task: SCORE-REPLAY-004 - Integrate ProofLedger into RiskScoring.Score()
// Description: Decorator that emits proof ledger nodes during scoring
// -----------------------------------------------------------------------------
using Microsoft.Extensions.Logging;
using StellaOps.Policy.Scoring;
namespace StellaOps.Policy.Engine.Scoring.Engines;
/// <summary>
/// Decorator that wraps a scoring engine and emits proof ledger nodes.
/// Per advisory "Determinism and Reproducibility Technical Reference" §11.2.
/// </summary>
public sealed class ProofAwareScoringEngine : IScoringEngine
{
private readonly IScoringEngine _inner;
private readonly ILogger<ProofAwareScoringEngine> _logger;
private readonly ProofAwareScoringOptions _options;
public ProofAwareScoringEngine(
IScoringEngine inner,
ILogger<ProofAwareScoringEngine> logger,
ProofAwareScoringOptions? options = null)
{
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options ?? ProofAwareScoringOptions.Default;
}
public ScoringProfile Profile => _inner.Profile;
public async Task<ScoringEngineResult> ScoreAsync(
ScoringInput input,
ScorePolicy policy,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(input);
ArgumentNullException.ThrowIfNull(policy);
// Initialize proof ledger for this scoring run
var ledger = new ProofLedger();
var seed = GenerateSeed(input);
var nodeCounter = 0;
// Emit input nodes for each scoring factor
EmitInputNodes(ledger, input, seed, ref nodeCounter);
// Delegate to inner engine
var result = await _inner.ScoreAsync(input, policy, ct);
// Emit delta nodes for each signal contribution
EmitDeltaNodes(ledger, result, input.AsOf, seed, ref nodeCounter);
// Emit final score node
var finalNode = ProofNode.CreateScore(
id: $"node-{nodeCounter++:D4}",
ruleId: "FINAL_SCORE",
actor: $"scoring-engine:{Profile.ToString().ToLowerInvariant()}",
tsUtc: input.AsOf,
seed: seed,
finalScore: result.FinalScore / 100.0,
parentIds: Enumerable.Range(0, nodeCounter - 1).Select(i => $"node-{i:D4}").TakeLast(5).ToArray());
ledger.Append(finalNode);
// Compute root hash
var rootHash = ledger.RootHash();
_logger.LogDebug(
"Proof ledger for {FindingId}: {NodeCount} nodes, rootHash={RootHash}",
input.FindingId, ledger.Count, rootHash);
// Attach proof ledger to result via extension
var proofResult = result.WithProofLedger(ledger, rootHash);
return proofResult;
}
private void EmitInputNodes(
ProofLedger ledger,
ScoringInput input,
byte[] seed,
ref int nodeCounter)
{
var ts = input.AsOf;
// CVSS input
ledger.Append(ProofNode.CreateInput(
id: $"node-{nodeCounter++:D4}",
ruleId: "CVSS_BASE",
actor: "scoring-input",
tsUtc: ts,
seed: seed,
initialValue: (double)input.CvssBase,
evidenceRefs: input.InputDigests?.TryGetValue("cvss", out var cvssDigest) == true
? [cvssDigest]
: []));
// Reachability input
var reachValue = input.Reachability.AdvancedScore ?? (input.Reachability.HopCount.HasValue ? 1.0 : 0.0);
ledger.Append(ProofNode.CreateInput(
id: $"node-{nodeCounter++:D4}",
ruleId: "REACHABILITY",
actor: "scoring-input",
tsUtc: ts.AddTicks(1),
seed: seed,
initialValue: reachValue,
evidenceRefs: input.InputDigests?.TryGetValue("reachability", out var reachDigest) == true
? [reachDigest]
: []));
// Evidence input
var evidenceValue = input.Evidence.AdvancedScore ?? (input.Evidence.Types.Count > 0 ? 0.5 : 0.0);
ledger.Append(ProofNode.CreateInput(
id: $"node-{nodeCounter++:D4}",
ruleId: "EVIDENCE",
actor: "scoring-input",
tsUtc: ts.AddTicks(2),
seed: seed,
initialValue: evidenceValue,
evidenceRefs: input.InputDigests?.TryGetValue("evidence", out var evidenceDigest) == true
? [evidenceDigest]
: []));
// Provenance input
var provValue = (int)input.Provenance.Level / 4.0; // Normalize to 0-1
ledger.Append(ProofNode.CreateInput(
id: $"node-{nodeCounter++:D4}",
ruleId: "PROVENANCE",
actor: "scoring-input",
tsUtc: ts.AddTicks(3),
seed: seed,
initialValue: provValue,
evidenceRefs: input.InputDigests?.TryGetValue("provenance", out var provDigest) == true
? [provDigest]
: []));
// KEV input
if (input.IsKnownExploited)
{
ledger.Append(ProofNode.CreateInput(
id: $"node-{nodeCounter++:D4}",
ruleId: "KEV_FLAG",
actor: "scoring-input",
tsUtc: ts.AddTicks(4),
seed: seed,
initialValue: 1.0));
}
}
private void EmitDeltaNodes(
ProofLedger ledger,
ScoringEngineResult result,
DateTimeOffset ts,
byte[] seed,
ref int nodeCounter)
{
var runningTotal = 0.0;
var inputNodeIds = Enumerable.Range(0, nodeCounter).Select(i => $"node-{i:D4}").ToList();
foreach (var (signal, contribution) in result.SignalContributions.OrderBy(x => x.Key))
{
var delta = contribution / 100.0; // Normalize to 0-1 scale
runningTotal += delta;
ledger.Append(ProofNode.CreateDelta(
id: $"node-{nodeCounter++:D4}",
ruleId: $"WEIGHT_{signal.ToUpperInvariant()}",
actor: $"scoring-engine:{Profile.ToString().ToLowerInvariant()}",
tsUtc: ts.AddMilliseconds(nodeCounter),
seed: seed,
delta: delta,
newTotal: Math.Clamp(runningTotal, 0, 1),
parentIds: inputNodeIds.Take(4).ToArray()));
}
}
private static byte[] GenerateSeed(ScoringInput input)
{
// Generate deterministic seed from input digests
using var sha256 = System.Security.Cryptography.SHA256.Create();
var inputString = $"{input.FindingId}:{input.TenantId}:{input.ProfileId}:{input.AsOf:O}";
foreach (var kvp in input.InputDigests?.OrderBy(x => x.Key) ?? [])
{
inputString += $":{kvp.Key}={kvp.Value}";
}
return sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(inputString));
}
}
/// <summary>
/// Options for proof-aware scoring.
/// </summary>
public sealed class ProofAwareScoringOptions
{
/// <summary>
/// Default options.
/// </summary>
public static readonly ProofAwareScoringOptions Default = new();
/// <summary>
/// Whether to emit detailed delta nodes for each signal.
/// </summary>
public bool EmitDetailedDeltas { get; init; } = true;
/// <summary>
/// Whether to include evidence references in nodes.
/// </summary>
public bool IncludeEvidenceRefs { get; init; } = true;
}
/// <summary>
/// Extension methods for scoring results with proof ledgers.
/// </summary>
public static class ScoringResultProofExtensions
{
private static readonly System.Runtime.CompilerServices.ConditionalWeakTable<ScoringEngineResult, ProofLedgerAttachment>
_proofAttachments = new();
/// <summary>
/// Attach a proof ledger to a scoring result.
/// </summary>
public static ScoringEngineResult WithProofLedger(
this ScoringEngineResult result,
ProofLedger ledger,
string rootHash)
{
_proofAttachments.Add(result, new ProofLedgerAttachment(ledger, rootHash));
return result;
}
/// <summary>
/// Get the attached proof ledger from a scoring result.
/// </summary>
public static ProofLedger? GetProofLedger(this ScoringEngineResult result)
{
return _proofAttachments.TryGetValue(result, out var attachment)
? attachment.Ledger
: null;
}
/// <summary>
/// Get the proof root hash from a scoring result.
/// </summary>
public static string? GetProofRootHash(this ScoringEngineResult result)
{
return _proofAttachments.TryGetValue(result, out var attachment)
? attachment.RootHash
: null;
}
/// <summary>
/// Check if a scoring result has a proof ledger attached.
/// </summary>
public static bool HasProofLedger(this ScoringEngineResult result)
{
return _proofAttachments.TryGetValue(result, out _);
}
private sealed record ProofLedgerAttachment(ProofLedger Ledger, string RootHash);
}