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,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();
});

View File

@@ -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();
});

View File

@@ -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();
});

View File

@@ -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();
});