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,454 @@
<?php
declare(strict_types=1);
namespace App\Framework\Waf\Layers;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Http\Request;
use App\Framework\Waf\DetectionCategory;
use App\Framework\Waf\DetectionSeverity;
use App\Framework\Waf\LayerResult;
use App\Framework\Waf\LayerStatus;
use App\Framework\Waf\ValueObjects\Detection;
use App\Framework\Waf\ValueObjects\LayerConfig;
use App\Framework\Waf\ValueObjects\LayerMetrics;
/**
* Command Injection Detection Layer
*
* Detects operating system command injection attempts.
* Protects against arbitrary command execution vulnerabilities.
*/
final class CommandInjectionLayer implements LayerInterface
{
private LayerConfig $config;
private LayerMetrics $metrics;
private bool $enabled = true;
/** Command injection patterns */
private const COMMAND_INJECTION_PATTERNS = [
// Command separators
'/[;&|`]+/',
'/\|\|/',
'/&&/',
// Common Unix/Linux commands
'/\b(ls|cat|pwd|whoami|id|uname|ps|netstat|ifconfig|mount)\b/i',
'/\b(grep|find|locate|which|whereis|file|head|tail|more|less)\b/i',
'/\b(chmod|chown|mkdir|rmdir|rm|cp|mv|ln|touch)\b/i',
'/\b(wget|curl|nc|netcat|telnet|ssh|ftp|tftp)\b/i',
'/\b(su|sudo|passwd|useradd|userdel|usermod)\b/i',
// Common Windows commands
'/\b(dir|type|copy|del|move|md|rd|cd|cls|ver)\b/i',
'/\b(net|ipconfig|ping|tracert|nslookup|arp|route)\b/i',
'/\b(tasklist|taskkill|sc|reg|wmic|powershell|cmd)\b/i',
'/\b(systeminfo|whoami|echo|set|path)\b/i',
// System information and process commands
'/\b(ps|top|htop|kill|killall|pkill|nohup|jobs|bg|fg)\b/i',
'/\b(crontab|at|service|systemctl|chkconfig|update-rc\.d)\b/i',
'/\b(iptables|netfilter|firewall|selinux|apparmor)\b/i',
// File operations and text processing
'/\b(awk|sed|sort|uniq|wc|diff|patch|tar|gzip|gunzip)\b/i',
'/\b(zip|unzip|compress|uncompress|ar|strings|od|hexdump)\b/i',
// Network and system utilities
'/\b(dig|nslookup|host|whois|traceroute|mtr|tcpdump|wireshark)\b/i',
'/\b(lsof|strace|ltrace|gdb|objdump|readelf|nm)\b/i',
// Dangerous system calls
'/\b(exec|system|shell_exec|passthru|eval|popen|proc_open)\b/i',
'/\b(Runtime\.getRuntime|ProcessBuilder|cmd\.exe|sh)\b/i',
// Command substitution
'/\$\(.*\)/',
'/`[^`]*`/',
'/\${.*}/',
// Input/Output redirection
'/[<>]+/',
'/\b(tee|xargs)\b/i',
// Environment variable manipulation
'/\$\w+/',
'/\bexport\s+\w+=/i',
'/\bset\s+\w+=/i',
// Script interpreters
'/\b(bash|sh|csh|tcsh|zsh|fish|ksh|dash)\b/i',
'/\b(python|perl|ruby|php|node|java|lua)\b/i',
// Encoded command attempts
'/%5C/', // Backslash
'/%7C/', // Pipe
'/%26/', // Ampersand
'/%3B/', // Semicolon
'/%60/', // Backtick
// Null byte injection for command termination
'/\x00/',
'/%00/',
];
public function __construct()
{
$this->config = new LayerConfig(
enabled: true,
timeout: Duration::fromMilliseconds(40),
confidenceThreshold: Percentage::from(92.0),
blockingMode: true,
logDetections: true,
maxDetectionsPerRequest: 5
);
$this->metrics = new LayerMetrics();
}
public function getName(): string
{
return 'command_injection';
}
public function analyze(Request $request): LayerResult
{
$startTime = microtime(true);
$detections = [];
try {
// Check query parameters
foreach ($request->queryParams as $key => $value) {
if (is_string($value)) {
$detection = $this->analyzeString($value, "query parameter '{$key}'");
if ($detection) {
$detections[] = $detection;
}
}
}
// Check POST data
if (isset($request->parsedBody->data) && is_array($request->parsedBody->data)) {
foreach ($request->parsedBody->data as $key => $value) {
if (is_string($value)) {
$detection = $this->analyzeString($value, "POST parameter '{$key}'");
if ($detection) {
$detections[] = $detection;
}
}
}
}
// Check headers that commonly contain command injection attempts
$suspiciousHeaders = ['User-Agent', 'Referer', 'X-Forwarded-For', 'Cookie', 'X-Real-IP'];
foreach ($suspiciousHeaders as $headerName) {
$headerValue = $request->headers->getFirst($headerName);
if ($headerValue) {
$detection = $this->analyzeString($headerValue, "header '{$headerName}'");
if ($detection) {
$detections[] = $detection;
}
}
}
// Check request path
$detection = $this->analyzeString($request->path, 'request path');
if ($detection) {
$detections[] = $detection;
}
$processingTime = Duration::fromMilliseconds((microtime(true) - $startTime) * 1000);
if (! empty($detections)) {
return LayerResult::threat(
$this->getName(),
'Command injection attempt detected',
LayerStatus::THREAT_DETECTED,
$detections,
$processingTime
);
}
return LayerResult::clean(
$this->getName(),
'No command injection patterns detected',
$processingTime
);
} catch (\Throwable $e) {
$processingTime = Duration::fromMilliseconds((microtime(true) - $startTime) * 1000);
return LayerResult::error(
$this->getName(),
'Command injection analysis failed: ' . $e->getMessage(),
$processingTime
);
}
}
/**
* Analyze string for command injection patterns
*/
private function analyzeString(string $input, string $location): ?Detection
{
$originalInput = $input;
// Multiple decoding passes
$input = urldecode($input);
$input = html_entity_decode($input, ENT_QUOTES | ENT_HTML5);
$input = urldecode($input); // Second pass for double encoding
foreach (self::COMMAND_INJECTION_PATTERNS as $pattern) {
if (preg_match($pattern, $input, $matches)) {
$severity = $this->calculateSeverity($pattern, $matches[0] ?? '', $input);
$riskScore = $this->calculateRiskScore($pattern, $location, $input);
return new Detection(
category: DetectionCategory::COMMAND_INJECTION,
severity: $severity,
message: "Command injection pattern detected in {$location}",
details: [
'location' => $location,
'pattern' => $pattern,
'matched_text' => $matches[0] ?? '',
'input_length' => strlen($originalInput),
'decoded_input' => substr($input, 0, 200),
'original_input' => substr($originalInput, 0, 200),
'command_type' => $this->identifyCommandType($pattern, $matches[0] ?? ''),
'risk_factors' => $this->analyzeRiskFactors($input),
],
confidence: 0.88,
riskScore: $riskScore
);
}
}
return null;
}
/**
* Calculate severity based on pattern and context
*/
private function calculateSeverity(string $pattern, string $match, string $fullInput): DetectionSeverity
{
// Critical - System administration commands
if (preg_match('/\b(sudo|su|passwd|useradd|rm|chmod|chown)\b/i', $match)) {
return DetectionSeverity::CRITICAL;
}
// Critical - Command execution functions
if (preg_match('/\b(exec|system|shell_exec|eval)\b/i', $match)) {
return DetectionSeverity::CRITICAL;
}
// Critical - Multiple command separators
if (preg_match('/[;&|`]{2,}/', $match)) {
return DetectionSeverity::CRITICAL;
}
// High - Network commands
if (preg_match('/\b(wget|curl|nc|netcat|ssh|ftp)\b/i', $match)) {
return DetectionSeverity::HIGH;
}
// High - Command substitution
if (preg_match('/(\$\(|\`|`|\${)/', $pattern)) {
return DetectionSeverity::HIGH;
}
// High - Process management
if (preg_match('/\b(ps|kill|killall|top|htop)\b/i', $match)) {
return DetectionSeverity::HIGH;
}
// Medium - File operations
if (preg_match('/\b(cat|ls|find|grep|head|tail)\b/i', $match)) {
return DetectionSeverity::MEDIUM;
}
// Medium - Single command separators
if (preg_match('/[;&|]/', $match)) {
return DetectionSeverity::MEDIUM;
}
return DetectionSeverity::LOW;
}
/**
* Calculate risk score
*/
private function calculateRiskScore(string $pattern, string $location, string $input): float
{
$baseScore = 75.0;
// Location-based risk adjustment
if ($location === 'request path') {
$baseScore += 10.0;
} elseif (str_contains($location, 'POST parameter')) {
$baseScore += 5.0;
}
// Pattern-based risk adjustment
if (preg_match('/\b(sudo|su|rm|passwd)\b/i', $input)) {
$baseScore += 20.0;
}
if (preg_match('/[;&|`]{2,}/', $input)) {
$baseScore += 15.0;
}
if (preg_match('/(\$\(|\`|`|\${)/', $input)) {
$baseScore += 10.0;
}
// Multiple command indicators
$commandCount = preg_match_all('/\b(ls|cat|pwd|whoami|wget|curl)\b/i', $input);
if ($commandCount > 1) {
$baseScore += ($commandCount * 5);
}
return min(100.0, $baseScore);
}
/**
* Identify command type for classification
*/
private function identifyCommandType(string $pattern, string $match): string
{
if (preg_match('/\b(ls|cat|pwd|find|grep)\b/i', $match)) {
return 'file_operations';
}
if (preg_match('/\b(wget|curl|nc|ssh|ftp)\b/i', $match)) {
return 'network_operations';
}
if (preg_match('/\b(ps|kill|top|htop|service)\b/i', $match)) {
return 'process_management';
}
if (preg_match('/\b(sudo|su|passwd|useradd)\b/i', $match)) {
return 'system_administration';
}
if (preg_match('/[;&|`]+/', $pattern)) {
return 'command_chaining';
}
if (preg_match('/(\$\(|\`|\${)/', $pattern)) {
return 'command_substitution';
}
return 'general_command';
}
/**
* Analyze additional risk factors
*/
private function analyzeRiskFactors(string $input): array
{
$factors = [];
if (preg_match('/[;&|`]{2,}/', $input)) {
$factors[] = 'multiple_command_separators';
}
if (preg_match('/\b(sudo|su)\b/i', $input)) {
$factors[] = 'privilege_escalation';
}
if (preg_match('/(\$\w+|%\w+%)/', $input)) {
$factors[] = 'environment_variables';
}
if (preg_match('/[<>]+/', $input)) {
$factors[] = 'io_redirection';
}
if (preg_match('/%[0-9a-f]{2}/i', $input)) {
$factors[] = 'encoded_content';
}
return $factors;
}
public function isEnabled(): bool
{
return $this->enabled;
}
public function isHealthy(): bool
{
return true;
}
public function getPriority(): int
{
return $this->config->get('priority', 98);
}
public function getConfidenceLevel(): Percentage
{
return Percentage::from($this->config->get('confidence', 0.92) * 100);
}
public function getTimeoutThreshold(): Duration
{
return Duration::fromMilliseconds($this->config->get('timeout', 40));
}
public function configure(LayerConfig $config): void
{
$this->config = $config;
}
public function getConfig(): LayerConfig
{
return $this->config;
}
public function getMetrics(): LayerMetrics
{
return $this->metrics;
}
public function reset(): void
{
$this->metrics = new LayerMetrics();
}
public function warmUp(): void
{
// Pre-compile regex patterns if needed
}
public function shutdown(): void
{
// Cleanup resources
}
public function getDependencies(): array
{
return [];
}
public function supportsParallelProcessing(): bool
{
return true;
}
public function getVersion(): string
{
return '1.0.0';
}
public function getSupportedCategories(): array
{
return [DetectionCategory::COMMAND_INJECTION, DetectionCategory::INJECTION];
}
}

