diff --git a/core/lib/Module/Module.php b/core/lib/Module/Module.php index cb54b2a..f15986f 100644 --- a/core/lib/Module/Module.php +++ b/core/lib/Module/Module.php @@ -11,9 +11,10 @@ use KTXC\Service\SystemFirewallStatusService; use KTXC\Service\TenantFirewallLogService; use KTXC\Service\TenantFirewallRuleService; use KTXC\Service\TenantFirewallStatusService; +use KTXC\Security\Event\AuthenticationFailedEvent; +use KTXC\Security\Event\SecurityEvent; use KTXF\Event\DeliveryMode; use KTXF\Event\EventListenerRegistrarInterface; -use KTXC\Security\Event\SecurityEvent; use KTXF\Module\ModuleBrowserInterface; use KTXF\Module\ModuleConsoleInterface; use KTXF\Module\ModuleInstanceAbstract; @@ -34,7 +35,7 @@ class Module extends ModuleInstanceAbstract implements ModuleConsoleInterface, M { $this->events->listen( 'core', - SecurityEvent::AUTH_FAILURE, + AuthenticationFailedEvent::class, FirewallService::class, 'handleAuthFailure', priority: 100, diff --git a/core/lib/Security/Event/AuthenticationFailedEvent.php b/core/lib/Security/Event/AuthenticationFailedEvent.php new file mode 100644 index 0000000..b4fcdd6 --- /dev/null +++ b/core/lib/Security/Event/AuthenticationFailedEvent.php @@ -0,0 +1,69 @@ + $userId, 'reason' => $reason], + $tenantId, + $identityId, + ); + } + + public function getIpAddress(): string + { + return $this->ipAddress; + } + + public function getDeviceFingerprint(): ?string + { + return $this->deviceFingerprint; + } + + public function getUserAgent(): ?string + { + return $this->userAgent; + } + + public function getRequestPath(): ?string + { + return $this->requestPath; + } + + public function getRequestMethod(): ?string + { + return $this->requestMethod; + } + + public function getUserId(): ?string + { + return $this->userId; + } + + public function getReason(): ?string + { + return $this->reason; + } + + public function getSeverity(): int + { + return SecurityEvent::SEVERITY_WARNING; + } +} diff --git a/core/lib/Security/Event/SecurityEvent.php b/core/lib/Security/Event/SecurityEvent.php index fd9d150..94a8fb0 100644 --- a/core/lib/Security/Event/SecurityEvent.php +++ b/core/lib/Security/Event/SecurityEvent.php @@ -9,11 +9,10 @@ use KTXF\Event\Event; /** * Security-specific event for authentication and access control events */ -final class SecurityEvent extends Event +final class SecurityEvent extends Event implements SecurityEventInterface { // Event names public const AUTH_SUCCESS = 'security.auth.success'; - public const AUTH_FAILURE = 'security.auth.failure'; public const AUTH_LOGOUT = 'security.auth.logout'; public const TOKEN_REFRESH = 'security.token.refresh'; public const TOKEN_REVOKED = 'security.token.revoked'; @@ -96,35 +95,6 @@ final class SecurityEvent extends Event ); } - /** - * Create an authentication failure event - */ - public static function authFailure( - string $ipAddress, - ?string $deviceFingerprint = null, - ?string $userId = null, - ?string $reason = null, - ?string $tenantId = null, - ?string $identityId = null, - ?string $userAgent = null, - ?string $requestPath = null, - ?string $requestMethod = null, - ): self { - return self::create( - self::AUTH_FAILURE, - $ipAddress, - $deviceFingerprint, - ['userId' => $userId, 'reason' => $reason], - $tenantId, - $identityId, - $userAgent, - $requestPath, - $requestMethod, - $userId, - $reason, - ); - } - /** * Create an authentication success event */ @@ -219,7 +189,6 @@ final class SecurityEvent extends Event self::ACCESS_GRANTED, self::TOKEN_REFRESH => self::SEVERITY_INFO, - self::AUTH_FAILURE, self::ACCESS_DENIED, self::AUTH_LOGOUT, self::TOKEN_REVOKED => self::SEVERITY_WARNING, diff --git a/core/lib/Security/Event/SecurityEventInterface.php b/core/lib/Security/Event/SecurityEventInterface.php new file mode 100644 index 0000000..958852e --- /dev/null +++ b/core/lib/Security/Event/SecurityEventInterface.php @@ -0,0 +1,36 @@ +getIpAddress(); $tenantId = $event->getTenantId() ?? $this->tenantContext->identifier(); @@ -225,7 +227,7 @@ class FirewallService /** * Log security event to firewall logs */ - public function logSecurityEvent(SecurityEvent $event): void + public function logSecurityEvent(SecurityEventInterface $event): void { $log = $this->securityLog($event); if ($log !== null) { @@ -233,7 +235,7 @@ class FirewallService } } - private function securityLog(SecurityEvent $event): ?FirewallLogObject + private function securityLog(SecurityEventInterface $event): ?FirewallLogObject { $tenantId = $event->getTenantId() ?? $this->tenantContext->identifier(); $ruleScope = $event->get('ruleScope'); @@ -264,7 +266,7 @@ class FirewallService private function mapEventToLogType(string $eventName): string { return match ($eventName) { - SecurityEvent::AUTH_FAILURE => FirewallLogObject::EVENT_AUTH_FAILURE, + AuthenticationFailedEvent::class => FirewallLogObject::EVENT_AUTH_FAILURE, SecurityEvent::AUTH_SUCCESS => FirewallLogObject::EVENT_ACCESS_CHECK, SecurityEvent::BRUTE_FORCE_DETECTED => FirewallLogObject::EVENT_BRUTE_FORCE, SecurityEvent::RATE_LIMIT_EXCEEDED => FirewallLogObject::EVENT_RATE_LIMIT, @@ -283,7 +285,7 @@ class FirewallService /** * Map security event to result */ - private function mapEventToResult(SecurityEvent $event): string + private function mapEventToResult(SecurityEventInterface $event): string { return match ($event->getName()) { SecurityEvent::AUTH_SUCCESS, diff --git a/tests/php/Unit/Event/AuthenticationFailedEventTest.php b/tests/php/Unit/Event/AuthenticationFailedEventTest.php new file mode 100644 index 0000000..8b698ef --- /dev/null +++ b/tests/php/Unit/Event/AuthenticationFailedEventTest.php @@ -0,0 +1,43 @@ +getName()); + self::assertSame('203.0.113.10', $event->getIpAddress()); + self::assertSame('device-a', $event->getDeviceFingerprint()); + self::assertSame('user-a', $event->getUserId()); + self::assertSame('Invalid credentials', $event->getReason()); + self::assertSame('tenant-a', $event->getTenantId()); + self::assertSame('identity-a', $event->getIdentityId()); + self::assertSame('Test Agent', $event->getUserAgent()); + self::assertSame('/login', $event->getRequestPath()); + self::assertSame('POST', $event->getRequestMethod()); + self::assertSame(SecurityEvent::SEVERITY_WARNING, $event->getSeverity()); + } +} diff --git a/tests/php/Unit/Event/SecurityEventTest.php b/tests/php/Unit/Event/SecurityEventTest.php index 3d0b901..60fb310 100644 --- a/tests/php/Unit/Event/SecurityEventTest.php +++ b/tests/php/Unit/Event/SecurityEventTest.php @@ -17,7 +17,7 @@ final class SecurityEventTest extends TestCase { self::assertSame( SecurityEvent::SEVERITY_WARNING, - (new SecurityEvent(SecurityEvent::AUTH_FAILURE))->getSeverity(), + (new SecurityEvent(SecurityEvent::AUTH_LOGOUT))->getSeverity(), ); self::assertSame( SecurityEvent::SEVERITY_ERROR, @@ -38,7 +38,7 @@ final class SecurityEventTest extends TestCase public function allowsSeverityOverride(): void { $event = new SecurityEvent( - SecurityEvent::AUTH_FAILURE, + SecurityEvent::AUTH_LOGOUT, severity: SecurityEvent::SEVERITY_CRITICAL, ); diff --git a/tests/php/Unit/Module/CoreModuleTest.php b/tests/php/Unit/Module/CoreModuleTest.php index 66c0423..737a7e6 100644 --- a/tests/php/Unit/Module/CoreModuleTest.php +++ b/tests/php/Unit/Module/CoreModuleTest.php @@ -15,9 +15,10 @@ use KTXC\Service\TenantFirewallLogService; use KTXC\Service\TenantFirewallStatusService; use KTXC\Service\SystemFirewallStatusService; use KTXC\Service\TenantFirewallRuleService; -use KTXF\Event\DeliveryMode; use KTXC\Event\EventListenerRegistry; +use KTXC\Security\Event\AuthenticationFailedEvent; use KTXC\Security\Event\SecurityEvent; +use KTXF\Event\DeliveryMode; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; @@ -38,9 +39,9 @@ final class CoreModuleTest extends TestCase self::assertSame(['core'], array_values(array_unique(array_column($definitions, 'module')))); self::assertSame( FirewallService::class, - $registry->listeners(SecurityEvent::AUTH_FAILURE, DeliveryMode::Immediate)[0]->service, + $registry->listeners(AuthenticationFailedEvent::class, DeliveryMode::Immediate)[0]->service, ); - self::assertSame([], $registry->listeners(SecurityEvent::AUTH_FAILURE, DeliveryMode::Deferred)); + self::assertSame([], $registry->listeners(AuthenticationFailedEvent::class, DeliveryMode::Deferred)); foreach ([ SecurityEvent::RATE_LIMIT_EXCEEDED, SecurityEvent::SUSPICIOUS_ACTIVITY, diff --git a/tests/php/Unit/Service/FirewallServiceTest.php b/tests/php/Unit/Service/FirewallServiceTest.php index 28df6ef..48032ac 100644 --- a/tests/php/Unit/Service/FirewallServiceTest.php +++ b/tests/php/Unit/Service/FirewallServiceTest.php @@ -8,6 +8,7 @@ use KTXC\Context\TenantContextInterface; use KTXC\Models\Firewall\FirewallRuleObject; use KTXC\Models\Firewall\FirewallLogObject; use KTXC\Models\Tenant\TenantConfiguration; +use KTXC\Security\Event\AuthenticationFailedEvent; use KTXC\Service\FirewallService; use KTXC\Service\FirewallRuleCache; use KTXC\Service\FirewallRuleManager; @@ -252,7 +253,7 @@ class FirewallServiceTest extends TestCase $this->store->expects($this->never())->method('createLog'); $this->service->logSecurityEvent( - \KTXC\Security\Event\SecurityEvent::authFailure('203.0.113.10') + new AuthenticationFailedEvent('203.0.113.10') ); } @@ -377,7 +378,7 @@ class FirewallServiceTest extends TestCase ->willReturn(4); $this->events->expects($this->never())->method('dispatch'); - $event = \KTXC\Security\Event\SecurityEvent::authFailure( + $event = new AuthenticationFailedEvent( '203.0.113.10', tenantId: 'tenant-a', ); @@ -403,7 +404,7 @@ class FirewallServiceTest extends TestCase ->with('tenant-a', '203.0.113.10', 300) ->willReturn(0); - $event = \KTXC\Security\Event\SecurityEvent::authFailure( + $event = new AuthenticationFailedEvent( '203.0.113.10', tenantId: 'tenant-a', ); @@ -459,7 +460,7 @@ class FirewallServiceTest extends TestCase } }); - $event = \KTXC\Security\Event\SecurityEvent::authFailure( + $event = new AuthenticationFailedEvent( '203.0.113.10', tenantId: 'tenant-event', ); @@ -485,7 +486,7 @@ class FirewallServiceTest extends TestCase $this->events->expects($this->never())->method('dispatch'); $this->service->handleAuthFailure( - \KTXC\Security\Event\SecurityEvent::authFailure('203.0.113.10') + new AuthenticationFailedEvent('203.0.113.10') ); } @@ -499,7 +500,7 @@ class FirewallServiceTest extends TestCase ->willReturn(0); $this->service->handleAuthFailure( - \KTXC\Security\Event\SecurityEvent::authFailure('203.0.113.10') + new AuthenticationFailedEvent('203.0.113.10') ); } @@ -511,7 +512,7 @@ class FirewallServiceTest extends TestCase $this->store->expects($this->never())->method('depositRule'); $this->service->handleAuthFailure( - \KTXC\Security\Event\SecurityEvent::authFailure('203.0.113.10') + new AuthenticationFailedEvent('203.0.113.10') ); } @@ -525,7 +526,7 @@ class FirewallServiceTest extends TestCase ->method('countRecentFailures') ->with('tenant-a', '203.0.113.10', 300) ->willReturn(1); - $event = \KTXC\Security\Event\SecurityEvent::authFailure('203.0.113.10'); + $event = new AuthenticationFailedEvent('203.0.113.10'); $eventId = $event->getEventId(); $this->service->handleAuthFailure($event);