- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
340 lines
11 KiB
PHP
340 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Waf\MachineLearning\ValueObjects;
|
|
|
|
use App\Framework\Core\ValueObjects\Duration;
|
|
use App\Framework\Core\ValueObjects\Percentage;
|
|
use App\Framework\Core\ValueObjects\Timestamp;
|
|
use App\Framework\Waf\MachineLearning\AnomalyType;
|
|
use App\Framework\Waf\MachineLearning\BehaviorType;
|
|
|
|
/**
|
|
* Represents a detected behavioral anomaly
|
|
*/
|
|
final readonly class AnomalyDetection
|
|
{
|
|
public function __construct(
|
|
public AnomalyType $type,
|
|
public BehaviorType $behaviorType,
|
|
public Percentage $confidence,
|
|
public float $anomalyScore,
|
|
public string $description,
|
|
public array $features,
|
|
public array $evidence,
|
|
public ?string $clientId = null,
|
|
public ?string $sessionId = null,
|
|
public ?Timestamp $detectedAt = null,
|
|
public ?Duration $analysisWindow = null,
|
|
public array $metadata = []
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Create anomaly detection with automatic confidence calculation
|
|
*/
|
|
public static function create(
|
|
AnomalyType $type,
|
|
BehaviorType $behaviorType,
|
|
float $anomalyScore,
|
|
string $description,
|
|
array $features = [],
|
|
array $evidence = []
|
|
): self {
|
|
// Calculate confidence based on anomaly score and feature consistency
|
|
$baseConfidence = min($anomalyScore * 100, 100.0);
|
|
|
|
// Adjust confidence based on feature agreement
|
|
if (! empty($features)) {
|
|
$featureAnomalyScores = array_map(
|
|
fn (BehaviorFeature $feature) => $feature->getAnomalyScore(),
|
|
$features
|
|
);
|
|
|
|
$meanFeatureScore = array_sum($featureAnomalyScores) / count($featureAnomalyScores);
|
|
$featureConsistency = 1.0 - (abs($anomalyScore - $meanFeatureScore) / max($anomalyScore, 0.01));
|
|
|
|
$baseConfidence *= $featureConsistency;
|
|
}
|
|
|
|
$confidence = Percentage::from(max(0.0, min(100.0, $baseConfidence)));
|
|
|
|
return new self(
|
|
type: $type,
|
|
behaviorType: $behaviorType,
|
|
confidence: $confidence,
|
|
anomalyScore: $anomalyScore,
|
|
description: $description,
|
|
features: $features,
|
|
evidence: $evidence,
|
|
detectedAt: Timestamp::fromFloat(microtime(true))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create frequency spike anomaly
|
|
*/
|
|
public static function frequencySpike(
|
|
float $currentRate,
|
|
float $baseline,
|
|
float $threshold = 3.0,
|
|
?string $clientId = null
|
|
): self {
|
|
$ratio = $baseline > 0 ? $currentRate / $baseline : $currentRate;
|
|
$anomalyScore = min(($ratio - 1.0) / $threshold, 1.0);
|
|
|
|
return self::create(
|
|
type: AnomalyType::FREQUENCY_SPIKE,
|
|
behaviorType: BehaviorType::REQUEST_FREQUENCY,
|
|
anomalyScore: $anomalyScore,
|
|
description: "Request frequency spike detected: {$currentRate}/s (baseline: {$baseline}/s, ratio: " . round($ratio, 2) . "x)",
|
|
evidence: [
|
|
'current_rate' => $currentRate,
|
|
'baseline_rate' => $baseline,
|
|
'spike_ratio' => $ratio,
|
|
'threshold' => $threshold,
|
|
]
|
|
);
|
|
|
|
return $clientId !== null ? $anomaly->withClientId($clientId) : $anomaly;
|
|
}
|
|
|
|
/**
|
|
* Create geographic anomaly
|
|
*/
|
|
public static function geographicAnomaly(
|
|
string $currentLocation,
|
|
array $normalLocations,
|
|
float $distance,
|
|
?string $clientId = null
|
|
): self {
|
|
$anomalyScore = min($distance / 10000, 1.0); // Normalize by 10,000 km
|
|
|
|
return self::create(
|
|
type: AnomalyType::GEOGRAPHIC_ANOMALY,
|
|
behaviorType: BehaviorType::GEOGRAPHIC_PATTERNS,
|
|
anomalyScore: $anomalyScore,
|
|
description: "Geographic anomaly: access from {$currentLocation}, distance: " . round($distance) . "km from normal locations",
|
|
evidence: [
|
|
'current_location' => $currentLocation,
|
|
'normal_locations' => $normalLocations,
|
|
'distance_km' => $distance,
|
|
]
|
|
);
|
|
|
|
return $clientId !== null ? $anomaly->withClientId($clientId) : $anomaly;
|
|
}
|
|
|
|
/**
|
|
* Create pattern deviation anomaly
|
|
*/
|
|
public static function patternDeviation(
|
|
BehaviorType $behaviorType,
|
|
string $pattern,
|
|
float $deviationScore,
|
|
array $features = []
|
|
): self {
|
|
return self::create(
|
|
type: AnomalyType::UNUSUAL_PATTERN,
|
|
behaviorType: $behaviorType,
|
|
anomalyScore: $deviationScore,
|
|
description: "Unusual pattern detected in {$behaviorType->getDescription()}: {$pattern}",
|
|
features: $features,
|
|
evidence: [
|
|
'pattern' => $pattern,
|
|
'deviation_score' => $deviationScore,
|
|
'feature_count' => count($features),
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create statistical anomaly
|
|
*/
|
|
public static function statisticalAnomaly(
|
|
BehaviorType $behaviorType,
|
|
string $metric,
|
|
float $value,
|
|
float $expectedValue,
|
|
float $standardDeviation,
|
|
?string $clientId = null
|
|
): self {
|
|
$zScore = $standardDeviation > 0 ? abs($value - $expectedValue) / $standardDeviation : 0;
|
|
$anomalyScore = min($zScore / 3.0, 1.0); // Normalize by 3 sigma
|
|
|
|
return self::create(
|
|
type: AnomalyType::STATISTICAL_ANOMALY,
|
|
behaviorType: $behaviorType,
|
|
anomalyScore: $anomalyScore,
|
|
description: "Statistical anomaly in {$metric}: value={$value}, expected={$expectedValue}, z-score=" . round($zScore, 2),
|
|
evidence: [
|
|
'metric' => $metric,
|
|
'value' => $value,
|
|
'expected_value' => $expectedValue,
|
|
'standard_deviation' => $standardDeviation,
|
|
'z_score' => $zScore,
|
|
]
|
|
);
|
|
|
|
return $clientId !== null ? $anomaly->withClientId($clientId) : $anomaly;
|
|
}
|
|
|
|
/**
|
|
* Add client ID
|
|
*/
|
|
public function withClientId(string $clientId): self
|
|
{
|
|
return new self(
|
|
type: $this->type,
|
|
behaviorType: $this->behaviorType,
|
|
confidence: $this->confidence,
|
|
anomalyScore: $this->anomalyScore,
|
|
description: $this->description,
|
|
features: $this->features,
|
|
evidence: $this->evidence,
|
|
clientId: $clientId,
|
|
sessionId: $this->sessionId,
|
|
detectedAt: $this->detectedAt,
|
|
analysisWindow: $this->analysisWindow,
|
|
metadata: $this->metadata
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Add session ID
|
|
*/
|
|
public function withSessionId(string $sessionId): self
|
|
{
|
|
return new self(
|
|
type: $this->type,
|
|
behaviorType: $this->behaviorType,
|
|
confidence: $this->confidence,
|
|
anomalyScore: $this->anomalyScore,
|
|
description: $this->description,
|
|
features: $this->features,
|
|
evidence: $this->evidence,
|
|
clientId: $this->clientId,
|
|
sessionId: $sessionId,
|
|
detectedAt: $this->detectedAt,
|
|
analysisWindow: $this->analysisWindow,
|
|
metadata: $this->metadata
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Add analysis window
|
|
*/
|
|
public function withAnalysisWindow(Duration $window): self
|
|
{
|
|
return new self(
|
|
type: $this->type,
|
|
behaviorType: $this->behaviorType,
|
|
confidence: $this->confidence,
|
|
anomalyScore: $this->anomalyScore,
|
|
description: $this->description,
|
|
features: $this->features,
|
|
evidence: $this->evidence,
|
|
clientId: $this->clientId,
|
|
sessionId: $this->sessionId,
|
|
detectedAt: $this->detectedAt,
|
|
analysisWindow: $window,
|
|
metadata: $this->metadata
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if anomaly requires immediate action
|
|
*/
|
|
public function requiresImmediateAction(): bool
|
|
{
|
|
return $this->type->requiresImmediateAction() &&
|
|
$this->confidence->getValue() >= $this->type->getConfidenceThreshold() * 100;
|
|
}
|
|
|
|
/**
|
|
* Get risk level
|
|
*/
|
|
public function getRiskLevel(): string
|
|
{
|
|
$confidenceScore = $this->confidence->getValue() / 100.0;
|
|
$combinedScore = ($this->anomalyScore + $confidenceScore) / 2.0;
|
|
|
|
return match (true) {
|
|
$combinedScore >= 0.8 => 'critical',
|
|
$combinedScore >= 0.6 => 'high',
|
|
$combinedScore >= 0.4 => 'medium',
|
|
$combinedScore >= 0.2 => 'low',
|
|
default => 'info'
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get recommended action
|
|
*/
|
|
public function getRecommendedAction(): string
|
|
{
|
|
return $this->type->getRecommendedAction();
|
|
}
|
|
|
|
/**
|
|
* Get severity score (0-100)
|
|
*/
|
|
public function getSeverityScore(): float
|
|
{
|
|
$typeWeight = match ($this->type->getSeverityLevel()) {
|
|
'high' => 0.9,
|
|
'medium' => 0.6,
|
|
'low' => 0.3,
|
|
default => 0.5
|
|
};
|
|
|
|
$confidenceWeight = $this->confidence->getValue() / 100.0;
|
|
$anomalyWeight = $this->anomalyScore;
|
|
|
|
return ($typeWeight * 0.4 + $confidenceWeight * 0.3 + $anomalyWeight * 0.3) * 100;
|
|
}
|
|
|
|
/**
|
|
* Convert to array for logging/storage
|
|
*/
|
|
public function toArray(): array
|
|
{
|
|
return [
|
|
'type' => $this->type->value,
|
|
'behavior_type' => $this->behaviorType->value,
|
|
'confidence' => $this->confidence->getValue(),
|
|
'anomaly_score' => $this->anomalyScore,
|
|
'description' => $this->description,
|
|
'client_id' => $this->clientId,
|
|
'session_id' => $this->sessionId,
|
|
'detected_at' => $this->detectedAt?->format('c'),
|
|
'analysis_window_seconds' => $this->analysisWindow?->toSeconds(),
|
|
'features' => array_map(fn (BehaviorFeature $f) => $f->toArray(), $this->features),
|
|
'evidence' => $this->evidence,
|
|
'risk_level' => $this->getRiskLevel(),
|
|
'severity_score' => $this->getSeverityScore(),
|
|
'requires_immediate_action' => $this->requiresImmediateAction(),
|
|
'recommended_action' => $this->getRecommendedAction(),
|
|
'metadata' => $this->metadata,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Create summary for dashboard/alerting
|
|
*/
|
|
public function getSummary(): array
|
|
{
|
|
return [
|
|
'id' => md5($this->type->value . $this->behaviorType->value . ($this->detectedAt?->format('c') ?? '')),
|
|
'type' => $this->type->value,
|
|
'description' => $this->description,
|
|
'risk_level' => $this->getRiskLevel(),
|
|
'confidence' => $this->confidence->getValue(),
|
|
'client_id' => $this->clientId,
|
|
'detected_at' => $this->detectedAt?->format('c'),
|
|
'requires_action' => $this->requiresImmediateAction(),
|
|
];
|
|
}
|
|
}
|