View File

@@ -0,0 +1,362 @@
<?php
declare(strict_types=1);
namespace App\Framework\Waf\Layers;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\DateTime\Clock;
use App\Framework\Http\Request;
use App\Framework\RateLimit\RateLimiter;
use App\Framework\Waf\DetectionCategory;
use App\Framework\Waf\DetectionSeverity;
use App\Framework\Waf\LayerResult;
use App\Framework\Waf\LayerStatus;
use App\Framework\Waf\ValueObjects\Detection;
use App\Framework\Waf\ValueObjects\LayerConfig;
use App\Framework\Waf\ValueObjects\LayerMetrics;
/**
* Intelligent Rate Limiting WAF Layer
*
* Integrates advanced WAF threat analysis with the existing RateLimit framework.
* Uses the enhanced RateLimiter with traffic pattern analysis, burst detection,
* and baseline deviation tracking for sophisticated DDoS protection.
*/
final class IntelligentRateLimitLayer implements LayerInterface
{
private LayerConfig $config;
private LayerMetrics $metrics;
// Rate limiting tiers for different threat levels
private const array RATE_LIMITS = [
'normal' => ['limit' => 100, 'window' => 60], // 100 req/min
'elevated' => ['limit' => 50, 'window' => 60], // 50 req/min
'high' => ['limit' => 20, 'window' => 60], // 20 req/min
'critical' => ['limit' => 5, 'window' => 60], // 5 req/min
];
public function __construct(
private readonly RateLimiter $rateLimiter,
private readonly Clock $clock
) {
$this->config = new LayerConfig(
enabled: true,
timeout: Duration::fromMilliseconds(100),
confidenceThreshold: Percentage::from(85.0),
blockingMode: true,
logDetections: true,
maxDetectionsPerRequest: 5,
clock: $this->clock
);
$this->metrics = LayerMetrics::empty($this->clock);
}
public function getName(): string
{
return 'intelligent_rate_limit';
}
public function analyze(Request $request): LayerResult
{
$startTime = microtime(true);
$detections = [];
try {
// Extract request context for intelligent analysis
$requestContext = $this->buildRequestContext($request);
$clientIp = $requestContext['client_ip'];
$rateLimitKey = "ip:{$clientIp}";
// Determine appropriate rate limit based on request characteristics
$rateLimitConfig = $this->selectRateLimitTier($requestContext);
// Perform intelligent rate limit check with threat analysis
$rateLimitResult = $this->rateLimiter->checkLimitWithAnalysis(
$rateLimitKey,
$rateLimitConfig['limit'],
$rateLimitConfig['window'],
$requestContext
);
// Process results and create detections
if (! $rateLimitResult->isAllowed()) {
$detections[] = $this->createRateLimitDetection($rateLimitResult, $requestContext);
}
// Check for suspicious patterns even if rate limit not exceeded
if ($rateLimitResult->isAttackSuspected()) {
$detections[] = $this->createSuspiciousPatternDetection($rateLimitResult, $requestContext);
}
// Check for anomalous traffic patterns
if ($rateLimitResult->hasAnomalousTraffic()) {
$detections[] = $this->createAnomalyDetection($rateLimitResult, $requestContext);
}
$processingTime = Duration::fromMilliseconds((microtime(true) - $startTime) * 1000);
// Determine overall result
if (! empty($detections)) {
$threatLevel = $rateLimitResult->getThreatLevel();
$status = $this->shouldBlock($threatLevel) ? LayerStatus::THREAT_DETECTED : LayerStatus::SUSPICIOUS;
return LayerResult::threat(
$this->getName(),
$this->buildThreatMessage($rateLimitResult, $threatLevel),
$status,
$detections,
$processingTime
);
}
return LayerResult::clean(
$this->getName(),
'Traffic patterns within normal parameters',
$processingTime
);
} catch (\Throwable $e) {
$processingTime = Duration::fromMilliseconds((microtime(true) - $startTime) * 1000);
return LayerResult::error(
$this->getName(),
'Intelligent rate limit analysis failed: ' . $e->getMessage(),
$processingTime
);
}
}
/**
* Build comprehensive request context for analysis
*/
private function buildRequestContext(Request $request): array
{
return [
'client_ip' => $request->server->getRemoteAddr(),
'request_path' => $request->path,
'user_agent' => $request->headers->getFirst('User-Agent') ?? '',
'request_method' => $request->method->value,
'query_params' => $request->queryParams,
'content_length' => $request->headers->getFirst('Content-Length') ?? '0',
'timestamp' => time(),
];
}
/**
* Select appropriate rate limit tier based on request characteristics
*/
private function selectRateLimitTier(array $requestContext): array
{
$userAgent = strtolower($requestContext['user_agent']);
$path = $requestContext['request_path'];
// Critical paths get stricter limits
if (str_contains($path, '/admin/') || str_contains($path, '/api/')) {
return self::RATE_LIMITS['elevated'];
}
// Known malicious user agents get strict limits
$maliciousPatterns = ['sqlmap', 'nikto', 'scanner', 'bot', 'crawler'];
foreach ($maliciousPatterns as $pattern) {
if (str_contains($userAgent, $pattern)) {
return self::RATE_LIMITS['critical'];
}
}
// Resource-intensive operations get elevated limits
if (str_contains($path, '/search') || str_contains($path, '/export')) {
return self::RATE_LIMITS['elevated'];
}
return self::RATE_LIMITS['normal'];
}
/**
* Create detection for rate limit exceeded
*/
private function createRateLimitDetection($rateLimitResult, array $context): Detection
{
$threatLevel = $rateLimitResult->getThreatLevel();
$severity = match ($threatLevel) {
'critical' => DetectionSeverity::CRITICAL,
'high' => DetectionSeverity::HIGH,
'medium' => DetectionSeverity::MEDIUM,
default => DetectionSeverity::LOW
};
return new Detection(
category: DetectionCategory::RATE_LIMITING,
severity: $severity,
message: sprintf(
'Rate limit exceeded: %d/%d requests from %s (threat level: %s)',
$rateLimitResult->getCurrent(),
$rateLimitResult->getLimit(),
$context['client_ip'],
$threatLevel
),
confidence: Percentage::from(95.0),
location: 'request_rate',
timestamp: null,
context: null
);
}
/**
* Create detection for suspicious attack patterns
*/
private function createSuspiciousPatternDetection($rateLimitResult, array $context): Detection
{
$attackPatterns = implode(', ', $rateLimitResult->attackPatterns);
return new Detection(
category: DetectionCategory::SUSPICIOUS_BEHAVIOR,
severity: DetectionSeverity::HIGH,
message: sprintf(
'Suspicious attack patterns detected from %s: %s',
$context['client_ip'],
$attackPatterns
),
confidence: Percentage::from(85.0),
location: 'traffic_analysis',
timestamp: null,
context: null
);
}
/**
* Create detection for traffic anomalies
*/
private function createAnomalyDetection($rateLimitResult, array $context): Detection
{
return new Detection(
category: DetectionCategory::ANOMALY,
severity: DetectionSeverity::MEDIUM,
message: sprintf(
'Anomalous traffic pattern from %s: %.2f standard deviations from baseline',
$context['client_ip'],
$rateLimitResult->baselineDeviation ?? 0.0
),
confidence: Percentage::from(75.0),
location: 'baseline_analysis',
timestamp: null,
context: null
);
}
/**
* Determine if threat level warrants blocking
*/
private function shouldBlock(string $threatLevel): bool
{
return in_array($threatLevel, ['critical', 'high']);
}
/**
* Build descriptive threat message
*/
private function buildThreatMessage($rateLimitResult, string $threatLevel): string
{
$messages = ['Intelligent rate limiting detected threat'];
if (! $rateLimitResult->isAllowed()) {
$messages[] = 'rate limit exceeded';
}
if ($rateLimitResult->isAttackSuspected()) {
$messages[] = 'attack patterns identified';
}
if ($rateLimitResult->hasAnomalousTraffic()) {
$messages[] = 'traffic anomalies detected';
}
$messages[] = "threat level: {$threatLevel}";
return implode(', ', $messages);
}
// ===== LayerInterface Implementation =====
public function isEnabled(): bool
{
return $this->config->enabled;
}
public function isHealthy(): bool
{
return true;
}
public function getPriority(): int
{
return 200; // High priority for rate limiting
}
public function getConfidenceLevel(): Percentage
{
return $this->config->getEffectiveConfidenceThreshold();
}
public function getTimeoutThreshold(): Duration
{
return $this->config->getEffectiveTimeout();
}
public function configure(LayerConfig $config): void
{
$this->config = $config;
}
public function getConfig(): LayerConfig
{
return $this->config;
}
public function getMetrics(): LayerMetrics
{
return $this->metrics;
}
public function reset(): void
{
$this->metrics = LayerMetrics::empty($this->clock);
}
public function warmUp(): void
{
// No warmup needed
}
public function shutdown(): void
{
// No cleanup needed
}
public function getDependencies(): array
{
return [RateLimiter::class, Clock::class];
}
public function supportsParallelProcessing(): bool
{
return true;
}
public function getVersion(): string
{
return '1.0.0';
}
public function getSupportedCategories(): array
{
return [
DetectionCategory::RATE_LIMITING,
DetectionCategory::SUSPICIOUS_BEHAVIOR,
DetectionCategory::ANOMALY,
];
}
}

