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,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);
|
||||
}
|
||||
Reference in New Issue
Block a user