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:
271
tests/Framework/Waf/Feedback/FeedbackIntegrationTest.php
Normal file
271
tests/Framework/Waf/Feedback/FeedbackIntegrationTest.php
Normal file
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Framework\Waf\Feedback;
|
||||
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\Waf\DetectionCategory;
|
||||
use App\Framework\Waf\DetectionSeverity;
|
||||
use App\Framework\Waf\Feedback\FeedbackLearningService;
|
||||
use App\Framework\Waf\Feedback\FeedbackService;
|
||||
use App\Framework\Waf\MachineLearning\ValueObjects\ModelAdjustment;
|
||||
use App\Framework\Waf\ValueObjects\Detection;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Integration tests for the WAF feedback system
|
||||
*/
|
||||
class FeedbackIntegrationTest extends TestCase
|
||||
{
|
||||
private TestClock $clock;
|
||||
|
||||
private TestMachineLearningEngine $mlEngine;
|
||||
|
||||
private InMemoryFeedbackRepository $repository;
|
||||
|
||||
private Logger $logger;
|
||||
|
||||
private FeedbackService $feedbackService;
|
||||
|
||||
private FeedbackLearningService $learningService;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->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' => '<script>alert("XSS")</script>']
|
||||
);
|
||||
|
||||
// Submit false negative feedback
|
||||
$this->feedbackService->submitFalseNegative(
|
||||
'detection5',
|
||||
'test-user',
|
||||
'This should have been detected as XSS',
|
||||
DetectionCategory::XSS,
|
||||
DetectionSeverity::HIGH,
|
||||
['payload' => '<img src="x" onerror="alert(\'XSS\')">']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an adjustment for a specific category in an array of adjustments
|
||||
*
|
||||
* @param array<string, ModelAdjustment> $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;
|
||||
}
|
||||
}
|
||||
464
tests/Framework/Waf/Feedback/FeedbackLearningServiceTest.php
Normal file
464
tests/Framework/Waf/Feedback/FeedbackLearningServiceTest.php
Normal file
@@ -0,0 +1,464 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Framework\Waf\Feedback;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Percentage;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\Waf\DetectionCategory;
|
||||
use App\Framework\Waf\DetectionSeverity;
|
||||
use App\Framework\Waf\Feedback\DetectionFeedback;
|
||||
use App\Framework\Waf\Feedback\FeedbackLearningService;
|
||||
use App\Framework\Waf\Feedback\FeedbackRepositoryInterface;
|
||||
use App\Framework\Waf\Feedback\FeedbackType;
|
||||
use App\Framework\Waf\MachineLearning\ValueObjects\ModelAdjustment;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests for the FeedbackLearningService
|
||||
*/
|
||||
class FeedbackLearningServiceTest extends TestCase
|
||||
{
|
||||
private FeedbackRepositoryInterface $repository;
|
||||
|
||||
private TestMachineLearningEngine $mlEngine;
|
||||
|
||||
private TestClock $clock;
|
||||
|
||||
private Logger $logger;
|
||||
|
||||
private FeedbackLearningService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->repository = $this->createMock(FeedbackRepositoryInterface::class);
|
||||
$this->mlEngine = new TestMachineLearningEngine();
|
||||
$this->clock = new TestClock('2025-08-04 18:39:00');
|
||||
$this->logger = $this->createMock(Logger::class);
|
||||
|
||||
$this->service = new FeedbackLearningService(
|
||||
$this->repository,
|
||||
$this->mlEngine,
|
||||
$this->clock,
|
||||
$this->logger,
|
||||
5, // minimumFeedbackThreshold
|
||||
0.3 // learningRate
|
||||
);
|
||||
}
|
||||
|
||||
public function testLearnFromFeedbackWithInsufficientData(): void
|
||||
{
|
||||
// Set up repository to return stats with insufficient feedback
|
||||
$this->repository->method('getFeedbackStats')->willReturn([
|
||||
'total_count' => 3, // Less than minimum threshold of 5
|
||||
]);
|
||||
|
||||
// Execute the learning process
|
||||
$result = $this->service->learnFromFeedback();
|
||||
|
||||
// Verify the result
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertEquals('Not enough feedback for learning', $result['message']);
|
||||
$this->assertEquals(3, $result['feedback_count']);
|
||||
$this->assertEquals(5, $result['minimum_threshold']);
|
||||
}
|
||||
|
||||
public function testLearnFromFeedbackWithSufficientData(): void
|
||||
{
|
||||
// Set up repository to return stats with sufficient feedback
|
||||
$this->repository->method('getFeedbackStats')->willReturn([
|
||||
'total_count' => 10,
|
||||
]);
|
||||
|
||||
// Set up repository to return feedback data
|
||||
$falsePositives = [
|
||||
$this->createFalsePositiveFeedback('detection1', DetectionCategory::SQL_INJECTION),
|
||||
$this->createFalsePositiveFeedback('detection2', DetectionCategory::SQL_INJECTION),
|
||||
$this->createFalsePositiveFeedback('detection3', DetectionCategory::SQL_INJECTION),
|
||||
$this->createFalsePositiveFeedback('detection4', DetectionCategory::XSS),
|
||||
];
|
||||
|
||||
$falseNegatives = [
|
||||
$this->createFalseNegativeFeedback('detection5', DetectionCategory::COMMAND_INJECTION),
|
||||
$this->createFalseNegativeFeedback('detection6', DetectionCategory::COMMAND_INJECTION),
|
||||
];
|
||||
|
||||
$severityAdjustments = [
|
||||
$this->createSeverityAdjustmentFeedback(
|
||||
'detection7',
|
||||
DetectionCategory::PATH_TRAVERSAL,
|
||||
DetectionSeverity::MEDIUM,
|
||||
DetectionSeverity::HIGH
|
||||
),
|
||||
$this->createSeverityAdjustmentFeedback(
|
||||
'detection8',
|
||||
DetectionCategory::PATH_TRAVERSAL,
|
||||
DetectionSeverity::MEDIUM,
|
||||
DetectionSeverity::HIGH
|
||||
),
|
||||
$this->createSeverityAdjustmentFeedback(
|
||||
'detection9',
|
||||
DetectionCategory::PATH_TRAVERSAL,
|
||||
DetectionSeverity::MEDIUM,
|
||||
DetectionSeverity::HIGH
|
||||
),
|
||||
];
|
||||
|
||||
$this->repository->method('getFeedbackByFeedbackType')
|
||||
->willReturnMap([
|
||||
[FeedbackType::FALSE_POSITIVE, null, $falsePositives],
|
||||
[FeedbackType::FALSE_NEGATIVE, null, $falseNegatives],
|
||||
[FeedbackType::SEVERITY_ADJUSTMENT, null, $severityAdjustments],
|
||||
[FeedbackType::CORRECT_DETECTION, null, []],
|
||||
]);
|
||||
|
||||
// Set up ML engine to return success for adjustments
|
||||
$this->mlEngine->withApplyFeedbackAdjustmentsResult([
|
||||
'success' => true,
|
||||
'applied_count' => 3,
|
||||
'failed_count' => 0,
|
||||
'results' => [],
|
||||
]);
|
||||
|
||||
// Execute the learning process
|
||||
$result = $this->service->learnFromFeedback();
|
||||
|
||||
// Verify the result
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertEquals(4, $result['false_positives_processed']);
|
||||
$this->assertEquals(2, $result['false_negatives_processed']);
|
||||
$this->assertEquals(3, $result['severity_adjustments_processed']);
|
||||
$this->assertGreaterThan(0, $result['total_adjustments_applied']);
|
||||
}
|
||||
|
||||
public function testProcessFalsePositives(): void
|
||||
{
|
||||
// Create a service with reflection to access private methods
|
||||
$service = new FeedbackLearningService(
|
||||
$this->repository,
|
||||
$this->mlEngine,
|
||||
$this->clock,
|
||||
$this->logger,
|
||||
5,
|
||||
0.3
|
||||
);
|
||||
|
||||
$reflectionMethod = new \ReflectionMethod(FeedbackLearningService::class, 'processfalsePositives');
|
||||
$reflectionMethod->setAccessible(true);
|
||||
|
||||
// Create test data
|
||||
$falsePositives = [
|
||||
$this->createFalsePositiveFeedback('detection1', DetectionCategory::SQL_INJECTION),
|
||||
$this->createFalsePositiveFeedback('detection2', DetectionCategory::SQL_INJECTION),
|
||||
$this->createFalsePositiveFeedback('detection3', DetectionCategory::SQL_INJECTION),
|
||||
$this->createFalsePositiveFeedback('detection4', DetectionCategory::XSS),
|
||||
];
|
||||
|
||||
// Execute the method
|
||||
$adjustments = $reflectionMethod->invoke($service, $falsePositives);
|
||||
|
||||
// Verify the adjustments
|
||||
$this->assertCount(2, $adjustments); // One for SQL_INJECTION, one for XSS
|
||||
|
||||
// Check SQL_INJECTION adjustment
|
||||
$sqlInjectionAdjustment = null;
|
||||
$xssAdjustment = null;
|
||||
|
||||
foreach ($adjustments as $adjustment) {
|
||||
if ($adjustment->category === DetectionCategory::SQL_INJECTION) {
|
||||
$sqlInjectionAdjustment = $adjustment;
|
||||
} elseif ($adjustment->category === DetectionCategory::XSS) {
|
||||
$xssAdjustment = $adjustment;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertNotNull($sqlInjectionAdjustment);
|
||||
$this->assertNotNull($xssAdjustment);
|
||||
|
||||
// SQL_INJECTION had 3 false positives, so should have stronger adjustment
|
||||
$this->assertGreaterThan(
|
||||
$xssAdjustment->thresholdAdjustment->getValue(),
|
||||
$sqlInjectionAdjustment->thresholdAdjustment->getValue()
|
||||
);
|
||||
|
||||
// Threshold adjustments should be positive for false positives (make detection harder)
|
||||
$this->assertGreaterThan(0, $sqlInjectionAdjustment->thresholdAdjustment->getValue());
|
||||
$this->assertGreaterThan(0, $xssAdjustment->thresholdAdjustment->getValue());
|
||||
|
||||
// Confidence adjustments should be negative for false positives (less certain)
|
||||
$this->assertLessThan(0, $sqlInjectionAdjustment->confidenceAdjustment->getValue());
|
||||
$this->assertLessThan(0, $xssAdjustment->confidenceAdjustment->getValue());
|
||||
}
|
||||
|
||||
public function testProcessFalseNegatives(): void
|
||||
{
|
||||
// Create a service with reflection to access private methods
|
||||
$service = new FeedbackLearningService(
|
||||
$this->repository,
|
||||
$this->mlEngine,
|
||||
$this->clock,
|
||||
$this->logger,
|
||||
5,
|
||||
0.3
|
||||
);
|
||||
|
||||
$reflectionMethod = new \ReflectionMethod(FeedbackLearningService::class, 'processFalseNegatives');
|
||||
$reflectionMethod->setAccessible(true);
|
||||
|
||||
// Create test data
|
||||
$falseNegatives = [
|
||||
$this->createFalseNegativeFeedback('detection1', DetectionCategory::COMMAND_INJECTION),
|
||||
$this->createFalseNegativeFeedback('detection2', DetectionCategory::COMMAND_INJECTION),
|
||||
];
|
||||
|
||||
// Execute the method
|
||||
$adjustments = $reflectionMethod->invoke($service, $falseNegatives);
|
||||
|
||||
// Verify the adjustments
|
||||
$this->assertCount(1, $adjustments); // One for COMMAND_INJECTION
|
||||
|
||||
$adjustment = reset($adjustments);
|
||||
|
||||
// Threshold adjustments should be negative for false negatives (make detection easier)
|
||||
$this->assertLessThan(0, $adjustment->thresholdAdjustment->getValue());
|
||||
|
||||
// Confidence adjustments should be positive for false negatives (more certain)
|
||||
$this->assertGreaterThan(0, $adjustment->confidenceAdjustment->getValue());
|
||||
}
|
||||
|
||||
public function testProcessSeverityAdjustments(): void
|
||||
{
|
||||
// Create a service with reflection to access private methods
|
||||
$service = new FeedbackLearningService(
|
||||
$this->repository,
|
||||
$this->mlEngine,
|
||||
$this->clock,
|
||||
$this->logger,
|
||||
5,
|
||||
0.3
|
||||
);
|
||||
|
||||
$reflectionMethod = new \ReflectionMethod(FeedbackLearningService::class, 'processSeverityAdjustments');
|
||||
$reflectionMethod->setAccessible(true);
|
||||
|
||||
// Create test data
|
||||
$severityAdjustments = [
|
||||
$this->createSeverityAdjustmentFeedback(
|
||||
'detection1',
|
||||
DetectionCategory::PATH_TRAVERSAL,
|
||||
DetectionSeverity::MEDIUM,
|
||||
DetectionSeverity::HIGH
|
||||
),
|
||||
$this->createSeverityAdjustmentFeedback(
|
||||
'detection2',
|
||||
DetectionCategory::PATH_TRAVERSAL,
|
||||
DetectionSeverity::MEDIUM,
|
||||
DetectionSeverity::HIGH
|
||||
),
|
||||
$this->createSeverityAdjustmentFeedback(
|
||||
'detection3',
|
||||
DetectionCategory::PATH_TRAVERSAL,
|
||||
DetectionSeverity::MEDIUM,
|
||||
DetectionSeverity::HIGH
|
||||
),
|
||||
];
|
||||
|
||||
// Execute the method
|
||||
$result = $reflectionMethod->invoke($service, $severityAdjustments);
|
||||
|
||||
// Verify the result
|
||||
$this->assertArrayHasKey('adjustments', $result);
|
||||
$this->assertArrayHasKey('severity_changes', $result);
|
||||
|
||||
$adjustments = $result['adjustments'];
|
||||
$severityChanges = $result['severity_changes'];
|
||||
|
||||
$this->assertCount(1, $adjustments); // One for PATH_TRAVERSAL
|
||||
$this->assertCount(1, $severityChanges); // One change from MEDIUM to HIGH
|
||||
|
||||
$adjustment = reset($adjustments);
|
||||
|
||||
// For severity increase, confidence adjustment should be positive
|
||||
$this->assertGreaterThan(0, $adjustment->confidenceAdjustment->getValue());
|
||||
|
||||
// Threshold should not be adjusted for severity changes
|
||||
$this->assertEquals(0, $adjustment->thresholdAdjustment->getValue());
|
||||
}
|
||||
|
||||
public function testGenerateFeatureWeightAdjustments(): void
|
||||
{
|
||||
// Create a service with reflection to access private methods
|
||||
$service = new FeedbackLearningService(
|
||||
$this->repository,
|
||||
$this->mlEngine,
|
||||
$this->clock,
|
||||
$this->logger,
|
||||
5,
|
||||
0.3
|
||||
);
|
||||
|
||||
$reflectionMethod = new \ReflectionMethod(FeedbackLearningService::class, 'generateFeatureWeightAdjustments');
|
||||
$reflectionMethod->setAccessible(true);
|
||||
|
||||
// Test for SQL_INJECTION false positive
|
||||
$sqlInjectionFalsePositiveAdjustments = $reflectionMethod->invoke(
|
||||
$service,
|
||||
DetectionCategory::SQL_INJECTION,
|
||||
true // isfalsePositive
|
||||
);
|
||||
|
||||
// Test for SQL_INJECTION false negative
|
||||
$sqlInjectionFalseNegativeAdjustments = $reflectionMethod->invoke(
|
||||
$service,
|
||||
DetectionCategory::SQL_INJECTION,
|
||||
false // isfalsePositive
|
||||
);
|
||||
|
||||
// Verify the adjustments
|
||||
$this->assertNotEmpty($sqlInjectionFalsePositiveAdjustments);
|
||||
$this->assertNotEmpty($sqlInjectionFalseNegativeAdjustments);
|
||||
|
||||
// For false positives, adjustments should be negative (reduce feature importance)
|
||||
foreach ($sqlInjectionFalsePositiveAdjustments as $feature => $adjustment) {
|
||||
$this->assertLessThan(0, $adjustment);
|
||||
}
|
||||
|
||||
// For false negatives, adjustments should be positive (increase feature importance)
|
||||
foreach ($sqlInjectionFalseNegativeAdjustments as $feature => $adjustment) {
|
||||
$this->assertGreaterThan(0, $adjustment);
|
||||
}
|
||||
}
|
||||
|
||||
public function testApplyModelAdjustments(): void
|
||||
{
|
||||
// Create test data
|
||||
$adjustments = [
|
||||
'adjustment1' => new ModelAdjustment(
|
||||
'adjustment1',
|
||||
DetectionCategory::SQL_INJECTION,
|
||||
Percentage::from(10.0),
|
||||
Percentage::from(-10.0),
|
||||
['sql_keywords_count' => -0.2],
|
||||
'Test adjustment 1',
|
||||
Timestamp::fromClock($this->clock)
|
||||
),
|
||||
'adjustment2' => new ModelAdjustment(
|
||||
'adjustment2',
|
||||
DetectionCategory::XSS,
|
||||
Percentage::from(5.0),
|
||||
Percentage::from(-5.0),
|
||||
['script_tag_count' => -0.3],
|
||||
'Test adjustment 2',
|
||||
Timestamp::fromClock($this->clock)
|
||||
),
|
||||
];
|
||||
|
||||
// Set up ML engine to return success
|
||||
$this->mlEngine->withApplyFeedbackAdjustmentsResult([
|
||||
'success' => true,
|
||||
'applied_count' => 2,
|
||||
'failed_count' => 0,
|
||||
'results' => [],
|
||||
]);
|
||||
|
||||
// Create a service with reflection to access private methods
|
||||
$service = new FeedbackLearningService(
|
||||
$this->repository,
|
||||
$this->mlEngine,
|
||||
$this->clock,
|
||||
$this->logger,
|
||||
5,
|
||||
0.3
|
||||
);
|
||||
|
||||
$reflectionMethod = new \ReflectionMethod(FeedbackLearningService::class, 'applyModelAdjustments');
|
||||
$reflectionMethod->setAccessible(true);
|
||||
|
||||
// Execute the method
|
||||
$result = $reflectionMethod->invoke($service, $adjustments);
|
||||
|
||||
// Verify the result
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertEquals(2, $result['applied_count']);
|
||||
$this->assertEquals(0, $result['failed_count']);
|
||||
|
||||
// Verify that the correct adjustments were passed to the ML engine
|
||||
$this->assertSame($adjustments, $this->mlEngine->getReceivedAdjustments());
|
||||
}
|
||||
|
||||
public function testWithMinimumFeedbackThreshold(): void
|
||||
{
|
||||
$newService = $this->service->withMinimumFeedbackThreshold(10);
|
||||
|
||||
$this->assertNotSame($this->service, $newService);
|
||||
$this->assertEquals(10, $newService->getMinimumFeedbackThreshold());
|
||||
$this->assertEquals(5, $this->service->getMinimumFeedbackThreshold());
|
||||
}
|
||||
|
||||
public function testWithLearningRate(): void
|
||||
{
|
||||
$newService = $this->service->withLearningRate(0.5);
|
||||
|
||||
$this->assertNotSame($this->service, $newService);
|
||||
$this->assertEquals(0.5, $newService->getLearningRate());
|
||||
$this->assertEquals(0.3, $this->service->getLearningRate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create a false positive feedback
|
||||
*/
|
||||
private function createFalsePositiveFeedback(
|
||||
string $detectionId,
|
||||
DetectionCategory $category,
|
||||
DetectionSeverity $severity = DetectionSeverity::MEDIUM
|
||||
): DetectionFeedback {
|
||||
return DetectionFeedback::falsePositive(
|
||||
$detectionId,
|
||||
'test-user',
|
||||
'This is a false positive',
|
||||
$category,
|
||||
$severity,
|
||||
['test_context' => true]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create a false negative feedback
|
||||
*/
|
||||
private function createFalseNegativeFeedback(
|
||||
string $detectionId,
|
||||
DetectionCategory $category,
|
||||
DetectionSeverity $severity = DetectionSeverity::MEDIUM
|
||||
): DetectionFeedback {
|
||||
return DetectionFeedback::falseNegative(
|
||||
$detectionId,
|
||||
'test-user',
|
||||
'This is a false negative',
|
||||
$category,
|
||||
$severity,
|
||||
['test_context' => true]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create a severity adjustment feedback
|
||||
*/
|
||||
private function createSeverityAdjustmentFeedback(
|
||||
string $detectionId,
|
||||
DetectionCategory $category,
|
||||
DetectionSeverity $currentSeverity,
|
||||
DetectionSeverity $suggestedSeverity
|
||||
): DetectionFeedback {
|
||||
return DetectionFeedback::severityAdjustment(
|
||||
$detectionId,
|
||||
'test-user',
|
||||
'This severity should be adjusted',
|
||||
$category,
|
||||
$currentSeverity,
|
||||
$suggestedSeverity,
|
||||
['test_context' => true]
|
||||
);
|
||||
}
|
||||
}
|
||||
157
tests/Framework/Waf/Feedback/InMemoryFeedbackRepository.php
Normal file
157
tests/Framework/Waf/Feedback/InMemoryFeedbackRepository.php
Normal file
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Framework\Waf\Feedback;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\Waf\DetectionCategory;
|
||||
use App\Framework\Waf\Feedback\DetectionFeedback;
|
||||
use App\Framework\Waf\Feedback\FeedbackRepositoryInterface;
|
||||
use App\Framework\Waf\Feedback\FeedbackType;
|
||||
|
||||
/**
|
||||
* In-memory implementation of FeedbackRepositoryInterface for testing
|
||||
*/
|
||||
class InMemoryFeedbackRepository implements FeedbackRepositoryInterface
|
||||
{
|
||||
/** @var DetectionFeedback[] */
|
||||
private array $feedback = [];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function saveFeedback(DetectionFeedback $feedback): void
|
||||
{
|
||||
$this->feedback[] = $feedback;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFeedbackForDetection(string $detectionId): array
|
||||
{
|
||||
return array_filter(
|
||||
$this->feedback,
|
||||
fn (DetectionFeedback $feedback) => $feedback->detectionId === $detectionId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFeedbackByCategory(DetectionCategory $category, ?Timestamp $since = null): array
|
||||
{
|
||||
return array_filter(
|
||||
$this->feedback,
|
||||
function (DetectionFeedback $feedback) use ($category, $since) {
|
||||
if ($feedback->category !== $category) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($since !== null && $feedback->timestamp->getTimestamp() < $since->getTimestamp()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFeedbackByFeedbackType(FeedbackType $feedbackType, ?Timestamp $since = null): array
|
||||
{
|
||||
return array_filter(
|
||||
$this->feedback,
|
||||
function (DetectionFeedback $feedback) use ($feedbackType, $since) {
|
||||
if ($feedback->feedbackType !== $feedbackType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($since !== null && $feedback->timestamp->getTimestamp() < $since->getTimestamp()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFeedbackStats(): array
|
||||
{
|
||||
$totalCount = count($this->feedback);
|
||||
|
||||
// Count by feedback type
|
||||
$byFeedbackType = [];
|
||||
foreach (FeedbackType::cases() as $type) {
|
||||
$byFeedbackType[$type->value] = count($this->getFeedbackByFeedbackType($type));
|
||||
}
|
||||
|
||||
// Count by category
|
||||
$byCategory = [];
|
||||
foreach (DetectionCategory::cases() as $category) {
|
||||
$count = count($this->getFeedbackByCategory($category));
|
||||
if ($count > 0) {
|
||||
$byCategory[$category->value] = $count;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate trend data (simplified for testing)
|
||||
$trendData = [];
|
||||
$today = date('Y-m-d');
|
||||
|
||||
foreach (FeedbackType::cases() as $type) {
|
||||
if (! isset($trendData[$today])) {
|
||||
$trendData[$today] = [];
|
||||
}
|
||||
|
||||
$trendData[$today][$type->value] = count($this->getFeedbackByFeedbackType($type));
|
||||
}
|
||||
|
||||
return [
|
||||
'total_count' => $totalCount,
|
||||
'by_feedback_type' => $byFeedbackType,
|
||||
'by_category' => $byCategory,
|
||||
'trend_data' => $trendData,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRecentFeedback(int $limit = 10): array
|
||||
{
|
||||
$feedback = $this->feedback;
|
||||
|
||||
// Sort by timestamp (newest first)
|
||||
usort($feedback, function (DetectionFeedback $a, DetectionFeedback $b) {
|
||||
return $b->timestamp->getTimestamp() <=> $a->timestamp->getTimestamp();
|
||||
});
|
||||
|
||||
// Limit the results
|
||||
return array_slice($feedback, 0, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all feedback stored in the repository
|
||||
*
|
||||
* @return DetectionFeedback[] All feedback
|
||||
*/
|
||||
public function getAllFeedback(): array
|
||||
{
|
||||
return $this->feedback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all feedback from the repository
|
||||
*/
|
||||
public function clear(): void
|
||||
{
|
||||
$this->feedback = [];
|
||||
}
|
||||
}
|
||||
93
tests/Framework/Waf/Feedback/TestClock.php
Normal file
93
tests/Framework/Waf/Feedback/TestClock.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Framework\Waf\Feedback;
|
||||
|
||||
use App\Framework\DateTime\Clock;
|
||||
|
||||
/**
|
||||
* Test implementation of the Clock interface for unit tests
|
||||
*/
|
||||
final class TestClock implements Clock
|
||||
{
|
||||
private \DateTimeImmutable $fixedTime;
|
||||
|
||||
/**
|
||||
* @param string $fixedTime Fixed time in format 'Y-m-d H:i:s'
|
||||
*/
|
||||
public function __construct(string $fixedTime = '2025-08-04 18:39:00')
|
||||
{
|
||||
$this->fixedTime = new \DateTimeImmutable($fixedTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function now(): \DateTimeImmutable
|
||||
{
|
||||
return $this->fixedTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function yesterday(): \DateTimeImmutable
|
||||
{
|
||||
return $this->fixedTime->modify('-1 day');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function today(): \DateTimeImmutable
|
||||
{
|
||||
return $this->fixedTime->setTime(0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function tomorrow(): \DateTimeImmutable
|
||||
{
|
||||
return $this->fixedTime->modify('+1 day');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function time(int $hour = 0, int $minute = 0, int $second = 0): \DateTimeImmutable
|
||||
{
|
||||
return $this->fixedTime->setTime($hour, $minute, $second);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function fromTimestamp(int $timestamp): \DateTimeImmutable
|
||||
{
|
||||
return (new \DateTimeImmutable())->setTimestamp($timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function fromString(string $datetime): \DateTimeImmutable
|
||||
{
|
||||
return new \DateTimeImmutable($datetime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the fixed time to a new value
|
||||
*
|
||||
* @param string $fixedTime Fixed time in format 'Y-m-d H:i:s'
|
||||
* @return self
|
||||
*/
|
||||
public function withFixedTime(string $fixedTime): self
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->fixedTime = new \DateTimeImmutable($fixedTime);
|
||||
|
||||
return $clone;
|
||||
}
|
||||
}
|
||||
91
tests/Framework/Waf/Feedback/TestMachineLearningEngine.php
Normal file
91
tests/Framework/Waf/Feedback/TestMachineLearningEngine.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Framework\Waf\Feedback;
|
||||
|
||||
use App\Framework\Waf\Analysis\ValueObjects\RequestAnalysisData;
|
||||
use App\Framework\Waf\MachineLearning\MachineLearningEngine;
|
||||
use App\Framework\Waf\MachineLearning\MachineLearningResult;
|
||||
|
||||
/**
|
||||
* Test implementation of MachineLearningEngine for unit tests
|
||||
*/
|
||||
class TestMachineLearningEngine extends MachineLearningEngine
|
||||
{
|
||||
private array $applyFeedbackAdjustmentsResult = [];
|
||||
|
||||
private array $analyzeRequestResult = [];
|
||||
|
||||
private array $receivedAdjustments = [];
|
||||
|
||||
/**
|
||||
* Constructor that bypasses parent constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
// Intentionally empty to bypass parent constructor
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the result to return from applyFeedbackAdjustments
|
||||
*
|
||||
* @param array $result Result to return
|
||||
* @return self
|
||||
*/
|
||||
public function withApplyFeedbackAdjustmentsResult(array $result): self
|
||||
{
|
||||
$this->applyFeedbackAdjustmentsResult = $result;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the result to return from analyzeRequest
|
||||
*
|
||||
* @param MachineLearningResult $result Result to return
|
||||
* @return self
|
||||
*/
|
||||
public function withAnalyzeRequestResult(MachineLearningResult $result): self
|
||||
{
|
||||
$this->analyzeRequestResult = $result;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the adjustments that were passed to applyFeedbackAdjustments
|
||||
*
|
||||
* @return array Adjustments that were passed
|
||||
*/
|
||||
public function getReceivedAdjustments(): array
|
||||
{
|
||||
return $this->receivedAdjustments;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applyFeedbackAdjustments(array $adjustments): array
|
||||
{
|
||||
$this->receivedAdjustments = $adjustments;
|
||||
|
||||
return $this->applyFeedbackAdjustmentsResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function analyzeRequest(RequestAnalysisData $requestData, array $context = []): MachineLearningResult
|
||||
{
|
||||
return $this->analyzeRequestResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Framework\Waf\MachineLearning\Detectors;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\DateTime\DateTime;
|
||||
use App\Framework\Waf\MachineLearning\AnomalyType;
|
||||
use App\Framework\Waf\MachineLearning\BehaviorType;
|
||||
use App\Framework\Waf\MachineLearning\Detectors\ClusteringAnomalyDetector;
|
||||
use App\Framework\Waf\MachineLearning\ValueObjects\BehaviorBaseline;
|
||||
use App\Framework\Waf\MachineLearning\ValueObjects\BehaviorFeature;
|
||||
|
||||
// Hilfsfunktion zum Erstellen einer Baseline für Tests
|
||||
function createTestBaseline(?BehaviorType $type = null): BehaviorBaseline
|
||||
{
|
||||
$type = $type ?? BehaviorType::PATH_PATTERNS;
|
||||
$now = Timestamp::fromDateTime(DateTime::fromTimestamp(time()));
|
||||
|
||||
return new BehaviorBaseline(
|
||||
type: $type,
|
||||
identifier: 'test-client',
|
||||
mean: 10.0,
|
||||
standardDeviation: 5.0,
|
||||
median: 10.0,
|
||||
minimum: 5.0,
|
||||
maximum: 25.0,
|
||||
percentiles: [
|
||||
25 => 7.5,
|
||||
75 => 15.0,
|
||||
90 => 18.0,
|
||||
95 => 20.0,
|
||||
99 => 22.0,
|
||||
],
|
||||
sampleCount: 20,
|
||||
createdAt: $now,
|
||||
lastUpdated: $now,
|
||||
windowSize: Duration::fromMinutes(30),
|
||||
confidence: 0.8
|
||||
);
|
||||
}
|
||||
|
||||
// Hilfsfunktion zum Erstellen von Testfeatures
|
||||
function createTestFeatures(): array
|
||||
{
|
||||
return [
|
||||
new BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'path_depth',
|
||||
value: 3.0,
|
||||
unit: 'count'
|
||||
),
|
||||
new BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'path_segments',
|
||||
value: 4.0,
|
||||
unit: 'count'
|
||||
),
|
||||
new BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'path_length',
|
||||
value: 25.0,
|
||||
unit: 'characters'
|
||||
),
|
||||
new BehaviorFeature(
|
||||
type: BehaviorType::PARAMETER_PATTERNS,
|
||||
name: 'param_count',
|
||||
value: 2.0,
|
||||
unit: 'count'
|
||||
),
|
||||
new BehaviorFeature(
|
||||
type: BehaviorType::PARAMETER_PATTERNS,
|
||||
name: 'param_length_avg',
|
||||
value: 8.0,
|
||||
unit: 'characters'
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
test('erkennt Cluster-Abweichungen', function () {
|
||||
// Arrange
|
||||
$detector = new ClusteringAnomalyDetector(
|
||||
enabled: true,
|
||||
confidenceThreshold: 0.5,
|
||||
maxClusters: 3,
|
||||
minClusterSize: 2,
|
||||
outlierThreshold: 0.8,
|
||||
maxIterations: 10,
|
||||
convergenceThreshold: 0.01,
|
||||
enableDensityAnalysis: true,
|
||||
enableGroupAnomalyDetection: true,
|
||||
clusterCenters: [],
|
||||
clusterAssignments: [],
|
||||
featureVectors: []
|
||||
);
|
||||
|
||||
// Normale Features
|
||||
$normalFeatures = createTestFeatures();
|
||||
|
||||
// Anomales Feature mit deutlich abweichenden Werten
|
||||
$anomalousFeature = new BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'path_length',
|
||||
value: 150.0, // Deutlich höher als normal
|
||||
unit: 'characters'
|
||||
);
|
||||
|
||||
$features = array_merge($normalFeatures, [$anomalousFeature]);
|
||||
|
||||
// Act
|
||||
$anomalies = $detector->detectAnomalies($features, null);
|
||||
|
||||
// Assert
|
||||
expect($anomalies)->not->toBeEmpty();
|
||||
expect($anomalies[0]->type)->toBe(AnomalyType::CLUSTERING_DEVIATION);
|
||||
});
|
||||
|
||||
test('gruppiert Features nach Typ', function () {
|
||||
// Arrange
|
||||
$detector = new ClusteringAnomalyDetector(
|
||||
enabled: true,
|
||||
confidenceThreshold: 0.5,
|
||||
maxClusters: 3,
|
||||
minClusterSize: 2,
|
||||
outlierThreshold: 0.8,
|
||||
maxIterations: 10,
|
||||
convergenceThreshold: 0.01,
|
||||
enableDensityAnalysis: true,
|
||||
enableGroupAnomalyDetection: true,
|
||||
clusterCenters: [],
|
||||
clusterAssignments: [],
|
||||
featureVectors: []
|
||||
);
|
||||
|
||||
// Features mit verschiedenen Typen
|
||||
$features = [
|
||||
new BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'path_feature',
|
||||
value: 10.0,
|
||||
unit: 'count'
|
||||
),
|
||||
new BehaviorFeature(
|
||||
type: BehaviorType::PARAMETER_PATTERNS,
|
||||
name: 'param_feature',
|
||||
value: 5.0,
|
||||
unit: 'count'
|
||||
),
|
||||
new BehaviorFeature(
|
||||
type: BehaviorType::REQUEST_FREQUENCY,
|
||||
name: 'freq_feature',
|
||||
value: 2.0,
|
||||
unit: 'requests/second'
|
||||
),
|
||||
];
|
||||
|
||||
// Wir können die private Methode nicht direkt testen, aber wir können testen,
|
||||
// dass der Detektor die Features analysieren kann
|
||||
|
||||
// Act & Assert
|
||||
expect($detector->canAnalyze($features))->toBeTrue();
|
||||
});
|
||||
|
||||
test('unterstützt verschiedene Verhaltenstypen', function () {
|
||||
// Arrange
|
||||
$detector = new ClusteringAnomalyDetector(
|
||||
enabled: true,
|
||||
confidenceThreshold: 0.5,
|
||||
maxClusters: 3,
|
||||
minClusterSize: 2,
|
||||
outlierThreshold: 0.8,
|
||||
maxIterations: 10,
|
||||
convergenceThreshold: 0.01,
|
||||
enableDensityAnalysis: true,
|
||||
enableGroupAnomalyDetection: true,
|
||||
clusterCenters: [],
|
||||
clusterAssignments: [],
|
||||
featureVectors: []
|
||||
);
|
||||
|
||||
// Act
|
||||
$supportedTypes = $detector->getSupportedBehaviorTypes();
|
||||
|
||||
// Assert
|
||||
expect($supportedTypes)->toBeArray();
|
||||
expect($supportedTypes)->toContain(BehaviorType::REQUEST_FREQUENCY);
|
||||
expect($supportedTypes)->toContain(BehaviorType::PATH_PATTERNS);
|
||||
expect($supportedTypes)->toContain(BehaviorType::PARAMETER_PATTERNS);
|
||||
expect($supportedTypes)->toContain(BehaviorType::USER_AGENT_PATTERNS);
|
||||
});
|
||||
|
||||
test('erkennt Dichte-Anomalien wenn aktiviert', function () {
|
||||
// Arrange
|
||||
$detector = new ClusteringAnomalyDetector(
|
||||
enabled: true,
|
||||
confidenceThreshold: 0.5,
|
||||
maxClusters: 3,
|
||||
minClusterSize: 2,
|
||||
outlierThreshold: 0.8,
|
||||
maxIterations: 10,
|
||||
convergenceThreshold: 0.01,
|
||||
enableDensityAnalysis: true,
|
||||
enableGroupAnomalyDetection: false,
|
||||
clusterCenters: [],
|
||||
clusterAssignments: [],
|
||||
featureVectors: []
|
||||
);
|
||||
|
||||
// Normale Features mit ähnlichen Werten
|
||||
$normalFeatures = [
|
||||
new BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'path_length',
|
||||
value: 20.0,
|
||||
unit: 'characters'
|
||||
),
|
||||
new BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'path_length',
|
||||
value: 22.0,
|
||||
unit: 'characters'
|
||||
),
|
||||
new BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'path_length',
|
||||
value: 19.0,
|
||||
unit: 'characters'
|
||||
),
|
||||
new BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'path_length',
|
||||
value: 21.0,
|
||||
unit: 'characters'
|
||||
),
|
||||
];
|
||||
|
||||
// Isoliertes Feature
|
||||
$isolatedFeature = new BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'path_length',
|
||||
value: 100.0, // Deutlich abseits der anderen
|
||||
unit: 'characters'
|
||||
);
|
||||
|
||||
$features = array_merge($normalFeatures, [$isolatedFeature]);
|
||||
|
||||
// Act
|
||||
$anomalies = $detector->detectAnomalies($features, null);
|
||||
|
||||
// Assert
|
||||
expect($anomalies)->not->toBeEmpty();
|
||||
// Je nach Implementierung könnte es verschiedene Anomalietypen sein
|
||||
expect($anomalies[0]->type)->toBe(AnomalyType::CLUSTERING_DEVIATION);
|
||||
});
|
||||
|
||||
test('aktualisiert Modell mit neuen Daten', function () {
|
||||
// Arrange
|
||||
$detector = new ClusteringAnomalyDetector(
|
||||
enabled: true,
|
||||
confidenceThreshold: 0.5,
|
||||
maxClusters: 3,
|
||||
minClusterSize: 2,
|
||||
outlierThreshold: 0.8,
|
||||
maxIterations: 10,
|
||||
convergenceThreshold: 0.01,
|
||||
enableDensityAnalysis: true,
|
||||
enableGroupAnomalyDetection: true,
|
||||
clusterCenters: [],
|
||||
clusterAssignments: [],
|
||||
featureVectors: []
|
||||
);
|
||||
|
||||
$features = createTestFeatures();
|
||||
|
||||
// Act - Keine Assertion möglich, da interne Daten private sind
|
||||
// Wir testen nur, dass keine Exception geworfen wird
|
||||
$detector->updateModel($features);
|
||||
|
||||
// Assert
|
||||
expect(true)->toBeTrue(); // Dummy assertion
|
||||
});
|
||||
|
||||
test('gibt Konfiguration korrekt zurück', function () {
|
||||
// Arrange
|
||||
$detector = new ClusteringAnomalyDetector(
|
||||
enabled: true,
|
||||
confidenceThreshold: 0.75,
|
||||
maxClusters: 5,
|
||||
minClusterSize: 3,
|
||||
outlierThreshold: 0.9,
|
||||
maxIterations: 20,
|
||||
convergenceThreshold: 0.005,
|
||||
enableDensityAnalysis: true,
|
||||
enableGroupAnomalyDetection: false,
|
||||
clusterCenters: [],
|
||||
clusterAssignments: [],
|
||||
featureVectors: []
|
||||
);
|
||||
|
||||
// Act
|
||||
$config = $detector->getConfiguration();
|
||||
|
||||
// Assert
|
||||
expect($config)->toBeArray();
|
||||
expect($config['enabled'])->toBeTrue();
|
||||
expect($config['confidence_threshold'])->toBe(0.75);
|
||||
expect($config['max_clusters'])->toBe(5);
|
||||
expect($config['min_cluster_size'])->toBe(3);
|
||||
expect($config['outlier_threshold'])->toBe(0.9);
|
||||
expect($config['max_iterations'])->toBe(20);
|
||||
expect($config['enable_density_analysis'])->toBeTrue();
|
||||
expect($config['enable_group_anomaly_detection'])->toBeFalse();
|
||||
});
|
||||
|
||||
test('gibt leere Ergebnisse zurück wenn deaktiviert', function () {
|
||||
// Arrange
|
||||
$detector = new ClusteringAnomalyDetector(
|
||||
enabled: false,
|
||||
confidenceThreshold: 0.5,
|
||||
maxClusters: 3,
|
||||
minClusterSize: 2,
|
||||
outlierThreshold: 0.8,
|
||||
maxIterations: 10,
|
||||
convergenceThreshold: 0.01,
|
||||
enableDensityAnalysis: true,
|
||||
enableGroupAnomalyDetection: true,
|
||||
clusterCenters: [],
|
||||
clusterAssignments: [],
|
||||
featureVectors: []
|
||||
);
|
||||
|
||||
$features = createTestFeatures();
|
||||
|
||||
// Act
|
||||
$anomalies = $detector->detectAnomalies($features, null);
|
||||
|
||||
// Assert
|
||||
expect($anomalies)->toBeEmpty();
|
||||
});
|
||||
@@ -0,0 +1,280 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Framework\Waf\MachineLearning\Detectors;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\DateTime\DateTime;
|
||||
use App\Framework\Waf\MachineLearning\AnomalyType;
|
||||
use App\Framework\Waf\MachineLearning\BehaviorType;
|
||||
use App\Framework\Waf\MachineLearning\Detectors\StatisticalAnomalyDetector;
|
||||
use App\Framework\Waf\MachineLearning\ValueObjects\AnomalyDetection;
|
||||
use App\Framework\Waf\MachineLearning\ValueObjects\BehaviorBaseline;
|
||||
use App\Framework\Waf\MachineLearning\ValueObjects\BehaviorFeature;
|
||||
|
||||
// Hilfsfunktion zum Erstellen einer Baseline für Tests
|
||||
function createTestBaselineSAD(?BehaviorType $type = null): BehaviorBaseline
|
||||
{
|
||||
$type = $type ?? BehaviorType::PATH_PATTERNS;
|
||||
$now = Timestamp::fromDateTime(DateTime::fromTimestamp(time()));
|
||||
|
||||
return new BehaviorBaseline(
|
||||
type: $type,
|
||||
identifier: 'test-client',
|
||||
mean: 10.0,
|
||||
standardDeviation: 5.0,
|
||||
median: 10.0,
|
||||
minimum: 5.0,
|
||||
maximum: 25.0,
|
||||
percentiles: [
|
||||
25 => 7.5,
|
||||
75 => 15.0,
|
||||
90 => 18.0,
|
||||
95 => 20.0,
|
||||
99 => 22.0,
|
||||
],
|
||||
sampleCount: 20,
|
||||
createdAt: $now,
|
||||
lastUpdated: $now,
|
||||
windowSize: Duration::fromMinutes(30),
|
||||
confidence: 0.8
|
||||
);
|
||||
}
|
||||
|
||||
test('erkennt Z-Score-Anomalien korrekt', function () {
|
||||
// Arrange
|
||||
$detector = new StatisticalAnomalyDetector(
|
||||
enabled: true,
|
||||
confidenceThreshold: 0.5,
|
||||
zScoreThreshold: 2.0,
|
||||
extremeZScoreThreshold: 3.0,
|
||||
minSampleSize: 5,
|
||||
enableOutlierDetection: true,
|
||||
enableTrendAnalysis: true,
|
||||
featureHistory: []
|
||||
);
|
||||
|
||||
$feature = new BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'test_feature',
|
||||
value: 42.0,
|
||||
unit: 'count'
|
||||
);
|
||||
|
||||
$baseline = createTestBaselineSAD();
|
||||
|
||||
// Act
|
||||
$anomalies = $detector->detectAnomalies([$feature], $baseline);
|
||||
|
||||
// Assert
|
||||
expect($anomalies)->toHaveCount(1);
|
||||
expect($anomalies[0])->toBeInstanceOf(AnomalyDetection::class);
|
||||
expect($anomalies[0]->type)->toBe(AnomalyType::STATISTICAL_ANOMALY);
|
||||
expect($anomalies[0]->behaviorType)->toBe(BehaviorType::PATH_PATTERNS);
|
||||
expect($anomalies[0]->confidence->getValue())->toBeGreaterThan(50.0);
|
||||
|
||||
// Z-Score sollte (42 - 10) / 5 = 6.4 sein, was deutlich über dem Schwellenwert liegt
|
||||
expect($anomalies[0]->anomalyScore)->toBeGreaterThan(0.7);
|
||||
});
|
||||
|
||||
test('ignoriert Werte innerhalb des normalen Bereichs', function () {
|
||||
// Arrange
|
||||
$detector = new StatisticalAnomalyDetector(
|
||||
enabled: true,
|
||||
confidenceThreshold: 0.5,
|
||||
zScoreThreshold: 2.0,
|
||||
extremeZScoreThreshold: 3.0,
|
||||
minSampleSize: 5,
|
||||
enableOutlierDetection: true,
|
||||
enableTrendAnalysis: true,
|
||||
featureHistory: []
|
||||
);
|
||||
|
||||
$feature = new BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'test_feature',
|
||||
value: 12.0, // Nahe am Mittelwert
|
||||
unit: 'count'
|
||||
);
|
||||
|
||||
$baseline = createTestBaselineSAD();
|
||||
|
||||
// Act
|
||||
$anomalies = $detector->detectAnomalies([$feature], $baseline);
|
||||
|
||||
// Assert
|
||||
expect($anomalies)->toBeEmpty();
|
||||
});
|
||||
|
||||
test('erkennt Ausreißer ohne Baseline', function () {
|
||||
// Arrange
|
||||
$detector = new StatisticalAnomalyDetector(
|
||||
enabled: true,
|
||||
confidenceThreshold: 0.5,
|
||||
zScoreThreshold: 2.0,
|
||||
extremeZScoreThreshold: 3.0,
|
||||
minSampleSize: 5,
|
||||
enableOutlierDetection: true,
|
||||
enableTrendAnalysis: false,
|
||||
featureHistory: [
|
||||
BehaviorType::PATH_PATTERNS->value => [
|
||||
'test_feature' => [10, 12, 9, 11, 10, 13, 8, 11, 10, 12],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$feature = new BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'test_feature',
|
||||
value: 30.0, // Deutlicher Ausreißer
|
||||
unit: 'count'
|
||||
);
|
||||
|
||||
// Act
|
||||
$anomalies = $detector->detectAnomalies([$feature], null);
|
||||
|
||||
// Assert
|
||||
expect($anomalies)->not->toBeEmpty();
|
||||
expect($anomalies[0]->type)->toBe(AnomalyType::OUTLIER_DETECTION);
|
||||
});
|
||||
|
||||
test('unterstützt verschiedene Verhaltenstypen', function () {
|
||||
// Arrange
|
||||
$detector = new StatisticalAnomalyDetector(
|
||||
enabled: true,
|
||||
confidenceThreshold: 0.5,
|
||||
zScoreThreshold: 2.0,
|
||||
extremeZScoreThreshold: 3.0,
|
||||
minSampleSize: 5,
|
||||
enableOutlierDetection: true,
|
||||
enableTrendAnalysis: true,
|
||||
featureHistory: []
|
||||
);
|
||||
|
||||
// Act
|
||||
$supportedTypes = $detector->getSupportedBehaviorTypes();
|
||||
|
||||
// Assert
|
||||
expect($supportedTypes)->toBeArray();
|
||||
expect($supportedTypes)->toContain(BehaviorType::REQUEST_FREQUENCY);
|
||||
expect($supportedTypes)->toContain(BehaviorType::PATH_PATTERNS);
|
||||
expect($supportedTypes)->toContain(BehaviorType::PARAMETER_PATTERNS);
|
||||
});
|
||||
|
||||
test('aktualisiert Modell mit neuen Daten', function () {
|
||||
// Arrange
|
||||
$detector = new StatisticalAnomalyDetector(
|
||||
enabled: true,
|
||||
confidenceThreshold: 0.5,
|
||||
zScoreThreshold: 2.0,
|
||||
extremeZScoreThreshold: 3.0,
|
||||
minSampleSize: 5,
|
||||
enableOutlierDetection: true,
|
||||
enableTrendAnalysis: true,
|
||||
featureHistory: []
|
||||
);
|
||||
|
||||
$feature1 = new BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'test_feature',
|
||||
value: 15.0,
|
||||
unit: 'count'
|
||||
);
|
||||
|
||||
$feature2 = new BehaviorFeature(
|
||||
type: BehaviorType::REQUEST_FREQUENCY,
|
||||
name: 'request_rate',
|
||||
value: 5.0,
|
||||
unit: 'requests/second'
|
||||
);
|
||||
|
||||
// Act - Keine Assertion möglich, da featureHistory private ist
|
||||
// Wir testen nur, dass keine Exception geworfen wird
|
||||
$detector->updateModel([$feature1, $feature2]);
|
||||
|
||||
// Assert
|
||||
expect(true)->toBeTrue(); // Dummy assertion
|
||||
});
|
||||
|
||||
test('gibt Konfiguration korrekt zurück', function () {
|
||||
// Arrange
|
||||
$detector = new StatisticalAnomalyDetector(
|
||||
enabled: true,
|
||||
confidenceThreshold: 0.75,
|
||||
zScoreThreshold: 2.5,
|
||||
extremeZScoreThreshold: 4.0,
|
||||
minSampleSize: 10,
|
||||
enableOutlierDetection: true,
|
||||
enableTrendAnalysis: false,
|
||||
featureHistory: []
|
||||
);
|
||||
|
||||
// Act
|
||||
$config = $detector->getConfiguration();
|
||||
|
||||
// Assert
|
||||
expect($config)->toBeArray();
|
||||
expect($config['enabled'])->toBeTrue();
|
||||
expect($config['confidence_threshold'])->toBe(0.75);
|
||||
expect($config['z_score_threshold'])->toBe(2.5);
|
||||
expect($config['extreme_z_score_threshold'])->toBe(4.0);
|
||||
expect($config['min_sample_size'])->toBe(10);
|
||||
expect($config['enable_outlier_detection'])->toBeTrue();
|
||||
expect($config['enable_trend_analysis'])->toBeFalse();
|
||||
});
|
||||
|
||||
test('kann Analyse durchführen wenn aktiviert', function () {
|
||||
// Arrange
|
||||
$detector = new StatisticalAnomalyDetector(
|
||||
enabled: true,
|
||||
confidenceThreshold: 0.5,
|
||||
zScoreThreshold: 2.0,
|
||||
extremeZScoreThreshold: 3.0,
|
||||
minSampleSize: 5,
|
||||
enableOutlierDetection: true,
|
||||
enableTrendAnalysis: true,
|
||||
featureHistory: []
|
||||
);
|
||||
|
||||
$feature = new BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'test_feature',
|
||||
value: 42.0,
|
||||
unit: 'count'
|
||||
);
|
||||
|
||||
// Act & Assert
|
||||
expect($detector->isEnabled())->toBeTrue();
|
||||
expect($detector->canAnalyze([$feature]))->toBeTrue();
|
||||
});
|
||||
|
||||
test('gibt leere Ergebnisse zurück wenn deaktiviert', function () {
|
||||
// Arrange
|
||||
$detector = new StatisticalAnomalyDetector(
|
||||
enabled: false,
|
||||
confidenceThreshold: 0.5,
|
||||
zScoreThreshold: 2.0,
|
||||
extremeZScoreThreshold: 3.0,
|
||||
minSampleSize: 5,
|
||||
enableOutlierDetection: true,
|
||||
enableTrendAnalysis: true,
|
||||
featureHistory: []
|
||||
);
|
||||
|
||||
$feature = new BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'test_feature',
|
||||
value: 42.0,
|
||||
unit: 'count'
|
||||
);
|
||||
|
||||
$baseline = createTestBaselineSAD();
|
||||
|
||||
// Act
|
||||
$anomalies = $detector->detectAnomalies([$feature], $baseline);
|
||||
|
||||
// Assert
|
||||
expect($anomalies)->toBeEmpty();
|
||||
});
|
||||
@@ -0,0 +1,289 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Framework\Waf\MachineLearning\Integration;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Percentage;
|
||||
use App\Framework\DateTime\Clock;
|
||||
use App\Framework\Waf\Analysis\ValueObjects\RequestAnalysisData;
|
||||
use App\Framework\Waf\MachineLearning\AnomalyDetectorInterface;
|
||||
use App\Framework\Waf\MachineLearning\AnomalyType;
|
||||
use App\Framework\Waf\MachineLearning\BehaviorType;
|
||||
use App\Framework\Waf\MachineLearning\FeatureExtractorInterface;
|
||||
use App\Framework\Waf\MachineLearning\MachineLearningEngine;
|
||||
use App\Framework\Waf\MachineLearning\ValueObjects\AnomalyDetection;
|
||||
use App\Framework\Waf\MachineLearning\ValueObjects\BehaviorFeature;
|
||||
use Mockery;
|
||||
use Mockery\MockInterface;
|
||||
|
||||
/**
|
||||
* Integrationstests für die WAF Machine Learning Pipeline
|
||||
*
|
||||
* Diese Tests überprüfen das Zusammenspiel der verschiedenen Komponenten:
|
||||
* - Feature-Extraktion
|
||||
* - Anomalie-Erkennung
|
||||
* - Gesamtprozess der Analyse
|
||||
*/
|
||||
|
||||
// Hilfsfunktion zum Erstellen von Testanfragen
|
||||
function createNormalRequest(): RequestAnalysisData
|
||||
{
|
||||
return RequestAnalysisData::minimal(
|
||||
method: 'GET',
|
||||
path: '/products/category/electronics',
|
||||
headers: [
|
||||
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language' => 'de,en-US;q=0.7,en;q=0.3',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
function createAnomalousRequest(): RequestAnalysisData
|
||||
{
|
||||
return RequestAnalysisData::minimal(
|
||||
method: 'GET',
|
||||
path: '/admin/config/system/../../../../../../etc/passwd',
|
||||
headers: [
|
||||
'User-Agent' => 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
|
||||
'Accept' => '*/*',
|
||||
'X-Forwarded-For' => '192.168.1.1, 10.0.0.1, 172.16.0.1',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Hilfsfunktion zum Erstellen eines Mock-Extraktors
|
||||
function createMockExtractor(bool $enabled = true, ?BehaviorType $behaviorType = null, array $features = []): MockInterface
|
||||
{
|
||||
$behaviorType = $behaviorType ?? BehaviorType::PATH_PATTERNS;
|
||||
|
||||
$extractor = Mockery::mock(FeatureExtractorInterface::class);
|
||||
$extractor->shouldReceive('isEnabled')->andReturn($enabled);
|
||||
$extractor->shouldReceive('getBehaviorType')->andReturn($behaviorType);
|
||||
$extractor->shouldReceive('getPriority')->andReturn(10);
|
||||
$extractor->shouldReceive('canExtract')->andReturn(true);
|
||||
$extractor->shouldReceive('extractFeatures')->andReturn($features);
|
||||
|
||||
return $extractor;
|
||||
}
|
||||
|
||||
// Hilfsfunktion zum Erstellen eines Mock-Detektors
|
||||
function createMockDetector(bool $enabled = true, array $supportedTypes = [], array $anomalies = []): MockInterface
|
||||
{
|
||||
$supportedTypes = $supportedTypes ?: [BehaviorType::PATH_PATTERNS];
|
||||
|
||||
$detector = Mockery::mock(AnomalyDetectorInterface::class);
|
||||
$detector->shouldReceive('isEnabled')->andReturn($enabled);
|
||||
$detector->shouldReceive('getName')->andReturn('MockDetector');
|
||||
$detector->shouldReceive('getSupportedBehaviorTypes')->andReturn($supportedTypes);
|
||||
$detector->shouldReceive('canAnalyze')->andReturn(true);
|
||||
$detector->shouldReceive('detectAnomalies')->andReturn($anomalies);
|
||||
$detector->shouldReceive('updateModel')->andReturn(null);
|
||||
|
||||
return $detector;
|
||||
}
|
||||
|
||||
// Hilfsfunktion zum Erstellen eines Mock-Clocks
|
||||
function createMockClock(): MockInterface
|
||||
{
|
||||
$clock = Mockery::mock(Clock::class);
|
||||
$dateTime = \App\Framework\DateTime\DateTime::fromString('2025-07-31 13:42:00');
|
||||
$timestamp = \App\Framework\Core\ValueObjects\Timestamp::fromDateTime($dateTime);
|
||||
$clock->shouldReceive('time')->andReturn($timestamp);
|
||||
|
||||
return $clock;
|
||||
}
|
||||
|
||||
test('vollständige ML-Pipeline erkennt normale Anfragen korrekt', function () {
|
||||
// Arrange
|
||||
$clock = createMockClock();
|
||||
|
||||
// Feature für normale Anfrage
|
||||
$normalFeature = new BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'path_depth',
|
||||
value: 3.0,
|
||||
unit: 'count'
|
||||
);
|
||||
|
||||
// Mock-Extraktoren erstellen
|
||||
$extractor = createMockExtractor(true, BehaviorType::PATH_PATTERNS, [$normalFeature]);
|
||||
|
||||
// Mock-Detektor erstellen (keine Anomalien für normale Anfrage)
|
||||
$detector = createMockDetector(true, [BehaviorType::PATH_PATTERNS], []);
|
||||
|
||||
// ML-Engine erstellen
|
||||
$engine = new MachineLearningEngine(
|
||||
enabled: true,
|
||||
extractors: [$extractor],
|
||||
detectors: [$detector],
|
||||
clock: $clock,
|
||||
analysisTimeout: Duration::fromSeconds(5),
|
||||
confidenceThreshold: Percentage::from(60.0)
|
||||
);
|
||||
|
||||
// Normale Anfrage erstellen
|
||||
$request = createNormalRequest();
|
||||
|
||||
// Act
|
||||
$result = $engine->analyzeRequest($request);
|
||||
|
||||
// Assert
|
||||
expect($result->features)->toHaveCount(1);
|
||||
expect($result->anomalies)->toBeEmpty();
|
||||
expect($result->confidence->getValue())->toBe(0.0);
|
||||
expect($result->error)->toBeNull();
|
||||
});
|
||||
|
||||
test('vollständige ML-Pipeline erkennt anomale Anfragen', function () {
|
||||
// Arrange
|
||||
$clock = createMockClock();
|
||||
|
||||
// Feature für anomale Anfrage
|
||||
$anomalousFeature = new BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'path_traversal',
|
||||
value: 5.0,
|
||||
unit: 'count'
|
||||
);
|
||||
|
||||
// Anomalie für die anomale Anfrage
|
||||
$anomaly = new AnomalyDetection(
|
||||
type: AnomalyType::STATISTICAL_ANOMALY,
|
||||
behaviorType: BehaviorType::PATH_PATTERNS,
|
||||
confidence: Percentage::from(80.0),
|
||||
anomalyScore: 0.9,
|
||||
description: 'Path traversal detected',
|
||||
features: [$anomalousFeature],
|
||||
evidence: [
|
||||
'path' => '/admin/config/system/../../../../../../etc/passwd',
|
||||
'traversal_depth' => 6,
|
||||
]
|
||||
);
|
||||
|
||||
// Mock-Extraktoren erstellen
|
||||
$extractor = createMockExtractor(true, BehaviorType::PATH_PATTERNS, [$anomalousFeature]);
|
||||
|
||||
// Mock-Detektor erstellen (gibt Anomalie zurück)
|
||||
$detector = createMockDetector(true, [BehaviorType::PATH_PATTERNS], [$anomaly]);
|
||||
|
||||
// ML-Engine erstellen
|
||||
$engine = new MachineLearningEngine(
|
||||
enabled: true,
|
||||
extractors: [$extractor],
|
||||
detectors: [$detector],
|
||||
clock: $clock,
|
||||
analysisTimeout: Duration::fromSeconds(5),
|
||||
confidenceThreshold: Percentage::from(60.0)
|
||||
);
|
||||
|
||||
// Anomale Anfrage erstellen
|
||||
$request = createAnomalousRequest();
|
||||
|
||||
// Act
|
||||
$result = $engine->analyzeRequest($request);
|
||||
|
||||
// Assert
|
||||
expect($result->features)->toHaveCount(1);
|
||||
expect($result->anomalies)->toHaveCount(1);
|
||||
expect($result->anomalies[0]->type)->toBe(AnomalyType::STATISTICAL_ANOMALY);
|
||||
expect($result->confidence->getValue())->toBeGreaterThan(70.0);
|
||||
expect($result->error)->toBeNull();
|
||||
});
|
||||
|
||||
test('ML-Pipeline mit deaktivierten Komponenten funktioniert korrekt', function () {
|
||||
// Arrange
|
||||
$clock = createMockClock();
|
||||
|
||||
// Feature für normale Anfrage
|
||||
$feature = new BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'path_depth',
|
||||
value: 3.0,
|
||||
unit: 'count'
|
||||
);
|
||||
|
||||
// Mock-Extraktoren erstellen (einer deaktiviert)
|
||||
$activeExtractor = createMockExtractor(true, BehaviorType::PATH_PATTERNS, [$feature]);
|
||||
$inactiveExtractor = createMockExtractor(false, BehaviorType::PARAMETER_PATTERNS, []);
|
||||
|
||||
// Mock-Detektoren erstellen (einer deaktiviert)
|
||||
$activeDetector = createMockDetector(true, [BehaviorType::PATH_PATTERNS], []);
|
||||
$inactiveDetector = createMockDetector(false, [BehaviorType::PARAMETER_PATTERNS], []);
|
||||
|
||||
// ML-Engine erstellen
|
||||
$engine = new MachineLearningEngine(
|
||||
enabled: true,
|
||||
extractors: [$activeExtractor, $inactiveExtractor],
|
||||
detectors: [$activeDetector, $inactiveDetector],
|
||||
clock: $clock,
|
||||
analysisTimeout: Duration::fromSeconds(5),
|
||||
confidenceThreshold: Percentage::from(60.0)
|
||||
);
|
||||
|
||||
// Anfrage erstellen
|
||||
$request = createNormalRequest();
|
||||
|
||||
// Act
|
||||
$result = $engine->analyzeRequest($request);
|
||||
|
||||
// Assert
|
||||
expect($result->features)->toHaveCount(1);
|
||||
expect($result->error)->toBeNull();
|
||||
|
||||
// Extractor-Ergebnisse prüfen
|
||||
$extractorResults = $result->extractorResults;
|
||||
expect($extractorResults)->toBeArray();
|
||||
|
||||
// Detector-Ergebnisse prüfen
|
||||
$detectorResults = $result->detectorResults;
|
||||
expect($detectorResults)->toBeArray();
|
||||
});
|
||||
|
||||
test('ML-Pipeline mit deaktivierter Engine gibt leeres Ergebnis zurück', function () {
|
||||
// Arrange
|
||||
$clock = createMockClock();
|
||||
|
||||
// Feature für normale Anfrage
|
||||
$feature = new BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'path_depth',
|
||||
value: 3.0,
|
||||
unit: 'count'
|
||||
);
|
||||
|
||||
// Mock-Extraktoren erstellen
|
||||
$extractor = createMockExtractor(true, BehaviorType::PATH_PATTERNS, [$feature]);
|
||||
|
||||
// Mock-Detektor erstellen
|
||||
$detector = createMockDetector(true, [BehaviorType::PATH_PATTERNS], []);
|
||||
|
||||
// ML-Engine erstellen (deaktiviert)
|
||||
$engine = new MachineLearningEngine(
|
||||
enabled: false,
|
||||
extractors: [$extractor],
|
||||
detectors: [$detector],
|
||||
clock: $clock,
|
||||
analysisTimeout: Duration::fromSeconds(5),
|
||||
confidenceThreshold: Percentage::from(60.0)
|
||||
);
|
||||
|
||||
// Anfrage erstellen
|
||||
$request = createNormalRequest();
|
||||
|
||||
// Act
|
||||
$result = $engine->analyzeRequest($request);
|
||||
|
||||
// Assert
|
||||
expect($result->enabled)->toBeFalse();
|
||||
expect($result->features)->toBeEmpty();
|
||||
expect($result->anomalies)->toBeEmpty();
|
||||
expect($result->confidence->getValue())->toBe(0.0);
|
||||
});
|
||||
|
||||
// Bereinigung nach jedem Test
|
||||
afterEach(function () {
|
||||
Mockery::close();
|
||||
});
|
||||
@@ -0,0 +1,400 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Framework\Waf\MachineLearning;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Percentage;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\DateTime\Clock;
|
||||
use App\Framework\DateTime\DateTime;
|
||||
use App\Framework\Waf\Analysis\ValueObjects\RequestAnalysisData;
|
||||
use App\Framework\Waf\MachineLearning\AnomalyDetectorInterface;
|
||||
use App\Framework\Waf\MachineLearning\AnomalyType;
|
||||
use App\Framework\Waf\MachineLearning\BehaviorType;
|
||||
use App\Framework\Waf\MachineLearning\FeatureExtractorInterface;
|
||||
use App\Framework\Waf\MachineLearning\MachineLearningEngine;
|
||||
use App\Framework\Waf\MachineLearning\MachineLearningResult;
|
||||
use App\Framework\Waf\MachineLearning\ValueObjects\AnomalyDetection;
|
||||
use App\Framework\Waf\MachineLearning\ValueObjects\BehaviorFeature;
|
||||
use Mockery;
|
||||
use Mockery\MockInterface;
|
||||
|
||||
// Hilfsfunktion zum Erstellen eines Mock-Extraktors
|
||||
function createMockExtractorMLE(bool $enabled = true, ?BehaviorType $behaviorType = null, array $features = []): MockInterface
|
||||
{
|
||||
$behaviorType = $behaviorType ?? BehaviorType::PATH_PATTERNS;
|
||||
|
||||
$extractor = Mockery::mock(FeatureExtractorInterface::class);
|
||||
$extractor->shouldReceive('isEnabled')->andReturn($enabled);
|
||||
$extractor->shouldReceive('getBehaviorType')->andReturn($behaviorType);
|
||||
$extractor->shouldReceive('getPriority')->andReturn(10);
|
||||
$extractor->shouldReceive('canExtract')->andReturn(true);
|
||||
$extractor->shouldReceive('extractFeatures')->andReturn($features);
|
||||
|
||||
return $extractor;
|
||||
}
|
||||
|
||||
// Hilfsfunktion zum Erstellen eines Mock-Detektors
|
||||
function createMockDetectorMLE(bool $enabled = true, array $supportedTypes = [], array $anomalies = []): MockInterface
|
||||
{
|
||||
$supportedTypes = $supportedTypes ?: [BehaviorType::PATH_PATTERNS];
|
||||
|
||||
$detector = Mockery::mock(AnomalyDetectorInterface::class);
|
||||
$detector->shouldReceive('isEnabled')->andReturn($enabled);
|
||||
$detector->shouldReceive('getName')->andReturn('MockDetector');
|
||||
$detector->shouldReceive('getSupportedBehaviorTypes')->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 RequestAnalysisData::minimal(
|
||||
method: 'GET',
|
||||
path: '/test',
|
||||
headers: ['User-Agent' => 'TestAgent']
|
||||
);
|
||||
}
|
||||
|
||||
// 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 BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'test_feature',
|
||||
value: 42.0,
|
||||
unit: 'count'
|
||||
);
|
||||
|
||||
$extractor = createMockExtractorMLE(true, BehaviorType::PATH_PATTERNS, [$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->features)->toHaveCount(1);
|
||||
expect($result->features[0])->toBeInstanceOf(BehaviorFeature::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 BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'test_feature',
|
||||
value: 42.0,
|
||||
unit: 'count'
|
||||
);
|
||||
|
||||
$anomaly = new AnomalyDetection(
|
||||
type: AnomalyType::STATISTICAL_ANOMALY,
|
||||
behaviorType: BehaviorType::PATH_PATTERNS,
|
||||
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, BehaviorType::PATH_PATTERNS, [$feature]);
|
||||
$detector = createMockDetectorMLE(true, [BehaviorType::PATH_PATTERNS], [$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 BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'test_feature',
|
||||
value: 42.0,
|
||||
unit: 'count'
|
||||
);
|
||||
|
||||
$highConfidenceAnomaly = new AnomalyDetection(
|
||||
type: AnomalyType::STATISTICAL_ANOMALY,
|
||||
behaviorType: BehaviorType::PATH_PATTERNS,
|
||||
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,
|
||||
behaviorType: BehaviorType::PATH_PATTERNS,
|
||||
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, BehaviorType::PATH_PATTERNS, [$feature]);
|
||||
$detector = createMockDetectorMLE(true, [BehaviorType::PATH_PATTERNS], [$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 BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'test_feature',
|
||||
value: 42.0,
|
||||
unit: 'count'
|
||||
);
|
||||
|
||||
$anomaly1 = new AnomalyDetection(
|
||||
type: AnomalyType::STATISTICAL_ANOMALY,
|
||||
behaviorType: BehaviorType::PATH_PATTERNS,
|
||||
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,
|
||||
behaviorType: BehaviorType::PATH_PATTERNS,
|
||||
confidence: Percentage::from(80.0),
|
||||
anomalyScore: 0.4,
|
||||
description: 'Anomaly 2',
|
||||
features: [$feature],
|
||||
evidence: ['value' => 42.0, 'expected_value' => 10.0]
|
||||
);
|
||||
|
||||
$extractor = createMockExtractorMLE(true, BehaviorType::PATH_PATTERNS, [$feature]);
|
||||
$detector = createMockDetectorMLE(true, [BehaviorType::PATH_PATTERNS], [$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 BehaviorFeature(
|
||||
type: BehaviorType::PATH_PATTERNS,
|
||||
name: 'test_feature',
|
||||
value: 42.0,
|
||||
unit: 'count'
|
||||
);
|
||||
|
||||
// Zwei Anomalien mit gleichem Typ und BehaviorType, aber unterschiedlicher Konfidenz
|
||||
$anomaly1 = new AnomalyDetection(
|
||||
type: AnomalyType::STATISTICAL_ANOMALY,
|
||||
behaviorType: BehaviorType::PATH_PATTERNS,
|
||||
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,
|
||||
behaviorType: BehaviorType::PATH_PATTERNS,
|
||||
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,
|
||||
behaviorType: BehaviorType::PATH_PATTERNS,
|
||||
confidence: Percentage::from(70.0),
|
||||
anomalyScore: 0.4,
|
||||
description: 'Anomaly 3',
|
||||
features: [$feature],
|
||||
evidence: ['value' => 42.0, 'expected_value' => 10.0]
|
||||
);
|
||||
|
||||
$extractor = createMockExtractorMLE(true, BehaviorType::PATH_PATTERNS, [$feature]);
|
||||
$detector = createMockDetectorMLE(true, [BehaviorType::PATH_PATTERNS], [$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);
|
||||
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(FeatureExtractorInterface::class);
|
||||
$extractor->shouldReceive('isEnabled')->andReturn(true);
|
||||
$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
|
||||
expect($result->error)->toBe('Test exception');
|
||||
expect($result->features)->toBeEmpty();
|
||||
expect($result->anomalies)->toBeEmpty();
|
||||
});
|
||||
|
||||
// Bereinigung nach jedem Test
|
||||
afterEach(function () {
|
||||
Mockery::close();
|
||||
});
|
||||
Reference in New Issue
Block a user