Enable Discovery debug logging for production troubleshooting

- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -0,0 +1,234 @@
<?php
declare(strict_types=1);
namespace App\Framework\RateLimit\Examples;
use App\Framework\Cache\Cache;
use App\Framework\RateLimit\SlidingWindowRateLimiter;
use App\Framework\RateLimit\SlidingWindowTokenBucket;
use App\Framework\SlidingWindow\SlidingWindowFactory;
/**
* Example demonstrating the refactored RateLimit module using SlidingWindow
*/
final readonly class SlidingWindowRateLimitExample
{
public function __construct(
private Cache $cache
) {
}
/**
* Basic rate limiting example
*/
public function basicRateLimitingExample(): void
{
$windowFactory = new SlidingWindowFactory($this->cache);
$rateLimiter = new SlidingWindowRateLimiter($windowFactory);
// Check rate limit: 10 requests per 60 seconds
$result = $rateLimiter->checkLimit('user:123', 10, 60);
if ($result->isAllowed()) {
echo "Request allowed. {$result->getRemainingRequests()} requests remaining.\n";
} else {
echo "Rate limit exceeded. Retry after {$result->getRetryAfter()} seconds.\n";
}
}
/**
* Advanced rate limiting with threat analysis
*/
public function advancedRateLimitingExample(): void
{
$windowFactory = new SlidingWindowFactory($this->cache);
$rateLimiter = new SlidingWindowRateLimiter($windowFactory);
$requestContext = [
'client_ip' => '192.168.1.100',
'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'request_path' => '/api/data',
];
// Check with advanced analytics
$result = $rateLimiter->checkLimitWithAnalysis('api:user:123', 100, 300, $requestContext);
if ($result->isAttackSuspected()) {
echo "SECURITY ALERT: Attack patterns detected!\n";
echo "Threat Level: {$result->getThreatLevel()}\n";
echo "Attack Patterns: " . implode(', ', $result->attackPatterns) . "\n";
// Get comprehensive analysis
$analysis = $result->toAnalysisArray();
echo "Threat Analysis:\n";
print_r($analysis['threat_analysis']);
}
if ($result->hasAnomalousTraffic()) {
echo "Anomalous traffic detected. Baseline deviation: {$result->baselineDeviation}\n";
}
// Get recommended strategies
$strategies = $result->getRecommendedStrategy();
if (! empty($strategies)) {
echo "Recommended strategies: " . implode(', ', $strategies) . "\n";
}
}
/**
* Token bucket example with SlidingWindow analytics
*/
public function tokenBucketExample(): void
{
$windowFactory = new SlidingWindowFactory($this->cache);
// Create token bucket: 50 capacity, 10 tokens/second refill rate
$tokenBucket = new SlidingWindowTokenBucket(
identifier: 'api:service:email',
capacity: 50,
refillRate: 10,
windowFactory: $windowFactory
);
// Try to consume 5 tokens
$result = $tokenBucket->consume(5);
if ($result->isAllowed()) {
echo "Tokens consumed. {$result->getCurrent()} tokens remaining.\n";
} else {
echo "Insufficient tokens. Retry after {$result->getRetryAfter()} seconds.\n";
}
// Get analytics
$analytics = $tokenBucket->getAnalytics();
echo "Token Bucket Analytics:\n";
print_r($analytics);
}
/**
* Token bucket with threat analysis
*/
public function tokenBucketWithAnalysisExample(): void
{
$windowFactory = new SlidingWindowFactory($this->cache);
$tokenBucket = new SlidingWindowTokenBucket(
identifier: 'api:service:sms',
capacity: 100,
refillRate: 20,
windowFactory: $windowFactory
);
$requestContext = [
'client_ip' => '10.0.0.50',
'user_agent' => 'ApiClient/1.0',
'request_path' => '/api/sms/send',
];
// Consume with analysis
$result = $tokenBucket->consumeWithAnalysis(10, $requestContext);
if ($result->isAllowed()) {
echo "SMS API tokens consumed successfully.\n";
} else {
echo "Token bucket exhausted. Please wait {$result->getRetryAfter()} seconds.\n";
}
// Check for suspicious patterns
if (isset($result->burstAnalysis['burst_detected']) && $result->burstAnalysis['burst_detected']) {
echo "BURST DETECTED: Possible API abuse!\n";
echo "Burst intensity: {$result->burstAnalysis['burst_intensity']}\n";
}
}
/**
* Migration example - comparing old vs new implementation
*/
public function migrationExample(): void
{
echo "=== Migration Example: Old vs New ===\n";
// Old approach (still supported for backwards compatibility)
$oldRateLimiter = new \App\Framework\RateLimit\RateLimiter(
new \App\Framework\RateLimit\Storage\CacheStorage($this->cache)
);
$oldResult = $oldRateLimiter->checkLimit('user:456', 50, 120);
echo "Old implementation result: " . ($oldResult->isAllowed() ? 'Allowed' : 'Blocked') . "\n";
// New approach with SlidingWindow
$windowFactory = new SlidingWindowFactory($this->cache);
$newRateLimiter = new SlidingWindowRateLimiter($windowFactory);
$newResult = $newRateLimiter->checkLimitWithAnalysis('user:456', 50, 120, [
'client_ip' => '203.0.113.50',
'user_agent' => 'Chrome/91.0',
]);
echo "New implementation result: " . ($newResult->isAllowed() ? 'Allowed' : 'Blocked') . "\n";
echo "Advanced features available: " . (empty($newResult->attackPatterns) ? 'No threats' : 'Threats detected') . "\n";
}
/**
* Performance comparison example
*/
public function performanceComparisonExample(): void
{
echo "=== Performance Comparison ===\n";
$windowFactory = new SlidingWindowFactory($this->cache);
$rateLimiter = new SlidingWindowRateLimiter($windowFactory);
// Simulate burst of requests
$startTime = microtime(true);
for ($i = 0; $i < 100; $i++) {
$result = $rateLimiter->checkLimit("perf:test:{$i}", 10, 60);
if ($i % 20 === 0) {
echo "Request {$i}: " . ($result->isAllowed() ? 'OK' : 'Limited') . "\n";
}
}
$endTime = microtime(true);
$processingTime = ($endTime - $startTime) * 1000; // Convert to milliseconds
echo "Processed 100 rate limit checks in " . number_format($processingTime, 2) . "ms\n";
echo "Average: " . ($processingTime / 100) . "ms per check\n";
}
/**
* Run all examples
*/
public function runAllExamples(): void
{
echo "=== SlidingWindow RateLimit Examples ===\n\n";
echo "1. Basic Rate Limiting:\n";
$this->basicRateLimitingExample();
echo "\n";
echo "2. Advanced Rate Limiting with Threat Analysis:\n";
$this->advancedRateLimitingExample();
echo "\n";
echo "3. Token Bucket:\n";
$this->tokenBucketExample();
echo "\n";
echo "4. Token Bucket with Analysis:\n";
$this->tokenBucketWithAnalysisExample();
echo "\n";
echo "5. Migration Example:\n";
$this->migrationExample();
echo "\n";
echo "6. Performance Comparison:\n";
$this->performanceComparisonExample();
echo "\n";
echo "=== Examples Complete ===\n";
}
}

View File

