Enable Discovery debug logging for production troubleshooting

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

View File

@@ -0,0 +1,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;
}
}

View 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]
);
}
}

View 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 = [];
}
}

View 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;
}
}

View 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;
}
}