feat(firewall): audit rule lifecycle changes
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -10,6 +10,7 @@ use KTXC\Service\FirewallRuleManager;
|
||||
use KTXC\Service\FirewallRuleScope;
|
||||
use KTXC\Stores\FirewallStore;
|
||||
use KTXF\Event\EventDispatcherInterface;
|
||||
use KTXF\Event\SecurityEvent;
|
||||
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
|
||||
use PHPUnit\Framework\Attributes\TestDox;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
@@ -106,4 +107,64 @@ class FirewallRuleManagerTest extends TestCase
|
||||
$manager->blockIp(FirewallRuleScope::tenant('tenant-a'), '203.0.113.10', null, 'admin');
|
||||
self::assertSame([], $cache->tenant('tenant-a'));
|
||||
}
|
||||
|
||||
#[TestDox('Rule creation emits complete lifecycle audit context')]
|
||||
public function testCreationAudit(): void
|
||||
{
|
||||
$this->store->method('findExactIpRule')->willReturn(null);
|
||||
$this->store->method('depositRule')->willReturnCallback(
|
||||
static function (FirewallRuleObject $rule): FirewallRuleObject {
|
||||
return $rule->setId('rule-123');
|
||||
}
|
||||
);
|
||||
$events = [];
|
||||
$this->events->expects($this->exactly(2))
|
||||
->method('dispatch')
|
||||
->willReturnCallback(static function (\KTXF\Event\Event $event) use (&$events): void {
|
||||
$events[$event->getName()] = $event;
|
||||
});
|
||||
|
||||
$this->manager->blockIp(
|
||||
FirewallRuleScope::tenant('tenant-a'),
|
||||
'203.0.113.10',
|
||||
'Repeated abuse',
|
||||
'admin-a',
|
||||
300
|
||||
);
|
||||
|
||||
$audit = $events[SecurityEvent::FIREWALL_RULE_CREATED];
|
||||
self::assertSame('rule-123', $audit->get('ruleId'));
|
||||
self::assertSame(FirewallRuleObject::SCOPE_TENANT, $audit->get('ruleScope'));
|
||||
self::assertSame(FirewallRuleObject::TYPE_IP, $audit->get('ruleType'));
|
||||
self::assertSame(FirewallRuleObject::ACTION_BLOCK, $audit->get('ruleAction'));
|
||||
self::assertSame(FirewallRuleManager::ORIGIN_MANUAL, $audit->get('origin'));
|
||||
self::assertSame('admin-a', $audit->getIdentityId());
|
||||
self::assertNotNull($audit->get('expiresAt'));
|
||||
}
|
||||
|
||||
#[TestDox('Disable and remove audits identify the acting administrator')]
|
||||
public function testLifecycleActors(): void
|
||||
{
|
||||
$rule = (new FirewallRuleObject())
|
||||
->setId('rule-123')
|
||||
->setScope(FirewallRuleObject::SCOPE_SYSTEM)
|
||||
->setTenantId(null)
|
||||
->setType(FirewallRuleObject::TYPE_IP)
|
||||
->setAction(FirewallRuleObject::ACTION_BLOCK)
|
||||
->setValue('203.0.113.10')
|
||||
->setCreatedBy('creator');
|
||||
$this->store->method('fetchRule')->willReturn($rule);
|
||||
$events = [];
|
||||
$this->events->expects($this->exactly(2))
|
||||
->method('dispatch')
|
||||
->willReturnCallback(static function (\KTXF\Event\Event $event) use (&$events): void {
|
||||
$events[$event->getName()] = $event;
|
||||
});
|
||||
|
||||
self::assertTrue($this->manager->disable(FirewallRuleScope::system(), 'rule-123', 'operator'));
|
||||
self::assertTrue($this->manager->remove(FirewallRuleScope::system(), 'rule-123', 'operator'));
|
||||
|
||||
self::assertSame('operator', $events[SecurityEvent::FIREWALL_RULE_DISABLED]->getIdentityId());
|
||||
self::assertSame('operator', $events[SecurityEvent::FIREWALL_RULE_REMOVED]->getIdentityId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,6 +309,33 @@ class FirewallServiceTest extends TestCase
|
||||
$this->service->logSecurityEvent($event);
|
||||
}
|
||||
|
||||
#[TestDox('Rule lifecycle events map to recorded audit entries')]
|
||||
public function testRuleLifecycleAudit(): void
|
||||
{
|
||||
$this->currentTenant = null;
|
||||
$this->store->expects($this->once())
|
||||
->method('createLog')
|
||||
->with(self::callback(static function (FirewallLogObject $log): bool {
|
||||
return $log->getEventType() === FirewallLogObject::EVENT_RULE_DISABLED
|
||||
&& $log->getResult() === FirewallLogObject::RESULT_RECORDED
|
||||
&& $log->getRuleId() === 'rule-123'
|
||||
&& $log->getRuleScope() === FirewallRuleObject::SCOPE_SYSTEM
|
||||
&& $log->getIdentityId() === 'operator';
|
||||
}))
|
||||
->willReturnArgument(0);
|
||||
$event = new \KTXF\Event\SecurityEvent(
|
||||
\KTXF\Event\SecurityEvent::FIREWALL_RULE_DISABLED,
|
||||
[
|
||||
'ruleId' => 'rule-123',
|
||||
'ruleScope' => FirewallRuleObject::SCOPE_SYSTEM,
|
||||
'origin' => FirewallRuleManager::ORIGIN_MANUAL,
|
||||
]
|
||||
);
|
||||
$event->setIdentityId('operator');
|
||||
|
||||
$this->service->logSecurityEvent($event);
|
||||
}
|
||||
|
||||
#[TestDox('Typed tenant firewall settings drive brute-force thresholds')]
|
||||
public function testFirewallConfiguration(): void
|
||||
{
|
||||
@@ -380,17 +407,25 @@ class FirewallServiceTest extends TestCase
|
||||
->willReturnArgument(0);
|
||||
|
||||
$publishedTenants = [];
|
||||
$this->events->expects($this->exactly(2))
|
||||
$lifecycleOrigin = null;
|
||||
$this->events->expects($this->exactly(3))
|
||||
->method('dispatch')
|
||||
->willReturnCallback(static function (\KTXF\Event\Event $event) use (&$publishedTenants): void {
|
||||
->willReturnCallback(static function (\KTXF\Event\Event $event) use (
|
||||
&$publishedTenants,
|
||||
&$lifecycleOrigin
|
||||
): void {
|
||||
$publishedTenants[] = $event->getTenantId();
|
||||
if ($event->getName() === \KTXF\Event\SecurityEvent::FIREWALL_RULE_CREATED) {
|
||||
$lifecycleOrigin = $event->get('origin');
|
||||
}
|
||||
});
|
||||
|
||||
$event = \KTXF\Event\SecurityEvent::authFailure('203.0.113.10');
|
||||
$event->setTenantId('tenant-event');
|
||||
$this->service->handleAuthFailure($event);
|
||||
|
||||
self::assertSame(['tenant-event', 'tenant-event'], $publishedTenants);
|
||||
self::assertSame(['tenant-event', 'tenant-event', 'tenant-event'], $publishedTenants);
|
||||
self::assertSame(FirewallRuleManager::ORIGIN_AUTOMATIC, $lifecycleOrigin);
|
||||
}
|
||||
|
||||
#[TestDox('Authentication events without a tenant use the current tenant')]
|
||||
|
||||
Reference in New Issue
Block a user