- 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
249 lines
9.4 KiB
PHP
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;
|
|
}
|
|
}
|