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,473 @@
<?php
declare(strict_types=1);
namespace App\Framework\Waf\Rules;
use App\Framework\Waf\DetectionCategory;
use App\Framework\Waf\DetectionSeverity;
use App\Framework\Waf\Rules\ValueObjects\RuleCondition;
use App\Framework\Waf\Rules\ValueObjects\RulePattern;
use App\Framework\Waf\ValueObjects\RuleId;
/**
* OWASP ModSecurity Core Rule Set (CRS) implementation
* Based on OWASP CRS v3.3+ patterns and rules
*/
final class OWASPCoreRuleSet
{
/**
* Get all OWASP CRS rules
*/
public static function getAllRules(): array
{
return array_merge(
self::getSqlInjectionRules(),
self::getXssRules(),
self::getPathTraversalRules(),
self::getCommandInjectionRules(),
self::getFileUploadRules(),
self::getUserAgentRules(),
self::getHttpProtocolRules(),
self::getApplicationAttackRules(),
self::getGenericAttackRules()
);
}
/**
* SQL Injection Detection Rules (OWASP Top 10 #3)
*/
public static function getSqlInjectionRules(): array
{
return [
// Basic SQL Injection patterns
new Rule(
id: RuleId::sql('920100'),
name: 'SQL Injection - Union Attack',
description: 'Detects SQL UNION attack patterns',
category: DetectionCategory::SQL_INJECTION,
severity: DetectionSeverity::CRITICAL,
condition: RuleCondition::requestBody(
RulePattern::regex('(?i:union[\s\/\*]+(?:all[\s\/\*]+)?select)', 'i')
),
action: RuleAction::BLOCK,
priority: 95,
tags: ['sql', 'injection', 'union', 'owasp-top10']
),
// SQL meta-characters
new Rule(
id: RuleId::sql('920110'),
name: 'SQL Injection - Meta Characters',
description: 'Detects SQL meta-characters in parameters',
category: DetectionCategory::SQL_INJECTION,
severity: DetectionSeverity::HIGH,
condition: RuleCondition::requestBody(
RulePattern::regex('(?i:[\'\"`][\s]*(?:or|and)[\s]*[\'\"`]*[\s]*(?:[\'\"`]*[\w]+[\'\"`]*[\s]*=[\s]*[\'\"`]*[\w]+|[\d]+[\s]*=[\s]*[\d]+))', 'i')
),
action: RuleAction::BLOCK,
priority: 90,
tags: ['sql', 'injection', 'meta-characters', 'owasp-top10']
),
// SQL comment attacks
new Rule(
id: RuleId::sql('920120'),
name: 'SQL Injection - Comment Attack',
description: 'Detects SQL comment-based injection',
category: DetectionCategory::SQL_INJECTION,
severity: DetectionSeverity::HIGH,
condition: RuleCondition::requestBody(
RulePattern::regex('(?i:(?:--|#|\/\*|\*\/))', 'i')
),
action: RuleAction::SCORE,
priority: 80,
tags: ['sql', 'injection', 'comments']
),
// SQL functions
new Rule(
id: RuleId::sql('920130'),
name: 'SQL Injection - Function Detection',
description: 'Detects SQL functions in input',
category: DetectionCategory::SQL_INJECTION,
severity: DetectionSeverity::HIGH,
condition: RuleCondition::requestBody(
RulePattern::regex('(?i:(?:concat|substring|ascii|char|count|group_concat|version|database|user|current_user|system_user|schema|table_name|column_name)[\s]*\()', 'i')
),
action: RuleAction::BLOCK,
priority: 85,
tags: ['sql', 'injection', 'functions', 'owasp-top10']
),
];
}
/**
* Cross-Site Scripting (XSS) Rules (OWASP Top 10 #7)
*/
public static function getXssRules(): array
{
return [
// Script tag injection
new Rule(
id: RuleId::xss('941100'),
name: 'XSS - Script Tag Attack',
description: 'Detects script tag injection attempts',
category: DetectionCategory::XSS,
severity: DetectionSeverity::HIGH,
condition: RuleCondition::requestBody(
RulePattern::regex('(?i:<script[^>]*>.*?<\/script>)', 'i')
),
action: RuleAction::BLOCK,
priority: 90,
tags: ['xss', 'script', 'owasp-top10']
),
// Event handler injection
new Rule(
id: RuleId::xss('941110'),
name: 'XSS - Event Handler Attack',
description: 'Detects JavaScript event handler injection',
category: DetectionCategory::XSS,
severity: DetectionSeverity::HIGH,
condition: RuleCondition::requestBody(
RulePattern::regex('(?i:on(?:load|error|click|focus|blur|change|submit|reset|select|keydown|keyup|keypress|mouseover|mouseout|mousedown|mouseup|mousemove)[\s]*=)', 'i')
),
action: RuleAction::BLOCK,
priority: 85,
tags: ['xss', 'events', 'owasp-top10']
),
// JavaScript pseudo protocol
new Rule(
id: RuleId::xss('941120'),
name: 'XSS - JavaScript Pseudo Protocol',
description: 'Detects javascript: pseudo protocol usage',
category: DetectionCategory::XSS,
severity: DetectionSeverity::HIGH,
condition: RuleCondition::requestBody(
RulePattern::regex('(?i:javascript[\s]*:)', 'i')
),
action: RuleAction::BLOCK,
priority: 80,
tags: ['xss', 'javascript', 'protocol']
),
// HTML injection
new Rule(
id: RuleId::xss('941130'),
name: 'XSS - HTML Tag Injection',
description: 'Detects potentially dangerous HTML tags',
category: DetectionCategory::XSS,
severity: DetectionSeverity::MEDIUM,
condition: RuleCondition::requestBody(
RulePattern::regex('(?i:<(?:iframe|object|embed|applet|meta|link|style|img|svg|math|details|template)[^>]*>)', 'i')
),
action: RuleAction::SCORE,
priority: 70,
tags: ['xss', 'html', 'tags']
),
];
}
/**
* Path Traversal Rules
*/
public static function getPathTraversalRules(): array
{
return [
// Directory traversal
new Rule(
id: RuleId::pathTraversal('930100'),
name: 'Path Traversal - Directory Traversal',
description: 'Detects directory traversal attempts',
category: DetectionCategory::PATH_TRAVERSAL,
severity: DetectionSeverity::HIGH,
condition: RuleCondition::urlPath(
RulePattern::regex('(?i:(?:\.\.[\\/])|(?:[\\/]\.\.)|(?:\.\.\\\\)|(?:\\\\\.\.)|(?:%2e%2e%2f)|(?:%2e%2e\\\\)|(?:\.\.%2f)|(?:\.\.%5c)|(?:%2e%2e%5c)|(?:%c0%ae%c0%ae%c0%af)|(?:%c1%9c%c1%9c%c1%af))', 'i')
),
action: RuleAction::BLOCK,
priority: 85,
tags: ['path-traversal', 'directory', 'file-access']
),
// Absolute path access
new Rule(
id: RuleId::pathTraversal('930110'),
name: 'Path Traversal - Absolute Path Access',
description: 'Detects absolute path access attempts',
category: DetectionCategory::PATH_TRAVERSAL,
severity: DetectionSeverity::MEDIUM,
condition: RuleCondition::requestBody(
RulePattern::regex('(?i:(?:\/etc\/passwd|\/etc\/shadow|\/etc\/hosts|\/proc\/|\/sys\/|c:[\\\\\\/]|\\\\\\\\))', 'i')
),
action: RuleAction::SCORE,
priority: 75,
tags: ['path-traversal', 'absolute-path', 'system-files']
),
];
}
/**
* Command Injection Rules
*/
public static function getCommandInjectionRules(): array
{
return [
// OS Command injection
new Rule(
id: RuleId::commandInjection('932100'),
name: 'Command Injection - OS Commands',
description: 'Detects OS command injection attempts',
category: DetectionCategory::COMMAND_INJECTION,
severity: DetectionSeverity::CRITICAL,
condition: RuleCondition::requestBody(
RulePattern::regex('(?i:(?:;|\||\|\||&&|&|`|\$\(|\${)[\s]*(?:cat|ls|pwd|id|uname|whoami|ps|kill|rm|mv|cp|chmod|chown|find|grep|awk|sed|sort|head|tail|wc|netstat|ifconfig|ping|wget|curl|nc|telnet|ssh|su|sudo|passwd|shadow|etc\/passwd|etc\/shadow|proc\/))', 'i')
),
action: RuleAction::BLOCK,
priority: 95,
tags: ['command-injection', 'os-injection', 'rce', 'owasp-top10']
),
// Shell metacharacters
new Rule(
id: RuleId::commandInjection('932110'),
name: 'Command Injection - Shell Metacharacters',
description: 'Detects shell metacharacters',
category: DetectionCategory::COMMAND_INJECTION,
severity: DetectionSeverity::HIGH,
condition: RuleCondition::requestBody(
RulePattern::regex('(?i:[;&|`$(){}[\]<>])', 'i')
),
action: RuleAction::SCORE,
priority: 80,
tags: ['command-injection', 'shell', 'metacharacters']
),
];
}
/**
* File Upload Rules
*/
public static function getFileUploadRules(): array
{
return [
// Malicious file extensions
new Rule(
id: RuleId::generic('933100'),
name: 'File Upload - Malicious Extensions',
description: 'Detects potentially malicious file extensions',
category: DetectionCategory::FILE_UPLOAD_ABUSE,
severity: DetectionSeverity::HIGH,
condition: RuleCondition::header(
'Content-Type',
RulePattern::regex('(?i:application\/(?:x-)?(?:php|jsp|asp|exe|bat|cmd|sh|python|perl|ruby|javascript)|text\/(?:x-)?(?:php|jsp|asp|python|perl|ruby))', 'i')
),
action: RuleAction::BLOCK,
priority: 85,
tags: ['file-upload', 'malware', 'webshell']
),
// Double extensions
new Rule(
id: RuleId::generic('933110'),
name: 'File Upload - Double Extensions',
description: 'Detects double file extensions',
category: DetectionCategory::FILE_UPLOAD_ABUSE,
severity: DetectionSeverity::MEDIUM,
condition: RuleCondition::requestBody(
RulePattern::regex('(?i:\.(?:jpg|jpeg|png|gif|bmp|doc|docx|pdf|txt)\.(?:php|jsp|asp|exe|bat|cmd|sh|py|pl|rb|js))', 'i')
),
action: RuleAction::SCORE,
priority: 70,
tags: ['file-upload', 'double-extension', 'bypass']
),
];
}
/**
* User Agent Rules
*/
public static function getUserAgentRules(): array
{
return [
// Security scanners
new Rule(
id: RuleId::generic('913100'),
name: 'User-Agent - Security Scanner Detection',
description: 'Detects known security scanning tools',
category: DetectionCategory::BOT_DETECTION,
severity: DetectionSeverity::MEDIUM,
condition: RuleCondition::userAgent(
RulePattern::regex('(?i:(?:sqlmap|nmap|nikto|w3af|acunetix|nessus|openvas|burp|havij|hydra|metasploit|python-requests|curl|wget))', 'i')
),
action: RuleAction::CHALLENGE,
priority: 70,
tags: ['user-agent', 'scanner', 'tool-detection']
),
// Empty or missing User-Agent
new Rule(
id: RuleId::generic('913110'),
name: 'User-Agent - Missing or Empty',
description: 'Detects missing or empty user agent strings',
category: DetectionCategory::BOT_DETECTION,
severity: DetectionSeverity::LOW,
condition: RuleCondition::userAgent(
RulePattern::regex('^$', '')
),
action: RuleAction::SCORE,
priority: 40,
tags: ['user-agent', 'empty', 'suspicious']
),
];
}
/**
* HTTP Protocol Rules
*/
public static function getHttpProtocolRules(): array
{
return [
// Invalid HTTP methods
new Rule(
id: RuleId::generic('911100'),
name: 'HTTP Protocol - Invalid Method',
description: 'Detects invalid HTTP methods',
category: DetectionCategory::PROTOCOL_ATTACK,
severity: DetectionSeverity::MEDIUM,
condition: RuleCondition::httpMethod(
RulePattern::regex('^(?!GET|POST|HEAD|PUT|DELETE|OPTIONS|PATCH|TRACE|CONNECT).*', 'i')
),
action: RuleAction::BLOCK,
priority: 60,
tags: ['http', 'protocol', 'method', 'invalid']
),
// Oversized request headers
new Rule(
id: RuleId::generic('911110'),
name: 'HTTP Protocol - Oversized Headers',
description: 'Detects oversized HTTP headers',
category: DetectionCategory::PROTOCOL_ATTACK,
severity: DetectionSeverity::MEDIUM,
condition: RuleCondition::header(
'*',
RulePattern::regex('.{8192,}', '') // Headers over 8KB
),
action: RuleAction::BLOCK,
priority: 65,
tags: ['http', 'protocol', 'headers', 'size']
),
];
}
/**
* Application Attack Rules
*/
public static function getApplicationAttackRules(): array
{
return [
// LDAP injection
new Rule(
id: RuleId::generic('950100'),
name: 'Application Attack - LDAP Injection',
description: 'Detects LDAP injection attempts',
category: DetectionCategory::INJECTION_ATTACK,
severity: DetectionSeverity::HIGH,
condition: RuleCondition::requestBody(
RulePattern::regex('(?i:(?:\()(?:&|\|)(?:\(|\)))', 'i')
),
action: RuleAction::BLOCK,
priority: 80,
tags: ['ldap', 'injection', 'application']
),
// XPath injection
new Rule(
id: RuleId::generic('950110'),
name: 'Application Attack - XPath Injection',
description: 'Detects XPath injection attempts',
category: DetectionCategory::INJECTION_ATTACK,
severity: DetectionSeverity::HIGH,
condition: RuleCondition::requestBody(
RulePattern::regex('(?i:(?:\/\/|\.\.|\[@|position\(\)|text\(\)|node\(\)|ancestor|descendant|following|preceding|self))', 'i')
),
action: RuleAction::BLOCK,
priority: 75,
tags: ['xpath', 'injection', 'xml']
),
];
}
/**
* Generic Attack Rules
*/
public static function getGenericAttackRules(): array
{
return [
// Null byte injection
new Rule(
id: RuleId::generic('960100'),
name: 'Generic Attack - Null Byte Injection',
description: 'Detects null byte injection attempts',
category: DetectionCategory::PROTOCOL_ATTACK,
severity: DetectionSeverity::HIGH,
condition: RuleCondition::requestBody(
RulePattern::regex('(?i:%00|\\x00|\0)', 'i')
),
action: RuleAction::BLOCK,
priority: 80,
tags: ['null-byte', 'injection', 'bypass']
),
// Unicode evasion
new Rule(
id: RuleId::generic('960110'),
name: 'Generic Attack - Unicode Evasion',
description: 'Detects Unicode-based evasion attempts',
category: DetectionCategory::EVASION,
severity: DetectionSeverity::MEDIUM,
condition: RuleCondition::requestBody(
RulePattern::regex('(?i:%u[0-9a-f]{4}|\\\\u[0-9a-f]{4})', 'i')
),
action: RuleAction::SCORE,
priority: 60,
tags: ['unicode', 'evasion', 'encoding']
),
];
}
/**
* Get rules by OWASP Top 10 category
*/
public static function getOwaspTop10Rules(): array
{
return array_filter(
self::getAllRules(),
fn (Rule $rule) => $rule->hasTag('owasp-top10')
);
}
/**
* Get critical severity rules only
*/
public static function getCriticalRules(): array
{
return array_filter(
self::getAllRules(),
fn (Rule $rule) => $rule->severity === DetectionSeverity::CRITICAL
);
}
/**
* Get high-priority rules (priority >= 80)
*/
public static function getHighPriorityRules(): array
{
return array_filter(
self::getAllRules(),
fn (Rule $rule) => $rule->priority >= 80
);
}
}