View File

@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace App\Framework\Waf\Layers;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Http\Request;
use App\Framework\Waf\LayerResult;
use App\Framework\Waf\ValueObjects\LayerConfig;
use App\Framework\Waf\ValueObjects\LayerMetrics;
/**
* Interface for all WAF security layers
* Each layer analyzes requests and returns threat assessment
*/
interface LayerInterface
{
/**
* Get unique layer name for identification
*/
public function getName(): string;
/**
* Analyze request and return threat assessment
*/
public function analyze(Request $request): LayerResult;
/**
* Check if layer is enabled and operational
*/
public function isEnabled(): bool;
/**
* Check layer health status
*/
public function isHealthy(): bool;
/**
* Get layer priority (higher = processed first)
* Allows for dependency-based processing order
*/
public function getPriority(): int;
/**
* Get layer confidence level in its assessments
* Used for weighted threat scoring
*/
public function getConfidenceLevel(): Percentage;
/**
* Get maximum processing time before timeout
*/
public function getTimeoutThreshold(): Duration;
/**
* Configure layer at runtime
* Allows dynamic reconfiguration without restart
*/
public function configure(LayerConfig $config): void;
/**
* Get current layer configuration
*/
public function getConfig(): LayerConfig;
/**
* Get layer performance metrics
*/
public function getMetrics(): LayerMetrics;
/**
* Reset layer state (for testing/debugging)
*/
public function reset(): void;
/**
* Warm up layer (preload rules, caches, etc.)
* Called during WAF initialization
*/
public function warmUp(): void;
/**
* Clean up layer resources
* Called during WAF shutdown
*/
public function shutdown(): void;
/**
* Get layer dependencies (other layers this depends on)
* Used for proper initialization order
*/
public function getDependencies(): array;
/**
* Check if layer supports parallel processing
*/
public function supportsParallelProcessing(): bool;
/**
* Get layer version for compatibility checking
*/
public function getVersion(): string;
/**
* Get supported threat categories
* Returns array of DetectionCategory enums this layer can detect
*/
public function getSupportedCategories(): array;
}

View File