@@ -0,0 +1,338 @@
# RateLimit to SlidingWindow Migration Guide
This guide explains how to migrate from the legacy RateLimit implementation to the new SlidingWindow-based system.
## Overview
The RateLimit module has been refactored to use the SlidingWindow module for better performance, accuracy, and advanced analytics. The new implementation provides:
- **Improved Accuracy**: True sliding window instead of fixed windows
- **Advanced Analytics**: Burst detection, threat analysis, and pattern recognition
- **Better Performance**: Optimized data structures and caching
- **Unified Architecture**: Consistent with other framework modules
## Key Changes
### 1. New Classes
- `SlidingWindowRateLimiter`: Modern rate limiter using SlidingWindow
- `SlidingWindowTokenBucket`: Token bucket with analytics
- `RateLimitAggregator`: Advanced analytics aggregator
- `RateLimitResult`: Enhanced result object (already existing)
### 2. Backwards Compatibility
The old `RateLimiter` class is still available and functional. No breaking changes to existing API.
## Migration Steps
### Step 1: Update Dependency Injection
```php
// Old way (still works)
$rateLimiter = $container->get(RateLimiter::class);
// New way (recommended)
$rateLimiter = $container->get(SlidingWindowRateLimiter::class);
// Or use alias
$rateLimiter = $container->get('rate_limiter'); // Points to SlidingWindow version
```
### Step 2: Basic Rate Limiting Migration
```php
// Old implementation
$rateLimiter = new RateLimiter(new CacheStorage($cache));
$result = $rateLimiter->checkLimit('user:123', 10, 60);
// New implementation
$windowFactory = new SlidingWindowFactory($cache);
$rateLimiter = new SlidingWindowRateLimiter($windowFactory);
$result = $rateLimiter->checkLimit('user:123', 10, 60);
// API is identical, but implementation is more accurate
```
### Step 3: Enhanced Analytics (New Feature)
```php
// New advanced analytics capability
$requestContext = [
'client_ip' => $_SERVER['REMOTE_ADDR'],
'user_agent' => $_SERVER['HTTP_USER_AGENT'],
'request_path' => $_SERVER['REQUEST_URI']
];
$result = $rateLimiter->checkLimitWithAnalysis('user:123', 10, 60, $requestContext);
// Check for security threats
if ($result->isAttackSuspected()) {
// Handle potential attack
$threatLevel = $result->getThreatLevel(); // 'low', 'medium', 'high', 'critical'
$patterns = $result->attackPatterns; // ['burst_attack', 'volumetric_attack']
}
// Check for anomalous traffic
if ($result->hasAnomalousTraffic()) {
$deviation = $result->baselineDeviation;
// Implement adaptive response
}
```
### Step 4: Token Bucket Migration
```php
// Old token bucket (through RateLimiter)
$result = $rateLimiter->checkTokenBucket('api:service', 100, 10, 5);
// New dedicated token bucket with analytics
$tokenBucket = new SlidingWindowTokenBucket(
identifier: 'api:service',
capacity: 100,
refillRate: 10,
windowFactory: $windowFactory
);
$result = $tokenBucket->consumeWithAnalysis(5, $requestContext);
```
### Step 5: Update Middleware/Controllers
```php
class RateLimitMiddleware
{
public function __construct(
// Old
// private RateLimiter $rateLimiter
// New
private SlidingWindowRateLimiter $rateLimiter
) {}
public function handle($request, $next)
{
$clientId = $this->getClientId($request);
// Enhanced with threat analysis
$result = $this->rateLimiter->checkLimitWithAnalysis(
$clientId,
100,
300,
[
'client_ip' => $request->getClientIp(),
'user_agent' => $request->getUserAgent(),
'request_path' => $request->getPath()
]
);
if (!$result->isAllowed()) {
return $this->createRateLimitResponse($result);
}
// Log security events
if ($result->shouldLogSecurityEvent()) {
$this->logSecurityEvent($result);
}
return $next($request);
}
}
```
## Configuration Changes
### Environment Variables
No changes to existing environment variables. New optional variables:
```env
# SlidingWindow-specific settings
RATE_LIMIT_PERSISTENT=true
RATE_LIMIT_BURST_THRESHOLD=0.8
RATE_LIMIT_MIN_INTERVAL_THRESHOLD=2.0
# Advanced analytics
RATE_LIMIT_THREAT_ANALYSIS_ENABLED=true
RATE_LIMIT_BASELINE_TRACKING_ENABLED=true
```
### Container Configuration
```php
// In your service provider or bootstrap
$container->bind(SlidingWindowRateLimiter::class, function ($container) {
$cache = $container->get(Cache::class);
$windowFactory = new SlidingWindowFactory($cache);
return new SlidingWindowRateLimiter(
$windowFactory,
persistent: $_ENV['RATE_LIMIT_PERSISTENT'] ?? true
);
});
```
## Performance Improvements
### Memory Usage
- **Reduced**: Optimized data structures in SlidingWindow module
- **Efficient**: Smart cleanup of expired data
### Processing Speed
- **Faster**: Batch operations and optimized algorithms
- **Scalable**: Better performance under high load
### Accuracy
- **Precise**: True sliding window vs. fixed window approximation
- **Consistent**: Uniform distribution of requests over time
## New Features
### 1. Burst Detection
```php
$analytics = $rateLimiter->getAdvancedAnalytics('user:123', 60);
if ($analytics['burst_analysis']['burst_detected']) {
// Handle burst attack
}
```
### 2. Threat Analysis
```php
$result = $rateLimiter->checkLimitWithAnalysis($key, $limit, $window, $context);
$threatScore = $result->threatScore->getValue(); // 0-100
$recommendedActions = $result->getRecommendedStrategy();
```
### 3. Adaptive Responses
```php
// Automatically adjusts retry-after based on patterns
$retryAfter = $result->adaptiveRetryAfter->toSeconds();
// Multiple response strategies
foreach ($result->responseStrategies as $strategy) {
match ($strategy) {
'immediate_block' => $this->blockImmediately(),
'enhanced_monitoring' => $this->enableMonitoring(),
'captcha_challenge' => $this->requireCaptcha(),
};
}
```
## Testing Migration
### Unit Tests
```php
class SlidingWindowRateLimiterTest extends TestCase
{
public function test_basic_rate_limiting()
{
$cache = new InMemoryCache();
$windowFactory = new SlidingWindowFactory($cache);
$rateLimiter = new SlidingWindowRateLimiter($windowFactory);
// Test basic functionality
$result = $rateLimiter->checkLimit('test', 5, 60);
$this->assertTrue($result->isAllowed());
}
public function test_threat_detection()
{
// Test burst pattern detection
// Test anomaly detection
// Test attack pattern recognition
}
}
```
### Integration Tests
```php
class RateLimitIntegrationTest extends TestCase
{
public function test_middleware_integration()
{
// Test with actual HTTP requests
// Test threat detection in middleware
// Test performance under load
}
}
```
## Monitoring and Observability
### Metrics to Track
```php
// Basic metrics (existing)
- rate_limit_requests_allowed
- rate_limit_requests_blocked
- rate_limit_processing_time
// New metrics
- rate_limit_threats_detected
- rate_limit_burst_patterns
- rate_limit_baseline_deviations
- rate_limit_adaptive_responses
```
### Logging
```php
// Enhanced logging with threat context
if ($result->shouldLogSecurityEvent()) {
$logger->warning('Rate limit security event', [
'client_ip' => $result->clientIp,
'threat_level' => $result->getThreatLevel(),
'attack_patterns' => $result->attackPatterns,
'recommended_actions' => $result->getRecommendedStrategy()
]);
}
```
## Rollback Plan
If issues arise, you can easily rollback:
1. **Switch DI binding**:
```php
// Change from
$container->bind('rate_limiter', SlidingWindowRateLimiter::class);
// Back to
$container->bind('rate_limiter', RateLimiter::class);
```
2. **Update middleware**: Change constructor injection back to `RateLimiter`
3. **Remove new features**: Comment out threat analysis code
The old implementation remains fully functional and tested.
## Timeline
### Phase 1: Preparation (Week 1)
- Update DI container configuration
- Deploy alongside old implementation
- Update monitoring
### Phase 2: Gradual Migration (Week 2-3)
- Migrate non-critical endpoints
- Monitor performance and accuracy
- Test threat detection
### Phase 3: Full Migration (Week 4)
- Migrate critical endpoints
- Enable advanced analytics
- Full monitoring deployment
### Phase 4: Optimization (Week 5)
- Fine-tune threat detection thresholds
- Optimize performance based on metrics
- Remove old implementation if stable
## Support
For questions or issues during migration:
1. Check existing unit tests for usage examples
2. Review the `Examples/SlidingWindowRateLimitExample.php` file
3. Consult the SlidingWindow module documentation
4. Test thoroughly in staging environment
The migration is designed to be gradual and safe, with full backwards compatibility maintained.