View File

@@ -0,0 +1,413 @@
<?php
declare(strict_types=1);
namespace App\Framework\Waf\Rules;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Waf\DetectionCategory;
use App\Framework\Waf\DetectionSeverity;
use App\Framework\Waf\Rules\ValueObjects\RuleCondition;
use App\Framework\Waf\Rules\ValueObjects\RuleMatch;
use App\Framework\Waf\ValueObjects\RuleId;
/**
* WAF security rule
*/
final readonly class Rule
{
public function __construct(
public RuleId $id,
public string $name,
public string $description,
public DetectionCategory $category,
public DetectionSeverity $severity,
public RuleCondition $condition,
public RuleAction $action,
public bool $enabled = true,
public int $priority = 50,
public ?Percentage $confidenceThreshold = null,
public array $tags = [],
public array $actionParameters = [],
public ?string $version = null,
public ?string $author = null,
public array $metadata = []
) {
}
/**
* Create SQL injection detection rule
*/
public static function sqlInjectionDetection(): self
{
return new self(
id: RuleId::sql('001'),
name: 'SQL Injection Detection',
description: 'Detects common SQL injection patterns in request parameters',
category: DetectionCategory::SQL_INJECTION,
severity: DetectionSeverity::HIGH,
condition: RuleCondition::requestBody(
\App\Framework\Waf\Rules\ValueObjects\RulePattern::sqlInjection()
),
action: RuleAction::BLOCK,
priority: 90,
tags: ['sql', 'injection', 'owasp-top10', 'database'],
version: '1.0',
author: 'WAF Team'
);
}
/**
* Create XSS detection rule
*/
public static function xssDetection(): self
{
return new self(
id: RuleId::xss('001'),
name: 'Cross-Site Scripting Detection',
description: 'Detects XSS attack patterns in request data',
category: DetectionCategory::XSS,
severity: DetectionSeverity::HIGH,
condition: RuleCondition::requestBody(
\App\Framework\Waf\Rules\ValueObjects\RulePattern::xssDetection()
),
action: RuleAction::BLOCK,
priority: 85,
tags: ['xss', 'scripting', 'owasp-top10', 'client-side'],
version: '1.0',
author: 'WAF Team'
);
}
/**
* Create path traversal detection rule
*/
public static function pathTraversalDetection(): self
{
return new self(
id: RuleId::pathTraversal('001'),
name: 'Path Traversal Detection',
description: 'Detects directory traversal attempts',
category: DetectionCategory::PATH_TRAVERSAL,
severity: DetectionSeverity::HIGH,
condition: RuleCondition::urlPath(
\App\Framework\Waf\Rules\ValueObjects\RulePattern::pathTraversal()
),
action: RuleAction::BLOCK,
priority: 80,
tags: ['path-traversal', 'directory-traversal', 'file-access'],
version: '1.0',
author: 'WAF Team'
);
}
/**
* Create command injection detection rule
*/
public static function commandInjectionDetection(): self
{
return new self(
id: RuleId::commandInjection('001'),
name: 'Command Injection Detection',
description: 'Detects OS command injection attempts',
category: DetectionCategory::COMMAND_INJECTION,
severity: DetectionSeverity::CRITICAL,
condition: RuleCondition::requestBody(
\App\Framework\Waf\Rules\ValueObjects\RulePattern::commandInjection()
),
action: RuleAction::BLOCK,
priority: 95,
tags: ['command-injection', 'os-injection', 'rce'],
version: '1.0',
author: 'WAF Team'
);
}
/**
* Create User-Agent validation rule
*/
public static function userAgentValidation(): self
{
return new self(
id: RuleId::generic('UA001'),
name: 'Suspicious User-Agent Detection',
description: 'Detects suspicious or malicious user agent strings',
category: DetectionCategory::BOT_DETECTION,
severity: DetectionSeverity::MEDIUM,
condition: RuleCondition::userAgent(
\App\Framework\Waf\Rules\ValueObjects\RulePattern::regex(
'(?i:(?:sqlmap|nmap|nikto|w3af|acunetix|nessus|openvas|burp|havij|hydra|metasploit|python-requests|curl|wget))',
'i'
)
),
action: RuleAction::CHALLENGE,
priority: 60,
tags: ['user-agent', 'bot', 'scanner', 'tool-detection'],
version: '1.0',
author: 'WAF Team'
);
}
/**
* Create file upload validation rule
*/
public static function fileUploadValidation(): self
{
return new self(
id: RuleId::generic('UP001'),
name: 'Malicious File Upload Detection',
description: 'Detects potentially malicious file uploads',
category: DetectionCategory::FILE_UPLOAD_ABUSE,
severity: DetectionSeverity::HIGH,
condition: RuleCondition::header(
'Content-Type',
\App\Framework\Waf\Rules\ValueObjects\RulePattern::regex(
'(?i:application\/(?:x-)?(?:php|jsp|asp|exe|bat|cmd|sh|python|perl|ruby|javascript)|text\/(?:x-)?(?:php|jsp|asp|python|perl|ruby))',
'i'
)
),
action: RuleAction::BLOCK,
priority: 75,
tags: ['file-upload', 'malware', 'webshell'],
version: '1.0',
author: 'WAF Team'
);
}
/**
* Evaluate rule against request data
*/
public function evaluate(array $requestData): ?RuleMatch
{
if (! $this->enabled) {
return null;
}
if (! $this->condition->evaluate($requestData)) {
return null;
}
$match = RuleMatch::fromCondition(
ruleId: $this->id,
ruleName: $this->name,
category: $this->category,
severity: $this->severity,
condition: $this->condition,
requestData: $requestData,
message: $this->description
);
// Check confidence threshold
if ($this->confidenceThreshold !== null &&
$match->confidence !== null &&
$match->confidence->getValue() < $this->confidenceThreshold->getValue()) {
return null;
}
return $match->withMetadata([
'rule_version' => $this->version,
'rule_author' => $this->author,
'rule_tags' => $this->tags,
'rule_priority' => $this->priority,
'additional_metadata' => $this->metadata,
]);
}
/**
* Enable rule
*/
public function enable(): self
{
return new self(
id: $this->id,
name: $this->name,
description: $this->description,
category: $this->category,
severity: $this->severity,
condition: $this->condition,
action: $this->action,
enabled: true,
priority: $this->priority,
confidenceThreshold: $this->confidenceThreshold,
tags: $this->tags,
actionParameters: $this->actionParameters,
version: $this->version,
author: $this->author,
metadata: $this->metadata
);
}
/**
* Disable rule
*/
public function disable(): self
{
return new self(
id: $this->id,
name: $this->name,
description: $this->description,
category: $this->category,
severity: $this->severity,
condition: $this->condition,
action: $this->action,
enabled: false,
priority: $this->priority,
confidenceThreshold: $this->confidenceThreshold,
tags: $this->tags,
actionParameters: $this->actionParameters,
version: $this->version,
author: $this->author,
metadata: $this->metadata
);
}
/**
* Set confidence threshold
*/
public function withConfidenceThreshold(Percentage $threshold): self
{
return new self(
id: $this->id,
name: $this->name,
description: $this->description,
category: $this->category,
severity: $this->severity,
condition: $this->condition,
action: $this->action,
enabled: $this->enabled,
priority: $this->priority,
confidenceThreshold: $threshold,
tags: $this->tags,
actionParameters: $this->actionParameters,
version: $this->version,
author: $this->author,
metadata: $this->metadata
);
}
/**
* Set priority
*/
public function withPriority(int $priority): self
{
return new self(
id: $this->id,
name: $this->name,
description: $this->description,
category: $this->category,
severity: $this->severity,
condition: $this->condition,
action: $this->action,
enabled: $this->enabled,
priority: $priority,
confidenceThreshold: $this->confidenceThreshold,
tags: $this->tags,
actionParameters: $this->actionParameters,
version: $this->version,
author: $this->author,
metadata: $this->metadata
);
}
/**
* Add tags
*/
public function withTags(array $tags): self
{
return new self(
id: $this->id,
name: $this->name,
description: $this->description,
category: $this->category,
severity: $this->severity,
condition: $this->condition,
action: $this->action,
enabled: $this->enabled,
priority: $this->priority,
confidenceThreshold: $this->confidenceThreshold,
tags: array_unique(array_merge($this->tags, $tags)),
actionParameters: $this->actionParameters,
version: $this->version,
author: $this->author,
metadata: $this->metadata
);
}
/**
* Check if rule has specific tag
*/
public function hasTag(string $tag): bool
{
return in_array($tag, $this->tags, true);
}
/**
* Check if rule is OWASP Top 10 related
*/
public function isOwaspTop10(): bool
{
return $this->hasTag('owasp-top10') || $this->category->isOwaspTop10();
}
/**
* Get rule complexity score
*/
public function getComplexityScore(): int
{
$score = 1;
// Pattern complexity
if ($this->condition->pattern->isRegex) {
$score += $this->condition->pattern->isPotentiallyDangerous() ? 5 : 2;
}
// Action complexity
if ($this->action->requiresParameters()) {
$score += 1;
}
// Transformation complexity
if ($this->condition->transformation !== null) {
$score += 1;
}
return $score;
}
/**
* Check if rule is computationally expensive
*/
public function isExpensive(): bool
{
return $this->condition->type->isExpensive() ||
$this->condition->pattern->isPotentiallyDangerous() ||
$this->getComplexityScore() > 3;
}
/**
* Convert to array
*/
public function toArray(): array
{
return [
'id' => $this->id->value,
'name' => $this->name,
'description' => $this->description,
'category' => $this->category->value,
'severity' => $this->severity->value,
'action' => $this->action->value,
'enabled' => $this->enabled,
'priority' => $this->priority,
'confidence_threshold' => $this->confidenceThreshold?->getValue(),
'tags' => $this->tags,
'action_parameters' => $this->actionParameters,
'version' => $this->version,
'author' => $this->author,
'is_owasp_top10' => $this->isOwaspTop10(),
'complexity_score' => $this->getComplexityScore(),
'is_expensive' => $this->isExpensive(),
'condition' => $this->condition->toArray(),
'metadata' => $this->metadata,
];
}
}

