fix(firewall): serialize automatic brute-force blocking

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-07-30 23:05:32 -04:00
parent a5c10e9b9b
commit ad6443bff3
4 changed files with 125 additions and 9 deletions
@@ -238,6 +238,33 @@ class FirewallStoreTest extends TestCase
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' => (new \DateTimeImmutable('-1 minute'))->format(\DateTimeInterface::ATOM)]]
);
self::assertTrue($this->store->claimBruteForce('tenant-a', '203.0.113.10', 3600));
$this->dataStore->selectCollection('firewall_brute_force_claims')->updateOne(
['_id' => $claimId],
['$set' => ['expiresAt' => (new \DateTimeImmutable('-1 minute'))->format(\DateTimeInterface::ATOM)]]
);
self::assertSame(1, $this->store->cleanupExpiredBruteForceClaims());
}
private function rule(
string $reason,
string $scope,
@@ -392,6 +392,10 @@ class FirewallServiceTest extends TestCase
->method('countRecentFailures')
->with('tenant-event', '203.0.113.10', 300)
->willReturn(5);
$this->store->expects($this->once())
->method('claimBruteForce')
->with('tenant-event', '203.0.113.10', 3600)
->willReturn(true);
$this->store->expects($this->once())
->method('findExactIpRule')
->with(
@@ -431,6 +435,26 @@ class FirewallServiceTest extends TestCase
self::assertSame(FirewallRuleManager::ORIGIN_AUTOMATIC, $lifecycleOrigin);
}
#[TestDox('Workers that lose the brute-force claim do not block or publish detection events')]
public function testAutomaticBlockClaimLoss(): void
{
$this->store->method('createLogOnce')->willReturn(true);
$this->store->expects($this->once())
->method('countRecentFailures')
->with('tenant-a', '203.0.113.10', 300)
->willReturn(5);
$this->store->expects($this->once())
->method('claimBruteForce')
->with('tenant-a', '203.0.113.10', 3600)
->willReturn(false);
$this->store->expects($this->never())->method('depositRule');
$this->events->expects($this->never())->method('dispatch');
$this->service->handleAuthFailure(
\KTXF\Event\SecurityEvent::authFailure('203.0.113.10')
);
}
#[TestDox('Authentication events without a tenant use the current tenant')]
public function testAutomaticBlockTenantFallback(): void
{