@@ -0,0 +1,387 @@
<?php
declare(strict_types=1);
namespace App\Framework\Waf\Layers;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Http\Request;
use App\Framework\Waf\DetectionCategory;
use App\Framework\Waf\DetectionSeverity;
use App\Framework\Waf\LayerResult;
use App\Framework\Waf\LayerStatus;
use App\Framework\Waf\ValueObjects\Detection;
use App\Framework\Waf\ValueObjects\LayerConfig;
use App\Framework\Waf\ValueObjects\LayerMetrics;
/**
* Path Traversal Detection Layer
*
* Detects directory traversal and path manipulation attempts.
* Protects against unauthorized file access and system disclosure.
*/
final class PathTraversalLayer implements LayerInterface
{
private LayerConfig $config;
private LayerMetrics $metrics;
private bool $enabled = true;
/** Path traversal patterns */
private const PATH_TRAVERSAL_PATTERNS = [
// Classic directory traversal
'/\.\.\//',
'/\.\.\\\/',
'/\.\.\\\\/',
// Encoded versions
'/%2e%2e%2f/',
'/%2e%2e%5c/',
'/%252e%252e%252f/',
'/\.\.\%2f/',
'/\.\.\%5c/',
// Unicode encoded (using hex notation instead of \u)
'/\x{002e}\x{002e}\x{002f}/u',
'/\x{ff0e}\x{ff0e}\x{ff0f}/u',
// Alternative representations
'/\.\.%252f/',
'/\.\.%c0%af/',
'/\.\.%c1%9c/',
// Null byte injection
'/\.\.\/.*\x00/',
'/\.\.\\\\.*\x00/',
// System file access attempts
'/\/etc\/passwd/',
'/\/etc\/shadow/',
'/\/etc\/hosts/',
'/\/proc\/version/',
'/\/proc\/self\/environ/',
'/\/windows\/system32\//',
'/\/winnt\/system32\//',
// Common sensitive files
'/\/etc\/.*/',
'/\/var\/log\/.*/',
'/\/root\/.*/',
'/\/home\/.*\/\.\w+/',
// Web application files
'/\/web\.config/',
'/\/app\.config/',
'/\/\.htaccess/',
'/\/\.htpasswd/',
'/\/wp-config\.php/',
'/\/configuration\.php/',
'/\/config\.inc\.php/',
// Backup and temporary files
'/.*\.bak$/',
'/.*\.backup$/',
'/.*\.old$/',
'/.*\.tmp$/',
'/.*~$/',
'/.*\.swp$/',
// Source code disclosure
'/.*\.php\.bak/',
'/.*\.asp\.old/',
'/.*\.jsp\.tmp/',
];
public function __construct()
{
$this->config = new LayerConfig(
enabled: true,
timeout: Duration::fromMilliseconds(30),
confidenceThreshold: Percentage::from(95.0),
blockingMode: true,
logDetections: true,
maxDetectionsPerRequest: 5
);
$this->metrics = new LayerMetrics();
}
public function getName(): string
{
return 'path_traversal';
}
public function analyze(Request $request): LayerResult
{
$startTime = microtime(true);
$detections = [];
try {
// Check query parameters
foreach ($request->queryParams as $key => $value) {
if (is_string($value)) {
$detection = $this->analyzeString($value, "query parameter '{$key}'");
if ($detection) {
$detections[] = $detection;
}
}
}
// Check POST data
if (isset($request->parsedBody->data) && is_array($request->parsedBody->data)) {
foreach ($request->parsedBody->data as $key => $value) {
if (is_string($value)) {
$detection = $this->analyzeString($value, "POST parameter '{$key}'");
if ($detection) {
$detections[] = $detection;
}
}
}
}
// Check request path (very important for path traversal)
$detection = $this->analyzeString($request->path, 'request path');
if ($detection) {
$detections[] = $detection;
}
// Check specific headers that might contain file paths
$pathHeaders = ['Referer', 'X-Original-URL', 'X-Rewrite-URL'];
foreach ($pathHeaders as $headerName) {
$headerValue = $request->headers->getFirst($headerName);
if ($headerValue) {
$detection = $this->analyzeString($headerValue, "header '{$headerName}'");
if ($detection) {
$detections[] = $detection;
}
}
}
$processingTime = Duration::fromMilliseconds((microtime(true) - $startTime) * 1000);
if (! empty($detections)) {
return LayerResult::threat(
$this->getName(),
'Path traversal attempt detected',
LayerStatus::THREAT_DETECTED,
$detections,
$processingTime
);
}
return LayerResult::clean(
$this->getName(),
'No path traversal patterns detected',
$processingTime
);
} catch (\Throwable $e) {
$processingTime = Duration::fromMilliseconds((microtime(true) - $startTime) * 1000);
return LayerResult::error(
$this->getName(),
'Path traversal analysis failed: ' . $e->getMessage(),
$processingTime
);
}
}
/**
* Analyze string for path traversal patterns
*/
private function analyzeString(string $input, string $location): ?Detection
{
$originalInput = $input;
// Multiple decoding passes to catch encoded attempts
$input = urldecode($input);
$input = urldecode($input); // Second pass for double encoding
foreach (self::PATH_TRAVERSAL_PATTERNS as $pattern) {
if (preg_match($pattern, $input, $matches)) {
$severity = $this->calculateSeverity($pattern, $matches[0] ?? '');
$riskScore = $this->calculateRiskScore($pattern, $location);
return new Detection(
category: DetectionCategory::PATH_TRAVERSAL,
severity: $severity,
message: "Path traversal pattern detected in {$location}",
details: [
'location' => $location,
'pattern' => $pattern,
'matched_text' => $matches[0] ?? '',
'input_length' => strlen($originalInput),
'decoded_input' => substr($input, 0, 200),
'original_input' => substr($originalInput, 0, 200),
'threat_type' => $this->identifyThreatType($pattern),
],
confidence: 0.9,
riskScore: $riskScore
);
}
}
return null;
}
/**
* Calculate severity based on pattern and context
*/
private function calculateSeverity(string $pattern, string $match): DetectionSeverity
{
// Critical - System file access
if (preg_match('/\/(etc|proc|root|windows|winnt)\//', $pattern)) {
return DetectionSeverity::CRITICAL;
}
// Critical - Sensitive application files
if (preg_match('/(passwd|shadow|config|htaccess|htpasswd)/', $pattern)) {
return DetectionSeverity::CRITICAL;
}
// High - Directory traversal with sensitive extensions
if (preg_match('/\.\.(\/|\\\\).*\.(php|asp|jsp|config)/', $match)) {
return DetectionSeverity::HIGH;
}
// High - Multiple directory traversals
if (substr_count($match, '..') > 2) {
return DetectionSeverity::HIGH;
}
// Medium - Basic directory traversal
return DetectionSeverity::MEDIUM;
}
/**
* Calculate risk score based on pattern and location
*/
private function calculateRiskScore(string $pattern, string $location): float
{
$baseScore = 70.0;
// Higher risk in request path
if ($location === 'request path') {
$baseScore += 15.0;
}
// System files are highest risk
if (preg_match('/\/(etc|proc|root|windows)\//', $pattern)) {
$baseScore += 20.0;
}
// Config files are high risk
if (preg_match('/(config|htaccess|passwd)/', $pattern)) {
$baseScore += 15.0;
}
// Encoded attempts show deliberate evasion
if (preg_match('/%[0-9a-f]{2}/', $pattern)) {
$baseScore += 10.0;
}
return min(100.0, $baseScore);
}
/**
* Identify the type of threat based on pattern
*/
private function identifyThreatType(string $pattern): string
{
if (preg_match('/\/(etc|proc)\//', $pattern)) {
return 'system_file_access';
}
if (preg_match('/(config|htaccess|passwd)/', $pattern)) {
return 'configuration_disclosure';
}
if (preg_match('/\.(bak|backup|old|tmp)/', $pattern)) {
return 'backup_file_access';
}
if (preg_match('/\.\./', $pattern)) {
return 'directory_traversal';
}
return 'file_access_attempt';
}
public function isEnabled(): bool
{
return $this->enabled;
}
public function isHealthy(): bool
{
return true;
}
public function getPriority(): int
{
return $this->config->get('priority', 95);
}
public function getConfidenceLevel(): Percentage
{
return Percentage::from($this->config->get('confidence', 0.95) * 100);
}
public function getTimeoutThreshold(): Duration
{
return Duration::fromMilliseconds($this->config->get('timeout', 30));
}
public function configure(LayerConfig $config): void
{
$this->config = $config;
}
public function getConfig(): LayerConfig
{
return $this->config;
}
public function getMetrics(): LayerMetrics
{
return $this->metrics;
}
public function reset(): void
{
$this->metrics = new LayerMetrics();
}
public function warmUp(): void
{
// Pre-compile regex patterns if needed
}
public function shutdown(): void
{
// Cleanup resources
}
public function getDependencies(): array
{
return [];
}
public function supportsParallelProcessing(): bool
{
return true;
}
public function getVersion(): string
{
return '1.0.0';
}
public function getSupportedCategories(): array
{
return [DetectionCategory::PATH_TRAVERSAL, DetectionCategory::BROKEN_ACCESS_CONTROL];
}
}