View File

@@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
namespace App\Framework\Waf\Rules;
/**
* Actions that can be taken when a rule matches
*/
enum RuleAction: string
{
case ALLOW = 'allow';
case BLOCK = 'block';
case LOG = 'log';
case WARN = 'warn';
case CHALLENGE = 'challenge';
case RATE_LIMIT = 'rate_limit';
case REDIRECT = 'redirect';
case SANITIZE = 'sanitize';
case SCORE = 'score';
case CHAIN = 'chain';
case SKIP = 'skip';
case DENY_WITH_STATUS = 'deny_with_status';
/**
* Get action description
*/
public function getDescription(): string
{
return match ($this) {
self::ALLOW => 'Allow request to proceed',
self::BLOCK => 'Block request with 403 Forbidden',
self::LOG => 'Log detection but allow request',
self::WARN => 'Log warning and allow request',
self::CHALLENGE => 'Present CAPTCHA or similar challenge',
self::RATE_LIMIT => 'Apply rate limiting to client',
self::REDIRECT => 'Redirect to different URL',
self::SANITIZE => 'Clean/sanitize input and continue',
self::SCORE => 'Add to anomaly score',
self::CHAIN => 'Chain to next rule for combined evaluation',
self::SKIP => 'Skip next N rules',
self::DENY_WITH_STATUS => 'Deny with custom HTTP status code'
};
}
/**
* Get default HTTP status code for action
*/
public function getDefaultHttpStatus(): int
{
return match ($this) {
self::ALLOW, self::LOG, self::WARN, self::SANITIZE,
self::SCORE, self::CHAIN, self::SKIP => 200,
self::BLOCK => 403,
self::CHALLENGE => 429,
self::RATE_LIMIT => 429,
self::REDIRECT => 302,
self::DENY_WITH_STATUS => 403
};
}
/**
* Check if action blocks request processing
*/
public function blocksRequest(): bool
{
return match ($this) {
self::BLOCK, self::CHALLENGE, self::RATE_LIMIT,
self::REDIRECT, self::DENY_WITH_STATUS => true,
default => false
};
}
/**
* Check if action allows request to continue
*/
public function allowsContinuation(): bool
{
return ! $this->blocksRequest();
}
/**
* Check if action requires logging
*/
public function requiresLogging(): bool
{
return match ($this) {
self::ALLOW => false,
default => true
};
}
/**
* Get logging severity level
*/
public function getLogLevel(): string
{
return match ($this) {
self::ALLOW, self::SKIP => 'debug',
self::LOG, self::SANITIZE, self::SCORE => 'info',
self::WARN, self::CHAIN => 'warning',
self::CHALLENGE, self::RATE_LIMIT, self::REDIRECT => 'notice',
self::BLOCK, self::DENY_WITH_STATUS => 'error'
};
}
/**
* Check if action affects anomaly score
*/
public function affectsAnomalyScore(): bool
{
return match ($this) {
self::SCORE, self::WARN, self::BLOCK, self::CHALLENGE => true,
default => false
};
}
/**
* Get default anomaly score impact
*/
public function getAnomalyScoreImpact(): int
{
return match ($this) {
self::SCORE => 5,
self::WARN => 3,
self::CHALLENGE => 8,
self::BLOCK => 10,
default => 0
};
}
/**
* Check if action can be chained
*/
public function canBeChained(): bool
{
return match ($this) {
self::SCORE, self::LOG, self::WARN, self::CHAIN => true,
default => false
};
}
/**
* Check if action requires additional parameters
*/
public function requiresParameters(): bool
{
return match ($this) {
self::REDIRECT, self::DENY_WITH_STATUS, self::SKIP => true,
default => false
};
}
/**
* Get required parameter names
*/
public function getRequiredParameters(): array
{
return match ($this) {
self::REDIRECT => ['url'],
self::DENY_WITH_STATUS => ['status_code'],
self::SKIP => ['count'],
default => []
};
}
}

