eventBus->subscribe( SecurityEvent::AUTH_FAILURE, [$this, 'handleAuthFailure'], 100 // High priority ); // Log all security events asynchronously $this->eventBus->subscribeAsync( SecurityEvent::AUTH_FAILURE, [$this, 'logSecurityEvent'] ); $this->eventBus->subscribeAsync( SecurityEvent::AUTH_SUCCESS, [$this, 'logSecurityEvent'] ); $this->eventBus->subscribeAsync( SecurityEvent::ACCESS_DENIED, [$this, 'logSecurityEvent'] ); $this->eventBus->subscribeAsync( SecurityEvent::BRUTE_FORCE_DETECTED, [$this, 'logSecurityEvent'] ); } /** * Check firewall rules for a request * Returns a Response if blocked, null if allowed */ public function authorized(Request $request): bool { $ipAddress = $request->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 { // Check if firewall is enabled for this tenant if (!$this->isEnabled()) { return new FirewallAnalyzeResult(true); } $tenantId = $this->tenant->identifier(); if (!$tenantId) { return new FirewallAnalyzeResult(true); } $rules = $this->getActiveRules(); // First check for explicit allow rules (whitelist takes precedence) foreach ($rules as $rule) { if ($rule->getAction() !== FirewallRuleObject::ACTION_ALLOW) { continue; } if ($this->ruleMatchesRequest($rule, $ipAddress, $deviceFingerprint)) { return new FirewallAnalyzeResult(true, $rule->getId(), 'Explicitly allowed'); } } // Then check for block rules foreach ($rules as $rule) { if ($rule->getAction() !== FirewallRuleObject::ACTION_BLOCK) { continue; } if ($this->ruleMatchesRequest($rule, $ipAddress, $deviceFingerprint)) { $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->tenant->identifier(); if (!$ipAddress || !$tenantId) { return; } // Check for brute force $windowSeconds = $this->getConfig( self::CONFIG_FAILURE_WINDOW, self::DEFAULT_AUTH_FAILURE_WINDOW ); $maxFailures = $this->getConfig( self::CONFIG_MAX_FAILURES, self::DEFAULT_MAX_AUTH_FAILURES ); $failureCount = $this->store->countRecentFailures( $tenantId, $ipAddress, $windowSeconds ); // Include current failure in count $failureCount++; if ($failureCount >= $maxFailures) { $this->handleBruteForce($ipAddress, $failureCount, $windowSeconds); } } /** * Handle detected brute force attack */ private function handleBruteForce( string $ipAddress, int $failureCount, int $windowSeconds ): void { // Publish brute force event $event = SecurityEvent::bruteForceDetected($ipAddress, $failureCount, $windowSeconds); $event->setTenantId($this->tenant->identifier()); $this->eventBus->publish($event); // Auto-block the IP $blockDuration = $this->getConfig( self::CONFIG_AUTO_BLOCK_DURATION, self::DEFAULT_AUTO_BLOCK_DURATION ); $this->blockIp( $ipAddress, sprintf('Auto-blocked: %d failed auth attempts in %d seconds', $failureCount, $windowSeconds), null, // System-created $blockDuration ); } /** * Log security event to firewall logs */ public function logSecurityEvent(SecurityEvent $event): void { $tenantId = $event->getTenantId() ?? $this->tenant->identifier(); if (!$tenantId) { return; } $log = new FirewallLogObject(); $log->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->getName())) ->setIdentityId($event->getUserId()) ->setTimestamp(new \DateTimeImmutable()) ->setMetadata($event->getData()); $this->store->createLog($log); } /** * 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, default => FirewallLogObject::EVENT_ACCESS_CHECK, }; } /** * Map security event to result */ private function mapEventToResult(string $eventName): string { return match ($eventName) { SecurityEvent::AUTH_SUCCESS, SecurityEvent::ACCESS_GRANTED => FirewallLogObject::RESULT_ALLOWED, 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->getReason() ); $event->setTenantId($this->tenant->identifier()); $this->eventBus->publish($event); } // ======================================== // Rule Management // ======================================== /** * Block an IP address */ public function blockIp( string $ipAddress, ?string $reason = null, ?string $createdBy = null, ?int $durationSeconds = null ): FirewallRuleObject { $tenantId = $this->tenant->identifier(); if (!$tenantId) { throw new \RuntimeException('Cannot create firewall rule: no tenant configured'); } // Check if already blocked $existing = $this->store->findExactIpRule( $tenantId, $ipAddress, FirewallRuleObject::ACTION_BLOCK ); if ($existing) { return $existing; } $rule = new FirewallRuleObject(); $rule->setTenantId($tenantId) ->setType(FirewallRuleObject::TYPE_IP) ->setAction(FirewallRuleObject::ACTION_BLOCK) ->setValue($ipAddress) ->setReason($reason ?? 'Blocked by administrator') ->setCreatedBy($createdBy) ->setCreatedAt(new \DateTimeImmutable()) ->setEnabled(true); if ($durationSeconds !== null) { $rule->setExpiresAt( (new \DateTimeImmutable())->modify("+{$durationSeconds} seconds") ); } $this->store->depositRule($rule); $this->clearRulesCache(); // Publish event $event = new SecurityEvent(SecurityEvent::IP_BLOCKED, ['ip' => $ipAddress, 'reason' => $reason]); $event->setIpAddress($ipAddress) ->setReason($reason) ->setTenantId($tenantId); $this->eventBus->publish($event); return $rule; } /** * Allow an IP address (whitelist) */ public function allowIp( string $ipAddress, ?string $reason = null, ?string $createdBy = null ): FirewallRuleObject { $tenantId = $this->tenant->identifier(); if (!$tenantId) { throw new \RuntimeException('Cannot create firewall rule: no tenant configured'); } $rule = new FirewallRuleObject(); $rule->setTenantId($tenantId) ->setType(FirewallRuleObject::TYPE_IP) ->setAction(FirewallRuleObject::ACTION_ALLOW) ->setValue($ipAddress) ->setReason($reason ?? 'Allowed by administrator') ->setCreatedBy($createdBy) ->setCreatedAt(new \DateTimeImmutable()) ->setEnabled(true); $this->store->depositRule($rule); $this->clearRulesCache(); // Publish event $event = new SecurityEvent(SecurityEvent::IP_ALLOWED, ['ip' => $ipAddress, 'reason' => $reason]); $event->setIpAddress($ipAddress) ->setReason($reason) ->setTenantId($tenantId); $this->eventBus->publish($event); return $rule; } /** * Block an IP range (CIDR notation) */ public function blockIpRange( string $cidr, ?string $reason = null, ?string $createdBy = null ): FirewallRuleObject { $tenantId = $this->tenant->identifier(); if (!$tenantId) { throw new \RuntimeException('Cannot create firewall rule: no tenant configured'); } $rule = new FirewallRuleObject(); $rule->setTenantId($tenantId) ->setType(FirewallRuleObject::TYPE_IP_RANGE) ->setAction(FirewallRuleObject::ACTION_BLOCK) ->setValue($cidr) ->setReason($reason ?? 'Range blocked by administrator') ->setCreatedBy($createdBy) ->setCreatedAt(new \DateTimeImmutable()) ->setEnabled(true); $this->store->depositRule($rule); $this->clearRulesCache(); return $rule; } /** * Block a device fingerprint */ public function blockDevice( string $fingerprint, ?string $reason = null, ?string $createdBy = null, ?int $durationSeconds = null ): FirewallRuleObject { $tenantId = $this->tenant->identifier(); if (!$tenantId) { throw new \RuntimeException('Cannot create firewall rule: no tenant configured'); } $rule = new FirewallRuleObject(); $rule->setTenantId($tenantId) ->setType(FirewallRuleObject::TYPE_DEVICE) ->setAction(FirewallRuleObject::ACTION_BLOCK) ->setValue($fingerprint) ->setReason($reason ?? 'Device blocked by administrator') ->setCreatedBy($createdBy) ->setCreatedAt(new \DateTimeImmutable()) ->setEnabled(true); if ($durationSeconds !== null) { $rule->setExpiresAt( (new \DateTimeImmutable())->modify("+{$durationSeconds} seconds") ); } $this->store->depositRule($rule); $this->clearRulesCache(); // Publish event $event = new SecurityEvent(SecurityEvent::DEVICE_BLOCKED, ['device' => $fingerprint, 'reason' => $reason]); $event->setDeviceFingerprint($fingerprint) ->setReason($reason) ->setTenantId($tenantId); $this->eventBus->publish($event); return $rule; } /** * Remove a rule by ID */ public function removeRule(string $ruleId): bool { $rule = $this->store->fetchRule($ruleId); if (!$rule) { return false; } // Verify tenant ownership if ($rule->getTenantId() !== $this->tenant->identifier()) { return false; } $this->store->destroyRule($rule); $this->clearRulesCache(); return true; } /** * Disable a rule (soft delete) */ public function disableRule(string $ruleId): bool { $rule = $this->store->fetchRule($ruleId); if (!$rule) { return false; } // Verify tenant ownership if ($rule->getTenantId() !== $this->tenant->identifier()) { return false; } $rule->setEnabled(false); $this->store->depositRule($rule); $this->clearRulesCache(); return true; } /** * Get all rules for current tenant */ public function listRules(bool $activeOnly = true): array { $tenantId = $this->tenant->identifier(); if (!$tenantId) { return []; } return $this->store->listRules($tenantId, $activeOnly); } /** * Get firewall logs for current tenant */ public function getLogs( ?string $ipAddress = null, ?string $eventType = null, ?string $result = null, int $limit = 100 ): array { $tenantId = $this->tenant->identifier(); if (!$tenantId) { return []; } return $this->store->listLogs($tenantId, $ipAddress, $eventType, $result, $limit); } /** * Get blocked requests count */ public function getBlockedCount(?\DateTimeImmutable $since = null): int { $tenantId = $this->tenant->identifier(); if (!$tenantId) { return 0; } return $this->store->countBlockedRequests($tenantId, $since); } // ======================================== // Helpers // ======================================== /** * 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->tenant->configuration(); $parts = explode('.', $key); foreach ($parts as $part) { if (!is_array($config) || !array_key_exists($part, $config)) { return $default; } $config = $config[$part]; } return $config; } /** * Get active rules (cached) * @return FirewallRuleObject[] */ private function getActiveRules(): array { if ($this->rulesCache === null) { $tenantId = $this->tenant->identifier(); $this->rulesCache = $tenantId ? $this->store->listRules($tenantId, true) : []; } return $this->rulesCache; } /** * Clear rules cache */ private function clearRulesCache(): void { $this->rulesCache = null; } /** * 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; } }