d919b70a2e
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
570 lines
23 KiB
PHP
570 lines
23 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace KTXT\Integration\Stores;
|
|
|
|
use KTXC\Db\DataStore;
|
|
use KTXC\Db\UTCDateTime;
|
|
use KTXC\Models\Firewall\FirewallRuleObject;
|
|
use KTXC\Models\Firewall\FirewallLogObject;
|
|
use KTXC\Service\FirewallRuleCache;
|
|
use KTXC\Service\FirewallRuleManager;
|
|
use KTXC\Service\FirewallRuleScope;
|
|
use KTXC\Stores\FirewallStore;
|
|
use KTXF\Event\EventDispatcherInterface;
|
|
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
|
|
use PHPUnit\Framework\Attributes\TestDox;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
#[AllowMockObjectsWithoutExpectations]
|
|
class FirewallStoreTest extends TestCase
|
|
{
|
|
private DataStore $dataStore;
|
|
private FirewallStore $store;
|
|
private bool $databaseAvailable = false;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$system = require dirname(__DIR__, 4).'/config/system.php';
|
|
$database = $system['database'];
|
|
$database['database'] = sprintf('ktrix_firewall_test_%d', getmypid());
|
|
|
|
$this->dataStore = new DataStore($database);
|
|
try {
|
|
$this->dataStore->getDatabase()->drop();
|
|
$this->databaseAvailable = true;
|
|
} catch (\MongoDB\Driver\Exception\Exception $error) {
|
|
self::markTestSkipped('MongoDB is unavailable: '.$error->getMessage());
|
|
}
|
|
$this->store = new FirewallStore($this->dataStore);
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
if ($this->databaseAvailable) {
|
|
$this->dataStore->getDatabase()->drop();
|
|
}
|
|
}
|
|
|
|
#[TestDox('System and tenant rule sets remain independently scoped')]
|
|
public function testApplicableScopes(): void
|
|
{
|
|
$tenantA = $this->rule('tenant-a', FirewallRuleObject::SCOPE_TENANT, 'tenant-a');
|
|
$tenantB = $this->rule('tenant-b', FirewallRuleObject::SCOPE_TENANT, 'tenant-b');
|
|
$system = $this->rule('system', FirewallRuleObject::SCOPE_SYSTEM);
|
|
$expiredSystem = $this->rule('expired-system', FirewallRuleObject::SCOPE_SYSTEM)
|
|
->setExpiresAt(new \DateTimeImmutable('-1 minute'));
|
|
$disabledSystem = $this->rule('disabled-system', FirewallRuleObject::SCOPE_SYSTEM)
|
|
->setEnabled(false);
|
|
|
|
foreach ([$tenantA, $tenantB, $system, $expiredSystem, $disabledSystem] as $rule) {
|
|
$this->store->depositRule($rule);
|
|
}
|
|
|
|
$rules = array_merge(
|
|
$this->store->listSystemRules(),
|
|
$this->store->listRules('tenant-a')
|
|
);
|
|
$reasons = array_map(static fn(FirewallRuleObject $rule): ?string => $rule->getReason(), $rules);
|
|
sort($reasons);
|
|
|
|
self::assertSame(['system', 'tenant-a'], $reasons);
|
|
}
|
|
|
|
#[TestDox('Tenant rule listings cannot expose system or other tenant rules')]
|
|
public function testTenantListingIsolation(): void
|
|
{
|
|
$this->store->depositRule($this->rule('tenant-a', FirewallRuleObject::SCOPE_TENANT, 'tenant-a'));
|
|
$this->store->depositRule($this->rule('tenant-b', FirewallRuleObject::SCOPE_TENANT, 'tenant-b'));
|
|
$this->store->depositRule($this->rule('system', FirewallRuleObject::SCOPE_SYSTEM));
|
|
|
|
$rules = $this->store->listRules('tenant-a');
|
|
|
|
self::assertCount(1, $rules);
|
|
self::assertSame('tenant-a', $rules[0]->getReason());
|
|
self::assertSame(FirewallRuleObject::SCOPE_TENANT, $rules[0]->getScope());
|
|
}
|
|
|
|
#[TestDox('Expired exact IP rules do not suppress a replacement block')]
|
|
public function testExpiredBlockReplacement(): void
|
|
{
|
|
$expired = $this->rule('expired', FirewallRuleObject::SCOPE_TENANT, 'tenant-a')
|
|
->setExpiresAt(new \DateTimeImmutable('-1 minute'));
|
|
$this->store->depositRule($expired);
|
|
|
|
self::assertNull($this->store->findExactIpRule(
|
|
'tenant-a',
|
|
'203.0.113.10',
|
|
FirewallRuleObject::ACTION_BLOCK
|
|
));
|
|
|
|
$events = $this->createMock(EventDispatcherInterface::class);
|
|
$cache = new FirewallRuleCache($this->store);
|
|
$manager = new FirewallRuleManager($this->store, $cache, $events);
|
|
$replacement = $manager->blockIp(
|
|
FirewallRuleScope::tenant('tenant-a'),
|
|
'203.0.113.10',
|
|
null,
|
|
null,
|
|
300
|
|
);
|
|
|
|
self::assertFalse($replacement->isExpired());
|
|
self::assertNotSame($expired->getId(), $replacement->getId());
|
|
self::assertSame(
|
|
$replacement->getId(),
|
|
$this->store->findExactIpRule(
|
|
'tenant-a',
|
|
'203.0.113.10',
|
|
FirewallRuleObject::ACTION_BLOCK
|
|
)?->getId()
|
|
);
|
|
self::assertCount(2, $this->store->listRules('tenant-a', false));
|
|
}
|
|
|
|
#[TestDox('Exact IP lookups respect tenant and system scope')]
|
|
public function testExactLookupScope(): void
|
|
{
|
|
$tenant = $this->rule('tenant', FirewallRuleObject::SCOPE_TENANT, 'tenant-a');
|
|
$system = $this->rule('system', FirewallRuleObject::SCOPE_SYSTEM);
|
|
$this->store->depositRule($tenant);
|
|
$this->store->depositRule($system);
|
|
|
|
self::assertSame(
|
|
$tenant->getId(),
|
|
$this->store->findExactIpRule(
|
|
'tenant-a',
|
|
'203.0.113.10',
|
|
FirewallRuleObject::ACTION_BLOCK
|
|
)?->getId()
|
|
);
|
|
self::assertSame(
|
|
$system->getId(),
|
|
$this->store->findExactIpRule(
|
|
null,
|
|
'203.0.113.10',
|
|
FirewallRuleObject::ACTION_BLOCK,
|
|
FirewallRuleObject::SCOPE_SYSTEM
|
|
)?->getId()
|
|
);
|
|
self::assertNull($this->store->findExactIpRule(
|
|
'tenant-b',
|
|
'203.0.113.10',
|
|
FirewallRuleObject::ACTION_BLOCK
|
|
));
|
|
}
|
|
|
|
#[TestDox('System rule listings exclude tenant, disabled, and expired rules')]
|
|
public function testSystemListing(): void
|
|
{
|
|
$this->store->depositRule($this->rule('system', FirewallRuleObject::SCOPE_SYSTEM));
|
|
$this->store->depositRule(
|
|
$this->rule('disabled', FirewallRuleObject::SCOPE_SYSTEM)->setEnabled(false)
|
|
);
|
|
$this->store->depositRule(
|
|
$this->rule('expired', FirewallRuleObject::SCOPE_SYSTEM)
|
|
->setExpiresAt(new \DateTimeImmutable('-1 minute'))
|
|
);
|
|
$this->store->depositRule(
|
|
$this->rule('tenant', FirewallRuleObject::SCOPE_TENANT, 'tenant-a')
|
|
);
|
|
|
|
$rules = $this->store->listSystemRules();
|
|
|
|
self::assertCount(1, $rules);
|
|
self::assertSame('system', $rules[0]->getReason());
|
|
self::assertTrue($rules[0]->isSystemScoped());
|
|
}
|
|
|
|
#[TestDox('Firewall log persistence retains matched rule context')]
|
|
public function testRuleAuditPersistence(): void
|
|
{
|
|
$log = (new FirewallLogObject())
|
|
->setTenantId('tenant-a')
|
|
->setIpAddress('203.0.113.10')
|
|
->setEventType(FirewallLogObject::EVENT_RULE_MATCH)
|
|
->setResult(FirewallLogObject::RESULT_BLOCKED)
|
|
->setRuleId('rule-123')
|
|
->setRuleScope(FirewallRuleObject::SCOPE_TENANT)
|
|
->setTimestamp(new \DateTimeImmutable());
|
|
$this->store->createLog($log);
|
|
|
|
$logs = $this->store->listLogs('tenant-a');
|
|
|
|
self::assertCount(1, $logs);
|
|
self::assertSame('rule-123', $logs[0]->getRuleId());
|
|
self::assertSame(FirewallRuleObject::SCOPE_TENANT, $logs[0]->getRuleScope());
|
|
}
|
|
|
|
#[TestDox('Rule lifecycle audit persistence retains actor and origin')]
|
|
public function testLifecycleAuditPersistence(): void
|
|
{
|
|
$log = (new FirewallLogObject())
|
|
->setTenantId('tenant-a')
|
|
->setEventType(FirewallLogObject::EVENT_RULE_CREATED)
|
|
->setResult(FirewallLogObject::RESULT_RECORDED)
|
|
->setRuleId('rule-456')
|
|
->setRuleScope(FirewallRuleObject::SCOPE_TENANT)
|
|
->setIdentityId('admin-a')
|
|
->setTimestamp(new \DateTimeImmutable())
|
|
->setMetadata(['origin' => FirewallRuleManager::ORIGIN_MANUAL]);
|
|
$this->store->createLog($log);
|
|
|
|
$logs = $this->store->listLogs('tenant-a');
|
|
|
|
self::assertCount(1, $logs);
|
|
self::assertSame(FirewallLogObject::EVENT_RULE_CREATED, $logs[0]->getEventType());
|
|
self::assertSame(FirewallLogObject::RESULT_RECORDED, $logs[0]->getResult());
|
|
self::assertSame('admin-a', $logs[0]->getIdentityId());
|
|
self::assertSame(FirewallRuleManager::ORIGIN_MANUAL, $logs[0]->getMetadata()['origin']);
|
|
}
|
|
|
|
#[TestDox('Event-backed firewall logs are inserted exactly once')]
|
|
public function testIdempotentLogPersistence(): void
|
|
{
|
|
$log = (new FirewallLogObject())
|
|
->setEventId('event-123')
|
|
->setTenantId('tenant-a')
|
|
->setIpAddress('203.0.113.10')
|
|
->setEventType(FirewallLogObject::EVENT_AUTH_FAILURE)
|
|
->setResult(FirewallLogObject::RESULT_BLOCKED)
|
|
->setTimestamp(new \DateTimeImmutable());
|
|
|
|
self::assertTrue($this->store->createLogOnce($log));
|
|
self::assertFalse($this->store->createLogOnce($log));
|
|
|
|
$logs = $this->store->listLogs('tenant-a');
|
|
self::assertCount(1, $logs);
|
|
self::assertSame('event-123', $logs[0]->getEventId());
|
|
}
|
|
|
|
#[TestDox('Only one worker can claim a tenant and IP brute-force response')]
|
|
public function testBruteForceClaim(): void
|
|
{
|
|
self::assertTrue($this->store->claimBruteForce('tenant-a', '203.0.113.10', 3600));
|
|
self::assertFalse($this->store->claimBruteForce('tenant-a', '203.0.113.10', 3600));
|
|
self::assertTrue($this->store->claimBruteForce('tenant-b', '203.0.113.10', 3600));
|
|
self::assertTrue($this->store->claimBruteForce('tenant-a', '203.0.113.11', 3600));
|
|
}
|
|
|
|
#[TestDox('Expired brute-force claims can be acquired again and cleaned up')]
|
|
public function testExpiredBruteForceClaim(): void
|
|
{
|
|
self::assertTrue($this->store->claimBruteForce('tenant-a', '203.0.113.10', 3600));
|
|
$claimId = hash('sha256', "tenant-a\0"."203.0.113.10");
|
|
$this->dataStore->selectCollection('firewall_brute_force_claims')->updateOne(
|
|
['_id' => $claimId],
|
|
['$set' => ['expiresAt' => UTCDateTime::fromDateTime(new \DateTimeImmutable('-1 minute'))]]
|
|
);
|
|
|
|
self::assertTrue($this->store->claimBruteForce('tenant-a', '203.0.113.10', 3600));
|
|
$this->dataStore->selectCollection('firewall_brute_force_claims')->updateOne(
|
|
['_id' => $claimId],
|
|
['$set' => ['expiresAt' => UTCDateTime::fromDateTime(new \DateTimeImmutable('-1 minute'))]]
|
|
);
|
|
self::assertSame(1, $this->store->cleanupExpiredBruteForceClaims());
|
|
}
|
|
|
|
#[TestDox('Firewall dates use BSON date storage and indexes are installed idempotently')]
|
|
public function testBsonDatesAndIndexes(): void
|
|
{
|
|
$rule = $this->rule('bson-date', FirewallRuleObject::SCOPE_TENANT, 'tenant-a')
|
|
->setExpiresAt(new \DateTimeImmutable('+1 hour'));
|
|
$this->store->depositRule($rule);
|
|
|
|
$log = (new FirewallLogObject())
|
|
->setTenantId('tenant-a')
|
|
->setEventType(FirewallLogObject::EVENT_ACCESS_CHECK)
|
|
->setResult(FirewallLogObject::RESULT_ALLOWED)
|
|
->setTimestamp(new \DateTimeImmutable());
|
|
$this->store->createLog($log);
|
|
|
|
$storedRule = $this->dataStore->selectCollection('firewall_rules')
|
|
->getMongoCollection()
|
|
->findOne(['_id' => new \MongoDB\BSON\ObjectId($rule->getId())]);
|
|
$storedLog = $this->dataStore->selectCollection('firewall_logs')
|
|
->getMongoCollection()
|
|
->findOne(['_id' => new \MongoDB\BSON\ObjectId($log->getId())]);
|
|
|
|
self::assertInstanceOf(\MongoDB\BSON\UTCDateTime::class, $storedRule['createdAt']);
|
|
self::assertInstanceOf(\MongoDB\BSON\UTCDateTime::class, $storedRule['expiresAt']);
|
|
self::assertInstanceOf(\MongoDB\BSON\UTCDateTime::class, $storedLog['timestamp']);
|
|
|
|
$expected = $this->store->ensureIndexes();
|
|
self::assertSame($expected, $this->store->ensureIndexes());
|
|
|
|
$claimIndex = null;
|
|
foreach ($this->dataStore->selectCollection('firewall_brute_force_claims')->getMongoCollection()->listIndexes() as $index) {
|
|
if ($index->getName() === 'claims_expiry') {
|
|
$claimIndex = $index;
|
|
}
|
|
}
|
|
self::assertNotNull($claimIndex);
|
|
self::assertSame(0, $claimIndex['expireAfterSeconds']);
|
|
}
|
|
|
|
#[TestDox('Automatic block extensions persist policy and expiration history')]
|
|
public function testAutomaticBlockExtensionPersistence(): void
|
|
{
|
|
$events = $this->createMock(EventDispatcherInterface::class);
|
|
$manager = new FirewallRuleManager($this->store, new FirewallRuleCache($this->store), $events);
|
|
$scope = FirewallRuleScope::tenant('tenant-a');
|
|
$rule = $manager->blockIp(
|
|
$scope,
|
|
'203.0.113.10',
|
|
'Initial attack',
|
|
null,
|
|
60,
|
|
FirewallRuleManager::ORIGIN_AUTOMATIC,
|
|
[
|
|
'failureThreshold' => 5,
|
|
'failureWindowSeconds' => 300,
|
|
'lastFailureCount' => 5,
|
|
'blockDurationSeconds' => 60,
|
|
]
|
|
);
|
|
$originalExpiry = $rule->getExpiresAt();
|
|
|
|
$extended = $manager->blockIp(
|
|
$scope,
|
|
'203.0.113.10',
|
|
'Continued attack',
|
|
null,
|
|
3600,
|
|
FirewallRuleManager::ORIGIN_AUTOMATIC,
|
|
[
|
|
'failureThreshold' => 5,
|
|
'failureWindowSeconds' => 300,
|
|
'lastFailureCount' => 9,
|
|
'blockDurationSeconds' => 3600,
|
|
]
|
|
);
|
|
$persisted = $this->store->fetchRule($rule->getId());
|
|
|
|
self::assertSame($rule->getId(), $extended->getId());
|
|
self::assertNotNull($persisted);
|
|
self::assertGreaterThan($originalExpiry, $persisted->getExpiresAt());
|
|
self::assertSame(9, $persisted->getMetadata()['lastFailureCount']);
|
|
self::assertCount(1, $persisted->getMetadata()['extensions']);
|
|
}
|
|
|
|
#[TestDox('Cleanup removes only expired rules, old logs, and expired claims')]
|
|
public function testCleanupBoundaries(): void
|
|
{
|
|
$this->store->depositRule(
|
|
$this->rule('expired', FirewallRuleObject::SCOPE_TENANT, 'tenant-a')
|
|
->setExpiresAt(new \DateTimeImmutable('-1 minute'))
|
|
);
|
|
$this->store->depositRule(
|
|
$this->rule('active', FirewallRuleObject::SCOPE_TENANT, 'tenant-a')
|
|
->setExpiresAt(new \DateTimeImmutable('+1 hour'))
|
|
);
|
|
foreach (['-31 days', '-29 days'] as $age) {
|
|
$this->store->createLog(
|
|
(new FirewallLogObject())
|
|
->setTenantId('tenant-a')
|
|
->setEventType(FirewallLogObject::EVENT_ACCESS_CHECK)
|
|
->setResult(FirewallLogObject::RESULT_ALLOWED)
|
|
->setTimestamp(new \DateTimeImmutable($age))
|
|
);
|
|
}
|
|
self::assertTrue($this->store->claimBruteForce('tenant-a', '203.0.113.10', 3600));
|
|
$claimId = hash('sha256', "tenant-a\0"."203.0.113.10");
|
|
$this->dataStore->selectCollection('firewall_brute_force_claims')->updateOne(
|
|
['_id' => $claimId],
|
|
['$set' => ['expiresAt' => UTCDateTime::fromDateTime(new \DateTimeImmutable('-1 minute'))]]
|
|
);
|
|
|
|
self::assertSame(1, $this->store->cleanupExpiredRules());
|
|
self::assertSame(1, $this->store->cleanupOldLogs());
|
|
self::assertSame(1, $this->store->cleanupExpiredBruteForceClaims());
|
|
self::assertCount(1, $this->store->listRules('tenant-a'));
|
|
self::assertCount(1, $this->store->listLogs('tenant-a'));
|
|
}
|
|
|
|
#[TestDox('Maintenance outcomes are persisted with BSON timestamps')]
|
|
public function testMaintenanceStatusPersistence(): void
|
|
{
|
|
$this->store->recordMaintenanceStatus(
|
|
new \DateTimeImmutable('-1 second'),
|
|
new \DateTimeImmutable(),
|
|
'success',
|
|
['expiredRules' => 2, 'oldLogs' => 3, 'expiredBruteForceClaims' => 4]
|
|
);
|
|
|
|
$status = $this->store->maintenanceStatus();
|
|
$raw = $this->dataStore->selectCollection('firewall_maintenance')
|
|
->getMongoCollection()
|
|
->findOne(['_id' => 'cleanup']);
|
|
|
|
self::assertSame('success', $status['status']);
|
|
self::assertSame(3, $status['result']['oldLogs']);
|
|
self::assertInstanceOf(\MongoDB\BSON\UTCDateTime::class, $raw['startedAt']);
|
|
self::assertInstanceOf(\MongoDB\BSON\UTCDateTime::class, $raw['completedAt']);
|
|
}
|
|
|
|
#[TestDox('Firewall query shapes select their intended indexes')]
|
|
public function testIndexedQueryShapes(): void
|
|
{
|
|
$this->store->ensureIndexes();
|
|
$this->store->depositRule($this->rule('indexed', FirewallRuleObject::SCOPE_TENANT, 'tenant-a'));
|
|
$this->store->createLog(
|
|
(new FirewallLogObject())
|
|
->setTenantId('tenant-a')
|
|
->setIpAddress('203.0.113.10')
|
|
->setEventType(FirewallLogObject::EVENT_AUTH_FAILURE)
|
|
->setResult(FirewallLogObject::RESULT_BLOCKED)
|
|
->setTimestamp(new \DateTimeImmutable())
|
|
);
|
|
|
|
$database = $this->dataStore->getDatabase()->getMongoDatabase();
|
|
$now = new \MongoDB\BSON\UTCDateTime();
|
|
$rulePlan = $database->command([
|
|
'explain' => [
|
|
'find' => 'firewall_rules',
|
|
'filter' => [
|
|
'scope' => FirewallRuleObject::SCOPE_TENANT,
|
|
'tenantId' => 'tenant-a',
|
|
'type' => FirewallRuleObject::TYPE_IP,
|
|
'value' => '203.0.113.10',
|
|
'action' => FirewallRuleObject::ACTION_BLOCK,
|
|
'enabled' => true,
|
|
'$or' => [['expiresAt' => null], ['expiresAt' => ['$gt' => $now]]],
|
|
],
|
|
],
|
|
'verbosity' => 'queryPlanner',
|
|
])->toArray()[0];
|
|
$failurePlan = $database->command([
|
|
'explain' => [
|
|
'find' => 'firewall_logs',
|
|
'filter' => [
|
|
'tenantId' => 'tenant-a',
|
|
'ipAddress' => '203.0.113.10',
|
|
'eventType' => FirewallLogObject::EVENT_AUTH_FAILURE,
|
|
'timestamp' => ['$gte' => new \MongoDB\BSON\UTCDateTime(0)],
|
|
],
|
|
],
|
|
'verbosity' => 'queryPlanner',
|
|
])->toArray()[0];
|
|
|
|
self::assertContains('rules_exact_lookup', self::indexNames($rulePlan));
|
|
self::assertContains('logs_auth_failures', self::indexNames($failurePlan));
|
|
}
|
|
|
|
#[TestDox('Administrative rule queries filter, paginate, and preserve scope')]
|
|
public function testAdministrativeRuleQuery(): void
|
|
{
|
|
$this->store->depositRule($this->rule('tenant-active', FirewallRuleObject::SCOPE_TENANT, 'tenant-a'));
|
|
$this->store->depositRule(
|
|
$this->rule('tenant-disabled', FirewallRuleObject::SCOPE_TENANT, 'tenant-a')->setEnabled(false)
|
|
);
|
|
$this->store->depositRule($this->rule('other-tenant', FirewallRuleObject::SCOPE_TENANT, 'tenant-b'));
|
|
$this->store->depositRule($this->rule('system', FirewallRuleObject::SCOPE_SYSTEM));
|
|
|
|
$active = $this->store->queryRules(
|
|
FirewallRuleObject::SCOPE_TENANT,
|
|
'tenant-a',
|
|
'active',
|
|
FirewallRuleObject::TYPE_IP,
|
|
FirewallRuleObject::ACTION_BLOCK,
|
|
1,
|
|
0
|
|
);
|
|
$disabled = $this->store->queryRules(
|
|
FirewallRuleObject::SCOPE_TENANT,
|
|
'tenant-a',
|
|
'disabled',
|
|
null,
|
|
null,
|
|
50,
|
|
0
|
|
);
|
|
|
|
self::assertSame(1, $active['total']);
|
|
self::assertCount(1, $active['items']);
|
|
self::assertSame('tenant-active', $active['items'][0]->getReason());
|
|
self::assertSame(1, $disabled['total']);
|
|
self::assertSame('tenant-disabled', $disabled['items'][0]->getReason());
|
|
}
|
|
|
|
#[TestDox('Administrative log queries enforce tenant scope and supported filters')]
|
|
public function testAdministrativeLogQuery(): void
|
|
{
|
|
$matching = (new FirewallLogObject())
|
|
->setTenantId('tenant-a')
|
|
->setIpAddress('203.0.113.10')
|
|
->setEventType(FirewallLogObject::EVENT_RULE_MATCH)
|
|
->setResult(FirewallLogObject::RESULT_BLOCKED)
|
|
->setRuleId('rule-123')
|
|
->setRuleScope(FirewallRuleObject::SCOPE_TENANT)
|
|
->setTimestamp(new \DateTimeImmutable('-1 hour'));
|
|
$otherTenant = (new FirewallLogObject())
|
|
->setTenantId('tenant-b')
|
|
->setIpAddress('203.0.113.10')
|
|
->setEventType(FirewallLogObject::EVENT_RULE_MATCH)
|
|
->setResult(FirewallLogObject::RESULT_BLOCKED)
|
|
->setRuleId('rule-123')
|
|
->setRuleScope(FirewallRuleObject::SCOPE_TENANT)
|
|
->setTimestamp(new \DateTimeImmutable('-1 hour'));
|
|
$tooOld = (new FirewallLogObject())
|
|
->setTenantId('tenant-a')
|
|
->setEventType(FirewallLogObject::EVENT_RULE_MATCH)
|
|
->setResult(FirewallLogObject::RESULT_BLOCKED)
|
|
->setRuleId('rule-123')
|
|
->setTimestamp(new \DateTimeImmutable('-3 days'));
|
|
foreach ([$matching, $otherTenant, $tooOld] as $log) {
|
|
$this->store->createLog($log);
|
|
}
|
|
|
|
$tenant = $this->store->queryTenantLogs('tenant-a', [
|
|
'ipAddress' => '203.0.113.10',
|
|
'eventType' => FirewallLogObject::EVENT_RULE_MATCH,
|
|
'result' => FirewallLogObject::RESULT_BLOCKED,
|
|
'ruleId' => 'rule-123',
|
|
'ruleScope' => FirewallRuleObject::SCOPE_TENANT,
|
|
'from' => new \DateTimeImmutable('-1 day'),
|
|
'to' => new \DateTimeImmutable(),
|
|
], 50, 0);
|
|
$system = $this->store->querySystemLogs(null, [], 2, 0);
|
|
|
|
self::assertSame(1, $tenant['total']);
|
|
self::assertSame('tenant-a', $tenant['items'][0]->getTenantId());
|
|
self::assertSame(3, $system['total']);
|
|
self::assertCount(2, $system['items']);
|
|
}
|
|
|
|
private function rule(
|
|
string $reason,
|
|
string $scope,
|
|
?string $tenantId = null
|
|
): FirewallRuleObject {
|
|
return (new FirewallRuleObject())
|
|
->setScope($scope)
|
|
->setTenantId($tenantId)
|
|
->setType(FirewallRuleObject::TYPE_IP)
|
|
->setAction(FirewallRuleObject::ACTION_BLOCK)
|
|
->setValue('203.0.113.10')
|
|
->setReason($reason)
|
|
->setCreatedAt(new \DateTimeImmutable())
|
|
->setEnabled(true);
|
|
}
|
|
|
|
private static function indexNames(mixed $value): array
|
|
{
|
|
if (is_object($value)) {
|
|
$value = (array)$value;
|
|
}
|
|
if (!is_array($value)) {
|
|
return [];
|
|
}
|
|
|
|
$names = isset($value['indexName']) ? [(string)$value['indexName']] : [];
|
|
foreach ($value as $child) {
|
|
$names = [...$names, ...self::indexNames($child)];
|
|
}
|
|
|
|
return array_values(array_unique($names));
|
|
}
|
|
}
|