View File

@@ -0,0 +1,426 @@
<?php
declare(strict_types=1);
namespace App\Framework\Waf\Rules;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Logging\Logger;
/**
* WAF Rule Engine - evaluates rules against requests
*/
final class RuleEngine
{
private array $rules = [];
private array $rulesByCategory = [];
private array $rulesByPriority = [];
private array $evaluationStats = [];
public function __construct(
private readonly Logger $logger,
private readonly Duration $maxEvaluationTime,
private readonly bool $enableCaching = true,
private readonly int $maxCacheSize = 10000
) {
}
/**
* Add rule to engine
*/
public function addRule(Rule $rule): void
{
$this->rules[$rule->id->value] = $rule;
// Index by category
$categoryKey = $rule->category->value;
if (! isset($this->rulesByCategory[$categoryKey])) {
$this->rulesByCategory[$categoryKey] = [];
}
$this->rulesByCategory[$categoryKey][] = $rule;
// Index by priority (sorted)
$this->rulesByPriority = [];
foreach ($this->rules as $r) {
$this->rulesByPriority[] = $r;
}
// Sort by priority (descending - higher priority first)
usort($this->rulesByPriority, fn (Rule $a, Rule $b) => $b->priority <=> $a->priority);
}
/**
* Add multiple rules
*/
public function addRules(array $rules): void
{
foreach ($rules as $rule) {
$this->addRule($rule);
}
}
/**
* Remove rule from engine
*/
public function removeRule(string $ruleId): void
{
if (! isset($this->rules[$ruleId])) {
return;
}
$rule = $this->rules[$ruleId];
unset($this->rules[$ruleId]);
// Remove from category index
$categoryKey = $rule->category->value;
if (isset($this->rulesByCategory[$categoryKey])) {
$this->rulesByCategory[$categoryKey] = array_filter(
$this->rulesByCategory[$categoryKey],
fn (Rule $r) => $r->id->value !== $ruleId
);
}
// Rebuild priority index
$this->rulesByPriority = array_filter(
$this->rulesByPriority,
fn (Rule $r) => $r->id->value !== $ruleId
);
}
/**
* Enable rule
*/
public function enableRule(string $ruleId): void
{
if (isset($this->rules[$ruleId])) {
$this->rules[$ruleId] = $this->rules[$ruleId]->enable();
}
}
/**
* Disable rule
*/
public function disableRule(string $ruleId): void
{
if (isset($this->rules[$ruleId])) {
$this->rules[$ruleId] = $this->rules[$ruleId]->disable();
}
}
/**
* Get rule by ID
*/
public function getRule(string $ruleId): ?Rule
{
return $this->rules[$ruleId] ?? null;
}
/**
* Get all rules
*/
public function getRules(): array
{
return array_values($this->rules);
}
/**
* Get enabled rules only
*/
public function getEnabledRules(): array
{
return array_filter($this->rules, fn (Rule $rule) => $rule->enabled);
}
/**
* Get rules by category
*/
public function getRulesByCategory(string $category): array
{
return $this->rulesByCategory[$category] ?? [];
}
/**
* Get rules by tag
*/
public function getRulesByTag(string $tag): array
{
return array_filter($this->rules, fn (Rule $rule) => $rule->hasTag($tag));
}
/**
* Evaluate all rules against request data
*/
public function evaluateAll(array $requestData): RuleEvaluationResult
{
$startTime = Timestamp::now();
$matches = [];
$evaluatedRules = 0;
$skippedRules = 0;
$errorCount = 0;
$errors = [];
// Process rules by priority
foreach ($this->rulesByPriority as $rule) {
if (! $rule->enabled) {
$skippedRules++;
continue;
}
// Check timeout
$elapsed = $startTime->diff(Timestamp::now());
if ($elapsed->isGreaterThan($this->maxEvaluationTime)) {
$this->logger->warning('Rule evaluation timeout reached', [
'elapsed_ms' => $elapsed->toMilliseconds(),
'max_ms' => $this->maxEvaluationTime->toMilliseconds(),
'evaluated_rules' => $evaluatedRules,
'remaining_rules' => count($this->rulesByPriority) - $evaluatedRules - $skippedRules,
]);
break;
}
try {
$match = $rule->evaluate($requestData);
if ($match !== null) {
$matches[] = $match;
// Log high-severity matches immediately
if ($match->isHighSeverityThreat()) {
$this->logger->warning('High-severity threat detected', [
'rule_id' => $rule->id->value,
'category' => $rule->category->value,
'severity' => $rule->severity->value,
'message' => $match->message,
]);
}
}
$evaluatedRules++;
} catch (\Throwable $e) {
$errorCount++;
$errors[] = [
'rule_id' => $rule->id->value,
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
];
$this->logger->error('Rule evaluation error', [
'rule_id' => $rule->id->value,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
}
}
$endTime = Timestamp::now();
$totalDuration = $startTime->diff($endTime);
// Update stats
$this->updateEvaluationStats($evaluatedRules, $totalDuration, $errorCount);
return new RuleEvaluationResult(
matches: $matches,
evaluatedRules: $evaluatedRules,
skippedRules: $skippedRules,
errorCount: $errorCount,
errors: $errors,
evaluationTime: $totalDuration,
timestamp: $endTime
);
}
/**
* Evaluate specific rule categories only
*/
public function evaluateCategories(array $categories, array $requestData): RuleEvaluationResult
{
$startTime = Timestamp::now();
$matches = [];
$evaluatedRules = 0;
$errorCount = 0;
$errors = [];
foreach ($categories as $category) {
$categoryRules = $this->getRulesByCategory($category);
foreach ($categoryRules as $rule) {
if (! $rule->enabled) {
continue;
}
try {
$match = $rule->evaluate($requestData);
if ($match !== null) {
$matches[] = $match;
}
$evaluatedRules++;
} catch (\Throwable $e) {
$errorCount++;
$errors[] = [
'rule_id' => $rule->id->value,
'error' => $e->getMessage(),
];
}
}
}
$endTime = Timestamp::now();
$totalDuration = $startTime->diff($endTime);
return new RuleEvaluationResult(
matches: $matches,
evaluatedRules: $evaluatedRules,
skippedRules: 0,
errorCount: $errorCount,
errors: $errors,
evaluationTime: $totalDuration,
timestamp: $endTime
);
}
/**
* Quick evaluation for high-priority rules only
*/
public function quickEvaluate(array $requestData): RuleEvaluationResult
{
// Only evaluate rules with priority >= 80
$highPriorityRules = array_filter(
$this->rulesByPriority,
fn (Rule $rule) => $rule->enabled && $rule->priority >= 80
);
$startTime = Timestamp::now();
$matches = [];
$evaluatedRules = 0;
foreach ($highPriorityRules as $rule) {
try {
$match = $rule->evaluate($requestData);
if ($match !== null) {
$matches[] = $match;
}
$evaluatedRules++;
} catch (\Throwable $e) {
$this->logger->error('Quick evaluation error', [
'rule_id' => $rule->id->value,
'error' => $e->getMessage(),
]);
}
}
$endTime = Timestamp::now();
$totalDuration = $startTime->diff($endTime);
return new RuleEvaluationResult(
matches: $matches,
evaluatedRules: $evaluatedRules,
skippedRules: count($this->getEnabledRules()) - count($highPriorityRules),
errorCount: 0,
errors: [],
evaluationTime: $totalDuration,
timestamp: $endTime
);
}
/**
* Get engine statistics
*/
public function getStatistics(): array
{
$enabledRules = $this->getEnabledRules();
$expensiveRules = array_filter($enabledRules, fn (Rule $rule) => $rule->isExpensive());
$owaspRules = array_filter($enabledRules, fn (Rule $rule) => $rule->isOwaspTop10());
$categoryStats = [];
foreach ($this->rulesByCategory as $category => $rules) {
$categoryStats[$category] = [
'total' => count($rules),
'enabled' => count(array_filter($rules, fn (Rule $r) => $r->enabled)),
'expensive' => count(array_filter($rules, fn (Rule $r) => $r->enabled && $r->isExpensive())),
];
}
return [
'total_rules' => count($this->rules),
'enabled_rules' => count($enabledRules),
'expensive_rules' => count($expensiveRules),
'owasp_top10_rules' => count($owaspRules),
'categories' => array_keys($this->rulesByCategory),
'category_stats' => $categoryStats,
'evaluation_stats' => $this->evaluationStats,
];
}
/**
* Get performance metrics
*/
public function getPerformanceMetrics(): array
{
return [
'avg_evaluation_time_ms' => $this->evaluationStats['avg_evaluation_time'] ?? 0,
'max_evaluation_time_ms' => $this->maxEvaluationTime->toMilliseconds(),
'total_evaluations' => $this->evaluationStats['total_evaluations'] ?? 0,
'total_errors' => $this->evaluationStats['total_errors'] ?? 0,
'success_rate' => $this->calculateSuccessRate(),
'cache_enabled' => $this->enableCaching,
'cache_max_size' => $this->maxCacheSize,
];
}
/**
* Clear all rules
*/
public function clearRules(): void
{
$this->rules = [];
$this->rulesByCategory = [];
$this->rulesByPriority = [];
}
/**
* Update evaluation statistics
*/
private function updateEvaluationStats(int $evaluatedRules, Duration $duration, int $errorCount): void
{
if (! isset($this->evaluationStats['total_evaluations'])) {
$this->evaluationStats['total_evaluations'] = 0;
$this->evaluationStats['total_duration_ms'] = 0;
$this->evaluationStats['total_errors'] = 0;
}
$this->evaluationStats['total_evaluations']++;
$this->evaluationStats['total_duration_ms'] += $duration->toMilliseconds();
$this->evaluationStats['total_errors'] += $errorCount;
$this->evaluationStats['avg_evaluation_time'] =
$this->evaluationStats['total_duration_ms'] / $this->evaluationStats['total_evaluations'];
$this->evaluationStats['last_evaluation'] = [
'rules_evaluated' => $evaluatedRules,
'duration_ms' => $duration->toMilliseconds(),
'errors' => $errorCount,
];
}
/**
* Calculate success rate
*/
private function calculateSuccessRate(): float
{
$totalEvaluations = $this->evaluationStats['total_evaluations'] ?? 0;
$totalErrors = $this->evaluationStats['total_errors'] ?? 0;
if ($totalEvaluations === 0) {
return 100.0;
}
return ((($totalEvaluations - $totalErrors) / $totalEvaluations) * 100.0);
}
}

View File

@@ -0,0 +1,300 @@
<?php
declare(strict_types=1);
namespace App\Framework\Waf\Rules;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Waf\Rules\ValueObjects\RuleMatch;
/**
* Result of rule engine evaluation
*/
final readonly class RuleEvaluationResult
{
public function __construct(
public array $matches,
public int $evaluatedRules,
public int $skippedRules,
public int $errorCount,
public array $errors,
public Duration $evaluationTime,
public Timestamp $timestamp
) {
}
/**
* Check if any rules matched
*/
public function hasMatches(): bool
{
return ! empty($this->matches);
}
/**
* Get number of matches
*/
public function getMatchCount(): int
{
return count($this->matches);
}
/**
* Get matches by severity
*/
public function getMatchesBySeverity(string $severity): array
{
return array_filter(
$this->matches,
fn (RuleMatch $match) => $match->severity->value === $severity
);
}
/**
* Get critical matches
*/
public function getCriticalMatches(): array
{
return $this->getMatchesBySeverity('critical');
}
/**
* Get high severity matches
*/
public function getHighSeverityMatches(): array
{
return $this->getMatchesBySeverity('high');
}
/**
* Get blocking matches (matches that should block the request)
*/
public function getBlockingMatches(): array
{
return array_filter(
$this->matches,
fn (RuleMatch $match) => $match->shouldBlock()
);
}
/**
* Get matches that should trigger alerts
*/
public function getAlertingMatches(): array
{
return array_filter(
$this->matches,
fn (RuleMatch $match) => $match->shouldAlert()
);
}
/**
* Get OWASP Top 10 matches
*/
public function getOwaspTop10Matches(): array
{
return array_filter(
$this->matches,
fn (RuleMatch $match) => $match->category->isOwaspTop10()
);
}
/**
* Get matches by category
*/
public function getMatchesByCategory(string $category): array
{
return array_filter(
$this->matches,
fn (RuleMatch $match) => $match->category->value === $category
);
}
/**
* Check if any blocking matches exist
*/
public function shouldBlock(): bool
{
return ! empty($this->getBlockingMatches());
}
/**
* Check if any alerting matches exist
*/
public function shouldAlert(): bool
{
return ! empty($this->getAlertingMatches());
}
/**
* Check if evaluation had errors
*/
public function hasErrors(): bool
{
return $this->errorCount > 0;
}
/**
* Get highest severity level from matches
*/
public function getHighestSeverity(): ?string
{
if (empty($this->matches)) {
return null;
}
$severityOrder = ['critical', 'high', 'medium', 'low', 'info'];
foreach ($severityOrder as $severity) {
if (! empty($this->getMatchesBySeverity($severity))) {
return $severity;
}
}
return null;
}
/**
* Get categories of all matches
*/
public function getMatchedCategories(): array
{
$categories = [];
foreach ($this->matches as $match) {
$categories[$match->category->value] = $match->category;
}
return array_values($categories);
}
/**
* Get rule IDs of all matches
*/
public function getMatchedRuleIds(): array
{
return array_map(
fn (RuleMatch $match) => $match->ruleId->value,
$this->matches
);
}
/**
* Calculate overall confidence score
*/
public function getOverallConfidence(): Percentage
{
if (empty($this->matches)) {
return Percentage::from(0.0);
}
$totalConfidence = 0.0;
$confidenceCount = 0;
foreach ($this->matches as $match) {
if ($match->confidence !== null) {
$totalConfidence += $match->confidence->getValue();
$confidenceCount++;
}
}
if ($confidenceCount === 0) {
return Percentage::from(0.0);
}
return Percentage::from($totalConfidence / $confidenceCount);
}
/**
* Calculate threat score based on matches
*/
public function getThreatScore(): Percentage
{
if (empty($this->matches)) {
return Percentage::from(0.0);
}
$score = 0.0;
$maxScore = 0.0;
foreach ($this->matches as $match) {
$matchScore = match ($match->severity->value) {
'critical' => 25.0,
'high' => 20.0,
'medium' => 10.0,
'low' => 5.0,
'info' => 1.0,
default => 0.0
};
// Apply confidence modifier
if ($match->confidence !== null) {
$matchScore *= ($match->confidence->getValue() / 100.0);
}
$score += $matchScore;
$maxScore = max($maxScore, $matchScore);
}
// Use weighted combination: 70% cumulative + 30% maximum
$finalScore = ($score * 0.7) + ($maxScore * 0.3);
return Percentage::from(min(100.0, $finalScore));
}
/**
* Get evaluation performance summary
*/
public function getPerformanceSummary(): array
{
$efficiency = 0.0;
if ($this->evaluatedRules > 0) {
$rulesPerMs = $this->evaluatedRules / max(1, $this->evaluationTime->toMilliseconds());
$efficiency = $rulesPerMs * 1000; // Rules per second
}
return [
'evaluation_time_ms' => $this->evaluationTime->toMilliseconds(),
'evaluated_rules' => $this->evaluatedRules,
'skipped_rules' => $this->skippedRules,
'error_count' => $this->errorCount,
'rules_per_second' => round($efficiency, 2),
'error_rate' => $this->evaluatedRules > 0
? round(($this->errorCount / $this->evaluatedRules) * 100, 2)
: 0.0,
];
}
/**
* Convert to array for serialization
*/
public function toArray(): array
{
return [
'timestamp' => $this->timestamp->toIsoString(),
'evaluation_time_ms' => $this->evaluationTime->toMilliseconds(),
'evaluated_rules' => $this->evaluatedRules,
'skipped_rules' => $this->skippedRules,
'error_count' => $this->errorCount,
'match_count' => $this->getMatchCount(),
'has_matches' => $this->hasMatches(),
'should_block' => $this->shouldBlock(),
'should_alert' => $this->shouldAlert(),
'highest_severity' => $this->getHighestSeverity(),
'threat_score' => $this->getThreatScore()->getValue(),
'overall_confidence' => $this->getOverallConfidence()->getValue(),
'matched_categories' => array_map(fn ($cat) => $cat->value, $this->getMatchedCategories()),
'matched_rule_ids' => $this->getMatchedRuleIds(),
'owasp_top10_matches' => count($this->getOwaspTop10Matches()),
'critical_matches' => count($this->getCriticalMatches()),
'high_severity_matches' => count($this->getHighSeverityMatches()),
'blocking_matches' => count($this->getBlockingMatches()),
'alerting_matches' => count($this->getAlertingMatches()),
'performance_summary' => $this->getPerformanceSummary(),
'matches' => array_map(fn (RuleMatch $match) => $match->toArray(), $this->matches),
'errors' => $this->errors,
];
}
}

View File

@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace App\Framework\Waf\Rules;
/**
* Types of WAF security rules
*/
enum RuleType: string
{
case PATTERN_MATCH = 'pattern_match';
case REGEX_MATCH = 'regex_match';
case HEADER_CHECK = 'header_check';
case PARAMETER_CHECK = 'parameter_check';
case BODY_CHECK = 'body_check';
case SIZE_LIMIT = 'size_limit';
case RATE_LIMIT = 'rate_limit';
case IP_WHITELIST = 'ip_whitelist';
case IP_BLACKLIST = 'ip_blacklist';
case GEOGRAPHIC_FILTER = 'geographic_filter';
case USER_AGENT_CHECK = 'user_agent_check';
case CONTENT_TYPE_CHECK = 'content_type_check';
case METHOD_CHECK = 'method_check';
case PATH_CHECK = 'path_check';
case COOKIE_CHECK = 'cookie_check';
/**
* Get rule type description
*/
public function getDescription(): string
{
return match ($this) {
self::PATTERN_MATCH => 'Pattern-based string matching',
self::REGEX_MATCH => 'Regular expression matching',
self::HEADER_CHECK => 'HTTP header validation',
self::PARAMETER_CHECK => 'Query/form parameter validation',
self::BODY_CHECK => 'Request body content validation',
self::SIZE_LIMIT => 'Content size limitations',
self::RATE_LIMIT => 'Request rate limiting',
self::IP_WHITELIST => 'IP address whitelist check',
self::IP_BLACKLIST => 'IP address blacklist check',
self::GEOGRAPHIC_FILTER => 'Geographic location filtering',
self::USER_AGENT_CHECK => 'User agent validation',
self::CONTENT_TYPE_CHECK => 'Content-Type header validation',
self::METHOD_CHECK => 'HTTP method validation',
self::PATH_CHECK => 'URL path validation',
self::COOKIE_CHECK => 'Cookie validation'
};
}
/**
* Get default processing priority
*/
public function getDefaultPriority(): int
{
return match ($this) {
self::IP_WHITELIST => 100, // Process first - bypass other rules
self::IP_BLACKLIST => 95, // Block malicious IPs early
self::RATE_LIMIT => 90, // Rate limiting before content analysis
self::METHOD_CHECK => 85, // HTTP method validation
self::SIZE_LIMIT => 80, // Size checks before content parsing
self::CONTENT_TYPE_CHECK => 75, // Content type validation
self::HEADER_CHECK => 70, // Header validation
self::PATH_CHECK => 65, // Path validation
self::GEOGRAPHIC_FILTER => 60, // Geographic filtering
self::USER_AGENT_CHECK => 55, // User agent checks
self::COOKIE_CHECK => 50, // Cookie validation
self::PARAMETER_CHECK => 45, // Parameter validation
self::BODY_CHECK => 40, // Body content analysis
self::PATTERN_MATCH => 35, // Pattern matching
self::REGEX_MATCH => 30 // Complex regex last (most expensive)
};
}
/**
* Check if rule type requires request body
*/
public function requiresBody(): bool
{
return match ($this) {
self::BODY_CHECK, self::PARAMETER_CHECK => true,
default => false
};
}
/**
* Check if rule type is computationally expensive
*/
public function isExpensive(): bool
{
return match ($this) {
self::REGEX_MATCH, self::BODY_CHECK, self::PATTERN_MATCH => true,
default => false
};
}
/**
* Check if rule type can cause false positives
*/
public function canCauseFalsePositives(): bool
{
return match ($this) {
self::PATTERN_MATCH, self::REGEX_MATCH, self::BODY_CHECK,
self::PARAMETER_CHECK, self::USER_AGENT_CHECK => true,
default => false
};
}
/**
* Get expected processing time category
*/
public function getProcessingTimeCategory(): string
{
return match ($this) {
self::IP_WHITELIST, self::IP_BLACKLIST, self::METHOD_CHECK => 'fast',
self::HEADER_CHECK, self::PATH_CHECK, self::SIZE_LIMIT => 'medium',
self::REGEX_MATCH, self::BODY_CHECK, self::PATTERN_MATCH => 'slow',
default => 'medium'
};
}
/**
* Check if rule type supports caching
*/
public function supportsCaching(): bool
{
return match ($this) {
self::IP_WHITELIST, self::IP_BLACKLIST, self::GEOGRAPHIC_FILTER => true,
default => false
};
}
}

View File

@@ -0,0 +1,299 @@
<?php
declare(strict_types=1);
namespace App\Framework\Waf\Rules\ValueObjects;
use App\Framework\Waf\Rules\RuleType;
/**
* Condition that defines what to check in a request
*/
final readonly class RuleCondition
{
public function __construct(
public RuleType $type,
public string $target,
public RulePattern $pattern,
public bool $negated = false,
public ?string $transformation = null
) {
}
/**
* Create condition for request headers
*/
public static function header(string $headerName, RulePattern $pattern, bool $negated = false): self
{
return new self(
type: RuleType::HEADER_CHECK,
target: "headers.{$headerName}",
pattern: $pattern,
negated: $negated
);
}
/**
* Create condition for query parameters
*/
public static function queryParameter(string $paramName, RulePattern $pattern, bool $negated = false): self
{
return new self(
type: RuleType::PARAMETER_CHECK,
target: "query.{$paramName}",
pattern: $pattern,
negated: $negated
);
}
/**
* Create condition for POST parameters
*/
public static function postParameter(string $paramName, RulePattern $pattern, bool $negated = false): self
{
return new self(
type: RuleType::PARAMETER_CHECK,
target: "post.{$paramName}",
pattern: $pattern,
negated: $negated
);
}
/**
* Create condition for request body
*/
public static function requestBody(RulePattern $pattern, bool $negated = false): self
{
return new self(
type: RuleType::BODY_CHECK,
target: 'body',
pattern: $pattern,
negated: $negated
);
}
/**
* Create condition for URL path
*/
public static function urlPath(RulePattern $pattern, bool $negated = false): self
{
return new self(
type: RuleType::PATH_CHECK,
target: 'path',
pattern: $pattern,
negated: $negated
);
}
/**
* Create condition for HTTP method
*/
public static function httpMethod(RulePattern $pattern, bool $negated = false): self
{
return new self(
type: RuleType::METHOD_CHECK,
target: 'method',
pattern: $pattern,
negated: $negated
);
}
/**
* Create condition for User-Agent header
*/
public static function userAgent(RulePattern $pattern, bool $negated = false): self
{
return new self(
type: RuleType::USER_AGENT_CHECK,
target: 'headers.User-Agent',
pattern: $pattern,
negated: $negated
);
}
/**
* Create condition for Content-Type header
*/
public static function contentType(RulePattern $pattern, bool $negated = false): self
{
return new self(
type: RuleType::CONTENT_TYPE_CHECK,
target: 'headers.Content-Type',
pattern: $pattern,
negated: $negated
);
}
/**
* Create condition for cookies
*/
public static function cookie(string $cookieName, RulePattern $pattern, bool $negated = false): self
{
return new self(
type: RuleType::COOKIE_CHECK,
target: "cookies.{$cookieName}",
pattern: $pattern,
negated: $negated
);
}
/**
* Create condition with transformation
*/
public function withTransformation(string $transformation): self
{
return new self(
type: $this->type,
target: $this->target,
pattern: $this->pattern,
negated: $this->negated,
transformation: $transformation
);
}
/**
* Create negated version of condition
*/
public function negate(): self
{
return new self(
type: $this->type,
target: $this->target,
pattern: $this->pattern,
negated: ! $this->negated,
transformation: $this->transformation
);
}
/**
* Extract value from request data based on target
*/
public function extractValue(array $requestData): ?string
{
$parts = explode('.', $this->target);
$value = $requestData;
foreach ($parts as $part) {
if (! is_array($value) || ! isset($value[$part])) {
return null;
}
$value = $value[$part];
}
if (! is_string($value)) {
$value = (string) $value;
}
// Apply transformation if specified
if ($this->transformation) {
$value = $this->applyTransformation($value);
}
return $value;
}
/**
* Evaluate condition against request data
*/
public function evaluate(array $requestData): bool
{
$value = $this->extractValue($requestData);
if ($value === null) {
// No value found - condition fails unless negated
return $this->negated;
}
$matches = $this->pattern->matches($value);
// Apply negation if specified
return $this->negated ? ! $matches : $matches;
}
/**
* Get all matches from request data
*/
public function getMatches(array $requestData): array
{
$value = $this->extractValue($requestData);
if ($value === null) {
return [];
}
return $this->pattern->getAllMatches($value);
}
/**
* Check if condition targets sensitive data
*/
public function targetsSensitiveData(): bool
{
$sensitiveTargets = [
'headers.Authorization',
'headers.Cookie',
'headers.X-Auth-Token',
'post.password',
'post.passwd',
'post.secret',
'post.token',
'query.password',
'query.passwd',
'query.secret',
'query.token',
];
return in_array($this->target, $sensitiveTargets, true);
}
/**
* Get target description
*/
public function getTargetDescription(): string
{
return match (true) {
str_starts_with($this->target, 'headers.') => 'HTTP Header: ' . substr($this->target, 8),
str_starts_with($this->target, 'query.') => 'Query Parameter: ' . substr($this->target, 6),
str_starts_with($this->target, 'post.') => 'POST Parameter: ' . substr($this->target, 5),
str_starts_with($this->target, 'cookies.') => 'Cookie: ' . substr($this->target, 8),
$this->target === 'body' => 'Request Body',
$this->target === 'path' => 'URL Path',
$this->target === 'method' => 'HTTP Method',
default => ucfirst($this->target)
};
}
/**
* Convert to array
*/
public function toArray(): array
{
return [
'type' => $this->type->value,
'target' => $this->target,
'target_description' => $this->getTargetDescription(),
'pattern' => $this->pattern->toArray(),
'negated' => $this->negated,
'transformation' => $this->transformation,
'targets_sensitive_data' => $this->targetsSensitiveData(),
];
}
/**
* Apply transformation to value
*/
private function applyTransformation(string $value): string
{
return match ($this->transformation) {
'lowercase' => strtolower($value),
'uppercase' => strtoupper($value),
'trim' => trim($value),
'url_decode' => urldecode($value),
'html_decode' => html_entity_decode($value, ENT_QUOTES | ENT_HTML5),
'base64_decode' => base64_decode($value, true) ?: $value,
'remove_whitespace' => preg_replace('/\s+/', '', $value),
'normalize_path' => str_replace(['\\', '//'], ['/', '/'], $value),
default => $value
};
}
}

View File

@@ -0,0 +1,306 @@
<?php
declare(strict_types=1);
namespace App\Framework\Waf\Rules\ValueObjects;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Waf\DetectionCategory;
use App\Framework\Waf\DetectionSeverity;
use App\Framework\Waf\ValueObjects\RuleId;
/**
* Represents a successful rule match
*/
final readonly class RuleMatch
{
public function __construct(
public RuleId $ruleId,
public string $ruleName,
public DetectionCategory $category,
public DetectionSeverity $severity,
public RuleCondition $condition,
public string $matchedValue,
public array $matches,
public ?Percentage $confidence = null,
public ?string $message = null,
public ?Timestamp $timestamp = null,
public array $metadata = []
) {
}
/**
* Create rule match from condition evaluation
*/
public static function fromCondition(
RuleId $ruleId,
string $ruleName,
DetectionCategory $category,
DetectionSeverity $severity,
RuleCondition $condition,
array $requestData,
?string $message = null
): self {
$matchedValue = $condition->extractValue($requestData) ?? '';
$matches = $condition->getMatches($requestData);
// Calculate confidence based on match quality
$confidence = self::calculateConfidence($matches, $condition->pattern);
$generatedMessage = $message ?? self::generateMessage($condition, $matchedValue);
return new self(
ruleId: $ruleId,
ruleName: $ruleName,
category: $category,
severity: $severity,
condition: $condition,
matchedValue: $matchedValue,
matches: $matches,
confidence: $confidence,
message: $generatedMessage,
timestamp: Timestamp::now(),
metadata: [
'target' => $condition->target,
'pattern_type' => $condition->pattern->isRegex ? 'regex' : 'string',
'negated' => $condition->negated,
]
);
}
/**
* Create high-confidence match
*/
public static function highConfidence(
RuleId $ruleId,
string $ruleName,
DetectionCategory $category,
DetectionSeverity $severity,
RuleCondition $condition,
string $matchedValue,
array $matches,
string $message
): self {
return new self(
ruleId: $ruleId,
ruleName: $ruleName,
category: $category,
severity: $severity,
condition: $condition,
matchedValue: $matchedValue,
matches: $matches,
confidence: Percentage::from(95.0),
message: $message,
timestamp: Timestamp::now()
);
}
/**
* Get sanitized matched value (safe for logging)
*/
public function getSanitizedMatchedValue(): string
{
// Truncate long values
$value = strlen($this->matchedValue) > 200
? substr($this->matchedValue, 0, 200) . '...'
: $this->matchedValue;
// Remove potential log injection attacks
$value = str_replace(["\r", "\n", "\t"], ['\r', '\n', '\t'], $value);
// Mask sensitive patterns
if ($this->condition->targetsSensitiveData()) {
return '***REDACTED***';
}
return $value;
}
/**
* Get match excerpt showing context around matches
*/
public function getMatchExcerpt(int $contextLength = 50): array
{
$excerpts = [];
foreach ($this->matches as $match) {
if (is_array($match) && isset($match['offset'])) {
$start = max(0, $match['offset'] - $contextLength);
$length = strlen($match['match']) + (2 * $contextLength);
$excerpt = substr($this->matchedValue, $start, $length);
$relativeOffset = $match['offset'] - $start;
$excerpts[] = [
'excerpt' => $excerpt,
'match_start' => $relativeOffset,
'match_length' => strlen($match['match']),
'absolute_offset' => $match['offset'],
];
}
}
return $excerpts;
}
/**
* Check if match indicates high-severity threat
*/
public function isHighSeverityThreat(): bool
{
return $this->severity === DetectionSeverity::CRITICAL ||
$this->severity === DetectionSeverity::HIGH;
}
/**
* Check if match should trigger immediate blocking
*/
public function shouldBlock(): bool
{
if ($this->isHighSeverityThreat()) {
return true;
}
// High confidence medium threats should also block
if ($this->severity === DetectionSeverity::MEDIUM &&
$this->confidence !== null &&
$this->confidence->getValue() >= 90.0) {
return true;
}
return false;
}
/**
* Check if match should trigger alert
*/
public function shouldAlert(): bool
{
return $this->severity->shouldAlert() ||
($this->confidence !== null && $this->confidence->getValue() >= 70.0);
}
/**
* Get OWASP category information
*/
public function getOwaspInfo(): array
{
return [
'category' => $this->category->value,
'owasp_rank' => $this->category->getOwaspRank(),
'is_top10' => $this->category->isOwaspTop10(),
'description' => $this->category->getDescription(),
];
}
/**
* Get rule performance metrics
*/
public function getPerformanceMetrics(): array
{
$complexity = 'low';
if ($this->condition->pattern->isRegex) {
$complexity = $this->condition->pattern->isPotentiallyDangerous() ? 'high' : 'medium';
}
return [
'pattern_complexity' => $complexity,
'pattern_type' => $this->condition->pattern->isRegex ? 'regex' : 'string',
'match_count' => count($this->matches),
'value_length' => strlen($this->matchedValue),
'targets_sensitive_data' => $this->condition->targetsSensitiveData(),
];
}
/**
* Add metadata
*/
public function withMetadata(array $metadata): self
{
return new self(
ruleId: $this->ruleId,
ruleName: $this->ruleName,
category: $this->category,
severity: $this->severity,
condition: $this->condition,
matchedValue: $this->matchedValue,
matches: $this->matches,
confidence: $this->confidence,
message: $this->message,
timestamp: $this->timestamp,
metadata: array_merge($this->metadata, $metadata)
);
}
/**
* Convert to array
*/
public function toArray(): array
{
return [
'rule_id' => $this->ruleId->value,
'rule_name' => $this->ruleName,
'category' => $this->category->value,
'severity' => $this->severity->value,
'message' => $this->message,
'confidence' => $this->confidence?->getValue(),
'matched_value' => $this->getSanitizedMatchedValue(),
'match_excerpts' => $this->getMatchExcerpt(),
'timestamp' => $this->timestamp?->toIsoString(),
'should_block' => $this->shouldBlock(),
'should_alert' => $this->shouldAlert(),
'owasp_info' => $this->getOwaspInfo(),
'performance_metrics' => $this->getPerformanceMetrics(),
'condition' => $this->condition->toArray(),
'metadata' => $this->metadata,
];
}
/**
* Calculate confidence based on match quality
*/
private static function calculateConfidence(array $matches, RulePattern $pattern): Percentage
{
if (empty($matches)) {
return Percentage::from(0.0);
}
$baseConfidence = 70.0;
// Boost confidence for regex patterns (more precise)
if ($pattern->isRegex) {
$baseConfidence += 10.0;
}
// Boost confidence for multiple matches
$matchBonus = min(20.0, count($matches) * 5.0);
$baseConfidence += $matchBonus;
// Reduce confidence for potentially dangerous patterns (could be false positive)
if ($pattern->isPotentiallyDangerous()) {
$baseConfidence -= 15.0;
}
return Percentage::from(max(10.0, min(100.0, $baseConfidence)));
}
/**
* Generate descriptive message for match
*/
private static function generateMessage(RuleCondition $condition, string $matchedValue): string
{
$target = $condition->getTargetDescription();
$patternType = $condition->pattern->isRegex ? 'pattern' : 'string';
$valuePreview = strlen($matchedValue) > 50
? substr($matchedValue, 0, 50) . '...'
: $matchedValue;
if ($condition->negated) {
return "Negative match: {$target} does not contain expected {$patternType}";
}
return "Suspicious {$patternType} detected in {$target}: '{$valuePreview}'";
}
}

View File

@@ -0,0 +1,314 @@
<?php
declare(strict_types=1);
namespace App\Framework\Waf\Rules\ValueObjects;
use InvalidArgumentException;
/**
* Pattern for rule matching (string or regex)
*/
final readonly class RulePattern
{
public function __construct(
public string $pattern,
public bool $isRegex = false,
public bool $caseSensitive = false,
public bool $multiline = false,
public ?string $modifiers = null
) {
$this->validate();
}
/**
* Create simple string pattern
*/
public static function string(string $pattern, bool $caseSensitive = false): self
{
return new self(
pattern: $pattern,
isRegex: false,
caseSensitive: $caseSensitive
);
}
/**
* Create regex pattern
*/
public static function regex(string $pattern, ?string $modifiers = null): self
{
return new self(
pattern: $pattern,
isRegex: true,
caseSensitive: true, // Controlled by regex modifiers
multiline: str_contains($modifiers ?? '', 'm'),
modifiers: $modifiers
);
}
/**
* Create case-insensitive string pattern
*/
public static function stringIgnoreCase(string $pattern): self
{
return new self(
pattern: $pattern,
isRegex: false,
caseSensitive: false
);
}
/**
* Create SQL injection detection pattern
*/
public static function sqlInjection(): self
{
return self::regex(
pattern: '(?i:(?:[\s\'"`´«»₴¢£¥€₽₹₨₩₪₫₦₡₵₲₴₸₼₽﷼]*(?:select|union|insert|delete|update|create|drop|alter|exec|execute|declare|cast|convert|script)[\s\'"`´«»₴¢£¥€₽₹₨₩₪₫₦₡₵₲₴₸₼₽﷼]*|(?:\'(?:[^\'\\\\]|\\\\.)*\'|\"(?:[^\"\\\\]|\\\\.)*\"|`(?:[^`\\\\]|\\\\.)*`)[\s\'"`´«»₴¢£¥€₽₹₨₩₪₫₦₡₵₲₴₸₼₽﷼]*(?:=|<|>|<>|!=|like)[\s\'"`´«»₴¢£¥€₽₹₨₩₪₫₦₡₵₲₴₸₼₽﷼]*(?:\'(?:[^\'\\\\]|\\\\.)*\'|\"(?:[^\"\\\\]|\\\\.)*\"|`(?:[^`\\\\]|\\\\.)*`)|(?:or|and)[\s\'"`´«»₴¢£¥€₽₹₨₩₪₫₦₡₵₲₴₸₼₽﷼]+(?:\'(?:[^\'\\\\]|\\\\.)*\'|\"(?:[^\"\\\\]|\\\\.)*\"|`(?:[^`\\\\]|\\\\.)*`|[\d]+)[\s\'"`´«»₴¢£¥€₽₹₨₩₪₫₦₡₵₲₴₸₼₽﷼]*(?:=|<|>|<>|!=|like))',
modifiers: 'i'
);
}
/**
* Create XSS detection pattern
*/
public static function xssDetection(): self
{
return self::regex(
pattern: '(?i:<(?:script|iframe|object|embed|applet|meta|link|style|img|svg|math|details|template)[^>]*>|(?:javascript|vbscript|data|file|about):|on(?:load|error|click|focus|blur|change|submit|reset|select|keydown|keyup|keypress|mouseover|mouseout|mousedown|mouseup|mousemove)[\s]*=|(?:eval|setTimeout|setInterval|Function|execScript|mshtml|expression)\s*\()',
modifiers: 'i'
);
}
/**
* Create path traversal detection pattern
*/
public static function pathTraversal(): self
{
return self::regex(
pattern: '(?i:(?:\.\.[\\/])|(?:[\\/]\.\.)|(?:\.\.\\\\)|(?:\\\\\.\.)|(?:%2e%2e%2f)|(?:%2e%2e\\\\)|(?:\.\.%2f)|(?:\.\.%5c)|(?:%2e%2e%5c)|(?:%c0%ae%c0%ae%c0%af)|(?:%c1%9c%c1%9c%c1%af))',
modifiers: 'i'
);
}
/**
* Create command injection detection pattern
*/
public static function commandInjection(): self
{
return self::regex(
pattern: '(?i:(?:;|\||\|\||&&|&|`|\$\(|\${)[\s]*(?:cat|ls|pwd|id|uname|whoami|ps|kill|rm|mv|cp|chmod|chown|find|grep|awk|sed|sort|head|tail|wc|netstat|ifconfig|ping|wget|curl|nc|telnet|ssh|su|sudo|passwd|shadow|etc\/passwd|etc\/shadow|proc\/)|(?:(?:cmd|command)\.exe|powershell|bash|sh|zsh|csh|tcsh|fish)[\s]*(?:\/c|\/k|-c|-e))',
modifiers: 'i'
);
}
/**
* Create PHP code injection detection pattern
*/
public static function phpCodeInjection(): self
{
return self::regex(
pattern: '(?i:(?:<\?(?:php)?|<\?=|\?>)|(?:eval|assert|create_function|call_user_func|call_user_func_array|preg_replace|system|exec|shell_exec|passthru|file_get_contents|file_put_contents|fopen|fwrite|include|require|include_once|require_once)[\s]*\(|(?:\$_(?:GET|POST|REQUEST|COOKIE|SESSION|SERVER|ENV)\[))',
modifiers: 'i'
);
}
/**
* Test pattern against input string
*/
public function matches(string $input): bool
{
if ($this->isRegex) {
return $this->matchesRegex($input);
}
return $this->matchesString($input);
}
/**
* Get all matches from input
*/
public function getAllMatches(string $input): array
{
if ($this->isRegex) {
$matches = [];
$flags = PREG_SET_ORDER;
if ($this->multiline) {
$flags = $flags | PREG_UNMATCHED_AS_NULL;
}
$pattern = $this->getCompiledRegex();
preg_match_all($pattern, $input, $matches, $flags);
return $matches;
}
// For string patterns, find all occurrences
$matches = [];
$searchInput = $this->caseSensitive ? $input : strtolower($input);
$searchPattern = $this->caseSensitive ? $this->pattern : strtolower($this->pattern);
$offset = 0;
while (($pos = strpos($searchInput, $searchPattern, $offset)) !== false) {
$matches[] = [
'match' => substr($input, $pos, strlen($this->pattern)),
'offset' => $pos,
];
$offset = $pos + 1;
}
return $matches;
}
/**
* Get match position in input
*/
public function getMatchPosition(string $input): ?int
{
if ($this->isRegex) {
$matches = [];
$pattern = $this->getCompiledRegex();
if (preg_match($pattern, $input, $matches, PREG_OFFSET_CAPTURE)) {
return $matches[0][1];
}
return null;
}
$searchInput = $this->caseSensitive ? $input : strtolower($input);
$searchPattern = $this->caseSensitive ? $this->pattern : strtolower($this->pattern);
$pos = strpos($searchInput, $searchPattern);
return $pos !== false ? $pos : null;
}
/**
* Get compiled regex pattern
*/
public function getCompiledRegex(): string
{
if (! $this->isRegex) {
throw new InvalidArgumentException('Cannot compile non-regex pattern');
}
$delimiters = ['/', '#', '~', '@', '%', '!'];
$delimiter = '/';
// Find a delimiter that's not in the pattern
foreach ($delimiters as $d) {
if (strpos($this->pattern, $d) === false) {
$delimiter = $d;
break;
}
}
$compiled = $delimiter . $this->pattern . $delimiter;
if ($this->modifiers) {
$compiled .= $this->modifiers;
}
return $compiled;
}
/**
* Check if pattern is potentially dangerous
*/
public function isPotentiallyDangerous(): bool
{
if (! $this->isRegex) {
return false;
}
// Check for potentially expensive regex patterns
$dangerousPatterns = [
'.*.*', // Nested quantifiers
'.+.+', // Nested quantifiers
'(.*)*', // Nested groups with quantifiers
'(.+)+', // Nested groups with quantifiers
'([^x]*)*', // Negated character class with quantifier
];
foreach ($dangerousPatterns as $dangerous) {
if (strpos($this->pattern, $dangerous) !== false) {
return true;
}
}
return false;
}
/**
* Convert to array
*/
public function toArray(): array
{
return [
'pattern' => $this->pattern,
'is_regex' => $this->isRegex,
'case_sensitive' => $this->caseSensitive,
'multiline' => $this->multiline,
'modifiers' => $this->modifiers,
'is_dangerous' => $this->isPotentiallyDangerous(),
];
}
/**
* Match against regex pattern
*/
private function matchesRegex(string $input): bool
{
$pattern = $this->getCompiledRegex();
return preg_match($pattern, $input) === 1;
}
/**
* Match against string pattern
*/
private function matchesString(string $input): bool
{
$searchInput = $this->caseSensitive ? $input : strtolower($input);
$searchPattern = $this->caseSensitive ? $this->pattern : strtolower($this->pattern);
return strpos($searchInput, $searchPattern) !== false;
}
/**
* Validate pattern
*/
private function validate(): void
{
if (empty($this->pattern)) {
throw new InvalidArgumentException('Pattern cannot be empty');
}
if ($this->isRegex) {
// Test regex compilation
$testPattern = $this->getCompiledRegex();
// Suppress warnings and test the regex
$result = @preg_match($testPattern, '');
if ($result === false) {
throw new InvalidArgumentException("Invalid regex pattern: {$this->pattern}");
}
// Check for dangerous patterns
if ($this->isPotentiallyDangerous()) {
// Log warning but don't fail - let the user decide
error_log("Warning: Potentially dangerous regex pattern detected: {$this->pattern}");
}
}
}
public function __toString(): string
{
return $this->pattern;
}
}