310 lines
8.8 KiB
PHP
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();
|
|
}
|
|
}
|