$tenantId]; if ($activeOnly) { $filter['enabled'] = true; $filter['$or'] = [ ['expiresAt' => null], ['expiresAt' => ['$gt' => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM)]] ]; } $cursor = $this->dataStore->selectCollection(self::RULES_COLLECTION)->find($filter); $list = []; foreach ($cursor as $entry) { $rule = (new FirewallRuleObject())->jsonDeserialize((array)$entry); $list[] = $rule; } return $list; } /** * Find rules by IP address */ public function findRulesByIp(string $tenantId, string $ipAddress): array { $filter = [ 'tenantId' => $tenantId, 'type' => ['$in' => [FirewallRuleObject::TYPE_IP, FirewallRuleObject::TYPE_IP_RANGE]], 'enabled' => true, '$or' => [ ['expiresAt' => null], ['expiresAt' => ['$gt' => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM)]] ] ]; $cursor = $this->dataStore->selectCollection(self::RULES_COLLECTION)->find($filter); $list = []; foreach ($cursor as $entry) { $rule = (new FirewallRuleObject())->jsonDeserialize((array)$entry); $list[] = $rule; } return $list; } /** * Find rules by device fingerprint */ public function findRulesByDevice(string $tenantId, string $deviceFingerprint): array { $filter = [ 'tenantId' => $tenantId, 'type' => FirewallRuleObject::TYPE_DEVICE, 'value' => $deviceFingerprint, 'enabled' => true, '$or' => [ ['expiresAt' => null], ['expiresAt' => ['$gt' => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM)]] ] ]; $cursor = $this->dataStore->selectCollection(self::RULES_COLLECTION)->find($filter); $list = []; foreach ($cursor as $entry) { $rule = (new FirewallRuleObject())->jsonDeserialize((array)$entry); $list[] = $rule; } return $list; } /** * Fetch a specific rule by ID */ public function fetchRule(string $id): ?FirewallRuleObject { $entry = $this->dataStore->selectCollection(self::RULES_COLLECTION)->findOne(['_id' => $id]); if (!$entry) { return null; } return (new FirewallRuleObject())->jsonDeserialize((array)$entry); } /** * Check if exact IP rule exists */ public function findExactIpRule(string $tenantId, string $ipAddress, string $action): ?FirewallRuleObject { $entry = $this->dataStore->selectCollection(self::RULES_COLLECTION)->findOne([ 'tenantId' => $tenantId, 'type' => FirewallRuleObject::TYPE_IP, 'value' => $ipAddress, 'action' => $action, 'enabled' => true, ]); if (!$entry) { return null; } return (new FirewallRuleObject())->jsonDeserialize((array)$entry); } /** * Create or update a rule */ public function depositRule(FirewallRuleObject $rule): ?FirewallRuleObject { if ($rule->getId()) { return $this->updateRule($rule); } else { return $this->createRule($rule); } } private function createRule(FirewallRuleObject $rule): ?FirewallRuleObject { $data = $rule->jsonSerialize(); unset($data['id']); // Remove id for insert $result = $this->dataStore->selectCollection(self::RULES_COLLECTION)->insertOne($data); $rule->setId((string)$result->getInsertedId()); return $rule; } private function updateRule(FirewallRuleObject $rule): ?FirewallRuleObject { $id = $rule->getId(); if (!$id) { return null; } $data = $rule->jsonSerialize(); unset($data['id']); $this->dataStore->selectCollection(self::RULES_COLLECTION)->updateOne( ['_id' => $id], ['$set' => $data] ); return $rule; } /** * Delete a rule */ public function destroyRule(FirewallRuleObject $rule): void { $id = $rule->getId(); if (!$id) { return; } $this->dataStore->selectCollection(self::RULES_COLLECTION)->deleteOne(['_id' => $id]); } /** * Delete expired rules */ public function cleanupExpiredRules(): int { $result = $this->dataStore->selectCollection(self::RULES_COLLECTION)->deleteMany([ 'expiresAt' => ['$lt' => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM)], 'expiresAt' => ['$ne' => null] ]); return $result->getDeletedCount(); } // ======================================== // Log Operations // ======================================== /** * Log a firewall event */ public function createLog(FirewallLogObject $log): FirewallLogObject { $data = $log->jsonSerialize(); unset($data['id']); $result = $this->dataStore->selectCollection(self::LOGS_COLLECTION)->insertOne($data); $log->setId((string)$result->getInsertedId()); return $log; } /** * Get logs for a tenant with optional filters */ public function listLogs( string $tenantId, ?string $ipAddress = null, ?string $eventType = null, ?string $result = null, int $limit = 100, int $offset = 0 ): array { $filter = ['tenantId' => $tenantId]; if ($ipAddress !== null) { $filter['ipAddress'] = $ipAddress; } if ($eventType !== null) { $filter['eventType'] = $eventType; } if ($result !== null) { $filter['result'] = $result; } $cursor = $this->dataStore->selectCollection(self::LOGS_COLLECTION)->find( $filter, [ 'sort' => ['timestamp' => -1], 'limit' => $limit, 'skip' => $offset ] ); $list = []; foreach ($cursor as $entry) { $log = (new FirewallLogObject())->jsonDeserialize((array)$entry); $list[] = $log; } return $list; } /** * Count recent failures from an IP within a time window */ public function countRecentFailures( string $tenantId, string $ipAddress, int $windowSeconds = 300 ): int { $since = (new \DateTimeImmutable())->modify("-{$windowSeconds} seconds"); return $this->dataStore->selectCollection(self::LOGS_COLLECTION)->countDocuments([ 'tenantId' => $tenantId, 'ipAddress' => $ipAddress, 'eventType' => FirewallLogObject::EVENT_AUTH_FAILURE, 'timestamp' => ['$gte' => $since->format(\DateTimeInterface::ATOM)] ]); } /** * Get blocked requests count for dashboard */ public function countBlockedRequests( string $tenantId, ?\DateTimeImmutable $since = null ): int { $filter = [ 'tenantId' => $tenantId, 'result' => FirewallLogObject::RESULT_BLOCKED ]; if ($since !== null) { $filter['timestamp'] = ['$gte' => $since->format(\DateTimeInterface::ATOM)]; } return $this->dataStore->selectCollection(self::LOGS_COLLECTION)->countDocuments($filter); } /** * Clean up old logs */ public function cleanupOldLogs(int $daysToKeep = 30): int { $cutoff = (new \DateTimeImmutable())->modify("-{$daysToKeep} days"); $result = $this->dataStore->selectCollection(self::LOGS_COLLECTION)->deleteMany([ 'timestamp' => ['$lt' => $cutoff->format(\DateTimeInterface::ATOM)] ]); return $result->getDeletedCount(); } }