getClientIp() ?? '0.0.0.0'; $deviceFingerprint = $request->headers->get('X-Device-Fingerprint'); $result = $this->analyze($ipAddress, $deviceFingerprint); if ($result->isBlocked()) { return false; } return true; } /** * Check if a request is allowed based on IP and device fingerprint */ public function analyze( string $ipAddress, ?string $deviceFingerprint = null ): FirewallAnalyzeResult { $tenantId = $this->tenantContext->identifier(); $ruleGroups = [ [$this->ruleCache->system(), FirewallRuleObject::ACTION_BLOCK], ]; if ($tenantId !== null && $this->isEnabled()) { $tenantRules = $this->ruleCache->tenant($tenantId); $ruleGroups[] = [$tenantRules, FirewallRuleObject::ACTION_ALLOW]; $ruleGroups[] = [$tenantRules, FirewallRuleObject::ACTION_BLOCK]; } $ruleGroups[] = [$this->ruleCache->system(), FirewallRuleObject::ACTION_ALLOW]; foreach ($ruleGroups as [$rules, $action]) { foreach ($rules as $rule) { if ($rule->getAction() !== $action) { continue; } if (!$this->ruleMatchesRequest($rule, $ipAddress, $deviceFingerprint)) { continue; } if ($action === FirewallRuleObject::ACTION_ALLOW) { return new FirewallAnalyzeResult(true, $rule->getId(), 'Explicitly allowed'); } $this->publishAccessDenied($ipAddress, $deviceFingerprint, $rule); return new FirewallAnalyzeResult(false, $rule->getId(), $rule->getReason()); } } return new FirewallAnalyzeResult(true); } /** * Check if a rule matches the request */ private function ruleMatchesRequest( FirewallRuleObject $rule, string $ipAddress, ?string $deviceFingerprint ): bool { $type = $rule->getType(); $value = $rule->getValue(); return match ($type) { FirewallRuleObject::TYPE_IP => $ipAddress === $value, FirewallRuleObject::TYPE_IP_RANGE => IpUtils::checkIp($ipAddress, $value), FirewallRuleObject::TYPE_DEVICE => $deviceFingerprint !== null && $deviceFingerprint === $value, default => false, }; } /** * Handle authentication failure event */ public function handleAuthFailure(SecurityEvent $event): void { $ipAddress = $event->getIpAddress(); $tenantId = $event->getTenantId() ?? $this->tenantContext->identifier(); if (!$ipAddress || !$tenantId) { return; } $event->setTenantId($tenantId); $log = $this->securityLog($event); if ($log === null || !$this->store->createLogOnce($log)) { return; } // Check for brute force $windowSeconds = $this->getBoundedIntegerConfig( self::CONFIG_FAILURE_WINDOW, self::DEFAULT_AUTH_FAILURE_WINDOW, self::MAX_AUTH_FAILURE_WINDOW ); $maxFailures = $this->getBoundedIntegerConfig( self::CONFIG_MAX_FAILURES, self::DEFAULT_MAX_AUTH_FAILURES, self::MAX_AUTH_FAILURES ); $failureCount = $this->store->countRecentFailures( $tenantId, $ipAddress, $windowSeconds ); if ($failureCount >= $maxFailures) { $this->handleBruteForce($tenantId, $ipAddress, $failureCount, $windowSeconds); } } /** * Handle detected brute force attack */ private function handleBruteForce( string $tenantId, string $ipAddress, int $failureCount, int $windowSeconds ): void { // Publish brute force event $event = SecurityEvent::bruteForceDetected($ipAddress, $failureCount, $windowSeconds); $event->setTenantId($tenantId); $this->events->dispatch($event); // Auto-block the IP $blockDuration = $this->getBoundedIntegerConfig( self::CONFIG_AUTO_BLOCK_DURATION, self::DEFAULT_AUTO_BLOCK_DURATION, self::MAX_AUTO_BLOCK_DURATION ); $this->rules->blockIp( FirewallRuleScope::tenant($tenantId), $ipAddress, sprintf('Auto-blocked: %d failed auth attempts in %d seconds', $failureCount, $windowSeconds), null, // System-created $blockDuration, FirewallRuleManager::ORIGIN_AUTOMATIC ); } /** * Log security event to firewall logs */ public function logSecurityEvent(SecurityEvent $event): void { $log = $this->securityLog($event); if ($log !== null) { $this->store->createLog($log); } } private function securityLog(SecurityEvent $event): ?FirewallLogObject { $tenantId = $event->getTenantId() ?? $this->tenantContext->identifier(); $ruleScope = $event->get('ruleScope'); if (!$tenantId && $ruleScope !== FirewallRuleObject::SCOPE_SYSTEM) { return null; } $log = new FirewallLogObject(); return $log->setEventId($event->getEventId()) ->setTenantId($tenantId) ->setIpAddress($event->getIpAddress()) ->setDeviceFingerprint($event->getDeviceFingerprint()) ->setUserAgent($event->getUserAgent()) ->setRequestPath($event->getRequestPath()) ->setRequestMethod($event->getRequestMethod()) ->setEventType($this->mapEventToLogType($event->getName())) ->setResult($this->mapEventToResult($event)) ->setRuleId($event->get('ruleId')) ->setRuleScope($ruleScope) ->setIdentityId($event->getUserId() ?? $event->getIdentityId()) ->setTimestamp(new \DateTimeImmutable()) ->setMetadata($event->getData()); } /** * Map security event name to log event type */ private function mapEventToLogType(string $eventName): string { return match ($eventName) { SecurityEvent::AUTH_FAILURE => FirewallLogObject::EVENT_AUTH_FAILURE, SecurityEvent::AUTH_SUCCESS => FirewallLogObject::EVENT_ACCESS_CHECK, SecurityEvent::BRUTE_FORCE_DETECTED => FirewallLogObject::EVENT_BRUTE_FORCE, SecurityEvent::RATE_LIMIT_EXCEEDED => FirewallLogObject::EVENT_RATE_LIMIT, SecurityEvent::ACCESS_DENIED => FirewallLogObject::EVENT_RULE_MATCH, SecurityEvent::SUSPICIOUS_ACTIVITY => FirewallLogObject::EVENT_SUSPICIOUS, SecurityEvent::FIREWALL_RULE_CREATED => FirewallLogObject::EVENT_RULE_CREATED, SecurityEvent::FIREWALL_RULE_DISABLED => FirewallLogObject::EVENT_RULE_DISABLED, SecurityEvent::FIREWALL_RULE_REMOVED => FirewallLogObject::EVENT_RULE_REMOVED, default => FirewallLogObject::EVENT_ACCESS_CHECK, }; } /** * Map security event to result */ private function mapEventToResult(SecurityEvent $event): string { return match ($event->getName()) { SecurityEvent::AUTH_SUCCESS, SecurityEvent::ACCESS_GRANTED => FirewallLogObject::RESULT_ALLOWED, SecurityEvent::FIREWALL_RULE_CREATED, SecurityEvent::FIREWALL_RULE_DISABLED, SecurityEvent::FIREWALL_RULE_REMOVED => FirewallLogObject::RESULT_RECORDED, default => FirewallLogObject::RESULT_BLOCKED, }; } /** * Publish access denied event */ private function publishAccessDenied( string $ipAddress, ?string $deviceFingerprint, FirewallRuleObject $rule ): void { $event = SecurityEvent::accessDenied( $ipAddress, $deviceFingerprint, $rule->getId(), $rule->getScope(), $rule->getReason() ); $event->setTenantId($this->tenantContext->identifier()); $this->events->dispatch($event); } /** * Check if firewall is enabled for current tenant */ private function isEnabled(): bool { return (bool) $this->getConfig(self::CONFIG_ENABLED, true); } /** * Get configuration value */ private function getConfig(string $key, mixed $default = null): mixed { $config = $this->tenantContext->configuration(); if ($config instanceof \JsonSerializable) { $config = $config->jsonSerialize(); } $parts = explode('.', $key); foreach ($parts as $part) { if (!is_array($config) || !array_key_exists($part, $config)) { return $default; } $config = $config[$part]; } return $config; } private function getBoundedIntegerConfig(string $key, int $default, int $maximum): int { $value = $this->getConfig($key, $default); if (!is_int($value) || $value < 1 || $value > $maximum) { return $default; } return $value; } /** * Cleanup maintenance tasks */ public function cleanup(): array { $expiredRules = $this->store->cleanupExpiredRules(); $oldLogs = $this->store->cleanupOldLogs(30); return [ 'expiredRules' => $expiredRules, 'oldLogs' => $oldLogs, ]; } } /** * Result of a firewall check */ class FirewallAnalyzeResult { public function __construct( public readonly bool $allowed, public readonly ?string $ruleId = null, public readonly ?string $reason = null ) {} public function isAllowed(): bool { return $this->allowed; } public function isBlocked(): bool { return !$this->allowed; } }