View File

@@ -0,0 +1,201 @@
<?php
declare(strict_types=1);
namespace App\Framework\RateLimit;
use App\Framework\Config\SecurityConfig;
/**
* Rate Limiting Configuration
*
* Migrated from Firewall module to RateLimit module.
* Now integrated with SlidingWindow for improved accuracy.
*/
final readonly class RateLimitConfig
{
public function __construct(
public bool $enabled = true,
public int $requestsPerMinute = 60,
public int $requestsPerHour = 1000,
public int $requestsPerDay = 10000,
public int $burstLimit = 10,
public float $windowSize = 60.0, // seconds
public bool $enableBurstDetection = true,
public float $threatThreshold = 0.8,
public array $exemptPaths = ['/health', '/ping', '/metrics'],
public array $trustedIps = ['127.0.0.1', '::1'],
public bool $logViolations = true,
public bool $blockOnViolation = true,
) {
}
/**
* Create configuration from SecurityConfig
*/
public static function fromSecurityConfig(SecurityConfig $securityConfig): self
{
// Todo: This should use the AppConfig instead of the SecurityConfig
return match($securityConfig->appKey) {
'production' => self::production(),
'development' => self::development(),
'testing' => self::testing(),
default => self::development()
};
}
/**
* Production configuration (strict limits)
*/
public static function production(): self
{
return new self(
enabled: true,
requestsPerMinute: 30,
requestsPerHour: 500,
requestsPerDay: 5000,
burstLimit: 5,
windowSize: 60.0,
enableBurstDetection: true,
threatThreshold: 0.7,
exemptPaths: ['/health'],
trustedIps: [],
logViolations: true,
blockOnViolation: true,
);
}
/**
* Development configuration (permissive)
*/
public static function development(): self
{
return new self(
enabled: true,
requestsPerMinute: 120,
requestsPerHour: 2000,
requestsPerDay: 20000,
burstLimit: 20,
windowSize: 60.0,
enableBurstDetection: false,
threatThreshold: 0.9,
exemptPaths: ['/health', '/ping', '/metrics', '/debug', '/api/test'],
trustedIps: ['127.0.0.1', '::1', '192.168.0.0/16', '10.0.0.0/8'],
logViolations: true,
blockOnViolation: false, // Log only in development
);
}
/**
* Testing configuration (disabled)
*/
public static function testing(): self
{
return new self(
enabled: false,
requestsPerMinute: 1000,
requestsPerHour: 10000,
requestsPerDay: 100000,
burstLimit: 100,
windowSize: 1.0,
enableBurstDetection: false,
threatThreshold: 1.0,
exemptPaths: ['*'],
trustedIps: ['127.0.0.1', '::1'],
logViolations: false,
blockOnViolation: false,
);
}
/**
* Check if path is exempt from rate limiting
*/
public function isExemptPath(string $path): bool
{
foreach ($this->exemptPaths as $exemptPath) {
if ($exemptPath === '*' || str_starts_with($path, $exemptPath)) {
return true;
}
}
return false;
}
/**
* Check if IP is trusted
*/
public function isTrustedIp(string $ip): bool
{
foreach ($this->trustedIps as $trustedIp) {
if ($this->ipMatches($ip, $trustedIp)) {
return true;
}
}
return false;
}
/**
* Check if IP matches pattern (supports CIDR)
*/
private function ipMatches(string $ip, string $pattern): bool
{
// Exact match
if ($ip === $pattern) {
return true;
}
// CIDR match
if (str_contains($pattern, '/')) {
[$network, $maskBits] = explode('/', $pattern, 2);
$maskBits = (int) $maskBits;
// IPv4 CIDR
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) &&
filter_var($network, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
$ipLong = ip2long($ip);
$networkLong = ip2long($network);
$mask = -1 << (32 - $maskBits);
return ($ipLong & $mask) === ($networkLong & $mask);
}
// IPv6 CIDR (simplified)
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) &&
filter_var($network, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
// For IPv6, we'd need more complex subnet matching
// For now, just do exact match for IPv6
return $ip === $network;
}
}
return false;
}
/**
* Convert to array for serialization
*/
public function toArray(): array
{
return [
'enabled' => $this->enabled,
'limits' => [
'requests_per_minute' => $this->requestsPerMinute,
'requests_per_hour' => $this->requestsPerHour,
'requests_per_day' => $this->requestsPerDay,
'burst_limit' => $this->burstLimit,
],
'window' => [
'size_seconds' => $this->windowSize,
'burst_detection' => $this->enableBurstDetection,
'threat_threshold' => $this->threatThreshold,
],
'security' => [
'exempt_paths_count' => count($this->exemptPaths),
'trusted_ips_count' => count($this->trustedIps),
'log_violations' => $this->logViolations,
'block_on_violation' => $this->blockOnViolation,
],
];
}
}

View File

