scope === FirewallRuleObject::SCOPE_SYSTEM ? $this->store->listSystemRules($activeOnly) : $this->store->listRules($scope->tenantId, $activeOnly); } public function blockIp( FirewallRuleScope $scope, string $ipAddress, ?string $reason, ?string $createdBy, ?int $durationSeconds = null, string $origin = self::ORIGIN_MANUAL, array $metadata = [] ): FirewallRuleObject { $ipAddress = FirewallRuleValidator::ipAddress($ipAddress); FirewallRuleValidator::duration($durationSeconds); $existing = $this->store->findExactIpRule( $scope->tenantId, $ipAddress, FirewallRuleObject::ACTION_BLOCK, $scope->scope ); if ($existing) { if ( $origin === self::ORIGIN_AUTOMATIC && ($existing->getMetadata()['origin'] ?? null) === self::ORIGIN_AUTOMATIC && $durationSeconds !== null ) { return $this->extendAutomaticBlock($existing, $durationSeconds, $metadata); } return $existing; } $rule = $this->create( $scope, FirewallRuleObject::TYPE_IP, FirewallRuleObject::ACTION_BLOCK, $ipAddress, $reason ?? 'Blocked by administrator', $createdBy, $durationSeconds, $origin, $metadata ); $this->publishIpEvent(SecurityEvent::IP_BLOCKED, $scope, $ipAddress, $reason); return $rule; } public function allowIp( FirewallRuleScope $scope, string $ipAddress, ?string $reason, ?string $createdBy, string $origin = self::ORIGIN_MANUAL ): FirewallRuleObject { $ipAddress = FirewallRuleValidator::ipAddress($ipAddress); $rule = $this->create( $scope, FirewallRuleObject::TYPE_IP, FirewallRuleObject::ACTION_ALLOW, $ipAddress, $reason ?? 'Allowed by administrator', $createdBy, null, $origin ); $this->publishIpEvent(SecurityEvent::IP_ALLOWED, $scope, $ipAddress, $reason); return $rule; } public function blockIpRange( FirewallRuleScope $scope, string $cidr, ?string $reason, ?string $createdBy, string $origin = self::ORIGIN_MANUAL ): FirewallRuleObject { return $this->create( $scope, FirewallRuleObject::TYPE_IP_RANGE, FirewallRuleObject::ACTION_BLOCK, FirewallRuleValidator::cidr($cidr), $reason ?? 'Range blocked by administrator', $createdBy, null, $origin ); } public function blockDevice( FirewallRuleScope $scope, string $fingerprint, ?string $reason, ?string $createdBy, ?int $durationSeconds = null, string $origin = self::ORIGIN_MANUAL ): FirewallRuleObject { FirewallRuleValidator::duration($durationSeconds); $fingerprint = FirewallRuleValidator::deviceFingerprint($fingerprint); $rule = $this->create( $scope, FirewallRuleObject::TYPE_DEVICE, FirewallRuleObject::ACTION_BLOCK, $fingerprint, $reason ?? 'Device blocked by administrator', $createdBy, $durationSeconds, $origin ); $event = new SecurityEvent(SecurityEvent::DEVICE_BLOCKED, ['device' => $fingerprint, 'reason' => $reason]); $event->setDeviceFingerprint($fingerprint)->setReason($reason)->setTenantId($scope->tenantId); $this->events->dispatch($event); return $rule; } public function disable(FirewallRuleScope $scope, string $ruleId, ?string $actorId = null): bool { $rule = $this->ownedRule($scope, $ruleId); if (!$rule) { return false; } $rule->setEnabled(false); $this->store->depositRule($rule); $this->cache->invalidate(); $this->publishLifecycleEvent(SecurityEvent::FIREWALL_RULE_DISABLED, $rule, $actorId); return true; } public function remove(FirewallRuleScope $scope, string $ruleId, ?string $actorId = null): bool { $rule = $this->ownedRule($scope, $ruleId); if (!$rule) { return false; } $this->store->destroyRule($rule); $this->cache->invalidate(); $this->publishLifecycleEvent(SecurityEvent::FIREWALL_RULE_REMOVED, $rule, $actorId); return true; } private function create( FirewallRuleScope $scope, string $type, string $action, string $value, string $reason, ?string $createdBy, ?int $durationSeconds = null, string $origin = self::ORIGIN_MANUAL, array $metadata = [] ): FirewallRuleObject { if (!in_array($origin, [self::ORIGIN_MANUAL, self::ORIGIN_AUTOMATIC], true)) { throw new \InvalidArgumentException("Invalid firewall rule origin: {$origin}"); } $rule = (new FirewallRuleObject()) ->setScope($scope->scope) ->setTenantId($scope->tenantId) ->setType($type) ->setAction($action) ->setValue($value) ->setReason($reason) ->setCreatedBy($createdBy) ->setCreatedAt(new \DateTimeImmutable()) ->setEnabled(true); if ($durationSeconds !== null) { $rule->setExpiresAt((new \DateTimeImmutable())->modify("+{$durationSeconds} seconds")); } $metadata = [...$metadata, 'origin' => $origin]; if ($origin === self::ORIGIN_AUTOMATIC && $rule->getExpiresAt() !== null) { $metadata['originalExpiresAt'] = $rule->getExpiresAt()->format(\DateTimeInterface::ATOM); $metadata['extensions'] = []; } $rule->setMetadata($metadata); $this->store->depositRule($rule); $this->cache->invalidate(); $this->publishLifecycleEvent(SecurityEvent::FIREWALL_RULE_CREATED, $rule); return $rule; } private function extendAutomaticBlock( FirewallRuleObject $rule, int $durationSeconds, array $policy ): FirewallRuleObject { $now = new \DateTimeImmutable(); $previousExpiry = $rule->getExpiresAt(); $newExpiry = $now->modify("+{$durationSeconds} seconds"); if ($previousExpiry !== null && $newExpiry <= $previousExpiry) { return $rule; } $metadata = $rule->getMetadata() ?? []; $extensions = is_array($metadata['extensions'] ?? null) ? $metadata['extensions'] : []; $extensions[] = [ 'extendedAt' => $now->format(\DateTimeInterface::ATOM), 'previousExpiresAt' => $previousExpiry?->format(\DateTimeInterface::ATOM), 'expiresAt' => $newExpiry->format(\DateTimeInterface::ATOM), 'failureCount' => $policy['lastFailureCount'] ?? null, ]; $rule->setExpiresAt($newExpiry)->setMetadata([ ...$metadata, ...$policy, 'origin' => self::ORIGIN_AUTOMATIC, 'originalExpiresAt' => $metadata['originalExpiresAt'] ?? $previousExpiry?->format(\DateTimeInterface::ATOM), 'extensions' => $extensions, 'lastExtendedAt' => $now->format(\DateTimeInterface::ATOM), ]); $this->store->depositRule($rule); $this->cache->invalidate(); $this->publishLifecycleEvent(SecurityEvent::FIREWALL_RULE_EXTENDED, $rule); return $rule; } private function ownedRule(FirewallRuleScope $scope, string $ruleId): ?FirewallRuleObject { $rule = $this->store->fetchRule($ruleId); return $rule && $scope->owns($rule) ? $rule : null; } private function publishIpEvent( string $name, FirewallRuleScope $scope, string $ipAddress, ?string $reason ): void { $event = new SecurityEvent($name, ['ip' => $ipAddress, 'reason' => $reason]); $event->setIpAddress($ipAddress)->setReason($reason)->setTenantId($scope->tenantId); $this->events->dispatch($event); } private function publishLifecycleEvent( string $name, FirewallRuleObject $rule, ?string $actorId = null ): void { $event = new SecurityEvent($name, [ 'ruleId' => $rule->getId(), 'ruleScope' => $rule->getScope(), 'ruleType' => $rule->getType(), 'ruleAction' => $rule->getAction(), 'ruleValue' => $rule->getValue(), 'reason' => $rule->getReason(), 'origin' => $rule->getMetadata()['origin'] ?? self::ORIGIN_MANUAL, 'expiresAt' => $rule->getExpiresAt()?->format(\DateTimeInterface::ATOM), ...($rule->getMetadata() ?? []), ]); $event->setTenantId($rule->getTenantId()) ->setIdentityId($actorId ?? $rule->getCreatedBy()); $this->events->dispatch($event); } }