clock = new TestClock('2025-08-04 18:39:00'); $this->mlEngine = new TestMachineLearningEngine(); $this->repository = new InMemoryFeedbackRepository(); $this->logger = $this->createMock(Logger::class); $this->feedbackService = new FeedbackService( $this->repository, $this->clock, $this->logger ); $this->learningService = new FeedbackLearningService( $this->repository, $this->mlEngine, $this->clock, $this->logger, 3, // Lower threshold for testing 0.5 // Higher learning rate for testing ); // Set up ML engine to return success for adjustments $this->mlEngine->withApplyFeedbackAdjustmentsResult([ 'success' => true, 'applied_count' => 0, 'failed_count' => 0, 'results' => [], ]); } /** * Test the complete feedback loop: * 1. Submit feedback * 2. Learn from feedback * 3. Apply model adjustments */ public function testCompleteFeedbackLoop(): void { // 1. Submit feedback $this->submitTestFeedback(); // Verify feedback was stored $this->assertCount(5, $this->repository->getAllFeedback()); // 2. Learn from feedback $learningResult = $this->learningService->learnFromFeedback(); // Verify learning result $this->assertTrue($learningResult['success']); $this->assertGreaterThan(0, $learningResult['total_adjustments_applied']); // 3. Verify model adjustments were applied $receivedAdjustments = $this->mlEngine->getReceivedAdjustments(); $this->assertNotEmpty($receivedAdjustments); // Verify SQL_INJECTION adjustments $sqlInjectionAdjustment = $this->findAdjustmentForCategory($receivedAdjustments, DetectionCategory::SQL_INJECTION); $this->assertNotNull($sqlInjectionAdjustment); // For false positives, threshold should be increased (positive adjustment) $this->assertGreaterThan(0, $sqlInjectionAdjustment->thresholdAdjustment->getValue()); // For false positives, confidence should be decreased (negative adjustment) $this->assertLessThan(0, $sqlInjectionAdjustment->confidenceAdjustment->getValue()); } /** * Test that severity adjustments are properly processed */ public function testSeverityAdjustments(): void { // Submit severity adjustment feedback $this->feedbackService->submitSeverityAdjustment( 'detection1', 'test-user', 'This should be higher severity', DetectionCategory::PATH_TRAVERSAL, DetectionSeverity::MEDIUM, DetectionSeverity::HIGH, ['test' => true] ); $this->feedbackService->submitSeverityAdjustment( 'detection2', 'test-user', 'This should be higher severity', DetectionCategory::PATH_TRAVERSAL, DetectionSeverity::MEDIUM, DetectionSeverity::HIGH, ['test' => true] ); $this->feedbackService->submitSeverityAdjustment( 'detection3', 'test-user', 'This should be higher severity', DetectionCategory::PATH_TRAVERSAL, DetectionSeverity::MEDIUM, DetectionSeverity::HIGH, ['test' => true] ); // Learn from feedback $learningResult = $this->learningService->learnFromFeedback(); // Verify learning result $this->assertTrue($learningResult['success']); // Verify model adjustments were applied $receivedAdjustments = $this->mlEngine->getReceivedAdjustments(); // Verify PATH_TRAVERSAL adjustments $pathTraversalAdjustment = $this->findAdjustmentForCategory($receivedAdjustments, DetectionCategory::PATH_TRAVERSAL); $this->assertNotNull($pathTraversalAdjustment); // For severity increase, confidence should be increased (positive adjustment) $this->assertGreaterThan(0, $pathTraversalAdjustment->confidenceAdjustment->getValue()); // For severity adjustments, threshold should not be adjusted $this->assertEquals(0, $pathTraversalAdjustment->thresholdAdjustment->getValue()); } /** * Test that false negative feedback is properly processed */ public function testFalseNegativeFeedback(): void { // Submit false negative feedback $this->feedbackService->submitFalseNegative( 'detection1', 'test-user', 'This should have been detected', DetectionCategory::COMMAND_INJECTION, DetectionSeverity::HIGH, ['test' => true] ); $this->feedbackService->submitFalseNegative( 'detection2', 'test-user', 'This should have been detected', DetectionCategory::COMMAND_INJECTION, DetectionSeverity::HIGH, ['test' => true] ); // Learn from feedback $learningResult = $this->learningService->learnFromFeedback(); // Verify learning result $this->assertTrue($learningResult['success']); // Verify model adjustments were applied $receivedAdjustments = $this->mlEngine->getReceivedAdjustments(); // Verify COMMAND_INJECTION adjustments $commandInjectionAdjustment = $this->findAdjustmentForCategory($receivedAdjustments, DetectionCategory::COMMAND_INJECTION); $this->assertNotNull($commandInjectionAdjustment); // For false negatives, threshold should be decreased (negative adjustment) $this->assertLessThan(0, $commandInjectionAdjustment->thresholdAdjustment->getValue()); // For false negatives, confidence should be increased (positive adjustment) $this->assertGreaterThan(0, $commandInjectionAdjustment->confidenceAdjustment->getValue()); } /** * Submit test feedback for various scenarios */ private function submitTestFeedback(): void { // Submit false positive feedback for SQL_INJECTION $this->feedbackService->submitFalsePositive( 'detection1', 'test-user', 'This is a legitimate query', DetectionCategory::SQL_INJECTION, DetectionSeverity::HIGH, ['query' => 'SELECT * FROM users WHERE id = 1'] ); $this->feedbackService->submitFalsePositive( 'detection2', 'test-user', 'Another legitimate query', DetectionCategory::SQL_INJECTION, DetectionSeverity::HIGH, ['query' => 'SELECT * FROM products WHERE category_id = 2'] ); $this->feedbackService->submitFalsePositive( 'detection3', 'test-user', 'Yet another legitimate query', DetectionCategory::SQL_INJECTION, DetectionSeverity::HIGH, ['query' => 'SELECT * FROM orders WHERE customer_id = 3'] ); // Submit correct detection feedback $this->feedbackService->submitCorrectDetection( 'detection4', 'test-user', 'This is indeed an XSS attempt', DetectionCategory::XSS, DetectionSeverity::CRITICAL, ['payload' => ''] ); // Submit false negative feedback $this->feedbackService->submitFalseNegative( 'detection5', 'test-user', 'This should have been detected as XSS', DetectionCategory::XSS, DetectionSeverity::HIGH, ['payload' => ''] ); } /** * Find an adjustment for a specific category in an array of adjustments * * @param array $adjustments Array of adjustments * @param DetectionCategory $category Category to find * @return ModelAdjustment|null The found adjustment or null */ private function findAdjustmentForCategory(array $adjustments, DetectionCategory $category): ?ModelAdjustment { foreach ($adjustments as $adjustment) { if ($adjustment->category === $category) { return $adjustment; } } return null; } }