View File

@@ -0,0 +1,292 @@
<?php
declare(strict_types=1);
namespace App\Framework\Waf\Layers;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Http\Request;
use App\Framework\Waf\DetectionCategory;
use App\Framework\Waf\DetectionSeverity;
use App\Framework\Waf\LayerResult;
use App\Framework\Waf\LayerStatus;
use App\Framework\Waf\ValueObjects\Detection;
use App\Framework\Waf\ValueObjects\LayerConfig;
use App\Framework\Waf\ValueObjects\LayerMetrics;
/**
* SQL Injection Detection Layer
*
* Detects SQL injection attempts in request parameters, headers, and body.
* Uses pattern matching and heuristic analysis to identify potential SQL injection attacks.
*/
final class SqlInjectionLayer implements LayerInterface
{
private LayerConfig $config;
private LayerMetrics $metrics;
private bool $enabled = true;
/** SQL Injection patterns (simplified for initial implementation) */
private const SQL_PATTERNS = [
// Classic injection patterns
'/(\bunion\s+select\b)/i',
'/(\bselect\b.*\bfrom\b)/i',
'/(\binsert\s+into\b)/i',
'/(\bupdate\s+.*\bset\b)/i',
'/(\bdelete\s+from\b)/i',
'/(\bdrop\s+(table|database)\b)/i',
// SQL comment indicators
'/(--|\#|\/\*|\*\/)/i',
// SQL operators and functions
'/(\bor\s+1\s*=\s*1\b)/i',
'/(\band\s+1\s*=\s*1\b)/i',
'/(\bor\s+\'[^\']*\'\s*=\s*\'[^\']*\'\b)/i',
'/(\bunion\s+all\s+select\b)/i',
// SQL string manipulation
'/(\bconcat\s*\()/i',
'/(\bchar\s*\()/i',
'/(\bhex\s*\()/i',
'/(\bascii\s*\()/i',
// Database-specific functions
'/(\bversion\s*\(\))/i',
'/(\buser\s*\(\))/i',
'/(\bdatabase\s*\(\))/i',
'/(\bsleep\s*\()/i',
'/(\bbenchmark\s*\()/i',
// Blind injection patterns
'/(\bwaitfor\s+delay\b)/i',
'/(\bif\s*\(.*,.*,.*\))/i',
// Quote and escape sequences
'/([\'\"]\s*;\s*)/i',
'/(\\\x[0-9a-f]{2})/i',
];
public function __construct()
{
$this->config = new LayerConfig(
enabled: true,
timeout: Duration::fromMilliseconds(50),
confidenceThreshold: Percentage::from(95.0),
blockingMode: true,
logDetections: true,
maxDetectionsPerRequest: 10
);
$this->metrics = new LayerMetrics();
}
public function getName(): string
{
return 'sql_injection';
}
public function analyze(Request $request): LayerResult
{
$startTime = microtime(true);
$detections = [];
// Debug logging
file_put_contents('/tmp/waf_debug.log', "SqlInjectionLayer: Starting analysis for path: " . ($request->path ?? '/') . "\n", FILE_APPEND);
file_put_contents('/tmp/waf_debug.log', "SqlInjectionLayer: Query params: " . json_encode($request->queryParams) . "\n", FILE_APPEND);
try {
// Check query parameters
foreach ($request->queryParams as $key => $value) {
if (is_string($value)) {
file_put_contents('/tmp/waf_debug.log', "SqlInjectionLayer: Analyzing query param '{$key}' = '{$value}'\n", FILE_APPEND);
$detection = $this->analyzeString($value, "query parameter '{$key}'");
if ($detection) {
file_put_contents('/tmp/waf_debug.log', "SqlInjectionLayer: DETECTION FOUND in query param '{$key}'!\n", FILE_APPEND);
$detections[] = $detection;
}
}
}
// Check POST data
if (isset($request->parsedBody->data) && is_array($request->parsedBody->data)) {
foreach ($request->parsedBody->data as $key => $value) {
if (is_string($value)) {
$detection = $this->analyzeString($value, "POST parameter '{$key}'");
if ($detection) {
$detections[] = $detection;
}
}
}
}
// Check headers (common injection points)
$suspiciousHeaders = ['User-Agent', 'Referer', 'X-Forwarded-For', 'Cookie'];
foreach ($suspiciousHeaders as $headerName) {
$headerValue = $request->headers->getFirst($headerName);
if ($headerValue) {
$detection = $this->analyzeString($headerValue, "header '{$headerName}'");
if ($detection) {
$detections[] = $detection;
}
}
}
// Check request path
$detection = $this->analyzeString($request->path, 'request path');
if ($detection) {
$detections[] = $detection;
}
$processingTime = Duration::fromMilliseconds((microtime(true) - $startTime) * 1000);
if (! empty($detections)) {
file_put_contents('/tmp/waf_debug.log', "SqlInjectionLayer: Returning THREAT result with " . count($detections) . " detections\n", FILE_APPEND);
return LayerResult::threat(
$this->getName(),
'SQL injection attempt detected',
LayerStatus::THREAT_DETECTED,
$detections,
$processingTime
);
}
return LayerResult::clean(
$this->getName(),
'No SQL injection patterns detected',
$processingTime
);
} catch (\Throwable $e) {
$processingTime = Duration::fromMilliseconds((microtime(true) - $startTime) * 1000);
return LayerResult::error(
$this->getName(),
'SQL injection analysis failed: ' . $e->getMessage(),
$processingTime
);
}
}
/**
* Analyze string for SQL injection patterns
*/
private function analyzeString(string $input, string $location): ?Detection
{
$originalInput = $input;
$input = urldecode($input); // Decode URL encoding
$input = html_entity_decode($input, ENT_QUOTES | ENT_HTML5); // Decode HTML entities
file_put_contents('/tmp/waf_debug.log', "SqlInjectionLayer: analyzeString - original: '{$originalInput}', decoded: '{$input}'\n", FILE_APPEND);
foreach (self::SQL_PATTERNS as $pattern) {
file_put_contents('/tmp/waf_debug.log', "SqlInjectionLayer: Testing pattern: {$pattern}\n", FILE_APPEND);
if (preg_match($pattern, $input, $matches)) {
file_put_contents('/tmp/waf_debug.log', "SqlInjectionLayer: PATTERN MATCH! Pattern: {$pattern}, Match: " . ($matches[0] ?? '') . "\n", FILE_APPEND);
try {
$detection = new Detection(
category: DetectionCategory::SQL_INJECTION,
severity: DetectionSeverity::CRITICAL,
message: "SQL injection pattern detected in {$location}: " . ($matches[0] ?? ''),
ruleId: null,
confidence: Percentage::from(90.0),
payload: null,
location: $location,
timestamp: null, // Will be set automatically by static methods if needed
context: null
);
file_put_contents('/tmp/waf_debug.log', "SqlInjectionLayer: Created Detection object, returning it\n", FILE_APPEND);
return $detection;
} catch (\Throwable $e) {
file_put_contents('/tmp/waf_debug.log', "SqlInjectionLayer: ERROR creating Detection: " . $e->getMessage() . "\n", FILE_APPEND);
// Continue to next pattern
}
}
}
file_put_contents('/tmp/waf_debug.log', "SqlInjectionLayer: No patterns matched for input: '{$input}'\n", FILE_APPEND);
return null;
}
public function isEnabled(): bool
{
return $this->enabled;
}
public function isHealthy(): bool
{
return true; // Simple implementation - always healthy
}
public function getPriority(): int
{
return 100; // High priority for SQL injection detection
}
public function getConfidenceLevel(): Percentage
{
return $this->config->getEffectiveConfidenceThreshold();
}
public function getTimeoutThreshold(): Duration
{
return $this->config->getEffectiveTimeout();
}
public function configure(LayerConfig $config): void
{
$this->config = $config;
}
public function getConfig(): LayerConfig
{
return $this->config;
}
public function getMetrics(): LayerMetrics
{
return $this->metrics;
}
public function reset(): void
{
$this->metrics = new LayerMetrics();
}
public function warmUp(): void
{
// Pre-compile regex patterns if needed
}
public function shutdown(): void
{
// Cleanup resources
}
public function getDependencies(): array
{
return []; // No dependencies
}
public function supportsParallelProcessing(): bool
{
return true;
}
public function getVersion(): string
{
return '1.0.0';
}
public function getSupportedCategories(): array
{
return [DetectionCategory::SQL_INJECTION, DetectionCategory::INJECTION];
}
}

