feat(firewall): complete operational reliability phase
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -167,4 +167,80 @@ class FirewallRuleManagerTest extends TestCase
|
||||
self::assertSame('operator', $events[SecurityEvent::FIREWALL_RULE_DISABLED]->getIdentityId());
|
||||
self::assertSame('operator', $events[SecurityEvent::FIREWALL_RULE_REMOVED]->getIdentityId());
|
||||
}
|
||||
|
||||
#[TestDox('Continued attacks extend automatic blocks and retain their audit history')]
|
||||
public function testAutomaticBlockExtension(): void
|
||||
{
|
||||
$originalExpiry = new \DateTimeImmutable('+5 minutes');
|
||||
$rule = (new FirewallRuleObject())
|
||||
->setId('rule-123')
|
||||
->setScope(FirewallRuleObject::SCOPE_TENANT)
|
||||
->setTenantId('tenant-a')
|
||||
->setType(FirewallRuleObject::TYPE_IP)
|
||||
->setAction(FirewallRuleObject::ACTION_BLOCK)
|
||||
->setValue('203.0.113.10')
|
||||
->setExpiresAt($originalExpiry)
|
||||
->setMetadata([
|
||||
'origin' => FirewallRuleManager::ORIGIN_AUTOMATIC,
|
||||
'originalExpiresAt' => $originalExpiry->format(\DateTimeInterface::ATOM),
|
||||
'extensions' => [],
|
||||
]);
|
||||
$this->store->method('findExactIpRule')->willReturn($rule);
|
||||
$this->store->expects(self::once())
|
||||
->method('depositRule')
|
||||
->with(self::callback(static function (FirewallRuleObject $extended) use ($originalExpiry): bool {
|
||||
$metadata = $extended->getMetadata();
|
||||
return $extended->getExpiresAt() > $originalExpiry
|
||||
&& $metadata['failureThreshold'] === 5
|
||||
&& $metadata['failureWindowSeconds'] === 300
|
||||
&& $metadata['lastFailureCount'] === 8
|
||||
&& $metadata['originalExpiresAt'] === $originalExpiry->format(\DateTimeInterface::ATOM)
|
||||
&& count($metadata['extensions']) === 1;
|
||||
}))
|
||||
->willReturnArgument(0);
|
||||
$this->events->expects(self::once())
|
||||
->method('dispatch')
|
||||
->with(self::callback(static fn(\KTXF\Event\Event $event): bool =>
|
||||
$event->getName() === SecurityEvent::FIREWALL_RULE_EXTENDED
|
||||
&& $event->get('lastFailureCount') === 8
|
||||
));
|
||||
|
||||
$extended = $this->manager->blockIp(
|
||||
FirewallRuleScope::tenant('tenant-a'),
|
||||
'203.0.113.10',
|
||||
'Continued attack',
|
||||
null,
|
||||
3600,
|
||||
FirewallRuleManager::ORIGIN_AUTOMATIC,
|
||||
[
|
||||
'failureThreshold' => 5,
|
||||
'failureWindowSeconds' => 300,
|
||||
'lastFailureCount' => 8,
|
||||
'blockDurationSeconds' => 3600,
|
||||
]
|
||||
);
|
||||
|
||||
self::assertSame('rule-123', $extended->getId());
|
||||
}
|
||||
|
||||
#[TestDox('Automatic detection never extends a manual block')]
|
||||
public function testManualBlockIsNotExtended(): void
|
||||
{
|
||||
$rule = (new FirewallRuleObject())
|
||||
->setScope(FirewallRuleObject::SCOPE_TENANT)
|
||||
->setTenantId('tenant-a')
|
||||
->setMetadata(['origin' => FirewallRuleManager::ORIGIN_MANUAL]);
|
||||
$this->store->method('findExactIpRule')->willReturn($rule);
|
||||
$this->store->expects(self::never())->method('depositRule');
|
||||
$this->events->expects(self::never())->method('dispatch');
|
||||
|
||||
self::assertSame($rule, $this->manager->blockIp(
|
||||
FirewallRuleScope::tenant('tenant-a'),
|
||||
'203.0.113.10',
|
||||
null,
|
||||
null,
|
||||
3600,
|
||||
FirewallRuleManager::ORIGIN_AUTOMATIC
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,7 +394,7 @@ class FirewallServiceTest extends TestCase
|
||||
->willReturn(5);
|
||||
$this->store->expects($this->once())
|
||||
->method('claimBruteForce')
|
||||
->with('tenant-event', '203.0.113.10', 3600)
|
||||
->with('tenant-event', '203.0.113.10', 300)
|
||||
->willReturn(true);
|
||||
$this->store->expects($this->once())
|
||||
->method('findExactIpRule')
|
||||
@@ -407,9 +407,14 @@ class FirewallServiceTest extends TestCase
|
||||
$this->store->expects($this->once())
|
||||
->method('depositRule')
|
||||
->with(self::callback(static function (FirewallRuleObject $rule): bool {
|
||||
$metadata = $rule->getMetadata();
|
||||
return $rule->getScope() === FirewallRuleObject::SCOPE_TENANT
|
||||
&& $rule->getTenantId() === 'tenant-event'
|
||||
&& $rule->getExpiresAt() !== null;
|
||||
&& $rule->getExpiresAt() !== null
|
||||
&& $metadata['failureThreshold'] === 5
|
||||
&& $metadata['failureWindowSeconds'] === 300
|
||||
&& $metadata['lastFailureCount'] === 5
|
||||
&& $metadata['blockDurationSeconds'] === 3600;
|
||||
}))
|
||||
->willReturnArgument(0);
|
||||
|
||||
@@ -445,7 +450,7 @@ class FirewallServiceTest extends TestCase
|
||||
->willReturn(5);
|
||||
$this->store->expects($this->once())
|
||||
->method('claimBruteForce')
|
||||
->with('tenant-a', '203.0.113.10', 3600)
|
||||
->with('tenant-a', '203.0.113.10', 300)
|
||||
->willReturn(false);
|
||||
$this->store->expects($this->never())->method('depositRule');
|
||||
$this->events->expects($this->never())->method('dispatch');
|
||||
@@ -500,6 +505,50 @@ class FirewallServiceTest extends TestCase
|
||||
self::assertSame($eventId, $event->getEventId());
|
||||
}
|
||||
|
||||
#[TestDox('Cleanup records successful maintenance counts')]
|
||||
public function testCleanupStatus(): void
|
||||
{
|
||||
$this->store->method('cleanupExpiredRules')->willReturn(2);
|
||||
$this->store->method('cleanupOldLogs')->with(30)->willReturn(3);
|
||||
$this->store->method('cleanupExpiredBruteForceClaims')->willReturn(4);
|
||||
$this->store->expects(self::once())
|
||||
->method('recordMaintenanceStatus')
|
||||
->with(
|
||||
self::isInstanceOf(\DateTimeImmutable::class),
|
||||
self::isInstanceOf(\DateTimeImmutable::class),
|
||||
'success',
|
||||
[
|
||||
'expiredRules' => 2,
|
||||
'oldLogs' => 3,
|
||||
'expiredBruteForceClaims' => 4,
|
||||
]
|
||||
);
|
||||
|
||||
self::assertSame([
|
||||
'expiredRules' => 2,
|
||||
'oldLogs' => 3,
|
||||
'expiredBruteForceClaims' => 4,
|
||||
], $this->service->cleanup());
|
||||
}
|
||||
|
||||
#[TestDox('Cleanup failures are recorded and rethrown')]
|
||||
public function testCleanupFailureStatus(): void
|
||||
{
|
||||
$this->store->method('cleanupExpiredRules')->willThrowException(new \RuntimeException('cleanup failed'));
|
||||
$this->store->expects(self::once())
|
||||
->method('recordMaintenanceStatus')
|
||||
->with(
|
||||
self::isInstanceOf(\DateTimeImmutable::class),
|
||||
self::isInstanceOf(\DateTimeImmutable::class),
|
||||
'failed',
|
||||
[],
|
||||
'cleanup failed'
|
||||
);
|
||||
$this->expectExceptionMessage('cleanup failed');
|
||||
|
||||
$this->service->cleanup();
|
||||
}
|
||||
|
||||
private function rule(
|
||||
string $id,
|
||||
string $scope,
|
||||
|
||||
Reference in New Issue
Block a user