Files
michaelschiemer/src/Framework/Http/Middlewares/WafMiddleware.php
Michael Schiemer 5050c7d73a docs: consolidate documentation into organized structure
- Move 12 markdown files from root to docs/ subdirectories
- Organize documentation by category:
  • docs/troubleshooting/ (1 file)  - Technical troubleshooting guides
  • docs/deployment/      (4 files) - Deployment and security documentation
  • docs/guides/          (3 files) - Feature-specific guides
  • docs/planning/        (4 files) - Planning and improvement proposals

Root directory cleanup:
- Reduced from 16 to 4 markdown files in root
- Only essential project files remain:
  • CLAUDE.md (AI instructions)
  • README.md (Main project readme)
  • CLEANUP_PLAN.md (Current cleanup plan)
  • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements)

This improves:
 Documentation discoverability
 Logical organization by purpose
 Clean root directory
 Better maintainability
2025-10-05 11:05:04 +02:00

249 lines
9.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\Http\Middlewares;
use App\Framework\Config\WafConfig;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\HttpResponse;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\Next;
use App\Framework\Http\RequestStateManager;
use App\Framework\Http\Responses\JsonResponse;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;
use App\Framework\Waf\LayerResult;
use App\Framework\Waf\WafEngine;
/**
* WAF (Web Application Firewall) Middleware
*
* Integrates the WAF engine into the HTTP request pipeline for security analysis
* and threat detection. Blocks or flags requests based on WAF layer results.
*/
#[MiddlewarePriorityAttribute(MiddlewarePriority::SECURITY)]
final readonly class WafMiddleware implements HttpMiddleware
{
public function __construct(
private WafEngine $wafEngine,
private Logger $logger,
private WafConfig $config
) {
// Ensure security layers are registered
$this->initializeSecurityLayers();
}
/**
* Initialize security layers if not already registered
*/
private function initializeSecurityLayers(): void
{
// Check if layers are already registered (avoid double registration)
$healthStatus = $this->wafEngine->getHealthStatus();
if ($healthStatus['total_layers'] > 0) {
return; // Already initialized
}
try {
// Register security layers directly
$this->wafEngine->registerLayer(new \App\Framework\Waf\Layers\SqlInjectionLayer());
$this->wafEngine->registerLayer(new \App\Framework\Waf\Layers\CommandInjectionLayer());
$this->wafEngine->registerLayer(new \App\Framework\Waf\Layers\PathTraversalLayer());
$this->wafEngine->registerLayer(new \App\Framework\Waf\Layers\XssLayer());
$this->wafEngine->registerLayer(new \App\Framework\Waf\Layers\SuspiciousUserAgentLayer());
$this->logger->info('WAF security layers initialized', LogContext::withData([
'layers_count' => 5,
'health_status' => $this->wafEngine->getHealthStatus(),
]));
} catch (\Throwable $e) {
$this->logger->error('Failed to initialize WAF security layers', LogContext::withData([
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]));
}
}
public function __invoke(MiddlewareContext $context, Next $next, RequestStateManager $stateManager): MiddlewareContext
{
if (! $this->config->enabled) {
$this->logger->debug('WAF disabled, skipping analysis');
return $next($context);
}
try {
$request = $context->request;
// Debug log request details
$this->logger->debug('WAF analyzing request', LogContext::withData([
'path' => $request->path ?? '/',
'method' => $request->method->value ?? 'UNKNOWN',
'query_params' => $request->queryParams,
'post_data' => $request->parsedBody->data ?? null,
'user_agent' => $request->headers->get('User-Agent', ''),
'client_ip' => $request->server->getClientIp()?->value ?? 'unknown',
'waf_config' => [
'enabled' => $this->config->enabled,
'blocking_mode' => $this->config->blockingMode,
'enabled_layers' => $this->config->enabledLayers,
],
]));
// Analyze request with WAF engine
$wafResult = $this->wafEngine->analyze($request);
// Debug log analysis result
$this->logger->debug('WAF analysis complete', LogContext::withData([
'result_status' => $wafResult->getStatus()->value ?? 'unknown',
'result_action' => $wafResult->getAction(),
'layer_name' => $wafResult->getLayerName(),
'message' => $wafResult->getMessage(),
'has_detections' => $wafResult->hasDetections(),
'detections_count' => $wafResult->hasDetections() ? count($wafResult->getDetections()->getAll()) : 0,
]));
// Handle based on result action
switch ($wafResult->getAction()) {
case LayerResult::ACTION_BLOCK:
$this->logger->info('WAF blocking request', LogContext::withData(['reason' => $wafResult->getMessage()]));
return $this->handleBlocked($context, $wafResult);
case LayerResult::ACTION_SUSPICIOUS:
$this->logger->info('WAF flagging suspicious request', LogContext::withData(['reason' => $wafResult->getMessage()]));
return $this->handleSuspicious($context, $wafResult, $next);
case LayerResult::ACTION_PASS:
default:
$this->logger->debug('WAF allowing request', LogContext::withData(['reason' => $wafResult->getMessage()]));
// Continue to next middleware
return $next($context);
}
} catch (\Throwable $e) {
// Log WAF error but don't block request on WAF failure
$this->logger->error('WAF middleware error', LogContext::withData([
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'request_path' => $context->request->path ?? '/',
'client_ip' => $context->request->server->getClientIp()?->value ?? 'unknown',
]));
// Continue processing on WAF error
return $next($context);
}
}
/**
* Handle blocked requests
*/
private function handleBlocked(MiddlewareContext $context, LayerResult $wafResult): MiddlewareContext
{
$request = $context->request;
$clientIp = $request->server->getClientIp()?->value ?? 'unknown';
// Log security event
$this->logger->warning('WAF blocked request', LogContext::withData([
'reason' => $wafResult->getMessage(),
'client_ip' => $clientIp,
'path' => $request->path ?? '/',
'user_agent' => $request->headers->get('User-Agent', ''),
'detections' => $this->formatDetections($wafResult),
'layer' => $wafResult->getLayerName(),
]));
if ($this->config->blockingMode) {
// Return 403 Forbidden response
$response = new JsonResponse([
'error' => 'Request blocked by security policy',
'code' => 'WAF_BLOCKED',
'request_id' => $request->id->value(),
], 403);
} else {
// Log-only mode - continue processing but log the threat
$this->logger->warning('WAF would block request (log-only mode)', LogContext::withData([
'reason' => $wafResult->getMessage(),
'client_ip' => $clientIp,
'path' => $request->path ?? '/',
]));
$response = new JsonResponse([
'warning' => 'Request flagged by security policy',
'code' => 'WAF_FLAGGED',
], 200);
}
return $context->withResponse($response);
}
/**
* Handle suspicious requests
*/
private function handleSuspicious(MiddlewareContext $context, LayerResult $wafResult, Next $next): MiddlewareContext
{
$request = $context->request;
$clientIp = $request->server->getClientIp()?->value ?? 'unknown';
// Log suspicious activity
$this->logger->info('WAF flagged suspicious request', LogContext::withData([
'reason' => $wafResult->getMessage(),
'client_ip' => $clientIp,
'path' => $request->path ?? '/',
'user_agent' => $request->headers->get('User-Agent', ''),
'detections' => $this->formatDetections($wafResult),
'layer' => $wafResult->getLayerName(),
]));
// Continue to next middleware
$resultContext = $next($context);
// Add WAF detection info to response headers for monitoring
if ($resultContext->hasResponse()) {
$response = $resultContext->response;
$updatedHeaders = $response->headers
->with('X-Waf-Status', 'suspicious')
->with('X-Waf-Layer', $wafResult->getLayerName());
$updatedResponse = new HttpResponse(
$response->body,
$response->statusCode,
$updatedHeaders
);
return $resultContext->withResponse($updatedResponse);
}
return $resultContext;
}
/**
* Format detection information for logging
*/
private function formatDetections(LayerResult $wafResult): array
{
if (! $wafResult->hasDetections()) {
return [];
}
$detections = [];
foreach ($wafResult->getDetections()->getAll() as $detection) {
$detections[] = [
'category' => $detection->category->value,
'severity' => $detection->severity->value,
'message' => $detection->message,
'confidence' => $detection->confidence,
'evidence' => $detection->evidence,
];
}
return $detections;
}
}