View File

@@ -0,0 +1,541 @@
<?php
declare(strict_types=1);
namespace App\Framework\Waf\Layers;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Http\Request;
use App\Framework\Waf\DetectionCategory;
use App\Framework\Waf\DetectionSeverity;
use App\Framework\Waf\LayerResult;
use App\Framework\Waf\LayerStatus;
use App\Framework\Waf\ValueObjects\Detection;
use App\Framework\Waf\ValueObjects\LayerConfig;
use App\Framework\Waf\ValueObjects\LayerMetrics;
/**
* Suspicious User Agent Detection Layer
*
* Detects malicious bots, scanners, and suspicious user agents.
* Helps identify automated attacks and reconnaissance attempts.
*/
final class SuspiciousUserAgentLayer implements LayerInterface
{
private LayerConfig $config;
private LayerMetrics $metrics;
private bool $enabled = true;
/** Suspicious User Agent patterns */
private const SUSPICIOUS_PATTERNS = [
// Security scanners and vulnerability assessment tools
'/nikto/i',
'/nessus/i',
'/openvas/i',
'/nmap/i',
'/masscan/i',
'/zmap/i',
'/sqlmap/i',
'/w3af/i',
'/owasp/i',
'/burp/i',
'/dirbuster/i',
'/dirb/i',
'/gobuster/i',
'/ffuf/i',
'/wfuzz/i',
// Web application scanners
'/acunetix/i',
'/appscan/i',
'/webinspect/i',
'/netsparker/i',
'/qualys/i',
'/rapid7/i',
'/veracode/i',
'/checkmarx/i',
// Generic scanner indicators
'/scanner/i',
'/security/i',
'/pentest/i',
'/audit/i',
'/vulnerability/i',
'/exploit/i',
// Command line HTTP clients
'/curl/i',
'/wget/i',
'/http_request/i',
'/lwp-request/i',
'/python-requests/i',
'/python-urllib/i',
'/go-http-client/i',
'/node-fetch/i',
'/axios/i',
// Scraping and crawling bots (malicious ones)
'/scrapy/i',
'/beautifulsoup/i',
'/mechanize/i',
'/selenium/i',
'/phantomjs/i',
'/headless/i',
'/bot/i',
'/spider/i',
'/crawler/i',
// SQL injection tools
'/havij/i',
'/pangolin/i',
'/safe3si/i',
'/sqlninja/i',
'/sqlsus/i',
// XSS and injection tools
'/xsser/i',
'/beef/i',
'/metasploit/i',
'/commix/i',
// Directory traversal and LFI tools
'/fimap/i',
'/dotdotpwn/i',
'/padbuster/i',
// Generic attack tools
'/hydra/i',
'/john/i',
'/hashcat/i',
'/aircrack/i',
'/reaver/i',
// Suspicious patterns
'/\<script/i',
'/javascript/i',
'/vbscript/i',
'/onload/i',
'/onerror/i',
// Empty or very short user agents
'/^$/i',
'/^.{1,3}$/i',
// Suspicious single characters or numbers
'/^[0-9]{1,2}$/i',
'/^[a-z]{1,2}$/i',
'/^[\-_\.]{1,3}$/i',
// Obvious fake user agents
'/mozilla\/1\./i',
'/mozilla\/2\./i',
'/mozilla\/3\./i',
'/msie [1-6]\./i',
// Library and framework indicators in suspicious contexts
'/libwww/i',
'/winhttp/i',
'/java\/1\.[0-4]/i',
// Reconnaissance tools
'/whatweb/i',
'/wappalyzer/i',
'/builtwith/i',
'/httprint/i',
'/webtech/i',
// Load testing tools (potentially abusive)
'/apache-httpclient/i',
'/jmeter/i',
'/siege/i',
'/bombardier/i',
'/wrk/i',
];
/** Legitimate bot patterns to whitelist */
private const LEGITIMATE_BOTS = [
'/googlebot/i',
'/bingbot/i',
'/slurp/i', // Yahoo
'/duckduckbot/i',
'/baiduspider/i',
'/yandexbot/i',
'/facebookexternalhit/i',
'/twitterbot/i',
'/linkedinbot/i',
'/whatsapp/i',
'/telegrambot/i',
'/discordbot/i',
'/slackbot/i',
'/applebot/i',
'/msnbot/i',
'/archive\.org_bot/i',
'/ia_archiver/i',
];
public function __construct()
{
$this->config = new LayerConfig(
enabled: true,
timeout: Duration::fromMilliseconds(20),
confidenceThreshold: Percentage::from(70.0),
blockingMode: false, // Usually just flag suspicious agents
logDetections: true,
maxDetectionsPerRequest: 3
);
$this->metrics = new LayerMetrics();
}
public function getName(): string
{
return 'suspicious_user_agent';
}
public function analyze(Request $request): LayerResult
{
$startTime = microtime(true);
$detections = [];
try {
$userAgent = $request->headers->getFirst('User-Agent', '');
if (empty($userAgent)) {
// Missing User-Agent can be suspicious
$detections[] = new Detection(
category: DetectionCategory::SUSPICIOUS_USER_AGENT,
severity: DetectionSeverity::LOW,
message: 'Missing User-Agent header',
details: [
'user_agent' => '',
'reason' => 'missing_header',
'client_ip' => $request->server->getClientIp()?->value ?? 'unknown',
],
confidence: 0.5,
riskScore: 30.0
);
} else {
// Check for suspicious patterns
$detection = $this->analyzeUserAgent($userAgent);
if ($detection) {
$detections[] = $detection;
}
}
$processingTime = Duration::fromMilliseconds((microtime(true) - $startTime) * 1000);
if (! empty($detections)) {
return LayerResult::threat(
$this->getName(),
'Suspicious User-Agent detected',
LayerStatus::SUSPICIOUS, // Use SUSPICIOUS instead of THREAT_DETECTED for lower severity
$detections,
$processingTime
);
}
return LayerResult::clean(
$this->getName(),
'User-Agent appears legitimate',
$processingTime
);
} catch (\Throwable $e) {
$processingTime = Duration::fromMilliseconds((microtime(true) - $startTime) * 1000);
return LayerResult::error(
$this->getName(),
'User-Agent analysis failed: ' . $e->getMessage(),
$processingTime
);
}
}
/**
* Analyze User-Agent string for suspicious patterns
*/
private function analyzeUserAgent(string $userAgent): ?Detection
{
// First check if it's a legitimate bot
foreach (self::LEGITIMATE_BOTS as $pattern) {
if (preg_match($pattern, $userAgent)) {
return null; // Legitimate bot, no detection
}
}
// Check for suspicious patterns
foreach (self::SUSPICIOUS_PATTERNS as $pattern) {
if (preg_match($pattern, $userAgent, $matches)) {
$threatType = $this->identifyThreatType($pattern, $matches[0] ?? '');
$severity = $this->calculateSeverity($threatType, $userAgent);
$riskScore = $this->calculateRiskScore($threatType, $userAgent);
return new Detection(
category: DetectionCategory::SUSPICIOUS_USER_AGENT,
severity: $severity,
message: "Suspicious User-Agent detected: {$threatType}",
details: [
'user_agent' => $userAgent,
'matched_pattern' => $pattern,
'matched_text' => $matches[0] ?? '',
'threat_type' => $threatType,
'user_agent_length' => strlen($userAgent),
'analysis' => $this->analyzeUserAgentStructure($userAgent),
],
confidence: $this->calculateConfidence($threatType, $userAgent),
riskScore: $riskScore
);
}
}
// Additional heuristic checks
return $this->performHeuristicAnalysis($userAgent);
}
/**
* Identify threat type based on pattern
*/
private function identifyThreatType(string $pattern, string $match): string
{
if (preg_match('/(nikto|nessus|nmap|sqlmap|burp|scanner|security)/i', $match)) {
return 'security_scanner';
}
if (preg_match('/(curl|wget|python|go-http)/i', $match)) {
return 'automated_client';
}
if (preg_match('/(scrapy|bot|spider|crawler)/i', $match)) {
return 'scraping_bot';
}
if (preg_match('/(script|javascript|vbscript|onload)/i', $match)) {
return 'xss_attempt';
}
if (preg_match('/(hydra|john|metasploit)/i', $match)) {
return 'attack_tool';
}
if (preg_match('/^.{1,3}$/i', $match)) {
return 'suspicious_short';
}
if (preg_match('/(mozilla\/[12]\.|msie [1-6]\.)/i', $match)) {
return 'fake_browser';
}
return 'unknown_suspicious';
}
/**
* Calculate severity based on threat type
*/
private function calculateSeverity(string $threatType, string $userAgent): DetectionSeverity
{
return match ($threatType) {
'security_scanner', 'attack_tool', 'xss_attempt' => DetectionSeverity::HIGH,
'automated_client', 'scraping_bot', 'fake_browser' => DetectionSeverity::MEDIUM,
'suspicious_short', 'unknown_suspicious' => DetectionSeverity::LOW,
default => DetectionSeverity::LOW
};
}
/**
* Calculate risk score
*/
private function calculateRiskScore(string $threatType, string $userAgent): float
{
$baseScore = match ($threatType) {
'security_scanner' => 85.0,
'attack_tool' => 90.0,
'xss_attempt' => 80.0,
'automated_client' => 60.0,
'scraping_bot' => 50.0,
'fake_browser' => 45.0,
'suspicious_short' => 35.0,
'unknown_suspicious' => 40.0,
default => 30.0
};
// Adjust based on additional factors
if (strlen($userAgent) < 10) {
$baseScore += 10.0;
}
if (preg_match('/[<>"\']/', $userAgent)) {
$baseScore += 15.0; // HTML/script injection characters
}
if (preg_match('/\b(admin|root|test|hack|exploit)\b/i', $userAgent)) {
$baseScore += 10.0;
}
return min(100.0, $baseScore);
}
/**
* Calculate confidence based on threat type and additional factors
*/
private function calculateConfidence(string $threatType, string $userAgent): float
{
$baseConfidence = match ($threatType) {
'security_scanner', 'attack_tool' => 0.9,
'automated_client', 'xss_attempt' => 0.8,
'scraping_bot', 'fake_browser' => 0.7,
'suspicious_short' => 0.6,
'unknown_suspicious' => 0.5,
default => 0.4
};
// Adjust confidence based on additional evidence
if (preg_match('/[<>"\']/', $userAgent)) {
$baseConfidence += 0.1; // HTML/script characters increase confidence
}
if (strlen($userAgent) < 5) {
$baseConfidence += 0.1; // Very short user agents are more suspicious
}
return min(1.0, $baseConfidence);
}
/**
* Perform heuristic analysis for patterns not caught by regex
*/
private function performHeuristicAnalysis(string $userAgent): ?Detection
{
// Check for extremely long user agents (potential buffer overflow attempts)
if (strlen($userAgent) > 2000) {
return new Detection(
category: DetectionCategory::SUSPICIOUS_USER_AGENT,
severity: DetectionSeverity::MEDIUM,
message: 'Extremely long User-Agent detected',
details: [
'user_agent' => substr($userAgent, 0, 200) . '...',
'user_agent_length' => strlen($userAgent),
'threat_type' => 'oversized_header',
'reason' => 'potential_overflow_attempt',
],
confidence: 0.7,
riskScore: 65.0
);
}
// Check for repeated suspicious characters
if (preg_match('/([<>"\'])\1{5,}/', $userAgent)) {
return new Detection(
category: DetectionCategory::SUSPICIOUS_USER_AGENT,
severity: DetectionSeverity::MEDIUM,
message: 'User-Agent contains repeated suspicious characters',
details: [
'user_agent' => $userAgent,
'threat_type' => 'pattern_anomaly',
'reason' => 'repeated_special_characters',
],
confidence: 0.6,
riskScore: 55.0
);
}
return null;
}
/**
* Analyze User-Agent structure for additional insights
*/
private function analyzeUserAgentStructure(string $userAgent): array
{
return [
'length' => strlen($userAgent),
'has_mozilla' => str_contains(strtolower($userAgent), 'mozilla'),
'has_webkit' => str_contains(strtolower($userAgent), 'webkit'),
'has_version' => preg_match('/\d+\.\d+/', $userAgent) ? true : false,
'special_chars' => preg_match_all('/[<>"\'\(\)\[\]{}]/', $userAgent),
'suspicious_keywords' => preg_match_all('/\b(hack|exploit|attack|scan|test|admin|root)\b/i', $userAgent),
];
}
public function isEnabled(): bool
{
return $this->enabled;
}
public function isHealthy(): bool
{
return true;
}
public function getPriority(): int
{
return $this->config->get('priority', 20);
}
public function getConfidenceLevel(): Percentage
{
return Percentage::from($this->config->get('confidence', 0.7) * 100);
}
public function getTimeoutThreshold(): Duration
{
return Duration::fromMilliseconds($this->config->get('timeout', 20));
}
public function configure(LayerConfig $config): void
{
$this->config = $config;
}
public function getConfig(): LayerConfig
{
return $this->config;
}
public function getMetrics(): LayerMetrics
{
return $this->metrics;
}
public function reset(): void
{
$this->metrics = new LayerMetrics();
}
public function warmUp(): void
{
// Pre-compile regex patterns if needed
}
public function shutdown(): void
{
// Cleanup resources
}
public function getDependencies(): array
{
return [];
}
public function supportsParallelProcessing(): bool
{
return true;
}
public function getVersion(): string
{
return '1.0.0';
}
public function getSupportedCategories(): array
{
return [
DetectionCategory::SUSPICIOUS_USER_AGENT,
DetectionCategory::MALICIOUS_BOT,
DetectionCategory::RECONNAISSANCE,
];
}
}