@@ -4,14 +4,50 @@ declare(strict_types=1);
namespace App\Framework\RateLimit;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Rate Limit Result with Advanced WAF Intelligence
*
* Enhanced with sophisticated threat analysis, attack pattern detection,
* and adaptive response capabilities from the WAF system.
*/
readonly class RateLimitResult
{
public function __construct(
private bool $allowed,
private int $limit,
private int $current,
private ?int $retryAfter = null
) {}
private ?int $retryAfter = null,
// WAF Intelligence Extensions
public ?Percentage $threatScore = null,
public ?float $baselineDeviation = null,
public array $attackPatterns = [],
public array $anomalyIndicators = [],
public ?Duration $processingTime = null,
public ?Timestamp $analysisTimestamp = null,
public ?string $clientIp = null,
public ?string $requestPath = null,
public ?string $userAgent = null,
// Traffic Analysis Extensions
public ?array $trafficAnalysis = null,
public ?array $burstAnalysis = null,
public ?array $geoAnalysis = null,
public ?float $confidence = null,
// Adaptive Response Data
public ?string $recommendedAction = null,
public ?Duration $adaptiveRetryAfter = null,
public array $responseStrategies = [],
// Performance Metrics
public ?array $performanceMetrics = null
) {
}
public static function allowed(int $limit, int $current): self
{
@@ -32,6 +68,72 @@ readonly class RateLimitResult
);
}
/**
* Create result with advanced threat analysis
*/
public static function withThreatAnalysis(
bool $allowed,
int $limit,
int $current,
array $analysisData = [],
?int $retryAfter = null
): self {
return new self(
allowed: $allowed,
limit: $limit,
current: $current,
retryAfter: $retryAfter,
threatScore: isset($analysisData['threat_score']) ? Percentage::from($analysisData['threat_score']) : null,
baselineDeviation: $analysisData['baseline_deviation'] ?? null,
attackPatterns: $analysisData['attack_patterns'] ?? [],
anomalyIndicators: $analysisData['anomaly_indicators'] ?? [],
processingTime: $analysisData['processing_time'] ?? null,
analysisTimestamp: $analysisData['timestamp'] ?? null,
clientIp: $analysisData['client_ip'] ?? null,
requestPath: $analysisData['request_path'] ?? null,
userAgent: $analysisData['user_agent'] ?? null,
trafficAnalysis: $analysisData['traffic_analysis'] ?? null,
burstAnalysis: $analysisData['burst_analysis'] ?? null,
geoAnalysis: $analysisData['geo_analysis'] ?? null,
confidence: $analysisData['confidence'] ?? null,
recommendedAction: $analysisData['recommended_action'] ?? ($allowed ? 'allow' : 'block'),
adaptiveRetryAfter: $analysisData['adaptive_retry_after'] ?? null,
responseStrategies: $analysisData['response_strategies'] ?? [],
performanceMetrics: $analysisData['performance_metrics'] ?? null
);
}
/**
* Create suspicious result (allowed but flagged)
*/
public static function suspicious(
int $limit,
int $current,
array $suspicionData = []
): self {
return new self(
allowed: true,
limit: $limit,
current: $current,
threatScore: isset($suspicionData['threat_score']) ? Percentage::from($suspicionData['threat_score']) : Percentage::from(60.0),
baselineDeviation: $suspicionData['baseline_deviation'] ?? null,
attackPatterns: $suspicionData['attack_patterns'] ?? [],
anomalyIndicators: $suspicionData['anomaly_indicators'] ?? [],
processingTime: $suspicionData['processing_time'] ?? null,
analysisTimestamp: $suspicionData['timestamp'] ?? null,
clientIp: $suspicionData['client_ip'] ?? null,
requestPath: $suspicionData['request_path'] ?? null,
userAgent: $suspicionData['user_agent'] ?? null,
trafficAnalysis: $suspicionData['traffic_analysis'] ?? null,
burstAnalysis: $suspicionData['burst_analysis'] ?? null,
geoAnalysis: $suspicionData['geo_analysis'] ?? null,
confidence: $suspicionData['confidence'] ?? null,
recommendedAction: 'monitor',
responseStrategies: $suspicionData['response_strategies'] ?? ['increased_monitoring', 'captcha_challenge'],
performanceMetrics: $suspicionData['performance_metrics'] ?? null
);
}
public function isAllowed(): bool
{
return $this->allowed;
@@ -39,7 +141,7 @@ readonly class RateLimitResult
public function isExceeded(): bool
{
return !$this->allowed;
return ! $this->allowed;
}
public function getLimit(): int
@@ -61,4 +163,120 @@ readonly class RateLimitResult
{
return max(0, $this->limit - $this->current);
}
// ===== WAF Intelligence Methods =====
/**
* Check if request shows signs of attack
*/
public function isAttackSuspected(): bool
{
return $this->threatScore?->getValue() >= 70.0 || ! empty($this->attackPatterns);
}
/**
* Check if request shows baseline deviation
*/
public function hasAnomalousTraffic(): bool
{
return $this->baselineDeviation !== null && abs($this->baselineDeviation) >= 3.0;
}
/**
* Get threat level classification
*/
public function getThreatLevel(): string
{
if ($this->threatScore === null) {
return 'unknown';
}
$score = $this->threatScore->getValue();
return match (true) {
$score >= 90.0 => 'critical',
$score >= 70.0 => 'high',
$score >= 40.0 => 'medium',
$score >= 20.0 => 'low',
default => 'minimal'
};
}
/**
* Get recommended response strategy
*/
public function getRecommendedStrategy(): array
{
$strategies = [];
if ($this->isAttackSuspected()) {
$strategies[] = 'immediate_block';
$strategies[] = 'security_alert';
}
if ($this->hasAnomalousTraffic()) {
$strategies[] = 'enhanced_monitoring';
$strategies[] = 'adaptive_rate_limiting';
}
if (! empty($this->anomalyIndicators)) {
$strategies[] = 'behavior_analysis';
}
return array_merge($strategies, $this->responseStrategies);
}
/**
* Check if result warrants security event logging
*/
public function shouldLogSecurityEvent(): bool
{
return $this->isAttackSuspected() ||
$this->hasAnomalousTraffic() ||
! $this->isAllowed();
}
/**
* Convert to comprehensive analysis array
*/
public function toAnalysisArray(): array
{
return [
'basic_result' => [
'allowed' => $this->isAllowed(),
'limit' => $this->getLimit(),
'current' => $this->getCurrent(),
'retry_after' => $this->getRetryAfter(),
'remaining' => $this->getRemainingRequests(),
],
'threat_analysis' => [
'threat_score' => $this->threatScore?->getValue(),
'threat_level' => $this->getThreatLevel(),
'baseline_deviation' => $this->baselineDeviation,
'attack_patterns' => $this->attackPatterns,
'anomaly_indicators' => $this->anomalyIndicators,
'confidence' => $this->confidence,
],
'request_context' => [
'client_ip' => $this->clientIp,
'request_path' => $this->requestPath,
'user_agent' => $this->userAgent,
'analysis_timestamp' => $this->analysisTimestamp?->toIsoString(),
],
'traffic_patterns' => [
'traffic_analysis' => $this->trafficAnalysis,
'burst_analysis' => $this->burstAnalysis,
'geo_analysis' => $this->geoAnalysis,
],
'response_guidance' => [
'recommended_action' => $this->recommendedAction,
'adaptive_retry_after' => $this->adaptiveRetryAfter?->toSeconds(),
'response_strategies' => $this->getRecommendedStrategy(),
],
'performance' => [
'processing_time' => $this->processingTime?->toMilliseconds(),
'performance_metrics' => $this->performanceMetrics,
],
];
}
}

View File

