- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
401 lines
14 KiB
PHP
401 lines
14 KiB
PHP
<?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();
|
|
});
|