View File

@@ -0,0 +1,341 @@
<?php
declare(strict_types=1);
namespace App\Framework\Waf\Layers;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Http\Request;
use App\Framework\Waf\DetectionCategory;
use App\Framework\Waf\DetectionSeverity;
use App\Framework\Waf\LayerResult;
use App\Framework\Waf\LayerStatus;
use App\Framework\Waf\ValueObjects\Detection;
use App\Framework\Waf\ValueObjects\LayerConfig;
use App\Framework\Waf\ValueObjects\LayerMetrics;
/**
* Cross-Site Scripting (XSS) Detection Layer
*
* Detects XSS attempts in request parameters, headers, and body.
* Covers reflected, stored, and DOM-based XSS patterns.
*/
final class XssLayer implements LayerInterface
{
private LayerConfig $config;
private LayerMetrics $metrics;
private bool $enabled = true;
/** XSS patterns (simplified for initial implementation) */
private const XSS_PATTERNS = [
// Script tags
'/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/i',
'/<script\b/i',
'/<\/script>/i',
// Event handlers
'/\bon\w+\s*=/i',
'/\bonload\s*=/i',
'/\bonclick\s*=/i',
'/\bonerror\s*=/i',
'/\bonmouseover\s*=/i',
'/\bonfocus\s*=/i',
// JavaScript protocols
'/javascript\s*:/i',
'/vbscript\s*:/i',
'/data\s*:/i',
// HTML entities for script injection
'/&lt;script/i',
'/&gt;&lt;\/script&gt;/i',
'/&#x3c;script/i',
'/&#60;script/i',
// Common XSS vectors
'/<iframe\b/i',
'/<object\b/i',
'/<embed\b/i',
'/<applet\b/i',
'/<meta\b/i',
'/<link\b/i',
'/<style\b/i',
// JavaScript functions
'/alert\s*\(/i',
'/confirm\s*\(/i',
'/prompt\s*\(/i',
'/eval\s*\(/i',
'/setTimeout\s*\(/i',
'/setInterval\s*\(/i',
'/document\.(write|writeln)/i',
'/window\.(location|open)/i',
// Encoded XSS attempts
'/%3Cscript/i',
'/%3C%2Fscript%3E/i',
'/\\\x3cscript/i',
'/\\\u003cscript/i',
// Expression() CSS injection
'/expression\s*\(/i',
'/javascript\s*\:/i',
'/-moz-binding/i',
// SVG XSS vectors
'/<svg\b/i',
'/onload\s*=/i',
'/onclick\s*=/i',
// Base64 encoded attempts (common patterns)
'/PHNjcmlwdA==/i', // <script base64
'/YWxlcnQ=/i', // alert base64
];
public function __construct()
{
$this->config = new LayerConfig(
enabled: true,
timeout: Duration::fromMilliseconds(50),
confidenceThreshold: Percentage::from(90.0),
blockingMode: true,
logDetections: true,
maxDetectionsPerRequest: 10
);
$this->metrics = new LayerMetrics();
}
public function getName(): string
{
return 'xss';
}
public function analyze(Request $request): LayerResult
{
$startTime = microtime(true);
$detections = [];
try {
// Check query parameters
foreach ($request->queryParams as $key => $value) {
if (is_string($value)) {
$detection = $this->analyzeString($value, "query parameter '{$key}'");
if ($detection) {
$detections[] = $detection;
}
}
}
// Check POST data
if (isset($request->parsedBody->data) && is_array($request->parsedBody->data)) {
foreach ($request->parsedBody->data as $key => $value) {
if (is_string($value)) {
$detection = $this->analyzeString($value, "POST parameter '{$key}'");
if ($detection) {
$detections[] = $detection;
}
}
}
}
// Check headers (especially User-Agent and Referer)
$suspiciousHeaders = ['User-Agent', 'Referer', 'X-Forwarded-For', 'Cookie'];
foreach ($suspiciousHeaders as $headerName) {
$headerValue = $request->headers->getFirst($headerName);
if ($headerValue) {
$detection = $this->analyzeString($headerValue, "header '{$headerName}'");
if ($detection) {
$detections[] = $detection;
}
}
}
// Check request path
$detection = $this->analyzeString($request->path, 'request path');
if ($detection) {
$detections[] = $detection;
}
$processingTime = Duration::fromMilliseconds((microtime(true) - $startTime) * 1000);
if (! empty($detections)) {
return LayerResult::threat(
$this->getName(),
'XSS attempt detected',
LayerStatus::THREAT_DETECTED,
$detections,
$processingTime
);
}
return LayerResult::clean(
$this->getName(),
'No XSS patterns detected',
$processingTime
);
} catch (\Throwable $e) {
$processingTime = Duration::fromMilliseconds((microtime(true) - $startTime) * 1000);
return LayerResult::error(
$this->getName(),
'XSS analysis failed: ' . $e->getMessage(),
$processingTime
);
}
}
/**
* Analyze string for XSS patterns
*/
private function analyzeString(string $input, string $location): ?Detection
{
$originalInput = $input;
// Multiple decoding passes to catch double-encoded attempts
$input = urldecode($input);
$input = html_entity_decode($input, ENT_QUOTES | ENT_HTML5);
$input = urldecode($input); // Second pass for double encoding
foreach (self::XSS_PATTERNS as $pattern) {
if (preg_match($pattern, $input, $matches)) {
// Calculate severity based on pattern matched
$severity = $this->calculateSeverity($pattern, $matches[0] ?? '');
return new Detection(
category: DetectionCategory::XSS,
severity: $severity,
message: "XSS pattern detected in {$location}",
details: [
'location' => $location,
'pattern' => $pattern,
'matched_text' => $matches[0] ?? '',
'input_length' => strlen($originalInput),
'decoded_input' => substr($input, 0, 200), // Limit for logging
'original_input' => substr($originalInput, 0, 200),
],
confidence: 0.85,
riskScore: $this->calculateRiskScore($pattern)
);
}
}
return null;
}
/**
* Calculate severity based on XSS pattern
*/
private function calculateSeverity(string $pattern, string $match): DetectionSeverity
{
// Critical patterns
if (preg_match('/(script|eval|setTimeout|setInterval)/i', $pattern)) {
return DetectionSeverity::CRITICAL;
}
// High severity patterns
if (preg_match('/(javascript|vbscript|on\w+|alert|confirm)/i', $pattern)) {
return DetectionSeverity::HIGH;
}
// Medium severity for other patterns
return DetectionSeverity::MEDIUM;
}
/**
* Calculate risk score based on pattern
*/
private function calculateRiskScore(string $pattern): float
{
// Critical patterns get higher scores
if (preg_match('/(script|eval|setTimeout|setInterval)/i', $pattern)) {
return 90.0;
}
if (preg_match('/(javascript|vbscript|on\w+)/i', $pattern)) {
return 80.0;
}
return 70.0;
}
public function isEnabled(): bool
{
return $this->enabled;
}
public function isHealthy(): bool
{
return true;
}
public function getPriority(): int
{
return $this->config->get('priority', 90);
}
public function getConfidenceLevel(): Percentage
{
return Percentage::from($this->config->get('confidence', 0.9) * 100);
}
public function getTimeoutThreshold(): Duration
{
return Duration::fromMilliseconds($this->config->get('timeout', 50));
}
public function configure(LayerConfig $config): void
{
$this->config = $config;
}
public function getConfig(): LayerConfig
{
return $this->config;
}
public function getMetrics(): LayerMetrics
{
return $this->metrics;
}
public function reset(): void
{
$this->metrics = new LayerMetrics();
}
public function warmUp(): void
{
// Pre-compile regex patterns if needed
}
public function shutdown(): void
{
// Cleanup resources
}
public function getDependencies(): array
{
return [];
}
public function supportsParallelProcessing(): bool
{
return true;
}
public function getVersion(): string
{
return '1.0.0';
}
public function getSupportedCategories(): array
{
return [DetectionCategory::XSS, DetectionCategory::INJECTION];
}
}