shouldReceive('isEnabled')->andReturn($enabled); $extractor->shouldReceive('getFeatureType')->andReturn($featureType); $extractor->shouldReceive('getPriority')->andReturn(10); $extractor->shouldReceive('canExtract')->with(Mockery::type(RequestAnalysisData::class))->andReturn(true); $extractor->shouldReceive('extractFeatures')->with(Mockery::type(RequestAnalysisData::class), Mockery::type('array'))->andReturn($features); $extractor->shouldReceive('getExpectedProcessingTime')->andReturn(10); $extractor->shouldReceive('supportsParallelExecution')->andReturn(true); $extractor->shouldReceive('getDependencies')->andReturn([]); return $extractor; } // Hilfsfunktion zum Erstellen eines Mock-Detektors function createMockDetectorMLE(bool $enabled = true, array $supportedTypes = [], array $anomalies = []): MockInterface { $supportedTypes = $supportedTypes ?: [FeatureType::STRUCTURAL_PATTERN]; $detector = Mockery::mock(AnomalyDetectorInterface::class); $detector->shouldReceive('isEnabled')->andReturn($enabled); $detector->shouldReceive('getName')->andReturn('MockDetector'); $detector->shouldReceive('getSupportedFeatureTypes')->andReturn($supportedTypes); $detector->shouldReceive('canAnalyze')->andReturn(true); $detector->shouldReceive('detectAnomalies')->andReturn($anomalies); $detector->shouldReceive('updateModel')->andReturn(null); return $detector; } // Hilfsfunktion zum Erstellen einer Beispiel-RequestAnalysisData function createSampleRequestData(): RequestAnalysisData { return new RequestAnalysisData( method: 'GET', url: '/test', path: '/test', queryString: '', headers: ['User-Agent' => 'TestAgent'], queryParameters: [], postParameters: [], cookies: [], body: '', files: [], clientIp: IpAddress::localhost(), timestamp: Timestamp::now() ); } // Hilfsfunktion zum Erstellen einer Beispiel-Clock function createMockClockMLE(): MockInterface { $clock = Mockery::mock(Clock::class); $dateTime = DateTime::fromString('2025-07-31 13:42:00'); $timestamp = Timestamp::fromDateTime($dateTime); $clock->shouldReceive('time')->andReturn($timestamp); return $clock; } test('gibt leeres Ergebnis zurück wenn deaktiviert', function () { // Arrange $engine = new MachineLearningEngine( enabled: false, extractors: [], detectors: [], clock: createMockClockMLE(), analysisTimeout: Duration::fromSeconds(10), confidenceThreshold: Percentage::from(50.0) ); // Act $result = $engine->analyzeRequest(createSampleRequestData()); // Assert expect($result)->toBeInstanceOf(MachineLearningResult::class); expect($result->enabled)->toBeFalse(); expect($result->features)->toBeEmpty(); expect($result->anomalies)->toBeEmpty(); expect($result->confidence->getValue())->toBe(0.0); }); test('extrahiert Features aus Request-Daten', function () { // Arrange $feature = new Feature( type: FeatureType::STRUCTURAL_PATTERN, name: 'test_feature', value: 42.0, unit: 'count' ); $extractor = createMockExtractorMLE(true, FeatureType::STRUCTURAL_PATTERN, [$feature]); $engine = new MachineLearningEngine( enabled: true, extractors: [$extractor], detectors: [], clock: createMockClockMLE(), analysisTimeout: Duration::fromSeconds(10), confidenceThreshold: Percentage::from(50.0) ); // Act $result = $engine->analyzeRequest(createSampleRequestData()); // Assert expect($result->error)->toBeNull(); expect($result->features)->toHaveCount(1); expect($result->features[0])->toBeInstanceOf(Feature::class); expect($result->features[0]->name)->toBe('test_feature'); expect($result->features[0]->value)->toBe(42.0); }); test('erkennt Anomalien in Features', function () { // Arrange $feature = new Feature( type: FeatureType::STRUCTURAL_PATTERN, name: 'test_feature', value: 42.0, unit: 'count' ); $anomaly = new AnomalyDetection( type: AnomalyType::STATISTICAL_ANOMALY, featureType: FeatureType::STRUCTURAL_PATTERN, confidence: Percentage::from(75.0), anomalyScore: 0.8, description: 'Test anomaly', features: [$feature], evidence: [ 'metric' => 'test_feature', 'value' => 42.0, 'expected_value' => 10.0, 'z_score' => 2.5, ] ); $extractor = createMockExtractorMLE(true, FeatureType::STRUCTURAL_PATTERN, [$feature]); $detector = createMockDetectorMLE(true, [FeatureType::STRUCTURAL_PATTERN], [$anomaly]); $engine = new MachineLearningEngine( enabled: true, extractors: [$extractor], detectors: [$detector], clock: createMockClockMLE(), analysisTimeout: Duration::fromSeconds(10), confidenceThreshold: Percentage::from(50.0) ); // Act $result = $engine->analyzeRequest(createSampleRequestData()); // Assert expect($result->anomalies)->toHaveCount(1); expect($result->anomalies[0])->toBeInstanceOf(AnomalyDetection::class); expect($result->anomalies[0]->type)->toBe(AnomalyType::STATISTICAL_ANOMALY); expect($result->anomalies[0]->confidence->getValue())->toBe(75.0); }); test('filtert Anomalien basierend auf Konfidenz-Schwellenwert', function () { // Arrange $feature = new Feature( type: FeatureType::STRUCTURAL_PATTERN, name: 'test_feature', value: 42.0, unit: 'count' ); $highConfidenceAnomaly = new AnomalyDetection( type: AnomalyType::STATISTICAL_ANOMALY, featureType: FeatureType::STRUCTURAL_PATTERN, confidence: Percentage::from(80.0), anomalyScore: 0.8, description: 'High confidence anomaly', features: [$feature], evidence: ['value' => 42.0, 'expected_value' => 10.0] ); $lowConfidenceAnomaly = new AnomalyDetection( type: AnomalyType::STATISTICAL_ANOMALY, featureType: FeatureType::STRUCTURAL_PATTERN, confidence: Percentage::from(40.0), anomalyScore: 0.3, description: 'Low confidence anomaly', features: [$feature], evidence: ['value' => 42.0, 'expected_value' => 30.0] ); $extractor = createMockExtractorMLE(true, FeatureType::STRUCTURAL_PATTERN, [$feature]); $detector = createMockDetectorMLE(true, [FeatureType::STRUCTURAL_PATTERN], [$highConfidenceAnomaly, $lowConfidenceAnomaly]); $engine = new MachineLearningEngine( enabled: true, extractors: [$extractor], detectors: [$detector], clock: createMockClockMLE(), analysisTimeout: Duration::fromSeconds(10), confidenceThreshold: Percentage::from(50.0) ); // Act $result = $engine->analyzeRequest(createSampleRequestData()); // Assert expect($result->anomalies)->toHaveCount(1); expect($result->anomalies[0]->confidence->getValue())->toBe(80.0); }); test('berechnet Gesamt-Konfidenz korrekt', function () { // Arrange $feature = new Feature( type: FeatureType::STRUCTURAL_PATTERN, name: 'test_feature', value: 42.0, unit: 'count' ); $anomaly1 = new AnomalyDetection( type: AnomalyType::STATISTICAL_ANOMALY, featureType: FeatureType::STRUCTURAL_PATTERN, confidence: Percentage::from(60.0), anomalyScore: 0.6, description: 'Anomaly 1', features: [$feature], evidence: ['value' => 42.0, 'expected_value' => 10.0] ); $anomaly2 = new AnomalyDetection( type: AnomalyType::CLUSTERING_DEVIATION, featureType: FeatureType::STRUCTURAL_PATTERN, confidence: Percentage::from(80.0), anomalyScore: 0.4, description: 'Anomaly 2', features: [$feature], evidence: ['value' => 42.0, 'expected_value' => 10.0] ); $extractor = createMockExtractorMLE(true, FeatureType::STRUCTURAL_PATTERN, [$feature]); $detector = createMockDetectorMLE(true, [FeatureType::STRUCTURAL_PATTERN], [$anomaly1, $anomaly2]); $engine = new MachineLearningEngine( enabled: true, extractors: [$extractor], detectors: [$detector], clock: createMockClockMLE(), analysisTimeout: Duration::fromSeconds(10), confidenceThreshold: Percentage::from(50.0) ); // Act $result = $engine->analyzeRequest(createSampleRequestData()); // Assert // Erwartete Konfidenz: (60.0 * 0.6 + 80.0 * 0.4) / (0.6 + 0.4) = (36 + 32) / 1 = 68 expect($result->confidence->getValue())->toBeGreaterThan(67.9); expect($result->confidence->getValue())->toBeLessThan(68.1); }); test('dedupliziert und sortiert Anomalien', function () { // Arrange $feature = new Feature( type: FeatureType::STRUCTURAL_PATTERN, name: 'test_feature', value: 42.0, unit: 'count' ); // Zwei Anomalien mit gleichem Typ und FeatureType, aber unterschiedlicher Konfidenz $anomaly1 = new AnomalyDetection( type: AnomalyType::STATISTICAL_ANOMALY, featureType: FeatureType::STRUCTURAL_PATTERN, confidence: Percentage::from(60.0), anomalyScore: 0.6, description: 'Anomaly 1', features: [$feature], evidence: ['value' => 42.0, 'expected_value' => 10.0] ); $anomaly2 = new AnomalyDetection( type: AnomalyType::STATISTICAL_ANOMALY, featureType: FeatureType::STRUCTURAL_PATTERN, confidence: Percentage::from(80.0), anomalyScore: 0.8, description: 'Anomaly 2', features: [$feature], evidence: ['value' => 42.0, 'expected_value' => 10.0] ); // Eine Anomalie mit anderem Typ $anomaly3 = new AnomalyDetection( type: AnomalyType::CLUSTERING_DEVIATION, featureType: FeatureType::STRUCTURAL_PATTERN, confidence: Percentage::from(70.0), anomalyScore: 0.4, description: 'Anomaly 3', features: [$feature], evidence: ['value' => 42.0, 'expected_value' => 10.0] ); $extractor = createMockExtractorMLE(true, FeatureType::STRUCTURAL_PATTERN, [$feature]); $detector = createMockDetectorMLE(true, [FeatureType::STRUCTURAL_PATTERN], [$anomaly1, $anomaly2, $anomaly3]); $engine = new MachineLearningEngine( enabled: true, extractors: [$extractor], detectors: [$detector], clock: createMockClockMLE(), analysisTimeout: Duration::fromSeconds(10), confidenceThreshold: Percentage::from(50.0) ); // Act $result = $engine->analyzeRequest(createSampleRequestData()); // Assert // Erwartet: 2 Anomalien (anomaly2 und anomaly3), da anomaly1 und anomaly2 dedupliziert werden // und anomaly2 mit höherer Konfidenz behalten wird expect($result->anomalies)->toHaveCount(2); // Sortierung nach anomalyScore (absteigend), also anomaly2 vor anomaly3 expect($result->anomalies[0]->type)->toBe(AnomalyType::STATISTICAL_ANOMALY); expect($result->anomalies[0]->confidence->getValue())->toBe(80.0); expect($result->anomalies[1]->type)->toBe(AnomalyType::CLUSTERING_DEVIATION); expect($result->anomalies[1]->confidence->getValue())->toBe(70.0); }); test('gibt Konfiguration korrekt zurück', function () { // Arrange $engine = new MachineLearningEngine( enabled: true, extractors: [createMockExtractorMLE()], detectors: [createMockDetectorMLE()], clock: createMockClockMLE(), analysisTimeout: Duration::fromSeconds(5), confidenceThreshold: Percentage::from(75.0), enableParallelProcessing: true, enableFeatureCaching: false, maxFeaturesPerRequest: 50 ); // Act $config = $engine->getConfiguration(); // Assert expect($config)->toBeArray(); expect($config['enabled'])->toBeTrue(); expect($config['analysis_timeout_ms'])->toBe(5000.0); expect($config['confidence_threshold'])->toBe(75.0); expect($config['enable_parallel_processing'])->toBeTrue(); expect($config['enable_feature_caching'])->toBeFalse(); expect($config['max_features_per_request'])->toBe(50); expect($config['extractor_count'])->toBe(1); expect($config['detector_count'])->toBe(1); }); test('fängt Ausnahmen ab und gibt Fehlermeldung zurück', function () { // Arrange $extractor = Mockery::mock(WafFeatureExtractor::class, FeatureExtractorMetadata::class); $extractor->shouldReceive('isEnabled')->andReturn(true); $extractor->shouldReceive('getFeatureType')->andReturn(FeatureType::STRUCTURAL_PATTERN); $extractor->shouldReceive('getPriority')->andReturn(10); $extractor->shouldReceive('canExtract')->andReturn(true); $extractor->shouldReceive('extractFeatures')->andThrow(new \RuntimeException('Test exception')); $engine = new MachineLearningEngine( enabled: true, extractors: [$extractor], detectors: [], clock: createMockClockMLE(), analysisTimeout: Duration::fromSeconds(10), confidenceThreshold: Percentage::from(50.0) ); // Act $result = $engine->analyzeRequest(createSampleRequestData()); // Assert // Individual extractor exceptions don't propagate to top-level error // They're caught and logged to extractorResults expect($result->error)->toBeNull(); expect($result->features)->toBeEmpty(); expect($result->anomalies)->toBeEmpty(); }); // Bereinigung nach jedem Test afterEach(function () { Mockery::close(); });