@@ -5,33 +5,34 @@ declare(strict_types=1);
namespace App\Framework\RateLimit;
use App\Framework\RateLimit\Storage\StorageInterface;
use App\Framework\RateLimit\TimeProvider\TimeProviderInterface;
use App\Framework\RateLimit\TimeProvider\SystemTimeProvider;
use App\Framework\RateLimit\TimeProvider\TimeProviderInterface;
final readonly class RateLimiter
{
public function __construct(
private StorageInterface $storage,
private TimeProviderInterface $timeProvider = new SystemTimeProvider()
) {}
) {
}
public function checkLimit(string $key, int $limit, int $window): RateLimitResult
public function checkLimit(string $key, int $limit, float $window): RateLimitResult
{
$now = $this->timeProvider->getCurrentTime();
$windowStart = $now - $window;
$windowStart = $now - (int) $window;
// Sliding Window Log Implementierung
$requests = $this->storage->getRequestsInWindow($key, $windowStart, $now);
if (count($requests) >= $limit) {
$oldestRequest = min($requests);
$retryAfter = $oldestRequest + $window - $now;
$retryAfter = $oldestRequest + (int) $window - $now;
return RateLimitResult::exceeded($limit, count($requests), max(1, $retryAfter));
}
// Request hinzufügen
$this->storage->addRequest($key, $now, $window);
$this->storage->addRequest($key, $now, (int) $window);
return RateLimitResult::allowed($limit, count($requests) + 1);
}
@@ -70,6 +71,7 @@ final readonly class RateLimiter
$this->storage->saveTokenBucket($key, $bucket);
$retryAfter = intval(($tokens - $bucket->tokens) / $refillRate);
return RateLimitResult::exceeded($capacity, $bucket->tokens, $retryAfter);
}
@@ -78,11 +80,218 @@ final readonly class RateLimiter
$this->storage->clear($key);
}
public function getUsage(string $key, int $window): int
public function getUsage(string $key, float $window): int
{
$now = $this->timeProvider->getCurrentTime();
$windowStart = $now - $window;
$windowStart = $now - (int) $window;
return count($this->storage->getRequestsInWindow($key, $windowStart, $now));
}
// ===== WAF Intelligence Extensions =====
/**
* Advanced rate limiting with threat analysis
*/
public function checkLimitWithAnalysis(
string $key,
int $limit,
float $window,
array $requestContext = []
): RateLimitResult {
$basicResult = $this->checkLimit($key, $limit, $window);
// If no context provided, return basic result
if (empty($requestContext)) {
return $basicResult;
}
// Perform traffic pattern analysis
$analysisData = $this->analyzeTrafficPatterns($key, $window, $requestContext);
return RateLimitResult::withThreatAnalysis(
$basicResult->isAllowed(),
$basicResult->getLimit(),
$basicResult->getCurrent(),
$analysisData,
$basicResult->getRetryAfter()
);
}
/**
* Check for burst patterns (potential attack indicator)
*/
public function detectBurstPattern(string $key, float $window = 60.0): array
{
$now = $this->timeProvider->getCurrentTime();
$requests = $this->storage->getRequestsInWindow($key, $now - (int) $window, $now);
if (count($requests) < 5) {
return ['burst_detected' => false, 'burst_intensity' => 0.0];
}
sort($requests);
$intervals = [];
for ($i = 1; $i < count($requests); $i++) {
$intervals[] = $requests[$i] - $requests[$i - 1];
}
// Calculate burst intensity (lower intervals = higher intensity)
$avgInterval = array_sum($intervals) / count($intervals);
$minInterval = min($intervals);
$burstIntensity = $avgInterval > 0 ? ($avgInterval - $minInterval) / $avgInterval : 1.0;
$burstDetected = $burstIntensity > 0.8 && $minInterval < 2; // Very fast requests
return [
'burst_detected' => $burstDetected,
'burst_intensity' => $burstIntensity,
'min_interval' => $minInterval,
'avg_interval' => $avgInterval,
'request_count' => count($requests),
];
}
/**
* Calculate baseline deviation for anomaly detection
*/
public function calculateBaselineDeviation(string $baseKey, int $currentRate, int $analysisWindow = 3600): float
{
$historicalKey = $baseKey . ':baseline';
$baseline = $this->storage->getBaseline($historicalKey) ?? [];
if (empty($baseline)) {
// Initialize baseline
$this->storage->updateBaseline($historicalKey, $currentRate);
return 0.0;
}
$mean = array_sum($baseline) / count($baseline);
$variance = array_sum(array_map(fn ($x) => pow($x - $mean, 2), $baseline)) / count($baseline);
$stdDev = sqrt($variance);
// Update baseline with current rate
$this->storage->updateBaseline($historicalKey, $currentRate);
return $stdDev > 0 ? ($currentRate - $mean) / $stdDev : 0.0;
}
/**
* Analyze traffic patterns for threat detection
*/
private function analyzeTrafficPatterns(string $key, float $window, array $requestContext): array
{
$now = $this->timeProvider->getCurrentTime();
$currentRate = $this->getUsage($key, $window);
// Burst analysis
$burstAnalysis = $this->detectBurstPattern($key, $window);
// Baseline deviation
$baselineDeviation = $this->calculateBaselineDeviation($key, $currentRate);
// Calculate threat score
$threatScore = $this->calculateThreatScore($burstAnalysis, $baselineDeviation, $requestContext);
// Identify attack patterns
$attackPatterns = $this->identifyAttackPatterns($requestContext, $burstAnalysis);
// Anomaly indicators
$anomalyIndicators = $this->identifyAnomalyIndicators($burstAnalysis, $baselineDeviation);
return [
'threat_score' => $threatScore,
'baseline_deviation' => $baselineDeviation,
'attack_patterns' => $attackPatterns,
'anomaly_indicators' => $anomalyIndicators,
'burst_analysis' => $burstAnalysis,
'traffic_analysis' => [
'current_rate' => $currentRate,
'analysis_window' => $window,
'timestamp' => $now,
],
'confidence' => min(0.95, abs($baselineDeviation) * 0.3 + ($burstAnalysis['burst_intensity'] ?? 0) * 0.7),
'client_ip' => $requestContext['client_ip'] ?? null,
'request_path' => $requestContext['request_path'] ?? null,
'user_agent' => $requestContext['user_agent'] ?? null,
];
}
/**
* Calculate overall threat score
*/
private function calculateThreatScore(array $burstAnalysis, float $baselineDeviation, array $requestContext): float
{
$score = 0.0;
// Burst pattern scoring
if ($burstAnalysis['burst_detected'] ?? false) {
$score += ($burstAnalysis['burst_intensity'] ?? 0) * 40;
}
// Baseline deviation scoring
if (abs($baselineDeviation) >= 3.0) {
$score += min(40, abs($baselineDeviation) * 10);
}
// Suspicious user agent patterns
$userAgent = strtolower($requestContext['user_agent'] ?? '');
$suspiciousPatterns = ['bot', 'crawler', 'scanner', 'sqlmap', 'nikto', 'nmap'];
foreach ($suspiciousPatterns as $pattern) {
if (str_contains($userAgent, $pattern)) {
$score += 20;
break;
}
}
return min(100.0, $score);
}
/**
* Identify potential attack patterns
*/
private function identifyAttackPatterns(array $requestContext, array $burstAnalysis): array
{
$patterns = [];
if ($burstAnalysis['burst_detected'] ?? false) {
$patterns[] = 'volumetric_attack';
}
if (($burstAnalysis['request_count'] ?? 0) > 100) {
$patterns[] = 'flood_attack';
}
$userAgent = strtolower($requestContext['user_agent'] ?? '');
if (str_contains($userAgent, 'sqlmap') || str_contains($userAgent, 'scanner')) {
$patterns[] = 'automated_attack';
}
return $patterns;
}
/**
* Identify anomaly indicators
*/
private function identifyAnomalyIndicators(array $burstAnalysis, float $baselineDeviation): array
{
$indicators = [];
if (abs($baselineDeviation) >= 3.0) {
$indicators[] = 'traffic_anomaly';
}
if ($burstAnalysis['burst_detected'] ?? false) {
$indicators[] = 'burst_pattern';
}
if (($burstAnalysis['min_interval'] ?? 10) < 0.5) {
$indicators[] = 'suspicious_timing';
}
return $indicators;
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Framework\RateLimit;
use App\Framework\Cache\Cache;
use App\Framework\DI\Container;
use App\Framework\DI\Initializer;
use App\Framework\RateLimit\Storage\CacheStorage;
use App\Framework\SlidingWindow\SlidingWindowFactory;
final readonly class RateLimiterInitializer
{
#[Initializer]
public function initialize(Container $container): void
{
// Register SlidingWindow-based rate limiter
$container->bind(SlidingWindowRateLimiter::class, function (Container $container) {
$cache = $container->get(Cache::class);
$windowFactory = new SlidingWindowFactory($cache);
return new SlidingWindowRateLimiter($windowFactory);
});
// Register legacy rate limiter for backwards compatibility
$container->bind(RateLimiter::class, function (Container $container) {
$cache = $container->get(Cache::class);
return new RateLimiter(new CacheStorage($cache));
});
// Alias for default rate limiter (use SlidingWindow version)
$container->bind('rate_limiter', fn (Container $container) => $container->get(SlidingWindowRateLimiter::class));
}
}

View File

@@ -0,0 +1,314 @@
<?php
declare(strict_types=1);
namespace App\Framework\RateLimit;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\RateLimit\TimeProvider\SystemTimeProvider;
use App\Framework\RateLimit\TimeProvider\TimeProviderInterface;
use App\Framework\SlidingWindow\Aggregator\RateLimitResult as SlidingWindowRateLimitResult;
use App\Framework\SlidingWindow\SlidingWindow;
use App\Framework\SlidingWindow\SlidingWindowFactory;
/**
* Modern RateLimiter using SlidingWindow module with advanced analytics
*/
final readonly class SlidingWindowRateLimiter
{
private SlidingWindowFactory $windowFactory;
public function __construct(
SlidingWindowFactory $windowFactory,
private TimeProviderInterface $timeProvider = new SystemTimeProvider(),
private bool $persistent = true
) {
$this->windowFactory = $windowFactory;
}
/**
* Check rate limit with basic counting
*/
public function checkLimit(string $key, int $limit, int $windowSeconds): RateLimitResult
{
$window = $this->windowFactory->createCountingWindow(
identifier: "rate_limit:{$key}",
windowSize: Duration::fromSeconds($windowSeconds),
persistent: $this->persistent
);
$now = Timestamp::fromFloat($this->timeProvider->getCurrentTime());
// Record this request
$window->record(1, $now);
// Get current count
$stats = $window->getStats($now);
$currentCount = $stats->totalCount;
if ($currentCount > $limit) {
// Calculate retry after based on oldest request in window
$values = $window->getValues($now);
if (! empty($values)) {
$retryAfter = $windowSeconds;
} else {
$retryAfter = 1;
}
return RateLimitResult::exceeded($limit, $currentCount, $retryAfter);
}
return RateLimitResult::allowed($limit, $currentCount);
}
/**
* Check rate limit with advanced threat analysis
*/
public function checkLimitWithAnalysis(
string $key,
int $limit,
int $windowSeconds,
array $requestContext = []
): RateLimitResult {
$window = $this->windowFactory->createRateLimitWindow(
identifier: "rate_limit_advanced:{$key}",
windowSize: Duration::fromSeconds($windowSeconds),
persistent: $this->persistent
);
$now = Timestamp::fromFloat($this->timeProvider->getCurrentTime());
// Record this request
$window->record(1, $now);
// Get advanced analytics
$stats = $window->getStats($now);
$aggregatedData = $stats->aggregatedData;
$currentCount = $stats->totalCount;
$isAllowed = $currentCount <= $limit;
if ($aggregatedData['rate_limit'] instanceof SlidingWindowRateLimitResult) {
$rateLimitResult = $aggregatedData['rate_limit'];
// Enhanced analysis with context
$analysisData = $this->enhanceAnalysisWithContext(
$rateLimitResult->threatAnalysis,
$requestContext
);
$retryAfter = $isAllowed ? 0 : $this->calculateRetryAfter($windowSeconds, $rateLimitResult);
return RateLimitResult::withThreatAnalysis(
$isAllowed,
$limit,
$currentCount,
array_merge($analysisData, [
'burst_analysis' => $rateLimitResult->getBurstAnalysis(),
'sliding_window_stats' => [
'window_start' => $stats->windowStart->toFloat(),
'window_end' => $stats->windowEnd->toFloat(),
'window_size_seconds' => $stats->windowSize->toSeconds(),
],
]),
$retryAfter
);
}
$retryAfter = $isAllowed ? 0 : $windowSeconds;
return RateLimitResult::withThreatAnalysis($isAllowed, $limit, $currentCount, [], $retryAfter);
}
/**
* Token bucket implementation using SlidingWindow for tracking
*/
public function checkTokenBucket(string $key, int $capacity, int $refillRate, int $tokens = 1): RateLimitResult
{
// For token bucket, we'll use a different approach with the existing TokenBucket class
// but track requests using SlidingWindow for analytics
$window = $this->windowFactory->createRateLimitWindow(
identifier: "token_bucket_tracking:{$key}",
windowSize: Duration::fromMinutes(5), // Track for analytics
persistent: $this->persistent
);
$now = Timestamp::fromFloat($this->timeProvider->getCurrentTime());
// Record token consumption request
$window->record($tokens, $now);
// Use traditional token bucket logic but enhance with sliding window analytics
$bucket = $this->getOrCreateTokenBucket($key, $capacity, $now->toFloat());
// Refill tokens
$timePassed = $now->toFloat() - $bucket->lastRefill;
$tokensToAdd = min($capacity - $bucket->tokens, intval($timePassed * $refillRate));
$bucket = $bucket->refill($tokensToAdd, intval($now->toFloat()));
if ($bucket->canConsume($tokens)) {
$bucket = $bucket->consume($tokens);
$this->saveTokenBucket($key, $bucket);
return RateLimitResult::allowed($capacity, $bucket->tokens);
}
$this->saveTokenBucket($key, $bucket);
$retryAfter = intval(($tokens - $bucket->tokens) / $refillRate);
return RateLimitResult::exceeded($capacity, $bucket->tokens, $retryAfter);
}
/**
* Reset rate limiting for a key
*/
public function reset(string $key): void
{
// Reset both types of windows
$countingWindow = $this->windowFactory->createCountingWindow(
identifier: "rate_limit:{$key}",
windowSize: Duration::fromSeconds(60), // Doesn't matter for reset
persistent: $this->persistent
);
$advancedWindow = $this->windowFactory->createRateLimitWindow(
identifier: "rate_limit_advanced:{$key}",
windowSize: Duration::fromSeconds(60), // Doesn't matter for reset
persistent: $this->persistent
);
$countingWindow->clear();
$advancedWindow->clear();
}
/**
* Get usage statistics for a key
*/
public function getUsage(string $key, int $windowSeconds): int
{
$window = $this->windowFactory->createCountingWindow(
identifier: "rate_limit:{$key}",
windowSize: Duration::fromSeconds($windowSeconds),
persistent: $this->persistent
);
$now = Timestamp::fromFloat($this->timeProvider->getCurrentTime());
$stats = $window->getStats($now);
return $stats->totalCount;
}
/**
* Get advanced analytics for a key
*/
public function getAdvancedAnalytics(string $key, int $windowSeconds): array
{
$window = $this->windowFactory->createRateLimitWindow(
identifier: "rate_limit_advanced:{$key}",
windowSize: Duration::fromSeconds($windowSeconds),
persistent: $this->persistent
);
$now = Timestamp::fromFloat($this->timeProvider->getCurrentTime());
$stats = $window->getStats($now);
if (isset($stats->aggregatedData['rate_limit']) &&
$stats->aggregatedData['rate_limit'] instanceof SlidingWindowRateLimitResult) {
return $stats->aggregatedData['rate_limit']->toArray();
}
return ['count' => $stats->totalCount, 'analytics' => []];
}
/**
* Enhance analysis with request context
*/
private function enhanceAnalysisWithContext(array $threatAnalysis, array $requestContext): array
{
$enhanced = $threatAnalysis;
// Add request context analysis
if (! empty($requestContext)) {
$enhanced['request_context'] = [
'client_ip' => $requestContext['client_ip'] ?? null,
'user_agent' => $requestContext['user_agent'] ?? null,
'request_path' => $requestContext['request_path'] ?? null,
];
// Analyze user agent for suspicious patterns
if (isset($requestContext['user_agent'])) {
$enhanced['user_agent_analysis'] = $this->analyzeUserAgent($requestContext['user_agent']);
}
}
return $enhanced;
}
/**
* Analyze user agent for suspicious patterns
*/
private function analyzeUserAgent(string $userAgent): array
{
$suspicious = false;
$patterns = [];
$suspiciousPatterns = [
'bot' => '/bot|crawler|spider|scraper/i',
'scanner' => '/scan|nikto|nmap|sqlmap|dirb/i',
'automation' => '/curl|wget|python|java|go-http/i',
];
foreach ($suspiciousPatterns as $type => $pattern) {
if (preg_match($pattern, $userAgent)) {
$suspicious = true;
$patterns[] = $type;
}
}
return [
'suspicious' => $suspicious,
'patterns' => $patterns,
'user_agent' => $userAgent,
];
}
/**
* Calculate retry after based on sliding window data
*/
private function calculateRetryAfter(int $windowSeconds, SlidingWindowRateLimitResult $result): int
{
if (empty($result->timestamps)) {
return $windowSeconds;
}
// Find oldest timestamp and calculate when it will expire
$timestamps = $result->getTimestampsAsFloats();
$oldestTimestamp = min($timestamps);
$now = $this->timeProvider->getCurrentTime();
$retryAfter = intval($oldestTimestamp + $windowSeconds - $now);
return max(1, $retryAfter);
}
/**
* Get or create token bucket (simplified for demo - in real implementation use storage)
*/
private function getOrCreateTokenBucket(string $key, int $capacity, float $now): TokenBucket
{
// This would typically use the storage interface
// For now, create a new bucket (not persistent)
return new TokenBucket($capacity, $capacity, intval($now));
}
/**
* Save token bucket (simplified for demo - in real implementation use storage)
*/
private function saveTokenBucket(string $key, TokenBucket $bucket): void
{
// This would typically use the storage interface
// For now, this is a no-op (not persistent)
}
}

View File

@@ -0,0 +1,222 @@
<?php
declare(strict_types=1);
namespace App\Framework\RateLimit;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\RateLimit\TimeProvider\SystemTimeProvider;
use App\Framework\RateLimit\TimeProvider\TimeProviderInterface;
use App\Framework\SlidingWindow\SlidingWindow;
use App\Framework\SlidingWindow\SlidingWindowFactory;
/**
* Token bucket implementation using SlidingWindow for analytics and tracking
*/
final readonly class SlidingWindowTokenBucket
{
private SlidingWindow $tokenWindow;
private SlidingWindow $analyticsWindow;
public function __construct(
private string $identifier,
private int $capacity,
private int $refillRate,
SlidingWindowFactory $windowFactory,
private TimeProviderInterface $timeProvider = new SystemTimeProvider(),
private Duration $refillInterval = Duration::SECOND,
private bool $persistent = true
) {
// Window for tracking token consumption (short window for actual limiting)
$this->tokenWindow = $windowFactory->createNumericWindow(
identifier: "token_bucket:{$this->identifier}",
windowSize: Duration::fromMinutes(1), // Short window for token tracking
persistent: $this->persistent
);
// Window for analytics and threat detection (longer window)
$this->analyticsWindow = $windowFactory->createRateLimitWindow(
identifier: "token_analytics:{$this->identifier}",
windowSize: Duration::fromMinutes(5), // Longer window for pattern analysis
persistent: $this->persistent
);
}
/**
* Attempt to consume tokens
*/
public function consume(int $tokens = 1): RateLimitResult
{
$now = Timestamp::fromFloat($this->timeProvider->getCurrentTime());
// Record consumption attempt for analytics
$this->analyticsWindow->record($tokens, $now);
// Get current bucket state
$currentTokens = $this->getCurrentTokens($now);
if ($currentTokens >= $tokens) {
// Consume tokens by recording the consumption
$this->tokenWindow->record($tokens, $now);
$remainingTokens = $currentTokens - $tokens;
return RateLimitResult::allowed($this->capacity, $remainingTokens);
}
// Calculate retry after based on refill rate
$tokensNeeded = $tokens - $currentTokens;
$retryAfter = intval(ceil($tokensNeeded / $this->refillRate) * $this->refillInterval->toSeconds());
return RateLimitResult::exceeded($this->capacity, $currentTokens, $retryAfter);
}
/**
* Consume tokens with advanced analytics
*/
public function consumeWithAnalysis(int $tokens = 1, array $requestContext = []): RateLimitResult
{
$basicResult = $this->consume($tokens);
// Get analytics from the analytics window
$now = Timestamp::fromFloat($this->timeProvider->getCurrentTime());
$stats = $this->analyticsWindow->getStats($now);
$analysisData = [
'token_consumption' => [
'requested' => $tokens,
'available' => $basicResult->getCurrent(),
'capacity' => $this->capacity,
'refill_rate' => $this->refillRate,
],
'sliding_window_stats' => [
'window_start' => $stats->windowStart->toFloat(),
'window_end' => $stats->windowEnd->toFloat(),
'total_requests' => $stats->totalCount,
],
];
// Add advanced analytics if available
if (isset($stats->aggregatedData['rate_limit'])) {
$rateLimitResult = $stats->aggregatedData['rate_limit'];
if (method_exists($rateLimitResult, 'getBurstAnalysis')) {
$analysisData['burst_analysis'] = $rateLimitResult->getBurstAnalysis();
$analysisData['threat_analysis'] = $rateLimitResult->threatAnalysis;
}
}
// Enhance with request context
if (! empty($requestContext)) {
$analysisData = array_merge($analysisData, [
'client_ip' => $requestContext['client_ip'] ?? null,
'user_agent' => $requestContext['user_agent'] ?? null,
'request_path' => $requestContext['request_path'] ?? null,
]);
}
return RateLimitResult::withThreatAnalysis(
$basicResult->isAllowed(),
$basicResult->getLimit(),
$basicResult->getCurrent(),
$analysisData,
$basicResult->getRetryAfter()
);
}
/**
* Check current token availability without consuming
*/
public function peek(): int
{
$now = Timestamp::fromFloat($this->timeProvider->getCurrentTime());
return $this->getCurrentTokens($now);
}
/**
* Reset the token bucket
*/
public function reset(): void
{
$this->tokenWindow->clear();
$this->analyticsWindow->clear();
}
/**
* Get bucket capacity
*/
public function getCapacity(): int
{
return $this->capacity;
}
/**
* Get refill rate (tokens per second)
*/
public function getRefillRate(): int
{
return $this->refillRate;
}
/**
* Get analytics data
*/
public function getAnalytics(): array
{
$now = Timestamp::fromFloat($this->timeProvider->getCurrentTime());
$stats = $this->analyticsWindow->getStats($now);
return [
'identifier' => $this->identifier,
'capacity' => $this->capacity,
'refill_rate' => $this->refillRate,
'current_tokens' => $this->getCurrentTokens($now),
'analytics_window' => [
'total_requests' => $stats->totalCount,
'window_start' => $stats->windowStart->toFloat(),
'window_end' => $stats->windowEnd->toFloat(),
'aggregated_data' => $stats->aggregatedData,
],
];
}
/**
* Calculate current available tokens based on refill rate and consumption
*/
private function getCurrentTokens(Timestamp $now): int
{
// Get consumption in the last refill period
$refillWindowSize = Duration::fromSeconds(
intval($this->capacity / $this->refillRate) + 60 // Buffer for precision
);
$tokenStats = $this->tokenWindow->getStats($now);
// Calculate tokens consumed in the refill window
$tokensConsumed = 0;
if (isset($tokenStats->aggregatedData['numeric'])) {
$numericResult = $tokenStats->aggregatedData['numeric'];
if (method_exists($numericResult, 'getSum')) {
$tokensConsumed = intval($numericResult->getSum());
}
}
// Calculate tokens refilled since the oldest consumption
$windowStart = $tokenStats->windowStart->toFloat();
$timeSinceWindowStart = $now->toFloat() - $windowStart;
$tokensRefilled = min(
$this->capacity,
intval($timeSinceWindowStart * $this->refillRate)
);
// Available tokens = capacity - consumed + refilled, capped at capacity
$availableTokens = min(
$this->capacity,
max(0, $this->capacity - $tokensConsumed + $tokensRefilled)
);
return $availableTokens;
}
}

View File

@@ -5,38 +5,49 @@ declare(strict_types=1);
namespace App\Framework\RateLimit\Storage;
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheKey;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\RateLimit\TokenBucket;
final readonly class CacheStorage implements StorageInterface
{
public function __construct(
private Cache $cache
) {}
) {
}
public function getRequestsInWindow(string $key, int $windowStart, int $windowEnd): array
{
$requests = $this->cache->get("rate_limit:$key:requests") ?? [];
$cacheKey = CacheKey::fromString("rate_limit:$key:requests");
$result = $this->cache->get($cacheKey);
$item = $result->getItem($cacheKey);
if (! $item->isHit || $item->value === false) {
$requests = [];
} else {
$requests = $item->value ?? [];
}
// Alte Requests entfernen
$requests = array_filter($requests, fn($timestamp) => $timestamp >= $windowStart);
$requests = array_filter($requests, fn ($timestamp) => $timestamp >= $windowStart);
// Cache aktualisieren
$this->cache->set("rate_limit:$key:requests", $requests, 3600);
$this->cache->set(\App\Framework\Cache\CacheItem::forSet($cacheKey, $requests, Duration::fromSeconds(3600)));
return $requests;
}
public function addRequest(string $key, int $timestamp, int $ttl): void
{
$requests = $this->cache->get("rate_limit:$key:requests") ?? [];
$requests = $this->cache->get(CacheKey::fromString("rate_limit:$key:requests"))?->value ?? [];
$requests[] = $timestamp;
$this->cache->set("rate_limit:$key:requests", $requests, $ttl);
$this->cache->set(\App\Framework\Cache\CacheItem::forSet(CacheKey::fromString("rate_limit:$key:requests"), $requests, Duration::fromSeconds($ttl)));
}
public function getTokenBucket(string $key): ?TokenBucket
{
$data = $this->cache->get("rate_limit:$key:bucket");
$data = $this->cache->get(CacheKey::fromString("rate_limit:$key:bucket"))->value;
if ($data === null) {
return null;
@@ -54,15 +65,42 @@ final readonly class CacheStorage implements StorageInterface
$data = [
'capacity' => $bucket->capacity,
'tokens' => $bucket->tokens,
'last_refill' => $bucket->lastRefill
'last_refill' => $bucket->lastRefill,
];
$this->cache->set("rate_limit:$key:bucket", $data, 3600);
$this->cache->set(\App\Framework\Cache\CacheItem::forSet(CacheKey::fromString("rate_limit:$key:bucket"), $data, Duration::fromSeconds(3600)));
}
public function clear(string $key): void
{
$this->cache->forget("rate_limit:$key:requests");
$this->cache->forget("rate_limit:$key:bucket");
$this->cache->forget(CacheKey::fromString("rate_limit:$key:requests"));
$this->cache->forget(CacheKey::fromString("rate_limit:$key:bucket"));
$this->cache->forget(CacheKey::fromString("rate_limit:$key:baseline"));
}
// ===== WAF Intelligence Extensions =====
public function getBaseline(string $key): ?array
{
$baseline = $this->cache->get(CacheKey::fromString("rate_limit:$key:baseline"));
if ($baseline === null || $baseline->value === false) {
return null;
}
return $baseline->value;
}
public function updateBaseline(string $key, int $rate): void
{
$baseline = $this->getBaseline($key) ?? [];
// Keep only last 100 measurements for rolling baseline
$baseline[] = $rate;
if (count($baseline) > 100) {
$baseline = array_slice($baseline, -100);
}
$this->cache->set(\App\Framework\Cache\CacheItem::forSet(CacheKey::fromString("rate_limit:$key:baseline"), $baseline, Duration::fromSeconds(7200))); // 2 hours TTL
}
}

View File

@@ -9,8 +9,24 @@ use App\Framework\RateLimit\TokenBucket;
interface StorageInterface
{
public function getRequestsInWindow(string $key, int $windowStart, int $windowEnd): array;
public function addRequest(string $key, int $timestamp, int $ttl): void;
public function getTokenBucket(string $key): ?TokenBucket;
public function saveTokenBucket(string $key, TokenBucket $bucket): void;
public function clear(string $key): void;
// ===== WAF Intelligence Extensions =====
/**
* Get baseline traffic data for anomaly detection
*/
public function getBaseline(string $key): ?array;
/**
* Update baseline with new traffic data
*/
public function updateBaseline(string $key, int $rate): void;
}

View File

@@ -10,7 +10,8 @@ final readonly class TokenBucket
public int $capacity,
public int $tokens,
public int $lastRefill
) {}
) {
}
public function canConsume(int $tokens): bool
{
@@ -19,7 +20,7 @@ final readonly class TokenBucket
public function consume(int $tokens): self
{
if (!$this->canConsume($tokens)) {
if (! $this->canConsume($tokens)) {
throw new \InvalidArgumentException('Not enough tokens available');
}