Files
server/core/lib/Stores/FirewallStore.php
2026-02-10 18:46:11 -05:00

310 lines
8.8 KiB
PHP

<?php
declare(strict_types=1);
namespace KTXC\Stores;
use KTXC\Db\DataStore;
use KTXC\Models\Firewall\FirewallRuleObject;
use KTXC\Models\Firewall\FirewallLogObject;
/**
* Store for firewall rules and access logs
*/
class FirewallStore
{
protected const RULES_COLLECTION = 'firewall_rules';
protected const LOGS_COLLECTION = 'firewall_logs';
public function __construct(
protected readonly DataStore $dataStore
) {}
// ========================================
// Rule Operations
// ========================================
/**
* List all rules for a tenant
*/
public function listRules(string $tenantId, bool $activeOnly = true): array
{
$filter = ['tenantId' => $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();
}
}