Complete Entrypoint Detection Re-Engineering Program (Sprints 0410-0415) and Sprint 3500.0002.0003 (Proof Replay + API)
Entrypoint Detection Program (100% complete): - Sprint 0411: Semantic Entrypoint Engine - all 25 tasks DONE - Sprint 0412: Temporal & Mesh Entrypoint - all 19 tasks DONE - Sprint 0413: Speculative Execution Engine - all 19 tasks DONE - Sprint 0414: Binary Intelligence - all 19 tasks DONE - Sprint 0415: Predictive Risk Scoring - all tasks DONE Key deliverables: - SemanticEntrypoint schema with ApplicationIntent/CapabilityClass - TemporalEntrypointGraph and MeshEntrypointGraph - ShellSymbolicExecutor with PathEnumerator and PathConfidenceScorer - CodeFingerprint index with symbol recovery - RiskScore with multi-dimensional risk assessment Sprint 3500.0002.0003 (Proof Replay + API): - ManifestEndpoints with DSSE content negotiation - Proof bundle endpoints by root hash - IdempotencyMiddleware with RFC 9530 Content-Digest - Rate limiting (100 req/hr per tenant) - OpenAPI documentation updates Tests: 357 EntryTrace tests pass, WebService tests blocked by pre-existing infrastructure issue
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// RateLimitingExtensions.cs
|
||||
// Sprint: SPRINT_3500_0002_0003_proof_replay_api
|
||||
// Task: T4 - Rate Limiting
|
||||
// Description: Rate limiting configuration for proof replay endpoints
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Threading.RateLimiting;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Scanner.WebService.Security;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for configuring rate limiting on proof replay endpoints.
|
||||
/// </summary>
|
||||
public static class RateLimitingExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Policy name for proof replay rate limiting (100 req/hr per tenant).
|
||||
/// </summary>
|
||||
public const string ProofReplayPolicy = "ProofReplay";
|
||||
|
||||
/// <summary>
|
||||
/// Policy name for scan manifest rate limiting (100 req/hr per tenant).
|
||||
/// </summary>
|
||||
public const string ManifestPolicy = "Manifest";
|
||||
|
||||
/// <summary>
|
||||
/// Add rate limiting services for scanner endpoints (proof replay, manifest, etc.).
|
||||
/// </summary>
|
||||
public static IServiceCollection AddScannerRateLimiting(this IServiceCollection services)
|
||||
{
|
||||
services.AddRateLimiter(options =>
|
||||
{
|
||||
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
||||
|
||||
// Proof replay: 100 requests per hour per tenant
|
||||
options.AddPolicy(ProofReplayPolicy, context =>
|
||||
{
|
||||
var tenantId = GetTenantId(context);
|
||||
return RateLimitPartition.GetFixedWindowLimiter(
|
||||
partitionKey: $"proof-replay:{tenantId}",
|
||||
factory: _ => new FixedWindowRateLimiterOptions
|
||||
{
|
||||
PermitLimit = 100,
|
||||
Window = TimeSpan.FromHours(1),
|
||||
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
|
||||
QueueLimit = 0 // No queuing; immediate rejection
|
||||
});
|
||||
});
|
||||
|
||||
// Manifest: 100 requests per hour per tenant
|
||||
options.AddPolicy(ManifestPolicy, context =>
|
||||
{
|
||||
var tenantId = GetTenantId(context);
|
||||
return RateLimitPartition.GetFixedWindowLimiter(
|
||||
partitionKey: $"manifest:{tenantId}",
|
||||
factory: _ => new FixedWindowRateLimiterOptions
|
||||
{
|
||||
PermitLimit = 100,
|
||||
Window = TimeSpan.FromHours(1),
|
||||
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
|
||||
QueueLimit = 0
|
||||
});
|
||||
});
|
||||
|
||||
// Configure rejection response
|
||||
options.OnRejected = async (context, cancellationToken) =>
|
||||
{
|
||||
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
|
||||
context.HttpContext.Response.Headers.RetryAfter = "3600"; // 1 hour
|
||||
|
||||
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
|
||||
{
|
||||
context.HttpContext.Response.Headers.RetryAfter =
|
||||
((int)retryAfter.TotalSeconds).ToString();
|
||||
}
|
||||
|
||||
await context.HttpContext.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
type = "https://stellaops.org/problems/rate-limit",
|
||||
title = "Too Many Requests",
|
||||
status = 429,
|
||||
detail = "Rate limit exceeded. Please wait before making more requests.",
|
||||
retryAfterSeconds = context.HttpContext.Response.Headers.RetryAfter.ToString()
|
||||
}, cancellationToken);
|
||||
};
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract tenant ID from the HTTP context for rate limiting partitioning.
|
||||
/// </summary>
|
||||
private static string GetTenantId(HttpContext context)
|
||||
{
|
||||
// Try to get tenant from claims
|
||||
var tenantClaim = context.User?.FindFirst(ScannerClaims.TenantId);
|
||||
if (tenantClaim is not null && !string.IsNullOrWhiteSpace(tenantClaim.Value))
|
||||
{
|
||||
return tenantClaim.Value;
|
||||
}
|
||||
|
||||
// Fallback to tenant header
|
||||
if (context.Request.Headers.TryGetValue("X-Tenant-Id", out var headerValue) &&
|
||||
!string.IsNullOrWhiteSpace(headerValue))
|
||||
{
|
||||
return headerValue.ToString();
|
||||
}
|
||||
|
||||
// Fallback to IP address for unauthenticated requests
|
||||
return context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scanner claims constants.
|
||||
/// </summary>
|
||||
public static class ScannerClaims
|
||||
{
|
||||
public const string TenantId = "tenant_id";
|
||||
}
|
||||
Reference in New Issue
Block a user