feat(firewall): complete rule-match audit context

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-07-30 22:46:27 -04:00
parent abc5bfcccc
commit 12037da367
6 changed files with 144 additions and 1 deletions
@@ -6,6 +6,7 @@ namespace KTXT\Integration\Stores;
use KTXC\Db\DataStore;
use KTXC\Models\Firewall\FirewallRuleObject;
use KTXC\Models\Firewall\FirewallLogObject;
use KTXC\Service\FirewallRuleCache;
use KTXC\Service\FirewallRuleManager;
use KTXC\Service\FirewallRuleScope;
@@ -175,6 +176,26 @@ class FirewallStoreTest extends TestCase
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());
}
private function rule(
string $reason,
string $scope,
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace KTXT\Unit\Models\Firewall;
use KTXC\Models\Firewall\FirewallLogObject;
use KTXC\Models\Firewall\FirewallRuleObject;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase;
class FirewallLogObjectTest extends TestCase
{
#[TestDox('Rule ID and scope survive firewall log serialization')]
public function testRuleContextSerialization(): void
{
$log = (new FirewallLogObject())
->setRuleId('rule-123')
->setRuleScope(FirewallRuleObject::SCOPE_SYSTEM);
$restored = (new FirewallLogObject())->jsonDeserialize($log->jsonSerialize());
self::assertSame('rule-123', $restored->getRuleId());
self::assertSame(FirewallRuleObject::SCOPE_SYSTEM, $restored->getRuleScope());
}
#[TestDox('Unknown rule scopes are rejected from firewall logs')]
public function testRuleScopeValidation(): void
{
$this->expectException(\InvalidArgumentException::class);
(new FirewallLogObject())->setRuleScope('unknown');
}
}
@@ -6,6 +6,7 @@ namespace KTXT\Unit\Service;
use KTXC\Context\TenantContextInterface;
use KTXC\Models\Firewall\FirewallRuleObject;
use KTXC\Models\Firewall\FirewallLogObject;
use KTXC\Models\Tenant\TenantConfiguration;
use KTXC\Service\FirewallService;
use KTXC\Service\FirewallRuleCache;
@@ -197,6 +198,64 @@ class FirewallServiceTest extends TestCase
self::assertTrue($this->service->analyze('203.0.113.10')->isAllowed());
}
#[TestDox('Tenant rule-match logs persist dedicated rule ID and scope fields')]
public function testTenantRuleAuditContext(): void
{
$this->store->expects($this->once())
->method('createLog')
->with(self::callback(static function (FirewallLogObject $log): bool {
return $log->getTenantId() === 'tenant-a'
&& $log->getRuleId() === 'tenant-rule'
&& $log->getRuleScope() === FirewallRuleObject::SCOPE_TENANT
&& $log->getEventType() === FirewallLogObject::EVENT_RULE_MATCH;
}))
->willReturnArgument(0);
$event = \KTXF\Event\SecurityEvent::accessDenied(
'203.0.113.10',
null,
'tenant-rule',
FirewallRuleObject::SCOPE_TENANT,
'Tenant block'
);
$event->setTenantId('tenant-a');
$this->service->logSecurityEvent($event);
}
#[TestDox('System rule matches are logged even when no tenant is resolved')]
public function testSystemRuleAuditContext(): void
{
$this->currentTenant = null;
$this->store->expects($this->once())
->method('createLog')
->with(self::callback(static function (FirewallLogObject $log): bool {
return $log->getTenantId() === null
&& $log->getRuleId() === 'system-rule'
&& $log->getRuleScope() === FirewallRuleObject::SCOPE_SYSTEM;
}))
->willReturnArgument(0);
$event = \KTXF\Event\SecurityEvent::accessDenied(
'203.0.113.10',
null,
'system-rule',
FirewallRuleObject::SCOPE_SYSTEM,
'System block'
);
$this->service->logSecurityEvent($event);
}
#[TestDox('Tenantless security events without system rule context are ignored')]
public function testTenantlessAuditBoundary(): void
{
$this->currentTenant = null;
$this->store->expects($this->never())->method('createLog');
$this->service->logSecurityEvent(
\KTXF\Event\SecurityEvent::authFailure('203.0.113.10')
);
}
#[TestDox('Typed tenant firewall settings drive brute-force thresholds')]
public function testFirewallConfiguration(): void
{