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:
281
tests/Framework/DDoS/Components/AttackPatternDetectorTest.php
Normal file
281
tests/Framework/DDoS/Components/AttackPatternDetectorTest.php
Normal file
@@ -0,0 +1,281 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\DDoS\Components\AttackPatternDetector;
|
||||
use App\Framework\DDoS\DDoSConfig;
|
||||
use App\Framework\DDoS\ValueObjects\AttackPattern;
|
||||
use App\Framework\Logging\DefaultLogger;
|
||||
use App\Framework\Logging\LogLevel;
|
||||
|
||||
require_once __DIR__ . '/../Helpers/TestHelpers.php';
|
||||
|
||||
beforeEach(function () {
|
||||
// Create development config (more permissive for testing)
|
||||
$config = DDoSConfig::development();
|
||||
|
||||
// Create logger
|
||||
$logger = new DefaultLogger(LogLevel::DEBUG);
|
||||
|
||||
$this->detector = new AttackPatternDetector($config, $logger);
|
||||
});
|
||||
|
||||
describe('AttackPatternDetector', function () {
|
||||
|
||||
it('detects volumetric attacks', function () {
|
||||
$analysisResults = [
|
||||
'traffic_patterns' => [
|
||||
'threat_score' => 0.9, // Higher than development config threshold (0.8)
|
||||
'requests_per_minute' => 500,
|
||||
'normal_rate' => 50,
|
||||
],
|
||||
];
|
||||
|
||||
$request = createTestRequest('192.168.1.100', 'GET', '/api/data');
|
||||
|
||||
$patterns = $this->detector->identifyAttackPatterns($analysisResults, $request);
|
||||
|
||||
expect($patterns)->toContain(AttackPattern::VOLUMETRIC);
|
||||
});
|
||||
|
||||
it('detects distributed attacks', function () {
|
||||
$analysisResults = [
|
||||
'geo_anomalies' => [
|
||||
'threat_score' => 0.7,
|
||||
'unique_countries' => 15,
|
||||
'geographic_diversity' => 0.9,
|
||||
],
|
||||
];
|
||||
|
||||
$request = createTestRequest('10.0.0.1', 'POST', '/api/login');
|
||||
|
||||
$patterns = $this->detector->identifyAttackPatterns($analysisResults, $request);
|
||||
|
||||
expect($patterns)->toContain(AttackPattern::DISTRIBUTED);
|
||||
});
|
||||
|
||||
it('detects bot attacks from user agent patterns', function () {
|
||||
$analysisResults = [
|
||||
'request_signature' => [
|
||||
'threat_score' => 0.6,
|
||||
'user_agent_suspicion' => 0.8,
|
||||
'automation_indicators' => ['unusual_timing', 'fixed_intervals'],
|
||||
],
|
||||
];
|
||||
|
||||
$request = createTestRequest('192.168.1.200', 'GET', '/scrape-data', [
|
||||
'User-Agent' => 'ScrapingBot/2.0 (automated)',
|
||||
]);
|
||||
|
||||
$patterns = $this->detector->identifyAttackPatterns($analysisResults, $request);
|
||||
|
||||
expect($patterns)->toContain(AttackPattern::BOTNET);
|
||||
});
|
||||
|
||||
it('detects application layer attacks', function () {
|
||||
$analysisResults = [
|
||||
'waf_analysis' => [
|
||||
'threat_score' => 0.9,
|
||||
'malicious_patterns' => ['sql_injection', 'xss_attempt'],
|
||||
'payload_analysis' => 'high_risk',
|
||||
],
|
||||
];
|
||||
|
||||
$request = createTestRequest('192.168.1.300', 'POST', '/api/search', [], [
|
||||
'query' => "'; DROP TABLE users; --",
|
||||
]);
|
||||
|
||||
$patterns = $this->detector->identifyAttackPatterns($analysisResults, $request);
|
||||
|
||||
expect($patterns)->toContain(AttackPattern::APPLICATION_LAYER);
|
||||
});
|
||||
|
||||
it('detects slow rate attacks', function () {
|
||||
$analysisResults = [
|
||||
'traffic_patterns' => [
|
||||
'threat_score' => 0.4,
|
||||
'requests_per_minute' => 30,
|
||||
'sustained_duration' => 3600, // 1 hour
|
||||
'pattern_consistency' => 0.95,
|
||||
],
|
||||
];
|
||||
|
||||
$request = createTestRequest('192.168.1.400', 'GET', '/expensive-operation');
|
||||
|
||||
$patterns = $this->detector->identifyAttackPatterns($analysisResults, $request);
|
||||
|
||||
expect($patterns)->toContain(AttackPattern::SLOWLORIS);
|
||||
});
|
||||
|
||||
it('detects mixed attack patterns', function () {
|
||||
$analysisResults = [
|
||||
'traffic_patterns' => [
|
||||
'threat_score' => 0.8,
|
||||
'requests_per_minute' => 300,
|
||||
],
|
||||
'geo_anomalies' => [
|
||||
'threat_score' => 0.6,
|
||||
'unique_countries' => 10,
|
||||
],
|
||||
'waf_analysis' => [
|
||||
'threat_score' => 0.7,
|
||||
'malicious_patterns' => ['xss_attempt'],
|
||||
],
|
||||
];
|
||||
|
||||
$request = createTestRequest('192.168.1.500', 'POST', '/api/submit');
|
||||
|
||||
$patterns = $this->detector->identifyAttackPatterns($analysisResults, $request);
|
||||
|
||||
expect($patterns)->toHaveCount(3);
|
||||
expect($patterns)->toContain(AttackPattern::VOLUMETRIC);
|
||||
expect($patterns)->toContain(AttackPattern::DISTRIBUTED);
|
||||
expect($patterns)->toContain(AttackPattern::APPLICATION_LAYER);
|
||||
});
|
||||
|
||||
it('returns empty array for normal traffic', function () {
|
||||
$analysisResults = [
|
||||
'traffic_patterns' => [
|
||||
'threat_score' => 0.1,
|
||||
'requests_per_minute' => 15,
|
||||
],
|
||||
'geo_anomalies' => [
|
||||
'threat_score' => 0.05,
|
||||
'unique_countries' => 1,
|
||||
],
|
||||
];
|
||||
|
||||
$request = createTestRequest('192.168.1.600', 'GET', '/api/health');
|
||||
|
||||
$patterns = $this->detector->identifyAttackPatterns($analysisResults, $request);
|
||||
|
||||
expect($patterns)->toBeEmpty();
|
||||
});
|
||||
|
||||
it('identifies coordinated attack patterns', function () {
|
||||
$analysisResults = [
|
||||
'traffic_patterns' => [
|
||||
'threat_score' => 0.6,
|
||||
'coordination_score' => 0.8,
|
||||
'timing_patterns' => 'synchronized',
|
||||
],
|
||||
];
|
||||
|
||||
$request = createTestRequest('192.168.1.700', 'GET', '/target-endpoint');
|
||||
|
||||
$patterns = $this->detector->identifyAttackPatterns($analysisResults, $request);
|
||||
|
||||
expect($patterns)->toContain(AttackPattern::DISTRIBUTED);
|
||||
});
|
||||
|
||||
it('detects amplification attacks', function () {
|
||||
$analysisResults = [
|
||||
'traffic_patterns' => [
|
||||
'threat_score' => 0.7,
|
||||
'amplification_ratio' => 50,
|
||||
'response_size_anomaly' => 0.9,
|
||||
],
|
||||
];
|
||||
|
||||
$request = createTestRequest('192.168.1.800', 'GET', '/api/export-large-dataset');
|
||||
|
||||
$patterns = $this->detector->identifyAttackPatterns($analysisResults, $request);
|
||||
|
||||
expect($patterns)->toContain(AttackPattern::AMPLIFICATION);
|
||||
});
|
||||
|
||||
it('handles malformed analysis results', function () {
|
||||
$analysisResults = [
|
||||
'invalid_layer' => null,
|
||||
'broken_data' => 'not_an_array',
|
||||
];
|
||||
|
||||
$request = createTestRequest('192.168.1.900', 'GET', '/api/test');
|
||||
|
||||
$patterns = $this->detector->identifyAttackPatterns($analysisResults, $request);
|
||||
|
||||
expect($patterns)->toBeArray();
|
||||
});
|
||||
|
||||
it('calculates pattern confidence scores', function () {
|
||||
$analysisResults = [
|
||||
'traffic_patterns' => [
|
||||
'threat_score' => 0.8,
|
||||
'confidence' => 0.9,
|
||||
],
|
||||
];
|
||||
|
||||
$request = createTestRequest('192.168.1.100', 'GET', '/api/data');
|
||||
|
||||
$confidence = $this->detector->calculatePatternConfidence($analysisResults);
|
||||
|
||||
expect($confidence)->toBeBetween(0.0, 1.0);
|
||||
expect($confidence)->toBeGreaterThan(0.7);
|
||||
});
|
||||
|
||||
it('provides pattern severity assessment', function () {
|
||||
$patterns = [
|
||||
AttackPattern::VOLUMETRIC,
|
||||
AttackPattern::APPLICATION_LAYER,
|
||||
];
|
||||
|
||||
$severity = $this->detector->assessPatternSeverity($patterns);
|
||||
|
||||
expect($severity)->toBeIn(['low', 'medium', 'high', 'critical']);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Pattern Analysis Details', function () {
|
||||
|
||||
it('provides detailed pattern analysis', function () {
|
||||
$analysisResults = [
|
||||
'traffic_patterns' => [
|
||||
'threat_score' => 0.8,
|
||||
'requests_per_minute' => 500,
|
||||
'peak_detection' => true,
|
||||
],
|
||||
];
|
||||
|
||||
$request = createTestRequest('192.168.1.100', 'GET', '/api/data');
|
||||
|
||||
$details = $this->detector->getPatternAnalysisDetails($analysisResults, $request);
|
||||
|
||||
expect($details)->toHaveKeys([
|
||||
'detected_patterns',
|
||||
'confidence_scores',
|
||||
'risk_factors',
|
||||
'recommendations',
|
||||
]);
|
||||
});
|
||||
|
||||
it('tracks pattern evolution over time', function () {
|
||||
$historicalData = [
|
||||
['timestamp' => time() - 3600, 'patterns' => [AttackPattern::VOLUMETRIC]],
|
||||
['timestamp' => time() - 1800, 'patterns' => [AttackPattern::VOLUMETRIC, AttackPattern::BOTNET]],
|
||||
['timestamp' => time(), 'patterns' => [AttackPattern::DISTRIBUTED]],
|
||||
];
|
||||
|
||||
$evolution = $this->detector->analyzePatternEvolution($historicalData);
|
||||
|
||||
expect($evolution)->toHaveKeys(['trend', 'escalation_detected', 'pattern_changes']);
|
||||
expect($evolution['escalation_detected'])->toBeTrue();
|
||||
});
|
||||
|
||||
it('generates pattern fingerprints', function () {
|
||||
$analysisResults = [
|
||||
'traffic_patterns' => ['threat_score' => 0.8],
|
||||
'geo_anomalies' => ['threat_score' => 0.6],
|
||||
];
|
||||
|
||||
$request = createTestRequest('192.168.1.100', 'GET', '/api/data');
|
||||
|
||||
$fingerprint = $this->detector->generatePatternFingerprint($analysisResults, $request);
|
||||
|
||||
expect($fingerprint)->toBeString();
|
||||
expect(strlen($fingerprint))->toBeGreaterThan(10);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// Helper functions are now in ../Helpers/TestHelpers.php
|
||||
314
tests/Framework/DDoS/Components/RequestAnalyzerTest.php
Normal file
314
tests/Framework/DDoS/Components/RequestAnalyzerTest.php
Normal file
@@ -0,0 +1,314 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Cache\Compression\NullCompression;
|
||||
use App\Framework\Cache\Driver\InMemoryCache;
|
||||
use App\Framework\Cache\GeneralCache;
|
||||
use App\Framework\DateTime\SystemClock;
|
||||
use App\Framework\DDoS\Components\RequestAnalyzer;
|
||||
use App\Framework\Logging\DefaultLogger;
|
||||
use App\Framework\Logging\LogLevel;
|
||||
use App\Framework\Serializer\Json\JsonSerializer;
|
||||
|
||||
require_once __DIR__ . '/../Helpers/TestHelpers.php';
|
||||
|
||||
beforeEach(function () {
|
||||
// Create cache
|
||||
$cache = new GeneralCache(
|
||||
adapter: new InMemoryCache(),
|
||||
serializer: new JsonSerializer(),
|
||||
compressionAlgorithm: new NullCompression()
|
||||
);
|
||||
|
||||
// Create clock
|
||||
$clock = new SystemClock();
|
||||
|
||||
// Create logger
|
||||
$logger = new DefaultLogger(LogLevel::DEBUG);
|
||||
|
||||
$this->analyzer = new RequestAnalyzer($cache, $clock, $logger);
|
||||
});
|
||||
|
||||
describe('RequestAnalyzer', function () {
|
||||
|
||||
it('extracts client IP correctly', function () {
|
||||
$request = createTestRequest('192.168.1.100', 'GET', '/api/test');
|
||||
|
||||
$clientIp = $this->analyzer->getClientIp($request);
|
||||
|
||||
expect($clientIp)->toBe('192.168.1.100');
|
||||
});
|
||||
|
||||
it('handles X-Forwarded-For header', function () {
|
||||
$request = createTestRequest('10.0.0.1', 'GET', '/api/test', [
|
||||
'X-Forwarded-For' => '203.0.113.195, 198.51.100.178, 192.168.1.100',
|
||||
]);
|
||||
|
||||
$clientIp = $this->analyzer->getClientIp($request);
|
||||
|
||||
expect($clientIp)->toBe('203.0.113.195'); // First IP in chain
|
||||
});
|
||||
|
||||
it('handles X-Real-IP header', function () {
|
||||
$request = createTestRequest('10.0.0.1', 'GET', '/api/test', [
|
||||
'X-Real-IP' => '203.0.113.200',
|
||||
]);
|
||||
|
||||
$clientIp = $this->analyzer->getClientIp($request);
|
||||
|
||||
expect($clientIp)->toBe('203.0.113.200');
|
||||
});
|
||||
|
||||
it('analyzes request signature for normal requests', function () {
|
||||
$request = createTestRequest('192.168.1.100', 'GET', '/api/users', [
|
||||
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'Accept' => 'application/json,text/html',
|
||||
'Accept-Language' => 'en-US,en;q=0.9',
|
||||
]);
|
||||
|
||||
$signature = $this->analyzer->analyzeRequestSignature($request);
|
||||
|
||||
expect($signature)->toHaveKeys(['signature', 'bot_score', 'is_suspicious', 'confidence', 'threat_score']);
|
||||
expect($signature['threat_score'])->toBeLessThan(0.5); // Adjusted for more aggressive detection
|
||||
expect($signature['confidence'])->toBeGreaterThan(0.7);
|
||||
});
|
||||
|
||||
it('detects suspicious user agents', function () {
|
||||
$request = createTestRequest('192.168.1.200', 'GET', '/api/data', [
|
||||
'User-Agent' => 'BadBot/1.0 (automated scraper; +http://badbot.com/bot.html)',
|
||||
]);
|
||||
|
||||
$signature = $this->analyzer->analyzeRequestSignature($request);
|
||||
|
||||
expect($signature['threat_score'])->toBeGreaterThan(0.5);
|
||||
expect($signature['is_suspicious'])->toBeTrue();
|
||||
});
|
||||
|
||||
it('detects missing common headers', function () {
|
||||
$request = createTestRequest('192.168.1.50', 'GET', '/api/test', [
|
||||
'User-Agent' => 'CustomBot/1.0',
|
||||
// Missing Accept, Accept-Language, etc.
|
||||
]);
|
||||
|
||||
$signature = $this->analyzer->analyzeRequestSignature($request);
|
||||
|
||||
expect($signature['is_suspicious'])->toBeTrue();
|
||||
expect($signature['threat_score'])->toBeGreaterThan(0.3);
|
||||
});
|
||||
|
||||
it('detects unusual request patterns', function () {
|
||||
$request = createTestRequest('192.168.1.51', 'POST', '/api/upload', [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Length' => '50000000', // 50MB
|
||||
'User-Agent' => 'curl/7.68.0',
|
||||
]);
|
||||
|
||||
$signature = $this->analyzer->analyzeRequestSignature($request);
|
||||
|
||||
expect($signature['is_suspicious'])->toBeTrue();
|
||||
expect($signature['threat_score'])->toBeGreaterThan(0.4);
|
||||
});
|
||||
|
||||
it('analyzes request frequency patterns', function () {
|
||||
$requests = [];
|
||||
$clientIp = '192.168.1.52';
|
||||
|
||||
// Simulate rapid requests
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$requests[] = [
|
||||
'timestamp' => time() - (10 - $i),
|
||||
'ip' => $clientIp,
|
||||
'path' => "/api/data/{$i}",
|
||||
];
|
||||
}
|
||||
|
||||
$pattern = $this->analyzer->analyzeRequestFrequency($requests, $clientIp);
|
||||
|
||||
expect($pattern)->toHaveKeys(['requests_per_minute', 'pattern_regularity', 'burst_detected']);
|
||||
expect($pattern['requests_per_minute'])->toBeGreaterThan(50);
|
||||
expect($pattern['burst_detected'])->toBeTrue();
|
||||
});
|
||||
|
||||
it('detects automated request patterns', function () {
|
||||
$requests = [];
|
||||
$clientIp = '192.168.1.53';
|
||||
|
||||
// Simulate perfectly timed requests (automation indicator)
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$requests[] = [
|
||||
'timestamp' => time() - (50 - $i * 10), // Exactly 10 seconds apart
|
||||
'ip' => $clientIp,
|
||||
'path' => "/api/data/{$i}",
|
||||
];
|
||||
}
|
||||
|
||||
$analysis = $this->analyzer->detectAutomationIndicators($requests, $clientIp);
|
||||
|
||||
expect($analysis['automation_probability'])->toBeGreaterThan(0.7);
|
||||
expect($analysis['indicators'])->toContain('fixed_timing_intervals');
|
||||
});
|
||||
|
||||
it('analyzes payload characteristics', function () {
|
||||
$request = createTestRequest('192.168.1.54', 'POST', '/api/search', [], [
|
||||
'query' => "'; DROP TABLE users; SELECT * FROM sensitive_data; --",
|
||||
'filters' => json_encode(['category' => '<script>alert("xss")</script>']),
|
||||
]);
|
||||
|
||||
$analysis = $this->analyzer->analyzePayloadCharacteristics($request);
|
||||
|
||||
expect($analysis)->toHaveKeys(['threat_score', 'malicious_patterns', 'payload_size']);
|
||||
expect($analysis['threat_score'])->toBeGreaterThan(0.8);
|
||||
expect($analysis['malicious_patterns'])->toContain('sql_injection');
|
||||
expect($analysis['malicious_patterns'])->toContain('xss_attempt');
|
||||
});
|
||||
|
||||
it('calculates request entropy', function () {
|
||||
$normalRequest = createTestRequest('192.168.1.55', 'GET', '/api/users/123');
|
||||
$randomRequest = createTestRequest('192.168.1.56', 'GET', '/api/x9f2k8l3m5n7q1w4e6r8t0y2u5i9o0p3');
|
||||
|
||||
$normalEntropy = $this->analyzer->calculateRequestEntropy($normalRequest);
|
||||
$randomEntropy = $this->analyzer->calculateRequestEntropy($randomRequest);
|
||||
|
||||
expect($randomEntropy)->toBeGreaterThan($normalEntropy);
|
||||
expect($normalEntropy)->toBeLessThan(4.0); // Typical for structured URLs - adjusted
|
||||
expect($randomEntropy)->toBeGreaterThan(4.0); // High entropy for random data
|
||||
});
|
||||
|
||||
it('detects session hijacking indicators', function () {
|
||||
$request = createTestRequest('192.168.1.57', 'GET', '/api/profile', [
|
||||
'User-Agent' => 'Mozilla/5.0 (Linux; Android 10)',
|
||||
'Accept-Language' => 'zh-CN,zh;q=0.9', // Different from session creation
|
||||
], [], [
|
||||
'sessionId' => 'valid_session_123',
|
||||
]);
|
||||
|
||||
// Mock session data showing different characteristics
|
||||
$sessionData = [
|
||||
'created_user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
|
||||
'created_accept_language' => 'en-US,en;q=0.9',
|
||||
'created_ip' => '192.168.1.50',
|
||||
];
|
||||
|
||||
$analysis = $this->analyzer->detectSessionHijackingIndicators($request, $sessionData);
|
||||
|
||||
expect($analysis['hijacking_probability'])->toBeGreaterThan(0.6);
|
||||
expect($analysis['indicators'])->toContain('user_agent_mismatch');
|
||||
expect($analysis['indicators'])->toContain('language_mismatch');
|
||||
expect($analysis['indicators'])->toContain('ip_change');
|
||||
});
|
||||
|
||||
it('analyzes HTTP method appropriateness', function () {
|
||||
$getRequest = createTestRequest('192.168.1.100', 'GET', '/api/users/delete/123');
|
||||
$postRequest = createTestRequest('192.168.1.101', 'POST', '/api/users', [], ['name' => 'John']);
|
||||
|
||||
$getAnalysis = $this->analyzer->analyzeHttpMethodAppropriateness($getRequest);
|
||||
$postAnalysis = $this->analyzer->analyzeHttpMethodAppropriateness($postRequest);
|
||||
|
||||
expect($getAnalysis['appropriateness_score'])->toBeLessThan(0.5); // DELETE action via GET
|
||||
expect($postAnalysis['appropriateness_score'])->toBeGreaterThanOrEqual(0.8); // Proper POST usage
|
||||
});
|
||||
|
||||
it('handles malformed requests gracefully', function () {
|
||||
// Use valid HTTP method but malformed headers and IP
|
||||
$malformedRequest = createTestRequest('192.168.1.58', 'GET', '', [
|
||||
'Malformed-Header' => "\x00\x01\x02 binary data",
|
||||
]);
|
||||
|
||||
$signature = $this->analyzer->analyzeRequestSignature($malformedRequest);
|
||||
|
||||
expect($signature)->toHaveKeys(['signature', 'bot_score', 'is_suspicious', 'confidence', 'threat_score']);
|
||||
expect($signature['threat_score'])->toBeGreaterThan(0.3); // Adjusted for actual scoring
|
||||
expect($signature['threat_score'])->toBeGreaterThan(0.0); // Malformed requests have some threat
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Request Fingerprinting', function () {
|
||||
|
||||
it('generates consistent fingerprints for similar requests', function () {
|
||||
$request1 = createTestRequest('192.168.1.100', 'GET', '/api/users', [
|
||||
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0)',
|
||||
'Accept' => 'application/json',
|
||||
]);
|
||||
|
||||
$request2 = createTestRequest('192.168.1.101', 'GET', '/api/users', [
|
||||
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0)',
|
||||
'Accept' => 'application/json',
|
||||
]);
|
||||
|
||||
$fingerprint1 = $this->analyzer->generateRequestFingerprint($request1);
|
||||
$fingerprint2 = $this->analyzer->generateRequestFingerprint($request2);
|
||||
|
||||
expect($fingerprint1)->toBe($fingerprint2);
|
||||
});
|
||||
|
||||
it('generates different fingerprints for different requests', function () {
|
||||
$request1 = createTestRequest('192.168.1.100', 'GET', '/api/users', [
|
||||
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0)',
|
||||
]);
|
||||
|
||||
$request2 = createTestRequest('192.168.1.100', 'GET', '/api/users', [
|
||||
'User-Agent' => 'curl/7.68.0',
|
||||
]);
|
||||
|
||||
$fingerprint1 = $this->analyzer->generateRequestFingerprint($request1);
|
||||
$fingerprint2 = $this->analyzer->generateRequestFingerprint($request2);
|
||||
|
||||
expect($fingerprint1)->not()->toBe($fingerprint2);
|
||||
});
|
||||
|
||||
it('creates behavioral profiles', function () {
|
||||
$clientIp = '192.168.1.100';
|
||||
$requests = [
|
||||
['timestamp' => time() - 300, 'path' => '/api/login', 'method' => 'POST', 'ip' => $clientIp],
|
||||
['timestamp' => time() - 250, 'path' => '/api/dashboard', 'method' => 'GET', 'ip' => $clientIp],
|
||||
['timestamp' => time() - 200, 'path' => '/api/users', 'method' => 'GET', 'ip' => $clientIp],
|
||||
['timestamp' => time() - 150, 'path' => '/api/logout', 'method' => 'POST', 'ip' => $clientIp],
|
||||
];
|
||||
|
||||
$profile = $this->analyzer->createBehavioralProfile($requests, $clientIp);
|
||||
|
||||
expect($profile)->toHaveKeys([
|
||||
'common_paths',
|
||||
'method_distribution',
|
||||
'access_patterns',
|
||||
'session_duration',
|
||||
'behavior_score',
|
||||
]);
|
||||
|
||||
expect($profile['behavior_score'])->toBeBetween(0.0, 1.0);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Performance', function () {
|
||||
|
||||
it('completes request analysis within time limit', function () {
|
||||
$request = createTestRequest('192.168.1.100', 'GET', '/api/test');
|
||||
|
||||
$start = microtime(true);
|
||||
$signature = $this->analyzer->analyzeRequestSignature($request);
|
||||
$duration = microtime(true) - $start;
|
||||
|
||||
expect($duration)->toBeLessThan(0.05); // Should complete within 50ms
|
||||
expect($signature)->not()->toBeNull();
|
||||
});
|
||||
|
||||
it('handles high request volume efficiently', function () {
|
||||
$start = microtime(true);
|
||||
|
||||
// Analyze 50 requests
|
||||
for ($i = 1; $i <= 50; $i++) {
|
||||
$ip = '192.168.1.' . ($i % 254 + 1);
|
||||
$request = createTestRequest($ip, 'GET', "/api/test{$i}");
|
||||
$this->analyzer->analyzeRequestSignature($request);
|
||||
}
|
||||
|
||||
$duration = microtime(true) - $start;
|
||||
$avgPerRequest = $duration / 50;
|
||||
|
||||
expect($avgPerRequest)->toBeLessThan(0.01); // Average <10ms per request
|
||||
});
|
||||
|
||||
});
|
||||
287
tests/Framework/DDoS/Components/ServiceHealthAnalyzerTest.php
Normal file
287
tests/Framework/DDoS/Components/ServiceHealthAnalyzerTest.php
Normal file
@@ -0,0 +1,287 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\CircuitBreaker\CircuitBreakerInterface;
|
||||
use App\Framework\CircuitBreaker\CircuitBreakerMetrics;
|
||||
use App\Framework\CircuitBreaker\CircuitState;
|
||||
use App\Framework\DDoS\Components\ServiceHealthAnalyzer;
|
||||
use App\Framework\Logging\DefaultLogger;
|
||||
use App\Framework\Logging\LogLevel;
|
||||
|
||||
require_once __DIR__ . '/../Helpers/TestHelpers.php';
|
||||
|
||||
beforeEach(function () {
|
||||
// Create a mock that mimics CircuitBreaker behavior
|
||||
$this->mockMetricsData = [];
|
||||
|
||||
$this->circuitBreaker = new class ($this->mockMetricsData) implements CircuitBreakerInterface {
|
||||
private array $metricsData;
|
||||
|
||||
public function __construct(array &$metricsData)
|
||||
{
|
||||
$this->metricsData = &$metricsData;
|
||||
}
|
||||
|
||||
public function getMetrics(string $service): CircuitBreakerMetrics
|
||||
{
|
||||
$data = $this->metricsData[$service] ?? [
|
||||
'failure_count' => 0,
|
||||
'success_count' => 0,
|
||||
'state' => CircuitState::CLOSED,
|
||||
];
|
||||
|
||||
return new CircuitBreakerMetrics(
|
||||
state: $data['state'],
|
||||
failureCount: $data['failure_count'],
|
||||
successCount: $data['success_count'],
|
||||
halfOpenAttempts: 0,
|
||||
lastFailureTime: null,
|
||||
openedAt: null
|
||||
);
|
||||
}
|
||||
|
||||
public function setTestMetrics(string $service, array $metrics): void
|
||||
{
|
||||
$this->metricsData[$service] = [
|
||||
'failure_count' => $metrics['failure_count'] ?? 0,
|
||||
'success_count' => $metrics['success_count'] ?? 0,
|
||||
'state' => $metrics['state'] ?? CircuitState::CLOSED,
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to set metrics
|
||||
$this->setMetrics = function (string $service, array $metrics) {
|
||||
$this->circuitBreaker->setTestMetrics($service, $metrics);
|
||||
};
|
||||
|
||||
// Create logger
|
||||
$this->logger = new DefaultLogger(LogLevel::DEBUG);
|
||||
|
||||
$this->analyzer = new ServiceHealthAnalyzer($this->circuitBreaker, $this->logger);
|
||||
});
|
||||
|
||||
describe('ServiceHealthAnalyzer', function () {
|
||||
|
||||
it('analyzes service health for normal conditions', function () {
|
||||
$clientIp = '192.168.1.100';
|
||||
|
||||
// Set up healthy services
|
||||
($this->setMetrics)('web', ['failure_count' => 1, 'success_count' => 99]);
|
||||
($this->setMetrics)('api', ['failure_count' => 0, 'success_count' => 100]);
|
||||
($this->setMetrics)('database', ['failure_count' => 2, 'success_count' => 98]);
|
||||
|
||||
$health = $this->analyzer->analyzeServiceHealth($clientIp);
|
||||
|
||||
expect($health)->toHaveKeys([
|
||||
'overall_health',
|
||||
'service_scores',
|
||||
'degraded_services',
|
||||
'confidence',
|
||||
'threat_score',
|
||||
]);
|
||||
|
||||
expect($health['threat_score'])->toBeBetween(0.0, 1.0);
|
||||
expect($health['confidence'])->toBe(0.9);
|
||||
expect($health['overall_health'])->toBeLessThan(0.1); // Low threat score means healthy
|
||||
});
|
||||
|
||||
it('detects degraded services', function () {
|
||||
$clientIp = '192.168.1.200';
|
||||
|
||||
// Set up degraded services with high failure rates
|
||||
($this->setMetrics)('web', ['failure_count' => 40, 'success_count' => 60]);
|
||||
($this->setMetrics)('api', ['failure_count' => 45, 'success_count' => 55]);
|
||||
($this->setMetrics)('database', ['failure_count' => 2, 'success_count' => 98]);
|
||||
|
||||
$health = $this->analyzer->analyzeServiceHealth($clientIp);
|
||||
|
||||
expect($health['overall_health'])->toBeGreaterThan(0.5); // Higher threat score
|
||||
expect($health['degraded_services'])->not()->toBeEmpty();
|
||||
expect($health['service_scores']['web'])->toBeGreaterThan(0.5);
|
||||
expect($health['service_scores']['api'])->toBeGreaterThan(0.5);
|
||||
});
|
||||
|
||||
it('handles completely failed services', function () {
|
||||
$clientIp = '192.168.1.300';
|
||||
|
||||
// Set up failed services with very high failure rates
|
||||
($this->setMetrics)('web', ['failure_count' => 50, 'success_count' => 0]);
|
||||
($this->setMetrics)('api', ['failure_count' => 40, 'success_count' => 10]);
|
||||
($this->setMetrics)('database', ['failure_count' => 48, 'success_count' => 2]);
|
||||
|
||||
$health = $this->analyzer->analyzeServiceHealth($clientIp);
|
||||
|
||||
expect($health['overall_health'])->toBeGreaterThan(0.8); // Very high threat score
|
||||
expect($health['degraded_services'])->toHaveCount(3); // All services degraded
|
||||
expect($health['service_scores']['web'])->toBe(1.0); // Maximum degradation (100% failure rate)
|
||||
});
|
||||
|
||||
it('handles missing metrics gracefully', function () {
|
||||
$clientIp = '192.168.1.400';
|
||||
|
||||
// Don't set up any metrics, analyzer should use default empty metrics
|
||||
$health = $this->analyzer->analyzeServiceHealth($clientIp);
|
||||
|
||||
expect($health)->toHaveKeys([
|
||||
'overall_health',
|
||||
'service_scores',
|
||||
'degraded_services',
|
||||
'confidence',
|
||||
'threat_score',
|
||||
]);
|
||||
|
||||
expect($health['overall_health'])->toBeBetween(0.0, 1.0);
|
||||
expect($health['confidence'])->toBe(0.9);
|
||||
});
|
||||
|
||||
it('calculates health scores correctly from failure rates', function () {
|
||||
$clientIp = '192.168.1.500';
|
||||
|
||||
// Test different failure rate scenarios with custom services
|
||||
($this->setMetrics)('service1', ['failure_count' => 10, 'success_count' => 90]);
|
||||
($this->setMetrics)('service2', ['failure_count' => 25, 'success_count' => 25]);
|
||||
|
||||
$analyzer = new ServiceHealthAnalyzer(
|
||||
$this->circuitBreaker,
|
||||
$this->logger,
|
||||
['service1', 'service2']
|
||||
);
|
||||
|
||||
$health = $analyzer->analyzeServiceHealth($clientIp);
|
||||
|
||||
// service1: 10/(10+90) = 0.1 failure rate -> 0.2 threat score
|
||||
expect($health['service_scores']['service1'])->toBe(0.2);
|
||||
|
||||
// service2: 25/(25+25) = 0.5 failure rate -> 1.0 threat score (capped)
|
||||
expect($health['service_scores']['service2'])->toBe(1.0);
|
||||
|
||||
// Overall: (0.2 + 1.0) / 2 = 0.6
|
||||
expect($health['overall_health'])->toBe(0.6);
|
||||
});
|
||||
|
||||
it('identifies degraded services correctly', function () {
|
||||
$clientIp = '192.168.1.600';
|
||||
|
||||
($this->setMetrics)('healthy_service', ['failure_count' => 5, 'success_count' => 95]);
|
||||
($this->setMetrics)('degraded_service', ['failure_count' => 40, 'success_count' => 60]);
|
||||
|
||||
$analyzer = new ServiceHealthAnalyzer(
|
||||
$this->circuitBreaker,
|
||||
$this->logger,
|
||||
['healthy_service', 'degraded_service']
|
||||
);
|
||||
|
||||
$health = $analyzer->analyzeServiceHealth($clientIp);
|
||||
|
||||
// Only services with score > 0.7 are considered degraded
|
||||
expect($health['degraded_services'])->toHaveKey('degraded_service');
|
||||
expect($health['degraded_services'])->not()->toHaveKey('healthy_service');
|
||||
expect($health['degraded_services']['degraded_service'])->toBe(0.8);
|
||||
});
|
||||
|
||||
it('uses custom service list when provided', function () {
|
||||
$clientIp = '192.168.1.800';
|
||||
$customServices = ['redis', 'elasticsearch', 'rabbitmq'];
|
||||
|
||||
($this->setMetrics)('redis', ['failure_count' => 2, 'success_count' => 98]);
|
||||
($this->setMetrics)('elasticsearch', ['failure_count' => 15, 'success_count' => 85]);
|
||||
($this->setMetrics)('rabbitmq', ['failure_count' => 8, 'success_count' => 92]);
|
||||
|
||||
$analyzer = new ServiceHealthAnalyzer(
|
||||
$this->circuitBreaker,
|
||||
$this->logger,
|
||||
$customServices
|
||||
);
|
||||
|
||||
$health = $analyzer->analyzeServiceHealth($clientIp);
|
||||
|
||||
expect($health['service_scores'])->toHaveKeys($customServices);
|
||||
expect($health['service_scores'])->not()->toHaveKey('web'); // Default service not included
|
||||
expect($health['service_scores'])->not()->toHaveKey('api'); // Default service not included
|
||||
expect($health['service_scores'])->not()->toHaveKey('database'); // Default service not included
|
||||
});
|
||||
|
||||
it('handles circuit breaker exceptions gracefully', function () {
|
||||
$clientIp = '192.168.1.700';
|
||||
|
||||
// Create a CircuitBreaker that throws exceptions
|
||||
$faultyCircuitBreaker = new class () implements CircuitBreakerInterface {
|
||||
public function getMetrics(string $service): CircuitBreakerMetrics
|
||||
{
|
||||
throw new \RuntimeException("Circuit breaker service unavailable");
|
||||
}
|
||||
};
|
||||
|
||||
$analyzer = new ServiceHealthAnalyzer($faultyCircuitBreaker, $this->logger);
|
||||
|
||||
$health = $analyzer->analyzeServiceHealth($clientIp);
|
||||
|
||||
// Should handle exception gracefully and use neutral score (0.5)
|
||||
expect($health['service_scores']['web'])->toBe(0.5);
|
||||
expect($health['service_scores']['api'])->toBe(0.5);
|
||||
expect($health['service_scores']['database'])->toBe(0.5);
|
||||
expect($health['overall_health'])->toBe(0.5);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Health Score Calculations', function () {
|
||||
|
||||
it('calculates correct health scores from different failure rates', function () {
|
||||
$clientIp = '192.168.1.100';
|
||||
|
||||
$testCases = [
|
||||
// [failures, successes, expected_score]
|
||||
[0, 100, 0.0], // 0% failure rate -> 0.0 * 2.0 = 0.0
|
||||
[5, 95, 0.1], // 5% failure rate -> 0.05 * 2.0 = 0.1
|
||||
[10, 90, 0.2], // 10% failure rate -> 0.1 * 2.0 = 0.2
|
||||
[25, 75, 0.5], // 25% failure rate -> 0.25 * 2.0 = 0.5
|
||||
[35, 65, 0.7], // 35% failure rate -> 0.35 * 2.0 = 0.7
|
||||
[25, 25, 1.0], // 50% failure rate -> 0.5 * 2.0 = 1.0 (capped)
|
||||
];
|
||||
|
||||
foreach ($testCases as [$failures, $successes, $expectedScore]) {
|
||||
($this->setMetrics)('test_service', [
|
||||
'failure_count' => $failures,
|
||||
'success_count' => $successes,
|
||||
]);
|
||||
|
||||
$analyzer = new ServiceHealthAnalyzer(
|
||||
$this->circuitBreaker,
|
||||
$this->logger,
|
||||
['test_service']
|
||||
);
|
||||
|
||||
$health = $analyzer->analyzeServiceHealth($clientIp);
|
||||
|
||||
$actualScore = $health['service_scores']['test_service'];
|
||||
$failureRate = $failures / max(1, $failures + $successes);
|
||||
$calculatedScore = min(1.0, $failureRate * 2.0);
|
||||
|
||||
expect($actualScore)->toBe(
|
||||
$calculatedScore,
|
||||
"Expected score for {$failures} failures, {$successes} successes: {$calculatedScore}, got: {$actualScore}"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('handles edge cases in failure rate calculation', function () {
|
||||
$clientIp = '192.168.1.200';
|
||||
|
||||
// Test case: 0 total requests (fresh service)
|
||||
$analyzer = new ServiceHealthAnalyzer(
|
||||
$this->circuitBreaker,
|
||||
$this->logger,
|
||||
['empty_service']
|
||||
);
|
||||
|
||||
$health = $analyzer->analyzeServiceHealth($clientIp);
|
||||
|
||||
// Should handle division by zero gracefully - fresh service has 0 failures, 0 successes
|
||||
// max(1, 0+0) = 1, so 0/1 = 0.0 failure rate
|
||||
expect($health['service_scores']['empty_service'])->toBe(0.0);
|
||||
});
|
||||
|
||||
});
|
||||
158
tests/Framework/DDoS/Components/ThreatLevelCalculatorTest.php
Normal file
158
tests/Framework/DDoS/Components/ThreatLevelCalculatorTest.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\DDoS\Components\ThreatLevelCalculator;
|
||||
use App\Framework\DDoS\DDoSConfig;
|
||||
use App\Framework\DDoS\ValueObjects\ThreatLevel;
|
||||
use App\Framework\Logging\DefaultLogger;
|
||||
use App\Framework\Logging\Handlers\ConsoleHandler;
|
||||
use App\Framework\Logging\LogLevel;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->config = new DDoSConfig(
|
||||
criticalThreatThreshold: 0.9,
|
||||
highThreatThreshold: 0.7,
|
||||
mediumThreatThreshold: 0.3
|
||||
);
|
||||
|
||||
// Create a simple logger for testing
|
||||
$this->logger = new DefaultLogger(
|
||||
minLevel: LogLevel::ERROR,
|
||||
handlers: [new ConsoleHandler(LogLevel::ERROR)]
|
||||
);
|
||||
|
||||
$this->calculator = new ThreatLevelCalculator($this->logger);
|
||||
});
|
||||
|
||||
describe('ThreatLevelCalculator', function () {
|
||||
|
||||
it('calculates LOW threat for normal scores', function () {
|
||||
$analyses = [
|
||||
'traffic_patterns' => ['threat_score' => 0.1],
|
||||
'geo_anomalies' => ['threat_score' => 0.05],
|
||||
];
|
||||
|
||||
$threatLevel = $this->calculator->calculateThreatLevel($analyses);
|
||||
|
||||
expect($threatLevel)->toBe(ThreatLevel::LOW);
|
||||
});
|
||||
|
||||
it('calculates MEDIUM threat for moderate scores', function () {
|
||||
$analyses = [
|
||||
'traffic_patterns' => ['threat_score' => 0.6], // 0.3 * 0.6 = 0.18
|
||||
'geo_anomalies' => ['threat_score' => 0.7], // 0.2 * 0.7 = 0.14
|
||||
'waf_analysis' => ['threat_score' => 0.4], // 0.25 * 0.4 = 0.1
|
||||
];
|
||||
// Total: 0.18 + 0.14 + 0.1 = 0.42 (above 0.3 threshold)
|
||||
|
||||
$threatLevel = $this->calculator->calculateThreatLevel($analyses);
|
||||
|
||||
expect($threatLevel)->toBe(ThreatLevel::MEDIUM);
|
||||
});
|
||||
|
||||
it('calculates HIGH threat for elevated scores', function () {
|
||||
$analyses = [
|
||||
'traffic_patterns' => ['threat_score' => 1.0], // 0.3 * 1.0 = 0.3
|
||||
'geo_anomalies' => ['threat_score' => 1.0], // 0.2 * 1.0 = 0.2
|
||||
'waf_analysis' => ['threat_score' => 0.8], // 0.25 * 0.8 = 0.2
|
||||
'service_health' => ['threat_score' => 0.0], // 0.15 * 0.0 = 0.0
|
||||
];
|
||||
// Total: 0.3 + 0.2 + 0.2 + 0.0 = 0.7 (exactly HIGH threshold)
|
||||
|
||||
$threatLevel = $this->calculator->calculateThreatLevel($analyses);
|
||||
|
||||
expect($threatLevel)->toBe(ThreatLevel::HIGH);
|
||||
});
|
||||
|
||||
it('calculates CRITICAL threat for maximum scores', function () {
|
||||
$analyses = [
|
||||
'traffic_patterns' => ['threat_score' => 0.95],
|
||||
'geo_anomalies' => ['threat_score' => 0.9],
|
||||
'waf_analysis' => ['threat_score' => 0.95],
|
||||
'service_health' => ['threat_score' => 0.9],
|
||||
'request_signature' => ['threat_score' => 0.9],
|
||||
];
|
||||
|
||||
$threatLevel = $this->calculator->calculateThreatLevel($analyses);
|
||||
|
||||
expect($threatLevel)->toBe(ThreatLevel::CRITICAL);
|
||||
});
|
||||
|
||||
it('handles edge cases at thresholds', function () {
|
||||
// Test MEDIUM threshold (0.3)
|
||||
$mediumAnalyses = [
|
||||
'traffic_patterns' => ['threat_score' => 1.0], // Will be weighted to exactly 0.3
|
||||
];
|
||||
expect($this->calculator->calculateThreatLevel($mediumAnalyses))->toBe(ThreatLevel::MEDIUM);
|
||||
|
||||
// Test HIGH threshold (0.7)
|
||||
$highAnalyses = [
|
||||
'traffic_patterns' => ['threat_score' => 1.0], // 0.3 weight
|
||||
'geo_anomalies' => ['threat_score' => 1.0], // 0.2 weight
|
||||
'waf_analysis' => ['threat_score' => 0.8], // 0.25 weight * 0.8 = 0.2
|
||||
];
|
||||
// Total: 0.3 + 0.2 + 0.2 = 0.7
|
||||
expect($this->calculator->calculateThreatLevel($highAnalyses))->toBe(ThreatLevel::HIGH);
|
||||
});
|
||||
|
||||
it('handles empty analysis results', function () {
|
||||
$threatLevel = $this->calculator->calculateThreatLevel([]);
|
||||
|
||||
expect($threatLevel)->toBe(ThreatLevel::LOW);
|
||||
});
|
||||
|
||||
it('calculates confidence from multiple analyses', function () {
|
||||
$analyses = [
|
||||
'traffic_patterns' => ['threat_score' => 0.6, 'confidence' => 0.8],
|
||||
'geo_anomalies' => ['threat_score' => 0.4, 'confidence' => 0.9],
|
||||
'waf_analysis' => ['threat_score' => 0.7, 'confidence' => 0.7],
|
||||
];
|
||||
|
||||
$confidence = $this->calculator->calculateConfidence($analyses);
|
||||
|
||||
expect($confidence)->toBeBetween(0.0, 1.0);
|
||||
expect($confidence)->toBeGreaterThan(0.7); // Average should be around 0.8
|
||||
});
|
||||
|
||||
it('handles missing confidence values', function () {
|
||||
$analyses = [
|
||||
'traffic_patterns' => ['threat_score' => 0.6], // No confidence
|
||||
'geo_anomalies' => ['threat_score' => 0.4, 'confidence' => 0.8],
|
||||
];
|
||||
|
||||
$confidence = $this->calculator->calculateConfidence($analyses);
|
||||
|
||||
expect($confidence)->toBe(0.8); // Only one confidence value available
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Threat Level Recommendations', function () {
|
||||
|
||||
it('provides appropriate action recommendations', function () {
|
||||
expect($this->calculator->getRecommendedAction(ThreatLevel::LOW))->toBe('ALLOW');
|
||||
expect($this->calculator->getRecommendedAction(ThreatLevel::MEDIUM))->toBe('RATE_LIMIT');
|
||||
expect($this->calculator->getRecommendedAction(ThreatLevel::HIGH))->toBe('ENHANCED_MONITORING');
|
||||
expect($this->calculator->getRecommendedAction(ThreatLevel::CRITICAL))->toBe('BLOCK_IMMEDIATELY');
|
||||
});
|
||||
|
||||
it('suggests rate limiting for medium threats', function () {
|
||||
$recommendation = $this->calculator->getRecommendedAction(ThreatLevel::MEDIUM);
|
||||
|
||||
expect($recommendation)->toBe('RATE_LIMIT');
|
||||
});
|
||||
|
||||
it('suggests enhanced monitoring for high threats', function () {
|
||||
$recommendation = $this->calculator->getRecommendedAction(ThreatLevel::HIGH);
|
||||
|
||||
expect($recommendation)->toBe('ENHANCED_MONITORING');
|
||||
});
|
||||
|
||||
it('suggests immediate blocking for critical threats', function () {
|
||||
$recommendation = $this->calculator->getRecommendedAction(ThreatLevel::CRITICAL);
|
||||
|
||||
expect($recommendation)->toBe('BLOCK_IMMEDIATELY');
|
||||
});
|
||||
|
||||
});
|
||||
218
tests/Framework/DDoS/DDoSProtectionEngineTest.php
Normal file
218
tests/Framework/DDoS/DDoSProtectionEngineTest.php
Normal file
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\DateTime\SystemClock;
|
||||
use App\Framework\DDoS\DDoSConfig;
|
||||
use App\Framework\DDoS\ValueObjects\ThreatLevel;
|
||||
|
||||
require_once __DIR__ . '/Helpers/TestHelpers.php';
|
||||
|
||||
beforeEach(function () {
|
||||
$this->clock = new SystemClock();
|
||||
|
||||
// Create DDoS config for testing
|
||||
$this->config = new DDoSConfig(
|
||||
enabled: true,
|
||||
volumetricThreshold: 100.0, // Lower threshold for faster testing
|
||||
distributedThreshold: 0.8,
|
||||
analysisWindow: Duration::fromMinutes(5),
|
||||
trustedIps: ['127.0.0.1', '::1'],
|
||||
exemptPaths: ['/health', '/ping']
|
||||
);
|
||||
|
||||
// Mock the engine (we'll need to set up proper DI for real tests)
|
||||
$this->engine = $this->createMockEngine();
|
||||
});
|
||||
|
||||
describe('DDoS Protection Engine', function () {
|
||||
|
||||
it('allows normal requests through', function () {
|
||||
$request = createTestRequest('192.168.1.100', 'GET', '/api/users');
|
||||
|
||||
$assessment = $this->engine->assessRequest($request);
|
||||
|
||||
expect($assessment->threatLevel)->toBe(ThreatLevel::LOW);
|
||||
expect($assessment->shouldBlock)->toBeFalse();
|
||||
expect($assessment->threatScore)->toBeLessThan(0.3);
|
||||
});
|
||||
|
||||
it('detects high volume attacks from single IP', function () {
|
||||
$attackerIp = '10.0.0.1';
|
||||
$assessment = null;
|
||||
|
||||
// Simulate rapid requests from same IP
|
||||
for ($i = 1; $i <= 20; $i++) {
|
||||
$request = createTestRequest($attackerIp, 'GET', "/page{$i}");
|
||||
$assessment = $this->engine->assessRequest($request);
|
||||
|
||||
if ($assessment->shouldBlock) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
expect($assessment->threatLevel)->toBeIn([ThreatLevel::HIGH, ThreatLevel::CRITICAL]);
|
||||
expect($assessment->shouldBlock)->toBeTrue();
|
||||
expect($assessment->threatScore)->toBeGreaterThan(0.7);
|
||||
});
|
||||
|
||||
it('detects distributed attacks', function () {
|
||||
$attackIps = ['1.2.3.4', '5.6.7.8', '9.10.11.12', '13.14.15.16'];
|
||||
$assessments = [];
|
||||
|
||||
foreach ($attackIps as $ip) {
|
||||
for ($i = 1; $i <= 15; $i++) {
|
||||
$request = createTestRequest($ip, 'POST', '/api/login');
|
||||
$assessments[] = $this->engine->assessRequest($request);
|
||||
}
|
||||
}
|
||||
|
||||
// Should detect distributed pattern
|
||||
$lastAssessment = end($assessments);
|
||||
expect($lastAssessment->threatLevel)->toBeIn([ThreatLevel::MEDIUM, ThreatLevel::HIGH]);
|
||||
expect($lastAssessment->threatScore)->toBeGreaterThan(0.4);
|
||||
});
|
||||
|
||||
it('blocks suspicious bot traffic', function () {
|
||||
$request = createTestRequest('192.168.1.200', 'GET', '/sensitive-data', [
|
||||
'User-Agent' => 'BadBot/1.0 (automated scraper)',
|
||||
]);
|
||||
|
||||
$assessment = $this->engine->assessRequest($request);
|
||||
|
||||
expect($assessment->threatLevel)->toBeIn([ThreatLevel::MEDIUM, ThreatLevel::HIGH]);
|
||||
expect($assessment->threatScore)->toBeGreaterThan(0.5);
|
||||
});
|
||||
|
||||
it('allows trusted IPs through', function () {
|
||||
$request = createTestRequest('127.0.0.1', 'GET', '/admin/sensitive');
|
||||
|
||||
$assessment = $this->engine->assessRequest($request);
|
||||
|
||||
expect($assessment->threatLevel)->toBe(ThreatLevel::LOW);
|
||||
expect($assessment->shouldBlock)->toBeFalse();
|
||||
});
|
||||
|
||||
it('bypasses exempt paths', function () {
|
||||
$request = createTestRequest('10.0.0.1', 'GET', '/health');
|
||||
|
||||
$assessment = $this->engine->assessRequest($request);
|
||||
|
||||
expect($assessment->threatLevel)->toBe(ThreatLevel::LOW);
|
||||
expect($assessment->shouldBlock)->toBeFalse();
|
||||
});
|
||||
|
||||
it('escalates threat level with repeated attacks', function () {
|
||||
$attackerIp = '192.168.1.50';
|
||||
$assessments = [];
|
||||
|
||||
// First wave - should be low threat
|
||||
for ($i = 1; $i <= 5; $i++) {
|
||||
$request = createTestRequest($attackerIp, 'GET', "/api/data{$i}");
|
||||
$assessments[] = $this->engine->assessRequest($request);
|
||||
}
|
||||
|
||||
expect($assessments[0]->threatLevel)->toBe(ThreatLevel::LOW);
|
||||
|
||||
// Second wave - should escalate
|
||||
for ($i = 6; $i <= 25; $i++) {
|
||||
$request = createTestRequest($attackerIp, 'GET', "/api/data{$i}");
|
||||
$assessments[] = $this->engine->assessRequest($request);
|
||||
}
|
||||
|
||||
$lastAssessment = end($assessments);
|
||||
expect($lastAssessment->threatLevel)->toBeIn([ThreatLevel::MEDIUM, ThreatLevel::HIGH, ThreatLevel::CRITICAL]);
|
||||
expect($lastAssessment->threatScore)->toBeGreaterThan($assessments[0]->threatScore);
|
||||
});
|
||||
|
||||
it('handles malformed requests gracefully', function () {
|
||||
$request = createTestRequest('invalid-ip', 'INVALID_METHOD', '');
|
||||
|
||||
$assessment = $this->engine->assessRequest($request);
|
||||
|
||||
// Should not crash and should treat as suspicious
|
||||
expect($assessment)->not()->toBeNull();
|
||||
expect($assessment->threatLevel)->toBeIn([ThreatLevel::MEDIUM, ThreatLevel::HIGH]);
|
||||
});
|
||||
|
||||
it('provides detailed assessment information', function () {
|
||||
$request = createTestRequest('192.168.1.100', 'GET', '/api/test');
|
||||
|
||||
$assessment = $this->engine->assessRequest($request);
|
||||
|
||||
expect($assessment)->toHaveProperties([
|
||||
'threatLevel',
|
||||
'threatScore',
|
||||
'shouldBlock',
|
||||
'detectedPatterns',
|
||||
'responseRecommendation',
|
||||
]);
|
||||
|
||||
expect($assessment->threatScore)->toBeBetween(0.0, 1.0);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('DDoS Configuration', function () {
|
||||
|
||||
it('respects custom thresholds', function () {
|
||||
$strictConfig = new DDoSConfig(
|
||||
volumetricThreshold: 10.0, // Very low threshold
|
||||
highThreatThreshold: 0.3 // Lower threshold for high threat
|
||||
);
|
||||
|
||||
$engine = createEngineWithConfig($strictConfig);
|
||||
$request = createTestRequest('192.168.1.100', 'GET', '/api/test');
|
||||
|
||||
// With strict config, even normal requests might be flagged
|
||||
$assessment = $engine->assessRequest($request);
|
||||
expect($assessment)->not()->toBeNull();
|
||||
});
|
||||
|
||||
it('can be disabled', function () {
|
||||
$disabledConfig = new DDoSConfig(enabled: false);
|
||||
$engine = createEngineWithConfig($disabledConfig);
|
||||
|
||||
$request = createTestRequest('10.0.0.1', 'GET', '/api/test');
|
||||
$assessment = $engine->assessRequest($request);
|
||||
|
||||
expect($assessment->threatLevel)->toBe(ThreatLevel::LOW);
|
||||
expect($assessment->shouldBlock)->toBeFalse();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Performance', function () {
|
||||
|
||||
it('completes assessment within reasonable time', function () {
|
||||
$start = microtime(true);
|
||||
|
||||
$request = createTestRequest('192.168.1.100', 'GET', '/api/test');
|
||||
$assessment = $this->engine->assessRequest($request);
|
||||
|
||||
$duration = microtime(true) - $start;
|
||||
|
||||
expect($duration)->toBeLessThan(0.1); // Should complete within 100ms
|
||||
expect($assessment)->not()->toBeNull();
|
||||
});
|
||||
|
||||
it('handles high request volume efficiently', function () {
|
||||
$start = microtime(true);
|
||||
|
||||
// Process 100 requests
|
||||
for ($i = 1; $i <= 100; $i++) {
|
||||
$ip = '192.168.1.' . ($i % 254 + 1);
|
||||
$request = createTestRequest($ip, 'GET', "/api/test{$i}");
|
||||
$this->engine->assessRequest($request);
|
||||
}
|
||||
|
||||
$duration = microtime(true) - $start;
|
||||
$avgPerRequest = $duration / 100;
|
||||
|
||||
expect($avgPerRequest)->toBeLessThan(0.01); // Average <10ms per request
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// Helper functions are now in Helpers/TestHelpers.php
|
||||
124
tests/Framework/DDoS/Helpers/TestHelpers.php
Normal file
124
tests/Framework/DDoS/Helpers/TestHelpers.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\DateTime\SystemClock;
|
||||
use App\Framework\DDoS\ValueObjects\DDoSAssessment;
|
||||
use App\Framework\DDoS\ValueObjects\ThreatLevel;
|
||||
use App\Framework\Http\Cookies\Cookies;
|
||||
use App\Framework\Http\Headers;
|
||||
use App\Framework\Http\HttpRequest;
|
||||
use App\Framework\Http\IpAddress;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Http\RequestBody;
|
||||
use App\Framework\Http\RequestId;
|
||||
use App\Framework\Http\ServerEnvironment;
|
||||
use App\Framework\Http\UploadedFiles;
|
||||
|
||||
/**
|
||||
* Helper function to create test HTTP requests
|
||||
*/
|
||||
function createTestRequest(string $ip, string $method, string $path, array $headers = [], array $postData = [], array $cookies = []): HttpRequest
|
||||
{
|
||||
$defaultHeaders = [
|
||||
'Host' => 'example.com',
|
||||
'User-Agent' => 'Mozilla/5.0 (compatible; TestClient/1.0)',
|
||||
'Accept' => 'text/html,application/json',
|
||||
];
|
||||
|
||||
$allHeaders = array_merge($defaultHeaders, $headers);
|
||||
|
||||
$serverVars = [
|
||||
'REQUEST_METHOD' => $method,
|
||||
'REQUEST_URI' => $path,
|
||||
'SERVER_NAME' => 'example.com',
|
||||
'REMOTE_ADDR' => $ip,
|
||||
'HTTP_HOST' => 'example.com',
|
||||
];
|
||||
|
||||
foreach ($allHeaders as $name => $value) {
|
||||
$serverKey = 'HTTP_' . strtoupper(str_replace('-', '_', $name));
|
||||
$serverVars[$serverKey] = $value;
|
||||
}
|
||||
|
||||
// Create HttpRequest using constructor
|
||||
return new HttpRequest(
|
||||
method: Method::from($method),
|
||||
headers: new Headers($allHeaders),
|
||||
body: ! empty($postData) ? json_encode($postData) : '',
|
||||
path: $path,
|
||||
queryParams: [],
|
||||
files: new UploadedFiles([]),
|
||||
cookies: new Cookies(),
|
||||
server: new ServerEnvironment($serverVars),
|
||||
id: new RequestId(),
|
||||
parsedBody: new RequestBody(
|
||||
Method::from($method),
|
||||
new Headers($allHeaders),
|
||||
! empty($postData) ? json_encode($postData) : '',
|
||||
$postData
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create test DDoS assessments
|
||||
*/
|
||||
function createThreatAssessment(ThreatLevel $threatLevel, float $confidence, array $attackPatterns = [], ?string $clientIp = null): DDoSAssessment
|
||||
{
|
||||
$clock = new SystemClock();
|
||||
|
||||
return new DDoSAssessment(
|
||||
threatLevel: $threatLevel,
|
||||
attackPatterns: $attackPatterns,
|
||||
clientIp: IpAddress::from($clientIp ?? '192.168.1.100'),
|
||||
analysisResults: ['threat_score' => $confidence],
|
||||
confidence: $confidence,
|
||||
recommendedAction: match($threatLevel) {
|
||||
ThreatLevel::LOW => 'allow',
|
||||
ThreatLevel::MEDIUM => 'rate_limit',
|
||||
ThreatLevel::HIGH => 'block',
|
||||
ThreatLevel::CRITICAL => 'block'
|
||||
},
|
||||
processingTime: Duration::fromMilliseconds(10),
|
||||
timestamp: $clock->time()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create mock DDoS engine
|
||||
*/
|
||||
function createMockEngine(): object
|
||||
{
|
||||
// In a real test, we'd use proper DI container setup
|
||||
// For now, return a basic mock that we can work with
|
||||
return new class () {
|
||||
public function assessRequest($request)
|
||||
{
|
||||
return createThreatAssessment(ThreatLevel::LOW, 0.1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create engine with specific config
|
||||
*/
|
||||
function createEngineWithConfig($config): object
|
||||
{
|
||||
// Factory method to create engine with specific config
|
||||
return new class ($config) {
|
||||
public function __construct(private $config)
|
||||
{
|
||||
}
|
||||
|
||||
public function assessRequest($request)
|
||||
{
|
||||
if (! $this->config->enabled) {
|
||||
return createThreatAssessment(ThreatLevel::LOW, 0.0);
|
||||
}
|
||||
|
||||
return createThreatAssessment(ThreatLevel::LOW, 0.1);
|
||||
}
|
||||
};
|
||||
}
|
||||
338
tests/Framework/DDoS/Response/AdaptiveResponseSystemTest.php
Normal file
338
tests/Framework/DDoS/Response/AdaptiveResponseSystemTest.php
Normal file
@@ -0,0 +1,338 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\DateTime\SystemClock;
|
||||
use App\Framework\DDoS\DDoSConfig;
|
||||
use App\Framework\DDoS\Response\AdaptiveResponseSystem;
|
||||
use App\Framework\DDoS\ValueObjects\AttackPattern;
|
||||
use App\Framework\DDoS\ValueObjects\ThreatLevel;
|
||||
|
||||
require_once __DIR__ . '/../Helpers/TestHelpers.php';
|
||||
|
||||
beforeEach(function () {
|
||||
$this->clock = new SystemClock();
|
||||
$this->config = new DDoSConfig();
|
||||
$this->responseSystem = new AdaptiveResponseSystem($this->clock, $this->config);
|
||||
});
|
||||
|
||||
describe('AdaptiveResponseSystem', function () {
|
||||
|
||||
it('allows low threat requests through', function () {
|
||||
$assessment = createThreatAssessment(ThreatLevel::LOW, 0.1);
|
||||
$request = createTestRequest('192.168.1.100', 'GET', '/api/users');
|
||||
|
||||
$response = $this->responseSystem->executeResponse($assessment, $request);
|
||||
|
||||
expect($response->action)->toBe('allow');
|
||||
expect($response->shouldBlock)->toBeFalse();
|
||||
expect($response->httpStatusCode)->toBe(200);
|
||||
});
|
||||
|
||||
it('applies rate limiting for medium threats', function () {
|
||||
$assessment = createThreatAssessment(ThreatLevel::MEDIUM, 0.5, [AttackPattern::VOLUMETRIC_ATTACK]);
|
||||
$request = createTestRequest('192.168.1.200', 'GET', '/api/data');
|
||||
|
||||
$response = $this->responseSystem->executeResponse($assessment, $request);
|
||||
|
||||
expect($response->action)->toBe('rate_limit');
|
||||
expect($response->shouldBlock)->toBeFalse();
|
||||
expect($response->rateLimitHeaders)->toHaveKeys(['X-RateLimit-Limit', 'X-RateLimit-Remaining']);
|
||||
expect($response->httpStatusCode)->toBe(429);
|
||||
});
|
||||
|
||||
it('blocks high threat requests', function () {
|
||||
$assessment = createThreatAssessment(ThreatLevel::HIGH, 0.8, [AttackPattern::BOT_ATTACK]);
|
||||
$request = createTestRequest('192.168.1.300', 'POST', '/api/sensitive');
|
||||
|
||||
$response = $this->responseSystem->executeResponse($assessment, $request);
|
||||
|
||||
expect($response->action)->toBe('block');
|
||||
expect($response->shouldBlock)->toBeTrue();
|
||||
expect($response->httpStatusCode)->toBe(403);
|
||||
expect($response->blockDuration)->toBeInstanceOf(Duration::class);
|
||||
});
|
||||
|
||||
it('immediately blocks critical threats', function () {
|
||||
$assessment = createThreatAssessment(ThreatLevel::CRITICAL, 0.95, [
|
||||
AttackPattern::VOLUMETRIC_ATTACK,
|
||||
AttackPattern::APPLICATION_LAYER_ATTACK,
|
||||
]);
|
||||
$request = createTestRequest('192.168.1.400', 'POST', '/admin/delete');
|
||||
|
||||
$response = $this->responseSystem->executeResponse($assessment, $request);
|
||||
|
||||
expect($response->action)->toBe('block');
|
||||
expect($response->shouldBlock)->toBeTrue();
|
||||
expect($response->httpStatusCode)->toBe(403);
|
||||
expect($response->blockDuration->toMinutes())->toBeGreaterThan(60); // Long block
|
||||
});
|
||||
|
||||
it('issues captcha challenges for suspicious requests', function () {
|
||||
$assessment = createThreatAssessment(ThreatLevel::MEDIUM, 0.6, [AttackPattern::BOT_ATTACK]);
|
||||
$request = createTestRequest('192.168.1.500', 'GET', '/api/search');
|
||||
|
||||
$response = $this->responseSystem->executeResponse($assessment, $request);
|
||||
|
||||
expect($response->action)->toBe('captcha_challenge');
|
||||
expect($response->shouldBlock)->toBeFalse();
|
||||
expect($response->challengeData)->toHaveKey('captcha_token');
|
||||
expect($response->httpStatusCode)->toBe(202);
|
||||
});
|
||||
|
||||
it('requires proof of work for sustained attacks', function () {
|
||||
$assessment = createThreatAssessment(ThreatLevel::HIGH, 0.75, [AttackPattern::COORDINATED_ATTACK]);
|
||||
$request = createTestRequest('192.168.1.600', 'GET', '/api/expensive-operation');
|
||||
|
||||
// Enable proof of work in config
|
||||
$config = new DDoSConfig(enableProofOfWork: true);
|
||||
$responseSystem = new AdaptiveResponseSystem($this->clock, $config);
|
||||
|
||||
$response = $responseSystem->executeResponse($assessment, $request);
|
||||
|
||||
expect($response->action)->toBe('proof_of_work');
|
||||
expect($response->challengeData)->toHaveKeys(['difficulty', 'challenge', 'algorithm']);
|
||||
expect($response->httpStatusCode)->toBe(202);
|
||||
});
|
||||
|
||||
it('adapts response based on attack patterns', function () {
|
||||
// Volumetric attack should trigger rate limiting
|
||||
$volumetricAssessment = createThreatAssessment(ThreatLevel::MEDIUM, 0.6, [AttackPattern::VOLUMETRIC_ATTACK]);
|
||||
$request1 = createTestRequest('192.168.1.700', 'GET', '/api/data');
|
||||
|
||||
$response1 = $this->responseSystem->executeResponse($volumetricAssessment, $request1);
|
||||
expect($response1->action)->toBe('rate_limit');
|
||||
|
||||
// Application layer attack should trigger blocking
|
||||
$appLayerAssessment = createThreatAssessment(ThreatLevel::MEDIUM, 0.6, [AttackPattern::APPLICATION_LAYER_ATTACK]);
|
||||
$request2 = createTestRequest('192.168.1.701', 'POST', '/api/upload');
|
||||
|
||||
$response2 = $this->responseSystem->executeResponse($appLayerAssessment, $request2);
|
||||
expect($response2->action)->toBe('block');
|
||||
});
|
||||
|
||||
it('escalates responses for repeated offenses', function () {
|
||||
$clientIp = '192.168.1.800';
|
||||
|
||||
// First offense - rate limit
|
||||
$assessment1 = createThreatAssessment(ThreatLevel::MEDIUM, 0.5);
|
||||
$request1 = createTestRequest($clientIp, 'GET', '/api/data1');
|
||||
$response1 = $this->responseSystem->executeResponse($assessment1, $request1);
|
||||
expect($response1->action)->toBe('rate_limit');
|
||||
|
||||
// Record the offense
|
||||
$this->responseSystem->recordOffense($clientIp, $assessment1);
|
||||
|
||||
// Second offense - should escalate to block
|
||||
$assessment2 = createThreatAssessment(ThreatLevel::MEDIUM, 0.5);
|
||||
$request2 = createTestRequest($clientIp, 'GET', '/api/data2');
|
||||
$response2 = $this->responseSystem->executeResponse($assessment2, $request2);
|
||||
expect($response2->action)->toBe('block');
|
||||
});
|
||||
|
||||
it('applies geographic blocking when enabled', function () {
|
||||
$config = new DDoSConfig(
|
||||
enableGeographicBlocking: true,
|
||||
blockedCountries: ['CN', 'RU']
|
||||
);
|
||||
$responseSystem = new AdaptiveResponseSystem($this->clock, $config);
|
||||
|
||||
$assessment = createThreatAssessment(ThreatLevel::LOW, 0.2);
|
||||
$request = createTestRequest('203.0.113.195', 'GET', '/api/test'); // Assume this IP is from CN
|
||||
|
||||
// Mock geographic detection
|
||||
$responseSystem->setGeographicInfo('203.0.113.195', 'CN');
|
||||
|
||||
$response = $responseSystem->executeResponse($assessment, $request);
|
||||
|
||||
expect($response->action)->toBe('block');
|
||||
expect($response->blockReason)->toContain('geographic');
|
||||
});
|
||||
|
||||
it('allows trusted IPs through regardless of threat level', function () {
|
||||
$config = new DDoSConfig(trustedIps: ['192.168.1.999']);
|
||||
$responseSystem = new AdaptiveResponseSystem($this->clock, $config);
|
||||
|
||||
$assessment = createThreatAssessment(ThreatLevel::CRITICAL, 0.95);
|
||||
$request = createTestRequest('192.168.1.999', 'GET', '/admin/critical');
|
||||
|
||||
$response = $responseSystem->executeResponse($assessment, $request);
|
||||
|
||||
expect($response->action)->toBe('allow');
|
||||
expect($response->shouldBlock)->toBeFalse();
|
||||
});
|
||||
|
||||
it('bypasses protection for exempt paths', function () {
|
||||
$config = new DDoSConfig(exemptPaths: ['/health', '/monitoring']);
|
||||
$responseSystem = new AdaptiveResponseSystem($this->clock, $config);
|
||||
|
||||
$assessment = createThreatAssessment(ThreatLevel::HIGH, 0.8);
|
||||
$request = createTestRequest('192.168.1.100', 'GET', '/health/check');
|
||||
|
||||
$response = $responseSystem->executeResponse($assessment, $request);
|
||||
|
||||
expect($response->action)->toBe('allow');
|
||||
expect($response->shouldBlock)->toBeFalse();
|
||||
});
|
||||
|
||||
it('provides detailed response metrics', function () {
|
||||
$assessment = createThreatAssessment(ThreatLevel::MEDIUM, 0.6);
|
||||
$request = createTestRequest('192.168.1.100', 'GET', '/api/test');
|
||||
|
||||
$response = $this->responseSystem->executeResponse($assessment, $request);
|
||||
|
||||
expect($response->metrics)->toHaveKeys([
|
||||
'processing_time_ms',
|
||||
'decision_confidence',
|
||||
'escalation_level',
|
||||
'historical_offenses',
|
||||
]);
|
||||
|
||||
expect($response->metrics['processing_time_ms'])->toBeLessThan(100);
|
||||
expect($response->metrics['decision_confidence'])->toBeBetween(0.0, 1.0);
|
||||
});
|
||||
|
||||
it('handles circuit breaker integration', function () {
|
||||
$config = new DDoSConfig(enableCircuitBreakerIntegration: true);
|
||||
$responseSystem = new AdaptiveResponseSystem($this->clock, $config);
|
||||
|
||||
// Simulate circuit breaker open state
|
||||
$responseSystem->setCircuitBreakerState('open');
|
||||
|
||||
$assessment = createThreatAssessment(ThreatLevel::LOW, 0.1);
|
||||
$request = createTestRequest('192.168.1.100', 'GET', '/api/test');
|
||||
|
||||
$response = $responseSystem->executeResponse($assessment, $request);
|
||||
|
||||
expect($response->action)->toBe('rate_limit'); // Fallback during circuit breaker open
|
||||
expect($response->responseHeaders)->toHaveKey('X-Circuit-Breaker-State');
|
||||
});
|
||||
|
||||
it('generates adaptive rate limits based on system load', function () {
|
||||
$assessment = createThreatAssessment(ThreatLevel::MEDIUM, 0.5);
|
||||
$request = createTestRequest('192.168.1.100', 'GET', '/api/data');
|
||||
|
||||
// Simulate high system load
|
||||
$this->responseSystem->setSystemLoad(0.9);
|
||||
|
||||
$response = $this->responseSystem->executeResponse($assessment, $request);
|
||||
|
||||
expect($response->action)->toBe('rate_limit');
|
||||
expect($response->rateLimitHeaders['X-RateLimit-Limit'])->toBeLessThan(60); // Stricter limits under load
|
||||
});
|
||||
|
||||
it('logs security events for blocked requests', function () {
|
||||
$assessment = createThreatAssessment(ThreatLevel::HIGH, 0.8);
|
||||
$request = createTestRequest('192.168.1.100', 'POST', '/api/sensitive');
|
||||
|
||||
$response = $this->responseSystem->executeResponse($assessment, $request);
|
||||
|
||||
expect($response->securityEventLogged)->toBeTrue();
|
||||
expect($response->logContext)->toHaveKeys([
|
||||
'client_ip',
|
||||
'threat_level',
|
||||
'attack_patterns',
|
||||
'response_action',
|
||||
]);
|
||||
});
|
||||
|
||||
it('provides response recommendations', function () {
|
||||
$assessment = createThreatAssessment(ThreatLevel::MEDIUM, 0.6, [AttackPattern::VOLUMETRIC_ATTACK]);
|
||||
$request = createTestRequest('192.168.1.100', 'GET', '/api/data');
|
||||
|
||||
$recommendations = $this->responseSystem->getResponseRecommendations($assessment, $request);
|
||||
|
||||
expect($recommendations)->toBeArray();
|
||||
expect($recommendations)->toContain('implement_rate_limiting');
|
||||
expect($recommendations)->toContain('monitor_traffic_patterns');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Response Strategy Selection', function () {
|
||||
|
||||
it('selects appropriate strategy for different attack types', function () {
|
||||
$strategies = [
|
||||
[AttackPattern::VOLUMETRIC_ATTACK, 'rate_limit'],
|
||||
[AttackPattern::BOT_ATTACK, 'captcha_challenge'],
|
||||
[AttackPattern::APPLICATION_LAYER_ATTACK, 'block'],
|
||||
[AttackPattern::DISTRIBUTED_ATTACK, 'rate_limit'],
|
||||
[AttackPattern::COORDINATED_ATTACK, 'block'],
|
||||
];
|
||||
|
||||
foreach ($strategies as [$pattern, $expectedAction]) {
|
||||
$assessment = createThreatAssessment(ThreatLevel::MEDIUM, 0.6, [$pattern]);
|
||||
$request = createTestRequest('192.168.1.100', 'GET', '/api/test');
|
||||
|
||||
$response = $this->responseSystem->executeResponse($assessment, $request);
|
||||
expect($response->action)->toBe($expectedAction);
|
||||
}
|
||||
});
|
||||
|
||||
it('combines multiple strategies for complex attacks', function () {
|
||||
$assessment = createThreatAssessment(ThreatLevel::HIGH, 0.8, [
|
||||
AttackPattern::VOLUMETRIC_ATTACK,
|
||||
AttackPattern::BOT_ATTACK,
|
||||
AttackPattern::APPLICATION_LAYER_ATTACK,
|
||||
]);
|
||||
$request = createTestRequest('192.168.1.100', 'POST', '/api/critical');
|
||||
|
||||
$response = $this->responseSystem->executeResponse($assessment, $request);
|
||||
|
||||
expect($response->action)->toBe('block'); // Most restrictive action
|
||||
expect($response->additionalMeasures)->toContain('enhanced_logging');
|
||||
expect($response->additionalMeasures)->toContain('alert_security_team');
|
||||
});
|
||||
|
||||
it('adjusts strategy based on request context', function () {
|
||||
// Same threat level but different paths should get different responses
|
||||
$assessment = createThreatAssessment(ThreatLevel::MEDIUM, 0.6);
|
||||
|
||||
$publicRequest = createTestRequest('192.168.1.100', 'GET', '/api/public-data');
|
||||
$adminRequest = createTestRequest('192.168.1.100', 'GET', '/admin/users');
|
||||
|
||||
$publicResponse = $this->responseSystem->executeResponse($assessment, $publicRequest);
|
||||
$adminResponse = $this->responseSystem->executeResponse($assessment, $adminRequest);
|
||||
|
||||
// Admin endpoint should be more strictly protected
|
||||
expect($adminResponse->action)->toBeIn(['block', 'captcha_challenge']);
|
||||
expect($publicResponse->action)->toBeIn(['allow', 'rate_limit']);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Performance', function () {
|
||||
|
||||
it('completes response execution within time limit', function () {
|
||||
$assessment = createThreatAssessment(ThreatLevel::MEDIUM, 0.5);
|
||||
$request = createTestRequest('192.168.1.100', 'GET', '/api/test');
|
||||
|
||||
$start = microtime(true);
|
||||
$response = $this->responseSystem->executeResponse($assessment, $request);
|
||||
$duration = microtime(true) - $start;
|
||||
|
||||
expect($duration)->toBeLessThan(0.05); // Should complete within 50ms
|
||||
expect($response)->not()->toBeNull();
|
||||
});
|
||||
|
||||
it('handles high request volume efficiently', function () {
|
||||
$start = microtime(true);
|
||||
|
||||
// Process 100 responses
|
||||
for ($i = 1; $i <= 100; $i++) {
|
||||
$assessment = createThreatAssessment(ThreatLevel::LOW, 0.1);
|
||||
$ip = '192.168.1.' . ($i % 254 + 1);
|
||||
$request = createTestRequest($ip, 'GET', "/api/test{$i}");
|
||||
$this->responseSystem->executeResponse($assessment, $request);
|
||||
}
|
||||
|
||||
$duration = microtime(true) - $start;
|
||||
$avgPerResponse = $duration / 100;
|
||||
|
||||
expect($avgPerResponse)->toBeLessThan(0.01); // Average <10ms per response
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// Helper functions are now in ../Helpers/TestHelpers.php
|
||||
272
tests/Framework/DDoS/Response/ValueObjects/DDoSResponseTest.php
Normal file
272
tests/Framework/DDoS/Response/ValueObjects/DDoSResponseTest.php
Normal file
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\DateTime\SystemClock;
|
||||
use App\Framework\DDoS\Response\ValueObjects\DDoSResponse;
|
||||
|
||||
require_once __DIR__ . '/../../Helpers/TestHelpers.php';
|
||||
|
||||
beforeEach(function () {
|
||||
$this->clock = new SystemClock();
|
||||
});
|
||||
|
||||
describe('DDoSResponse', function () {
|
||||
|
||||
it('creates allow response correctly', function () {
|
||||
$response = DDoSResponse::allow();
|
||||
|
||||
expect($response->action)->toBe('allow');
|
||||
expect($response->shouldBlock)->toBeFalse();
|
||||
expect($response->httpStatusCode)->toBe(200);
|
||||
expect($response->blockDuration)->toBeNull();
|
||||
});
|
||||
|
||||
it('creates rate limit response with headers', function () {
|
||||
$response = DDoSResponse::rateLimit(
|
||||
limit: 60,
|
||||
remaining: 45,
|
||||
resetTime: $this->clock->time()->add(Duration::fromMinutes(1))
|
||||
);
|
||||
|
||||
expect($response->action)->toBe('rate_limit');
|
||||
expect($response->shouldBlock)->toBeFalse();
|
||||
expect($response->httpStatusCode)->toBe(429);
|
||||
expect($response->rateLimitHeaders)->toHaveKeys([
|
||||
'X-RateLimit-Limit',
|
||||
'X-RateLimit-Remaining',
|
||||
'X-RateLimit-Reset',
|
||||
]);
|
||||
expect($response->rateLimitHeaders['X-RateLimit-Limit'])->toBe('60');
|
||||
expect($response->rateLimitHeaders['X-RateLimit-Remaining'])->toBe('45');
|
||||
});
|
||||
|
||||
it('creates block response with duration', function () {
|
||||
$blockDuration = Duration::fromMinutes(30);
|
||||
$response = DDoSResponse::block(
|
||||
duration: $blockDuration,
|
||||
reason: 'Malicious activity detected'
|
||||
);
|
||||
|
||||
expect($response->action)->toBe('block');
|
||||
expect($response->shouldBlock)->toBeTrue();
|
||||
expect($response->httpStatusCode)->toBe(403);
|
||||
expect($response->blockDuration)->toBe($blockDuration);
|
||||
expect($response->blockReason)->toBe('Malicious activity detected');
|
||||
});
|
||||
|
||||
it('creates captcha challenge response', function () {
|
||||
$challengeData = [
|
||||
'captcha_token' => 'abc123',
|
||||
'challenge_url' => '/captcha/verify',
|
||||
'expires_at' => time() + 300,
|
||||
];
|
||||
|
||||
$response = DDoSResponse::captchaChallenge($challengeData);
|
||||
|
||||
expect($response->action)->toBe('captcha_challenge');
|
||||
expect($response->shouldBlock)->toBeFalse();
|
||||
expect($response->httpStatusCode)->toBe(202);
|
||||
expect($response->challengeData)->toBe($challengeData);
|
||||
expect($response->challengeData['captcha_token'])->toBe('abc123');
|
||||
});
|
||||
|
||||
it('creates proof of work challenge response', function () {
|
||||
$challengeData = [
|
||||
'difficulty' => 4,
|
||||
'challenge' => 'find_hash_with_4_leading_zeros',
|
||||
'algorithm' => 'sha256',
|
||||
'expires_at' => time() + 600,
|
||||
];
|
||||
|
||||
$response = DDoSResponse::proofOfWork($challengeData);
|
||||
|
||||
expect($response->action)->toBe('proof_of_work');
|
||||
expect($response->shouldBlock)->toBeFalse();
|
||||
expect($response->httpStatusCode)->toBe(202);
|
||||
expect($response->challengeData)->toBe($challengeData);
|
||||
expect($response->challengeData['difficulty'])->toBe(4);
|
||||
});
|
||||
|
||||
it('adds custom response headers', function () {
|
||||
$response = DDoSResponse::allow()
|
||||
->withHeader('X-DDoS-Protection', 'Active')
|
||||
->withHeader('X-Request-ID', 'req-123');
|
||||
|
||||
expect($response->responseHeaders)->toHaveKeys([
|
||||
'X-DDoS-Protection',
|
||||
'X-Request-ID',
|
||||
]);
|
||||
expect($response->responseHeaders['X-DDoS-Protection'])->toBe('Active');
|
||||
expect($response->responseHeaders['X-Request-ID'])->toBe('req-123');
|
||||
});
|
||||
|
||||
it('sets security event logging', function () {
|
||||
$logContext = [
|
||||
'client_ip' => '192.168.1.100',
|
||||
'threat_level' => 'HIGH',
|
||||
'attack_patterns' => ['VOLUMETRIC_ATTACK'],
|
||||
];
|
||||
|
||||
$response = DDoSResponse::block(Duration::fromMinutes(15), 'High threat detected')
|
||||
->withSecurityEvent($logContext);
|
||||
|
||||
expect($response->securityEventLogged)->toBeTrue();
|
||||
expect($response->logContext)->toBe($logContext);
|
||||
});
|
||||
|
||||
it('adds response metrics', function () {
|
||||
$metrics = [
|
||||
'processing_time_ms' => 25,
|
||||
'decision_confidence' => 0.8,
|
||||
'escalation_level' => 2,
|
||||
'historical_offenses' => 3,
|
||||
];
|
||||
|
||||
$response = DDoSResponse::rateLimit(60, 30, $this->clock->time())
|
||||
->withMetrics($metrics);
|
||||
|
||||
expect($response->metrics)->toBe($metrics);
|
||||
expect($response->metrics['processing_time_ms'])->toBe(25);
|
||||
expect($response->metrics['decision_confidence'])->toBe(0.8);
|
||||
});
|
||||
|
||||
it('adds additional protective measures', function () {
|
||||
$measures = ['enhanced_logging', 'alert_security_team', 'increase_monitoring'];
|
||||
|
||||
$response = DDoSResponse::block(Duration::fromHours(1), 'Critical threat')
|
||||
->withAdditionalMeasures($measures);
|
||||
|
||||
expect($response->additionalMeasures)->toBe($measures);
|
||||
expect($response->additionalMeasures)->toContain('enhanced_logging');
|
||||
expect($response->additionalMeasures)->toContain('alert_security_team');
|
||||
});
|
||||
|
||||
it('converts to HTTP response array', function () {
|
||||
$response = DDoSResponse::rateLimit(60, 45, $this->clock->time())
|
||||
->withHeader('X-Protection', 'Active')
|
||||
->withMetrics(['processing_time_ms' => 30]);
|
||||
|
||||
$httpResponse = $response->toHttpResponse();
|
||||
|
||||
expect($httpResponse)->toHaveKeys([
|
||||
'status_code',
|
||||
'headers',
|
||||
'body',
|
||||
]);
|
||||
|
||||
expect($httpResponse['status_code'])->toBe(429);
|
||||
expect($httpResponse['headers'])->toHaveKey('X-RateLimit-Limit');
|
||||
expect($httpResponse['headers'])->toHaveKey('X-Protection');
|
||||
expect($httpResponse['body'])->toContain('Rate limit exceeded');
|
||||
});
|
||||
|
||||
it('exports to array for serialization', function () {
|
||||
$response = DDoSResponse::block(Duration::fromMinutes(30), 'Threat detected')
|
||||
->withMetrics(['confidence' => 0.9])
|
||||
->withSecurityEvent(['ip' => '192.168.1.100']);
|
||||
|
||||
$array = $response->toArray();
|
||||
|
||||
expect($array)->toHaveKeys([
|
||||
'action',
|
||||
'should_block',
|
||||
'http_status_code',
|
||||
'block_duration_seconds',
|
||||
'block_reason',
|
||||
'response_headers',
|
||||
'metrics',
|
||||
'security_event_logged',
|
||||
'log_context',
|
||||
]);
|
||||
|
||||
expect($array['action'])->toBe('block');
|
||||
expect($array['should_block'])->toBeTrue();
|
||||
expect($array['block_duration_seconds'])->toBe(1800); // 30 minutes
|
||||
});
|
||||
|
||||
it('creates response from array data', function () {
|
||||
$data = [
|
||||
'action' => 'rate_limit',
|
||||
'should_block' => false,
|
||||
'http_status_code' => 429,
|
||||
'rate_limit_headers' => [
|
||||
'X-RateLimit-Limit' => '60',
|
||||
'X-RateLimit-Remaining' => '30',
|
||||
],
|
||||
'response_headers' => ['X-Protection' => 'Active'],
|
||||
'metrics' => ['processing_time_ms' => 40],
|
||||
];
|
||||
|
||||
$response = DDoSResponse::fromArray($data);
|
||||
|
||||
expect($response->action)->toBe('rate_limit');
|
||||
expect($response->shouldBlock)->toBeFalse();
|
||||
expect($response->httpStatusCode)->toBe(429);
|
||||
expect($response->rateLimitHeaders['X-RateLimit-Limit'])->toBe('60');
|
||||
expect($response->responseHeaders['X-Protection'])->toBe('Active');
|
||||
expect($response->metrics['processing_time_ms'])->toBe(40);
|
||||
});
|
||||
|
||||
it('validates response consistency', function () {
|
||||
$blockResponse = DDoSResponse::block(Duration::fromMinutes(15), 'Threat');
|
||||
$allowResponse = DDoSResponse::allow();
|
||||
|
||||
expect($blockResponse->shouldBlock)->toBeTrue();
|
||||
expect($blockResponse->action)->toBe('block');
|
||||
expect($blockResponse->httpStatusCode)->toBe(403);
|
||||
|
||||
expect($allowResponse->shouldBlock)->toBeFalse();
|
||||
expect($allowResponse->action)->toBe('allow');
|
||||
expect($allowResponse->httpStatusCode)->toBe(200);
|
||||
});
|
||||
|
||||
it('handles challenge expiration', function () {
|
||||
$challengeData = [
|
||||
'captcha_token' => 'abc123',
|
||||
'expires_at' => time() - 300, // Expired 5 minutes ago
|
||||
];
|
||||
|
||||
$response = DDoSResponse::captchaChallenge($challengeData);
|
||||
|
||||
expect($response->isChallengeExpired())->toBeTrue();
|
||||
|
||||
$challengeData['expires_at'] = time() + 300; // Expires in 5 minutes
|
||||
$response = DDoSResponse::captchaChallenge($challengeData);
|
||||
|
||||
expect($response->isChallengeExpired())->toBeFalse();
|
||||
});
|
||||
|
||||
it('calculates response severity', function () {
|
||||
$allowResponse = DDoSResponse::allow();
|
||||
$rateLimitResponse = DDoSResponse::rateLimit(60, 30, $this->clock->time());
|
||||
$blockResponse = DDoSResponse::block(Duration::fromHours(1), 'Critical');
|
||||
|
||||
expect($allowResponse->getSeverity())->toBe('low');
|
||||
expect($rateLimitResponse->getSeverity())->toBe('medium');
|
||||
expect($blockResponse->getSeverity())->toBe('high');
|
||||
});
|
||||
|
||||
it('provides response recommendations', function () {
|
||||
$response = DDoSResponse::block(Duration::fromMinutes(30), 'Volumetric attack')
|
||||
->withAdditionalMeasures(['monitor_traffic', 'alert_ops']);
|
||||
|
||||
$recommendations = $response->getRecommendations();
|
||||
|
||||
expect($recommendations)->toBeArray();
|
||||
expect($recommendations)->toContain('Review traffic patterns');
|
||||
expect($recommendations)->toContain('Consider IP reputation check');
|
||||
});
|
||||
|
||||
it('tracks response effectiveness', function () {
|
||||
$response = DDoSResponse::rateLimit(60, 30, $this->clock->time());
|
||||
|
||||
// Simulate tracking effectiveness
|
||||
$response->recordEffectiveness(0.8, 'Successfully reduced request rate');
|
||||
|
||||
expect($response->getEffectivenessScore())->toBe(0.8);
|
||||
expect($response->getEffectivenessNote())->toBe('Successfully reduced request rate');
|
||||
});
|
||||
|
||||
});
|
||||
278
tests/Framework/DDoS/ValueObjects/DDoSAssessmentTest.php
Normal file
278
tests/Framework/DDoS/ValueObjects/DDoSAssessmentTest.php
Normal file
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\DateTime\SystemClock;
|
||||
use App\Framework\DDoS\ValueObjects\AttackPattern;
|
||||
use App\Framework\DDoS\ValueObjects\DDoSAssessment;
|
||||
use App\Framework\DDoS\ValueObjects\ThreatLevel;
|
||||
use App\Framework\Http\IpAddress;
|
||||
|
||||
require_once __DIR__ . '/../Helpers/TestHelpers.php';
|
||||
|
||||
beforeEach(function () {
|
||||
$this->clock = new SystemClock();
|
||||
});
|
||||
|
||||
describe('DDoSAssessment', function () {
|
||||
|
||||
it('creates valid assessment with all required data', function () {
|
||||
$assessment = new DDoSAssessment(
|
||||
threatLevel: ThreatLevel::MEDIUM,
|
||||
attackPatterns: [AttackPattern::VOLUMETRIC_ATTACK],
|
||||
clientIp: IpAddress::from('192.168.1.100'),
|
||||
analysisResults: ['traffic_score' => 0.6],
|
||||
confidence: 0.8,
|
||||
recommendedAction: 'rate_limit',
|
||||
processingTime: Duration::fromMilliseconds(50),
|
||||
timestamp: $this->clock->time()
|
||||
);
|
||||
|
||||
expect($assessment->threatLevel)->toBe(ThreatLevel::MEDIUM);
|
||||
expect($assessment->attackPatterns)->toContain(AttackPattern::VOLUMETRIC_ATTACK);
|
||||
expect($assessment->clientIp->value)->toBe('192.168.1.100');
|
||||
expect($assessment->confidence)->toBe(0.8);
|
||||
expect($assessment->recommendedAction)->toBe('rate_limit');
|
||||
});
|
||||
|
||||
it('creates safe assessment for normal traffic', function () {
|
||||
$assessment = DDoSAssessment::createSafe($this->clock);
|
||||
|
||||
expect($assessment->threatLevel)->toBe(ThreatLevel::LOW);
|
||||
expect($assessment->attackPatterns)->toBeEmpty();
|
||||
expect($assessment->confidence)->toBeLessThan(0.3);
|
||||
expect($assessment->recommendedAction)->toBe('allow');
|
||||
});
|
||||
|
||||
it('creates critical assessment for severe threats', function () {
|
||||
$assessment = DDoSAssessment::createCritical(
|
||||
clientIp: IpAddress::from('10.0.0.1'),
|
||||
attackPatterns: [AttackPattern::VOLUMETRIC_ATTACK, AttackPattern::BOT_ATTACK],
|
||||
analysisResults: ['threat_score' => 0.95],
|
||||
clock: $this->clock
|
||||
);
|
||||
|
||||
expect($assessment->threatLevel)->toBe(ThreatLevel::CRITICAL);
|
||||
expect($assessment->attackPatterns)->toHaveCount(2);
|
||||
expect($assessment->confidence)->toBeGreaterThan(0.9);
|
||||
expect($assessment->recommendedAction)->toBe('block');
|
||||
});
|
||||
|
||||
it('determines if request should be blocked', function () {
|
||||
$lowThreat = new DDoSAssessment(
|
||||
threatLevel: ThreatLevel::LOW,
|
||||
attackPatterns: [],
|
||||
clientIp: IpAddress::from('192.168.1.100'),
|
||||
analysisResults: [],
|
||||
confidence: 0.2,
|
||||
recommendedAction: 'allow',
|
||||
processingTime: Duration::fromMilliseconds(10),
|
||||
timestamp: $this->clock->time()
|
||||
);
|
||||
|
||||
$highThreat = new DDoSAssessment(
|
||||
threatLevel: ThreatLevel::HIGH,
|
||||
attackPatterns: [AttackPattern::APPLICATION_LAYER_ATTACK],
|
||||
clientIp: IpAddress::from('192.168.1.100'),
|
||||
analysisResults: [],
|
||||
confidence: 0.8,
|
||||
recommendedAction: 'block',
|
||||
processingTime: Duration::fromMilliseconds(10),
|
||||
timestamp: $this->clock->time()
|
||||
);
|
||||
|
||||
expect($lowThreat->shouldBlock())->toBeFalse();
|
||||
expect($highThreat->shouldBlock())->toBeTrue();
|
||||
});
|
||||
|
||||
it('calculates threat score from analysis results', function () {
|
||||
$assessment = new DDoSAssessment(
|
||||
threatLevel: ThreatLevel::MEDIUM,
|
||||
attackPatterns: [],
|
||||
clientIp: IpAddress::from('192.168.1.100'),
|
||||
analysisResults: [
|
||||
'traffic_patterns' => ['threat_score' => 0.6],
|
||||
'geo_anomalies' => ['threat_score' => 0.4],
|
||||
'waf_analysis' => ['threat_score' => 0.8],
|
||||
],
|
||||
confidence: 0.7,
|
||||
recommendedAction: 'rate_limit',
|
||||
processingTime: Duration::fromMilliseconds(25),
|
||||
timestamp: $this->clock->time()
|
||||
);
|
||||
|
||||
$threatScore = $assessment->getThreatScore();
|
||||
|
||||
expect($threatScore)->toBeBetween(0.0, 1.0);
|
||||
expect($threatScore)->toBeGreaterThan(0.5); // Should be influenced by high scores
|
||||
});
|
||||
|
||||
it('provides human readable summary', function () {
|
||||
$assessment = new DDoSAssessment(
|
||||
threatLevel: ThreatLevel::HIGH,
|
||||
attackPatterns: [AttackPattern::VOLUMETRIC_ATTACK, AttackPattern::BOT_ATTACK],
|
||||
clientIp: IpAddress::from('192.168.1.100'),
|
||||
analysisResults: ['threat_score' => 0.8],
|
||||
confidence: 0.9,
|
||||
recommendedAction: 'block',
|
||||
processingTime: Duration::fromMilliseconds(75),
|
||||
timestamp: $this->clock->time()
|
||||
);
|
||||
|
||||
$summary = $assessment->getSummary();
|
||||
|
||||
expect($summary)->toBeString();
|
||||
expect($summary)->toContain('HIGH');
|
||||
expect($summary)->toContain('192.168.1.100');
|
||||
expect($summary)->toContain('VOLUMETRIC_ATTACK');
|
||||
expect($summary)->toContain('BOT_ATTACK');
|
||||
});
|
||||
|
||||
it('exports to array for serialization', function () {
|
||||
$assessment = new DDoSAssessment(
|
||||
threatLevel: ThreatLevel::MEDIUM,
|
||||
attackPatterns: [AttackPattern::DISTRIBUTED_ATTACK],
|
||||
clientIp: IpAddress::from('192.168.1.100'),
|
||||
analysisResults: ['geo_score' => 0.6],
|
||||
confidence: 0.75,
|
||||
recommendedAction: 'captcha_challenge',
|
||||
processingTime: Duration::fromMilliseconds(30),
|
||||
timestamp: $this->clock->time()
|
||||
);
|
||||
|
||||
$array = $assessment->toArray();
|
||||
|
||||
expect($array)->toHaveKeys([
|
||||
'threat_level',
|
||||
'attack_patterns',
|
||||
'client_ip',
|
||||
'analysis_results',
|
||||
'confidence',
|
||||
'recommended_action',
|
||||
'processing_time_ms',
|
||||
'timestamp',
|
||||
]);
|
||||
|
||||
expect($array['threat_level'])->toBe('MEDIUM');
|
||||
expect($array['client_ip'])->toBe('192.168.1.100');
|
||||
expect($array['confidence'])->toBe(0.75);
|
||||
});
|
||||
|
||||
it('creates assessment from array data', function () {
|
||||
$data = [
|
||||
'threat_level' => 'HIGH',
|
||||
'attack_patterns' => ['VOLUMETRIC_ATTACK'],
|
||||
'client_ip' => '10.0.0.1',
|
||||
'analysis_results' => ['traffic_score' => 0.8],
|
||||
'confidence' => 0.85,
|
||||
'recommended_action' => 'block',
|
||||
'processing_time_ms' => 40,
|
||||
'timestamp' => $this->clock->time()->format('c'),
|
||||
];
|
||||
|
||||
$assessment = DDoSAssessment::fromArray($data, $this->clock);
|
||||
|
||||
expect($assessment->threatLevel)->toBe(ThreatLevel::HIGH);
|
||||
expect($assessment->attackPatterns)->toContain(AttackPattern::VOLUMETRIC_ATTACK);
|
||||
expect($assessment->clientIp->value)->toBe('10.0.0.1');
|
||||
expect($assessment->confidence)->toBe(0.85);
|
||||
});
|
||||
|
||||
it('validates confidence values', function () {
|
||||
expect(fn () => new DDoSAssessment(
|
||||
threatLevel: ThreatLevel::LOW,
|
||||
attackPatterns: [],
|
||||
clientIp: IpAddress::from('192.168.1.100'),
|
||||
analysisResults: [],
|
||||
confidence: 1.5, // Invalid confidence > 1.0
|
||||
recommendedAction: 'allow',
|
||||
processingTime: Duration::fromMilliseconds(10),
|
||||
timestamp: $this->clock->time()
|
||||
))->toThrow(InvalidArgumentException::class);
|
||||
|
||||
expect(fn () => new DDoSAssessment(
|
||||
threatLevel: ThreatLevel::LOW,
|
||||
attackPatterns: [],
|
||||
clientIp: IpAddress::from('192.168.1.100'),
|
||||
analysisResults: [],
|
||||
confidence: -0.1, // Invalid confidence < 0.0
|
||||
recommendedAction: 'allow',
|
||||
processingTime: Duration::fromMilliseconds(10),
|
||||
timestamp: $this->clock->time()
|
||||
))->toThrow(InvalidArgumentException::class);
|
||||
});
|
||||
|
||||
it('compares threat levels correctly', function () {
|
||||
$lowAssessment = DDoSAssessment::createSafe($this->clock);
|
||||
$highAssessment = DDoSAssessment::createCritical(
|
||||
clientIp: IpAddress::from('10.0.0.1'),
|
||||
attackPatterns: [AttackPattern::VOLUMETRIC_ATTACK],
|
||||
analysisResults: [],
|
||||
clock: $this->clock
|
||||
);
|
||||
|
||||
expect($lowAssessment->isLessThreatThan($highAssessment))->toBeTrue();
|
||||
expect($highAssessment->isMoreThreatThan($lowAssessment))->toBeTrue();
|
||||
expect($lowAssessment->isMoreThreatThan($highAssessment))->toBeFalse();
|
||||
});
|
||||
|
||||
it('identifies coordinated attacks', function () {
|
||||
$coordinatedAssessment = new DDoSAssessment(
|
||||
threatLevel: ThreatLevel::HIGH,
|
||||
attackPatterns: [
|
||||
AttackPattern::VOLUMETRIC_ATTACK,
|
||||
AttackPattern::DISTRIBUTED_ATTACK,
|
||||
AttackPattern::COORDINATED_ATTACK,
|
||||
],
|
||||
clientIp: IpAddress::from('192.168.1.100'),
|
||||
analysisResults: [],
|
||||
confidence: 0.9,
|
||||
recommendedAction: 'block',
|
||||
processingTime: Duration::fromMilliseconds(50),
|
||||
timestamp: $this->clock->time()
|
||||
);
|
||||
|
||||
expect($coordinatedAssessment->isCoordinatedAttack())->toBeTrue();
|
||||
expect($coordinatedAssessment->getAttackComplexity())->toBe('high');
|
||||
});
|
||||
|
||||
it('calculates assessment age', function () {
|
||||
$pastTime = $this->clock->time()->subtract(Duration::fromMinutes(5));
|
||||
|
||||
$assessment = new DDoSAssessment(
|
||||
threatLevel: ThreatLevel::MEDIUM,
|
||||
attackPatterns: [],
|
||||
clientIp: IpAddress::from('192.168.1.100'),
|
||||
analysisResults: [],
|
||||
confidence: 0.5,
|
||||
recommendedAction: 'rate_limit',
|
||||
processingTime: Duration::fromMilliseconds(20),
|
||||
timestamp: $pastTime
|
||||
);
|
||||
|
||||
$age = $assessment->getAge($this->clock->time());
|
||||
|
||||
expect($age->toMinutes())->toBeGreaterThan(4);
|
||||
expect($age->toMinutes())->toBeLessThan(6);
|
||||
});
|
||||
|
||||
it('determines if assessment is stale', function () {
|
||||
$oldTime = $this->clock->time()->subtract(Duration::fromMinutes(10));
|
||||
|
||||
$assessment = new DDoSAssessment(
|
||||
threatLevel: ThreatLevel::MEDIUM,
|
||||
attackPatterns: [],
|
||||
clientIp: IpAddress::from('192.168.1.100'),
|
||||
analysisResults: [],
|
||||
confidence: 0.5,
|
||||
recommendedAction: 'rate_limit',
|
||||
processingTime: Duration::fromMilliseconds(20),
|
||||
timestamp: $oldTime
|
||||
);
|
||||
|
||||
expect($assessment->isStale($this->clock->time(), Duration::fromMinutes(5)))->toBeTrue();
|
||||
expect($assessment->isStale($this->clock->time(), Duration::fromMinutes(15)))->toBeFalse();
|
||||
});
|
||||
|
||||
});
|
||||
84
tests/Framework/DDoS/ValueObjects/ThreatScoreTest.php
Normal file
84
tests/Framework/DDoS/ValueObjects/ThreatScoreTest.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\DDoS\ValueObjects\ThreatScore;
|
||||
|
||||
describe('ThreatScore Value Object', function () {
|
||||
|
||||
it('creates threat score from float', function () {
|
||||
$threatScore = ThreatScore::fromFloat(0.8);
|
||||
|
||||
expect($threatScore->getScore()->value())->toBe(0.8);
|
||||
expect($threatScore->requiresBlocking())->toBeTrue();
|
||||
});
|
||||
|
||||
it('creates safe threat score', function () {
|
||||
$safe = ThreatScore::safe();
|
||||
|
||||
expect($safe->getScore()->value())->toBe(0.0);
|
||||
expect($safe->requiresBlocking())->toBeFalse();
|
||||
expect($safe->getRecommendedAction())->toBe('allow');
|
||||
});
|
||||
|
||||
it('creates critical threat score', function () {
|
||||
$critical = ThreatScore::critical();
|
||||
|
||||
expect($critical->getScore()->isCritical())->toBeTrue();
|
||||
expect($critical->requiresBlocking())->toBeTrue();
|
||||
expect($critical->getRecommendedAction())->toBe('block_immediately');
|
||||
});
|
||||
|
||||
it('determines correct actions based on level', function () {
|
||||
$low = ThreatScore::fromFloat(0.1);
|
||||
$medium = ThreatScore::fromFloat(0.5);
|
||||
$high = ThreatScore::fromFloat(0.8);
|
||||
$critical = ThreatScore::fromFloat(0.95);
|
||||
|
||||
expect($low->getRecommendedAction())->toBe('allow');
|
||||
expect($medium->getRecommendedAction())->toBe('rate_limit');
|
||||
expect($high->getRecommendedAction())->toBe('enhanced_monitoring');
|
||||
expect($critical->getRecommendedAction())->toBe('block_immediately');
|
||||
});
|
||||
|
||||
it('creates from multiple analyses', function () {
|
||||
$analyses = [
|
||||
'traffic_patterns' => ['threat_score' => 0.8, 'indicators' => ['high_volume']],
|
||||
'geo_anomalies' => ['threat_score' => 0.6, 'indicators' => ['unusual_location']],
|
||||
'waf_analysis' => ['threat_score' => 0.9, 'indicators' => ['malicious_payload']],
|
||||
];
|
||||
|
||||
$threatScore = ThreatScore::fromAnalyses($analyses);
|
||||
|
||||
expect($threatScore->getScore()->value())->toBeGreaterThan(0.7);
|
||||
expect($threatScore->getIndicators())->toContain('high_volume');
|
||||
expect($threatScore->getSources())->toContain('traffic_patterns');
|
||||
});
|
||||
|
||||
it('combines threat scores correctly', function () {
|
||||
$score1 = ThreatScore::fromFloat(0.8);
|
||||
$score2 = ThreatScore::fromFloat(0.6);
|
||||
|
||||
$combined = $score1->combineWith($score2, 0.7);
|
||||
|
||||
expect($combined->getScore()->value())->toBe(0.74); // 0.8 * 0.7 + 0.6 * 0.3
|
||||
});
|
||||
|
||||
it('provides detailed description', function () {
|
||||
$threatScore = ThreatScore::fromFloat(0.75);
|
||||
$description = $threatScore->getDescription();
|
||||
|
||||
expect($description)->toContain('High threat level');
|
||||
expect($description)->toContain('75.0%');
|
||||
});
|
||||
|
||||
it('serializes and deserializes correctly', function () {
|
||||
$original = ThreatScore::fromFloat(0.8);
|
||||
$array = $original->toArray();
|
||||
$restored = ThreatScore::fromArray($array);
|
||||
|
||||
expect($restored->getScore()->value())->toBe($original->getScore()->value());
|
||||
expect($restored->requiresBlocking())->toBe($original->requiresBlocking());
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user