1 Commits

Author SHA1 Message Date
Sebastian 470961a955 chore(deps): update dependency vue-i18n to v11.4.8
Build Test / build (pull_request) Successful in 15s
JS Unit Tests / test (pull_request) Successful in 16s
PHP Integration Tests / Integration Tests (pull_request) Failing after 1m9s
PHP Unit Tests / test (pull_request) Failing after 1m10s
2026-08-04 03:01:37 +00:00
26 changed files with 451 additions and 687 deletions
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace KTXC\Console\Event; namespace KTXC\Console\Event;
use KTXC\Event\EventListenerRegistry; use KTXF\Event\EventListenerRegistry;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
+3 -5
View File
@@ -27,11 +27,10 @@ use KTXC\Module\ModuleManager;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use KTXC\Logger\LoggerFactory; use KTXC\Logger\LoggerFactory;
use KTXC\Logger\TenantAwareLogger; use KTXC\Logger\TenantAwareLogger;
use KTXC\Event\DeferredEventProcessorInterface; use KTXF\Event\DeferredEventProcessorInterface;
use KTXC\Event\EventDispatcher; use KTXF\Event\EventDispatcher;
use KTXC\Event\EventListenerRegistry;
use KTXF\Event\EventDispatcherInterface; use KTXF\Event\EventDispatcherInterface;
use KTXF\Event\EventListenerRegistrarInterface; use KTXF\Event\EventListenerRegistry;
use KTXF\Cache\EphemeralCacheInterface; use KTXF\Cache\EphemeralCacheInterface;
use KTXF\Cache\PersistentCacheInterface; use KTXF\Cache\PersistentCacheInterface;
use KTXF\Cache\BlobCacheInterface; use KTXF\Cache\BlobCacheInterface;
@@ -411,7 +410,6 @@ class Kernel implements KernelInterface
EventDispatcherInterface::class => \DI\get(EventDispatcher::class), EventDispatcherInterface::class => \DI\get(EventDispatcher::class),
DeferredEventProcessorInterface::class => \DI\get(EventDispatcher::class), DeferredEventProcessorInterface::class => \DI\get(EventDispatcher::class),
EventListenerRegistrarInterface::class => \DI\get(EventListenerRegistry::class),
// Ephemeral Cache - for short-lived data (sessions, rate limits, challenges) // Ephemeral Cache - for short-lived data (sessions, rate limits, challenges)
EphemeralCacheInterface::class => function(ContainerInterface $c) use ($projectDir) { EphemeralCacheInterface::class => function(ContainerInterface $c) use ($projectDir) {
$storeType = $c->has('cache.ephemeral') ? $c->get('cache.ephemeral') : 'file'; $storeType = $c->has('cache.ephemeral') ? $c->get('cache.ephemeral') : 'file';
+4 -5
View File
@@ -11,10 +11,9 @@ use KTXC\Service\SystemFirewallStatusService;
use KTXC\Service\TenantFirewallLogService; use KTXC\Service\TenantFirewallLogService;
use KTXC\Service\TenantFirewallRuleService; use KTXC\Service\TenantFirewallRuleService;
use KTXC\Service\TenantFirewallStatusService; use KTXC\Service\TenantFirewallStatusService;
use KTXC\Security\Event\AuthenticationFailedEvent;
use KTXC\Security\Event\SecurityEvent;
use KTXF\Event\DeliveryMode; use KTXF\Event\DeliveryMode;
use KTXF\Event\EventListenerRegistrarInterface; use KTXF\Event\EventListenerRegistry;
use KTXF\Event\SecurityEvent;
use KTXF\Module\ModuleBrowserInterface; use KTXF\Module\ModuleBrowserInterface;
use KTXF\Module\ModuleConsoleInterface; use KTXF\Module\ModuleConsoleInterface;
use KTXF\Module\ModuleInstanceAbstract; use KTXF\Module\ModuleInstanceAbstract;
@@ -27,7 +26,7 @@ use KTXF\Module\ModuleInstanceAbstract;
class Module extends ModuleInstanceAbstract implements ModuleConsoleInterface, ModuleBrowserInterface class Module extends ModuleInstanceAbstract implements ModuleConsoleInterface, ModuleBrowserInterface
{ {
public function __construct( public function __construct(
private readonly EventListenerRegistrarInterface $events, private readonly EventListenerRegistry $events,
) { ) {
} }
@@ -35,7 +34,7 @@ class Module extends ModuleInstanceAbstract implements ModuleConsoleInterface, M
{ {
$this->events->listen( $this->events->listen(
'core', 'core',
AuthenticationFailedEvent::class, SecurityEvent::AUTH_FAILURE,
FirewallService::class, FirewallService::class,
'handleAuthFailure', 'handleAuthFailure',
priority: 100, priority: 100,
@@ -1,69 +0,0 @@
<?php
declare(strict_types=1);
namespace KTXC\Security\Event;
use KTXF\Event\Event;
final class AuthenticationFailedEvent extends Event implements SecurityEventInterface
{
public function __construct(
private readonly string $ipAddress,
private readonly ?string $deviceFingerprint = null,
private readonly ?string $userId = null,
private readonly ?string $reason = null,
?string $tenantId = null,
?string $identityId = null,
private readonly ?string $userAgent = null,
private readonly ?string $requestPath = null,
private readonly ?string $requestMethod = null,
) {
parent::__construct(
self::class,
['userId' => $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;
}
}
-249
View File
@@ -1,249 +0,0 @@
<?php
declare(strict_types=1);
namespace KTXC\Security\Event;
use KTXF\Event\Event;
/**
* Security-specific event for authentication and access control events
*/
final class SecurityEvent extends Event implements SecurityEventInterface
{
// Event names
public const AUTH_SUCCESS = 'security.auth.success';
public const AUTH_LOGOUT = 'security.auth.logout';
public const TOKEN_REFRESH = 'security.token.refresh';
public const TOKEN_REVOKED = 'security.token.revoked';
public const ACCESS_DENIED = 'security.access.denied';
public const ACCESS_GRANTED = 'security.access.granted';
public const BRUTE_FORCE_DETECTED = 'security.brute_force.detected';
public const RATE_LIMIT_EXCEEDED = 'security.rate_limit.exceeded';
public const SUSPICIOUS_ACTIVITY = 'security.suspicious.activity';
public const IP_BLOCKED = 'security.ip.blocked';
public const IP_ALLOWED = 'security.ip.allowed';
public const DEVICE_BLOCKED = 'security.device.blocked';
public const FIREWALL_RULE_CREATED = 'security.firewall.rule.created';
public const FIREWALL_RULE_EXTENDED = 'security.firewall.rule.extended';
public const FIREWALL_RULE_ENABLED = 'security.firewall.rule.enabled';
public const FIREWALL_RULE_DISABLED = 'security.firewall.rule.disabled';
public const FIREWALL_RULE_REMOVED = 'security.firewall.rule.removed';
public const FIREWALL_SETTINGS_UPDATED = 'security.firewall.settings.updated';
// Severity levels
public const SEVERITY_DEBUG = 0;
public const SEVERITY_INFO = 1;
public const SEVERITY_WARNING = 2;
public const SEVERITY_ERROR = 3;
public const SEVERITY_CRITICAL = 4;
private readonly int $severity;
public function __construct(
string $name,
array $data = [],
?string $tenantId = null,
?string $identityId = null,
private readonly ?string $ipAddress = null,
private readonly ?string $deviceFingerprint = null,
private readonly ?string $userAgent = null,
private readonly ?string $requestPath = null,
private readonly ?string $requestMethod = null,
private readonly ?string $userId = null,
private readonly ?string $reason = null,
?int $severity = null,
) {
parent::__construct($name, $data, $tenantId, $identityId);
$this->severity = $severity ?? self::getSeverityForEvent($name);
}
/**
* Create a security event with common parameters
*/
public static function create(
string $name,
?string $ipAddress = null,
?string $deviceFingerprint = null,
array $data = [],
?string $tenantId = null,
?string $identityId = null,
?string $userAgent = null,
?string $requestPath = null,
?string $requestMethod = null,
?string $userId = null,
?string $reason = null,
?int $severity = null,
): self {
return new self(
$name,
$data,
$tenantId,
$identityId,
$ipAddress,
$deviceFingerprint,
$userAgent,
$requestPath,
$requestMethod,
$userId,
$reason,
$severity,
);
}
/**
* Create an authentication success event
*/
public static function authSuccess(
string $ipAddress,
?string $deviceFingerprint = null,
?string $userId = null,
?string $tenantId = null,
): self {
return self::create(
self::AUTH_SUCCESS,
$ipAddress,
$deviceFingerprint,
['userId' => $userId],
tenantId: $tenantId,
userId: $userId,
);
}
/**
* Create a brute force detection event
*/
public static function bruteForceDetected(
string $ipAddress,
int $failureCount,
int $windowSeconds,
?string $tenantId = null,
): self {
return self::create(
self::BRUTE_FORCE_DETECTED,
$ipAddress,
data: ['failureCount' => $failureCount, 'windowSeconds' => $windowSeconds],
tenantId: $tenantId,
reason: sprintf('%d failed attempts in %d seconds', $failureCount, $windowSeconds),
);
}
/**
* Create a rate limit exceeded event
*/
public static function rateLimitExceeded(
string $ipAddress,
int $requestCount,
int $windowSeconds,
?string $endpoint = null,
?string $tenantId = null,
): self {
return self::create(
self::RATE_LIMIT_EXCEEDED,
$ipAddress,
data: [
'requestCount' => $requestCount,
'windowSeconds' => $windowSeconds,
'endpoint' => $endpoint,
],
tenantId: $tenantId,
requestPath: $endpoint,
reason: sprintf('%d requests in %d seconds', $requestCount, $windowSeconds),
);
}
/**
* Create an access denied event
*/
public static function accessDenied(
string $ipAddress,
?string $deviceFingerprint = null,
?string $ruleId = null,
?string $ruleScope = null,
?string $reason = null,
?string $tenantId = null,
?string $identityId = null,
): self {
return self::create(
self::ACCESS_DENIED,
$ipAddress,
$deviceFingerprint,
['ruleId' => $ruleId, 'ruleScope' => $ruleScope, 'reason' => $reason],
$tenantId,
$identityId,
reason: $reason,
);
}
/**
* Get default severity for event types
*/
private static function getSeverityForEvent(string $eventName): int
{
return match ($eventName) {
self::AUTH_SUCCESS,
self::ACCESS_GRANTED,
self::TOKEN_REFRESH => self::SEVERITY_INFO,
self::ACCESS_DENIED,
self::AUTH_LOGOUT,
self::TOKEN_REVOKED => self::SEVERITY_WARNING,
self::RATE_LIMIT_EXCEEDED,
self::SUSPICIOUS_ACTIVITY => self::SEVERITY_ERROR,
self::BRUTE_FORCE_DETECTED,
self::IP_BLOCKED,
self::DEVICE_BLOCKED => self::SEVERITY_CRITICAL,
default => self::SEVERITY_INFO,
};
}
// Getters and setters
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 $this->severity;
}
}
@@ -1,36 +0,0 @@
<?php
declare(strict_types=1);
namespace KTXC\Security\Event;
interface SecurityEventInterface
{
public function getName(): string;
public function get(string $key, mixed $default = null): mixed;
public function getData(): array;
public function getEventId(): string;
public function getTenantId(): ?string;
public function getIdentityId(): ?string;
public function getIpAddress(): ?string;
public function getDeviceFingerprint(): ?string;
public function getUserAgent(): ?string;
public function getRequestPath(): ?string;
public function getRequestMethod(): ?string;
public function getUserId(): ?string;
public function getReason(): ?string;
public function getSeverity(): int;
}
+9 -22
View File
@@ -7,7 +7,7 @@ namespace KTXC\Service;
use KTXC\Models\Firewall\FirewallRuleObject; use KTXC\Models\Firewall\FirewallRuleObject;
use KTXC\Stores\FirewallStore; use KTXC\Stores\FirewallStore;
use KTXF\Event\EventDispatcherInterface; use KTXF\Event\EventDispatcherInterface;
use KTXC\Security\Event\SecurityEvent; use KTXF\Event\SecurityEvent;
use KTXF\IpUtils; use KTXF\IpUtils;
final class FirewallRuleManager final class FirewallRuleManager
@@ -254,13 +254,8 @@ final class FirewallRuleManager
$origin $origin
); );
$event = new SecurityEvent( $event = new SecurityEvent(SecurityEvent::DEVICE_BLOCKED, ['device' => $fingerprint, 'reason' => $reason]);
SecurityEvent::DEVICE_BLOCKED, $event->setDeviceFingerprint($fingerprint)->setReason($reason)->setTenantId($scope->tenantId);
['device' => $fingerprint, 'reason' => $reason],
tenantId: $scope->tenantId,
deviceFingerprint: $fingerprint,
reason: $reason,
);
$this->events->dispatch($event); $this->events->dispatch($event);
return $rule; return $rule;
@@ -504,13 +499,8 @@ final class FirewallRuleManager
string $ipAddress, string $ipAddress,
?string $reason ?string $reason
): void { ): void {
$event = new SecurityEvent( $event = new SecurityEvent($name, ['ip' => $ipAddress, 'reason' => $reason]);
$name, $event->setIpAddress($ipAddress)->setReason($reason)->setTenantId($scope->tenantId);
['ip' => $ipAddress, 'reason' => $reason],
tenantId: $scope->tenantId,
ipAddress: $ipAddress,
reason: $reason,
);
$this->events->dispatch($event); $this->events->dispatch($event);
} }
@@ -521,9 +511,7 @@ final class FirewallRuleManager
array $change = [] array $change = []
): void ): void
{ {
$event = new SecurityEvent( $event = new SecurityEvent($name, [
$name,
[
'ruleId' => $rule->getId(), 'ruleId' => $rule->getId(),
'ruleScope' => $rule->getScope(), 'ruleScope' => $rule->getScope(),
'ruleType' => $rule->getType(), 'ruleType' => $rule->getType(),
@@ -534,10 +522,9 @@ final class FirewallRuleManager
'expiresAt' => $rule->getExpiresAt()?->format(\DateTimeInterface::ATOM), 'expiresAt' => $rule->getExpiresAt()?->format(\DateTimeInterface::ATOM),
...($rule->getMetadata() ?? []), ...($rule->getMetadata() ?? []),
...$change, ...$change,
], ]);
tenantId: $rule->getTenantId(), $event->setTenantId($rule->getTenantId())
identityId: $actorId ?? $rule->getCreatedBy(), ->setIdentityId($actorId ?? $rule->getCreatedBy());
);
$this->events->dispatch($event); $this->events->dispatch($event);
} }
} }
+11 -16
View File
@@ -9,10 +9,8 @@ use KTXC\Models\Firewall\FirewallRuleObject;
use KTXC\Models\Firewall\FirewallLogObject; use KTXC\Models\Firewall\FirewallLogObject;
use KTXC\Stores\FirewallStore; use KTXC\Stores\FirewallStore;
use KTXC\Context\TenantContextInterface; use KTXC\Context\TenantContextInterface;
use KTXC\Security\Event\AuthenticationFailedEvent;
use KTXC\Security\Event\SecurityEvent;
use KTXC\Security\Event\SecurityEventInterface;
use KTXF\Event\EventDispatcherInterface; use KTXF\Event\EventDispatcherInterface;
use KTXF\Event\SecurityEvent;
use KTXF\IpUtils; use KTXF\IpUtils;
/** /**
@@ -132,7 +130,7 @@ class FirewallService
/** /**
* Handle authentication failure event * Handle authentication failure event
*/ */
public function handleAuthFailure(AuthenticationFailedEvent $event): void public function handleAuthFailure(SecurityEvent $event): void
{ {
$ipAddress = $event->getIpAddress(); $ipAddress = $event->getIpAddress();
$tenantId = $event->getTenantId() ?? $this->tenantContext->identifier(); $tenantId = $event->getTenantId() ?? $this->tenantContext->identifier();
@@ -141,6 +139,7 @@ class FirewallService
return; return;
} }
$event->setTenantId($tenantId);
$log = $this->securityLog($event); $log = $this->securityLog($event);
if ($log === null || !$this->store->createLogOnce($log)) { if ($log === null || !$this->store->createLogOnce($log)) {
return; return;
@@ -196,12 +195,8 @@ class FirewallService
int $blockDuration int $blockDuration
): void { ): void {
// Publish brute force event // Publish brute force event
$event = SecurityEvent::bruteForceDetected( $event = SecurityEvent::bruteForceDetected($ipAddress, $failureCount, $windowSeconds);
$ipAddress, $event->setTenantId($tenantId);
$failureCount,
$windowSeconds,
$tenantId,
);
$this->events->dispatch($event); $this->events->dispatch($event);
$this->rules->blockIp( $this->rules->blockIp(
@@ -227,7 +222,7 @@ class FirewallService
/** /**
* Log security event to firewall logs * Log security event to firewall logs
*/ */
public function logSecurityEvent(SecurityEventInterface $event): void public function logSecurityEvent(SecurityEvent $event): void
{ {
$log = $this->securityLog($event); $log = $this->securityLog($event);
if ($log !== null) { if ($log !== null) {
@@ -235,7 +230,7 @@ class FirewallService
} }
} }
private function securityLog(SecurityEventInterface $event): ?FirewallLogObject private function securityLog(SecurityEvent $event): ?FirewallLogObject
{ {
$tenantId = $event->getTenantId() ?? $this->tenantContext->identifier(); $tenantId = $event->getTenantId() ?? $this->tenantContext->identifier();
$ruleScope = $event->get('ruleScope'); $ruleScope = $event->get('ruleScope');
@@ -266,7 +261,7 @@ class FirewallService
private function mapEventToLogType(string $eventName): string private function mapEventToLogType(string $eventName): string
{ {
return match ($eventName) { return match ($eventName) {
AuthenticationFailedEvent::class => FirewallLogObject::EVENT_AUTH_FAILURE, SecurityEvent::AUTH_FAILURE => FirewallLogObject::EVENT_AUTH_FAILURE,
SecurityEvent::AUTH_SUCCESS => FirewallLogObject::EVENT_ACCESS_CHECK, SecurityEvent::AUTH_SUCCESS => FirewallLogObject::EVENT_ACCESS_CHECK,
SecurityEvent::BRUTE_FORCE_DETECTED => FirewallLogObject::EVENT_BRUTE_FORCE, SecurityEvent::BRUTE_FORCE_DETECTED => FirewallLogObject::EVENT_BRUTE_FORCE,
SecurityEvent::RATE_LIMIT_EXCEEDED => FirewallLogObject::EVENT_RATE_LIMIT, SecurityEvent::RATE_LIMIT_EXCEEDED => FirewallLogObject::EVENT_RATE_LIMIT,
@@ -285,7 +280,7 @@ class FirewallService
/** /**
* Map security event to result * Map security event to result
*/ */
private function mapEventToResult(SecurityEventInterface $event): string private function mapEventToResult(SecurityEvent $event): string
{ {
return match ($event->getName()) { return match ($event->getName()) {
SecurityEvent::AUTH_SUCCESS, SecurityEvent::AUTH_SUCCESS,
@@ -313,9 +308,9 @@ class FirewallService
$deviceFingerprint, $deviceFingerprint,
$rule->getId(), $rule->getId(),
$rule->getScope(), $rule->getScope(),
$rule->getReason(), $rule->getReason()
$this->tenantContext->identifier(),
); );
$event->setTenantId($this->tenantContext->identifier());
$this->events->dispatch($event); $this->events->dispatch($event);
} }
+4 -8
View File
@@ -6,7 +6,7 @@ namespace KTXC\Service;
use KTXC\Models\Tenant\TenantConfiguration; use KTXC\Models\Tenant\TenantConfiguration;
use KTXF\Event\EventDispatcherInterface; use KTXF\Event\EventDispatcherInterface;
use KTXC\Security\Event\SecurityEvent; use KTXF\Event\SecurityEvent;
final class FirewallSettingsService final class FirewallSettingsService
{ {
@@ -52,17 +52,13 @@ final class FirewallSettingsService
$tenant->setConfiguration($configuration); $tenant->setConfiguration($configuration);
$this->tenants->deposit($tenant); $this->tenants->deposit($tenant);
$event = new SecurityEvent( $event = new SecurityEvent(SecurityEvent::FIREWALL_SETTINGS_UPDATED, [
SecurityEvent::FIREWALL_SETTINGS_UPDATED,
[
'changeReason' => $reason, 'changeReason' => $reason,
'changeOrigin' => FirewallRuleManager::ORIGIN_MANUAL, 'changeOrigin' => FirewallRuleManager::ORIGIN_MANUAL,
'previous' => $previous, 'previous' => $previous,
'current' => $current, 'current' => $current,
], ]);
tenantId: $tenantId, $event->setTenantId($tenantId)->setIdentityId($actorId);
identityId: $actorId,
);
$this->events->dispatch($event); $this->events->dispatch($event);
return $current; return $current;
@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace KTXC\Event; namespace KTXF\Event;
interface DeferredEventProcessorInterface interface DeferredEventProcessorInterface
{ {
@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace KTXC\Event; namespace KTXF\Event;
final readonly class DeferredProcessingResult final readonly class DeferredProcessingResult
{ {
+43 -19
View File
@@ -10,18 +10,16 @@ namespace KTXF\Event;
class Event class Event
{ {
private bool $propagationStopped = false; private bool $propagationStopped = false;
private readonly array $data; private array $data = [];
private readonly float $timestamp; private float $timestamp;
private readonly string $eventId; private string $eventId;
private ?string $tenantId = null;
private ?string $identityId = null;
public function __construct( public function __construct(
private readonly string $name, private readonly string $name,
array $data = [], array $data = []
private readonly ?string $tenantId = null,
private readonly ?string $identityId = null,
) { ) {
self::validateData($data);
$this->data = $data; $this->data = $data;
$this->timestamp = microtime(true); $this->timestamp = microtime(true);
$this->eventId = bin2hex(random_bytes(16)); $this->eventId = bin2hex(random_bytes(16));
@@ -43,6 +41,15 @@ class Event
return $this->data[$key] ?? $default; return $this->data[$key] ?? $default;
} }
/**
* Set a data value
*/
public function set(string $key, mixed $value): self
{
$this->data[$key] = $value;
return $this;
}
/** /**
* Check if a data key exists * Check if a data key exists
*/ */
@@ -104,6 +111,15 @@ class Event
return $this->tenantId; return $this->tenantId;
} }
/**
* Set tenant ID for multi-tenant context
*/
public function setTenantId(?string $tenantId): self
{
$this->tenantId = $tenantId;
return $this;
}
/** /**
* Get identity ID (user who triggered the event) * Get identity ID (user who triggered the event)
*/ */
@@ -112,18 +128,26 @@ class Event
return $this->identityId; return $this->identityId;
} }
private static function validateData(array $data): void /**
* Set identity ID
*/
public function setIdentityId(?string $identityId): self
{ {
foreach ($data as $value) { $this->identityId = $identityId;
if (is_array($value)) { return $this;
self::validateData($value);
continue;
}
if ($value !== null && !is_scalar($value)) {
throw new \InvalidArgumentException(
'Event data must contain only scalar, null, or array values.',
);
}
} }
/**
* Convert event to array for serialization/logging
*/
public function toArray(): array
{
return [
'name' => $this->name,
'data' => $this->data,
'timestamp' => $this->timestamp,
'tenantId' => $this->tenantId,
'identityId' => $this->identityId,
];
} }
} }
@@ -2,12 +2,8 @@
declare(strict_types=1); declare(strict_types=1);
namespace KTXC\Event; namespace KTXF\Event;
use KTXF\Event\DeliveryMode;
use KTXF\Event\Event;
use KTXF\Event\EventDispatcherInterface;
use KTXF\Event\FailurePolicy;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@@ -2,10 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace KTXC\Event; namespace KTXF\Event;
use KTXF\Event\DeliveryMode;
use KTXF\Event\FailurePolicy;
final readonly class EventListenerDefinition final readonly class EventListenerDefinition
{ {
@@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
namespace KTXF\Event;
interface EventListenerRegistrarInterface
{
/**
* @param class-string $service
*/
public function listen(
string $module,
string $event,
string $service,
string $method,
DeliveryMode $delivery = DeliveryMode::Immediate,
int $priority = 0,
FailurePolicy $failurePolicy = FailurePolicy::Continue,
): void;
}
@@ -2,14 +2,11 @@
declare(strict_types=1); declare(strict_types=1);
namespace KTXC\Event; namespace KTXF\Event;
use KTXF\Event\DeliveryMode;
use KTXF\Event\EventListenerRegistrarInterface;
use KTXF\Event\FailurePolicy;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
final class EventListenerRegistry implements EventListenerRegistrarInterface final class EventListenerRegistry
{ {
/** @var array<string, list<EventListenerDefinition>> */ /** @var array<string, list<EventListenerDefinition>> */
private array $listeners = []; private array $listeners = [];
+311
View File
@@ -0,0 +1,311 @@
<?php
declare(strict_types=1);
namespace KTXF\Event;
/**
* Security-specific event for authentication and access control events
*/
class SecurityEvent extends Event
{
// 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';
public const ACCESS_DENIED = 'security.access.denied';
public const ACCESS_GRANTED = 'security.access.granted';
public const BRUTE_FORCE_DETECTED = 'security.brute_force.detected';
public const RATE_LIMIT_EXCEEDED = 'security.rate_limit.exceeded';
public const SUSPICIOUS_ACTIVITY = 'security.suspicious.activity';
public const IP_BLOCKED = 'security.ip.blocked';
public const IP_ALLOWED = 'security.ip.allowed';
public const DEVICE_BLOCKED = 'security.device.blocked';
public const FIREWALL_RULE_CREATED = 'security.firewall.rule.created';
public const FIREWALL_RULE_EXTENDED = 'security.firewall.rule.extended';
public const FIREWALL_RULE_ENABLED = 'security.firewall.rule.enabled';
public const FIREWALL_RULE_DISABLED = 'security.firewall.rule.disabled';
public const FIREWALL_RULE_REMOVED = 'security.firewall.rule.removed';
public const FIREWALL_SETTINGS_UPDATED = 'security.firewall.settings.updated';
private ?string $ipAddress = null;
private ?string $deviceFingerprint = null;
private ?string $userAgent = null;
private ?string $requestPath = null;
private ?string $requestMethod = null;
private ?string $userId = null;
private ?string $reason = null;
private int $severity = self::SEVERITY_INFO;
// Severity levels
public const SEVERITY_DEBUG = 0;
public const SEVERITY_INFO = 1;
public const SEVERITY_WARNING = 2;
public const SEVERITY_ERROR = 3;
public const SEVERITY_CRITICAL = 4;
/**
* Create a security event with common parameters
*/
public static function create(
string $name,
?string $ipAddress = null,
?string $deviceFingerprint = null,
array $data = []
): self {
$event = new self($name, $data);
$event->ipAddress = $ipAddress;
$event->deviceFingerprint = $deviceFingerprint;
// Set default severity based on event type
$event->severity = self::getSeverityForEvent($name);
return $event;
}
/**
* Create an authentication failure event
*/
public static function authFailure(
string $ipAddress,
?string $deviceFingerprint = null,
?string $userId = null,
?string $reason = null
): self {
$event = self::create(self::AUTH_FAILURE, $ipAddress, $deviceFingerprint, [
'userId' => $userId,
'reason' => $reason,
]);
$event->userId = $userId;
$event->reason = $reason;
return $event;
}
/**
* Create an authentication success event
*/
public static function authSuccess(
string $ipAddress,
?string $deviceFingerprint = null,
string $userId = null
): self {
$event = self::create(self::AUTH_SUCCESS, $ipAddress, $deviceFingerprint, [
'userId' => $userId,
]);
$event->userId = $userId;
return $event;
}
/**
* Create a brute force detection event
*/
public static function bruteForceDetected(
string $ipAddress,
int $failureCount,
int $windowSeconds
): self {
$event = self::create(self::BRUTE_FORCE_DETECTED, $ipAddress, null, [
'failureCount' => $failureCount,
'windowSeconds' => $windowSeconds,
]);
$event->reason = sprintf(
'%d failed attempts in %d seconds',
$failureCount,
$windowSeconds
);
return $event;
}
/**
* Create a rate limit exceeded event
*/
public static function rateLimitExceeded(
string $ipAddress,
int $requestCount,
int $windowSeconds,
?string $endpoint = null
): self {
$event = self::create(self::RATE_LIMIT_EXCEEDED, $ipAddress, null, [
'requestCount' => $requestCount,
'windowSeconds' => $windowSeconds,
'endpoint' => $endpoint,
]);
$event->requestPath = $endpoint;
$event->reason = sprintf(
'%d requests in %d seconds',
$requestCount,
$windowSeconds
);
return $event;
}
/**
* Create an access denied event
*/
public static function accessDenied(
string $ipAddress,
?string $deviceFingerprint = null,
?string $ruleId = null,
?string $ruleScope = null,
?string $reason = null
): self {
$event = self::create(self::ACCESS_DENIED, $ipAddress, $deviceFingerprint, [
'ruleId' => $ruleId,
'ruleScope' => $ruleScope,
'reason' => $reason,
]);
$event->reason = $reason;
return $event;
}
/**
* Get default severity for event types
*/
private static function getSeverityForEvent(string $eventName): int
{
return match ($eventName) {
self::AUTH_SUCCESS,
self::ACCESS_GRANTED,
self::TOKEN_REFRESH => self::SEVERITY_INFO,
self::AUTH_FAILURE,
self::ACCESS_DENIED,
self::AUTH_LOGOUT,
self::TOKEN_REVOKED => self::SEVERITY_WARNING,
self::RATE_LIMIT_EXCEEDED,
self::SUSPICIOUS_ACTIVITY => self::SEVERITY_ERROR,
self::BRUTE_FORCE_DETECTED,
self::IP_BLOCKED,
self::DEVICE_BLOCKED => self::SEVERITY_CRITICAL,
default => self::SEVERITY_INFO,
};
}
// Getters and setters
public function getIpAddress(): ?string
{
return $this->ipAddress;
}
public function setIpAddress(?string $ipAddress): self
{
$this->ipAddress = $ipAddress;
return $this;
}
public function getDeviceFingerprint(): ?string
{
return $this->deviceFingerprint;
}
public function setDeviceFingerprint(?string $deviceFingerprint): self
{
$this->deviceFingerprint = $deviceFingerprint;
return $this;
}
public function getUserAgent(): ?string
{
return $this->userAgent;
}
public function setUserAgent(?string $userAgent): self
{
$this->userAgent = $userAgent;
return $this;
}
public function getRequestPath(): ?string
{
return $this->requestPath;
}
public function setRequestPath(?string $requestPath): self
{
$this->requestPath = $requestPath;
return $this;
}
public function getRequestMethod(): ?string
{
return $this->requestMethod;
}
public function setRequestMethod(?string $requestMethod): self
{
$this->requestMethod = $requestMethod;
return $this;
}
public function getUserId(): ?string
{
return $this->userId;
}
public function setUserId(?string $userId): self
{
$this->userId = $userId;
return $this;
}
public function getReason(): ?string
{
return $this->reason;
}
public function setReason(?string $reason): self
{
$this->reason = $reason;
return $this;
}
public function getSeverity(): int
{
return $this->severity;
}
public function setSeverity(int $severity): self
{
$this->severity = $severity;
return $this;
}
public function getSeverityLabel(): string
{
return match ($this->severity) {
self::SEVERITY_DEBUG => 'DEBUG',
self::SEVERITY_INFO => 'INFO',
self::SEVERITY_WARNING => 'WARNING',
self::SEVERITY_ERROR => 'ERROR',
self::SEVERITY_CRITICAL => 'CRITICAL',
default => 'UNKNOWN',
};
}
/**
* Override toArray to include security-specific fields
*/
public function toArray(): array
{
return array_merge(parent::toArray(), [
'ipAddress' => $this->ipAddress,
'deviceFingerprint' => $this->deviceFingerprint,
'userAgent' => $this->userAgent,
'requestPath' => $this->requestPath,
'requestMethod' => $this->requestMethod,
'userId' => $this->userId,
'reason' => $this->reason,
'severity' => $this->severity,
'severityLabel' => $this->getSeverityLabel(),
]);
}
}
+3 -3
View File
@@ -17,9 +17,9 @@ use KTXC\Service\TenantService;
use KTXF\Cache\BlobCacheInterface; use KTXF\Cache\BlobCacheInterface;
use KTXF\Cache\EphemeralCacheInterface; use KTXF\Cache\EphemeralCacheInterface;
use KTXF\Cache\PersistentCacheInterface; use KTXF\Cache\PersistentCacheInterface;
use KTXC\Event\DeferredEventProcessorInterface; use KTXF\Event\DeferredEventProcessorInterface;
use KTXC\Event\DeferredProcessingResult; use KTXF\Event\DeferredProcessingResult;
use KTXC\Event\EventListenerRegistry; use KTXF\Event\EventListenerRegistry;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
@@ -1,43 +0,0 @@
<?php
declare(strict_types=1);
namespace KTXT\Unit\Event;
use KTXC\Security\Event\AuthenticationFailedEvent;
use KTXC\Security\Event\SecurityEvent;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase;
final class AuthenticationFailedEventTest extends TestCase
{
#[Test]
#[TestDox('Authentication failure state is typed and complete at construction')]
public function constructsTypedState(): void
{
$event = new AuthenticationFailedEvent(
'203.0.113.10',
'device-a',
'user-a',
'Invalid credentials',
'tenant-a',
'identity-a',
'Test Agent',
'/login',
'POST',
);
self::assertSame(AuthenticationFailedEvent::class, $event->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());
}
}
+2 -2
View File
@@ -6,8 +6,8 @@ namespace KTXT\Unit\Event;
use KTXF\Event\DeliveryMode; use KTXF\Event\DeliveryMode;
use KTXF\Event\Event; use KTXF\Event\Event;
use KTXC\Event\EventDispatcher; use KTXF\Event\EventDispatcher;
use KTXC\Event\EventListenerRegistry; use KTXF\Event\EventListenerRegistry;
use KTXF\Event\FailurePolicy; use KTXF\Event\FailurePolicy;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
-41
View File
@@ -1,41 +0,0 @@
<?php
declare(strict_types=1);
namespace KTXT\Unit\Event;
use KTXF\Event\Event;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase;
final class EventTest extends TestCase
{
#[Test]
#[TestDox('Event data and context are supplied during construction')]
public function constructsCompleteEventState(): void
{
$event = new Event(
'test.event',
['nested' => ['value' => 'original']],
'tenant-a',
'identity-a',
);
$copy = $event->getData();
$copy['nested']['value'] = 'changed';
self::assertSame('original', $event->get('nested')['value']);
self::assertSame('tenant-a', $event->getTenantId());
self::assertSame('identity-a', $event->getIdentityId());
}
#[Test]
#[TestDox('Event data rejects mutable object references')]
public function rejectsMutablePayloadValues(): void
{
$this->expectException(\InvalidArgumentException::class);
new Event('test.event', ['mutable' => new \stdClass()]);
}
}
@@ -1,68 +0,0 @@
<?php
declare(strict_types=1);
namespace KTXT\Unit\Event;
use KTXC\Security\Event\SecurityEvent;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase;
final class SecurityEventTest extends TestCase
{
#[Test]
#[TestDox('Direct construction applies the event type default severity')]
public function appliesDefaultSeverityDuringConstruction(): void
{
self::assertSame(
SecurityEvent::SEVERITY_WARNING,
(new SecurityEvent(SecurityEvent::AUTH_LOGOUT))->getSeverity(),
);
self::assertSame(
SecurityEvent::SEVERITY_ERROR,
(new SecurityEvent(SecurityEvent::RATE_LIMIT_EXCEEDED))->getSeverity(),
);
self::assertSame(
SecurityEvent::SEVERITY_CRITICAL,
(new SecurityEvent(SecurityEvent::DEVICE_BLOCKED))->getSeverity(),
);
self::assertSame(
SecurityEvent::SEVERITY_INFO,
(new SecurityEvent(SecurityEvent::FIREWALL_RULE_CREATED))->getSeverity(),
);
}
#[Test]
#[TestDox('Construction can override the event type default severity')]
public function allowsSeverityOverride(): void
{
$event = new SecurityEvent(
SecurityEvent::AUTH_LOGOUT,
severity: SecurityEvent::SEVERITY_CRITICAL,
);
self::assertSame(SecurityEvent::SEVERITY_CRITICAL, $event->getSeverity());
}
#[Test]
#[TestDox('Event state exposes no mutation methods')]
public function exposesNoMutationMethods(): void
{
foreach ([
'set',
'setTenantId',
'setIdentityId',
'setIpAddress',
'setDeviceFingerprint',
'setUserAgent',
'setRequestPath',
'setRequestMethod',
'setUserId',
'setReason',
'setSeverity',
] as $method) {
self::assertFalse(method_exists(SecurityEvent::class, $method));
}
}
}
+4 -5
View File
@@ -15,10 +15,9 @@ use KTXC\Service\TenantFirewallLogService;
use KTXC\Service\TenantFirewallStatusService; use KTXC\Service\TenantFirewallStatusService;
use KTXC\Service\SystemFirewallStatusService; use KTXC\Service\SystemFirewallStatusService;
use KTXC\Service\TenantFirewallRuleService; use KTXC\Service\TenantFirewallRuleService;
use KTXC\Event\EventListenerRegistry;
use KTXC\Security\Event\AuthenticationFailedEvent;
use KTXC\Security\Event\SecurityEvent;
use KTXF\Event\DeliveryMode; use KTXF\Event\DeliveryMode;
use KTXF\Event\EventListenerRegistry;
use KTXF\Event\SecurityEvent;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@@ -39,9 +38,9 @@ final class CoreModuleTest extends TestCase
self::assertSame(['core'], array_values(array_unique(array_column($definitions, 'module')))); self::assertSame(['core'], array_values(array_unique(array_column($definitions, 'module'))));
self::assertSame( self::assertSame(
FirewallService::class, FirewallService::class,
$registry->listeners(AuthenticationFailedEvent::class, DeliveryMode::Immediate)[0]->service, $registry->listeners(SecurityEvent::AUTH_FAILURE, DeliveryMode::Immediate)[0]->service,
); );
self::assertSame([], $registry->listeners(AuthenticationFailedEvent::class, DeliveryMode::Deferred)); self::assertSame([], $registry->listeners(SecurityEvent::AUTH_FAILURE, DeliveryMode::Deferred));
foreach ([ foreach ([
SecurityEvent::RATE_LIMIT_EXCEEDED, SecurityEvent::RATE_LIMIT_EXCEEDED,
SecurityEvent::SUSPICIOUS_ACTIVITY, SecurityEvent::SUSPICIOUS_ACTIVITY,
@@ -11,7 +11,7 @@ use KTXC\Service\FirewallRuleManager;
use KTXC\Service\FirewallRuleScope; use KTXC\Service\FirewallRuleScope;
use KTXC\Stores\FirewallStore; use KTXC\Stores\FirewallStore;
use KTXF\Event\EventDispatcherInterface; use KTXF\Event\EventDispatcherInterface;
use KTXC\Security\Event\SecurityEvent; use KTXF\Event\SecurityEvent;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
+33 -41
View File
@@ -8,7 +8,6 @@ use KTXC\Context\TenantContextInterface;
use KTXC\Models\Firewall\FirewallRuleObject; use KTXC\Models\Firewall\FirewallRuleObject;
use KTXC\Models\Firewall\FirewallLogObject; use KTXC\Models\Firewall\FirewallLogObject;
use KTXC\Models\Tenant\TenantConfiguration; use KTXC\Models\Tenant\TenantConfiguration;
use KTXC\Security\Event\AuthenticationFailedEvent;
use KTXC\Service\FirewallService; use KTXC\Service\FirewallService;
use KTXC\Service\FirewallRuleCache; use KTXC\Service\FirewallRuleCache;
use KTXC\Service\FirewallRuleManager; use KTXC\Service\FirewallRuleManager;
@@ -211,14 +210,14 @@ class FirewallServiceTest extends TestCase
&& $log->getEventType() === FirewallLogObject::EVENT_RULE_MATCH; && $log->getEventType() === FirewallLogObject::EVENT_RULE_MATCH;
})) }))
->willReturnArgument(0); ->willReturnArgument(0);
$event = \KTXC\Security\Event\SecurityEvent::accessDenied( $event = \KTXF\Event\SecurityEvent::accessDenied(
'203.0.113.10', '203.0.113.10',
null, null,
'tenant-rule', 'tenant-rule',
FirewallRuleObject::SCOPE_TENANT, FirewallRuleObject::SCOPE_TENANT,
'Tenant block', 'Tenant block'
'tenant-a',
); );
$event->setTenantId('tenant-a');
$this->service->logSecurityEvent($event); $this->service->logSecurityEvent($event);
} }
@@ -235,7 +234,7 @@ class FirewallServiceTest extends TestCase
&& $log->getRuleScope() === FirewallRuleObject::SCOPE_SYSTEM; && $log->getRuleScope() === FirewallRuleObject::SCOPE_SYSTEM;
})) }))
->willReturnArgument(0); ->willReturnArgument(0);
$event = \KTXC\Security\Event\SecurityEvent::accessDenied( $event = \KTXF\Event\SecurityEvent::accessDenied(
'203.0.113.10', '203.0.113.10',
null, null,
'system-rule', 'system-rule',
@@ -253,7 +252,7 @@ class FirewallServiceTest extends TestCase
$this->store->expects($this->never())->method('createLog'); $this->store->expects($this->never())->method('createLog');
$this->service->logSecurityEvent( $this->service->logSecurityEvent(
new AuthenticationFailedEvent('203.0.113.10') \KTXF\Event\SecurityEvent::authFailure('203.0.113.10')
); );
} }
@@ -272,13 +271,13 @@ class FirewallServiceTest extends TestCase
&& $metadata['windowSeconds'] === 60; && $metadata['windowSeconds'] === 60;
})) }))
->willReturnArgument(0); ->willReturnArgument(0);
$event = \KTXC\Security\Event\SecurityEvent::rateLimitExceeded( $event = \KTXF\Event\SecurityEvent::rateLimitExceeded(
'203.0.113.10', '203.0.113.10',
101, 101,
60, 60,
'/login', '/login'
'tenant-a',
); );
$event->setTenantId('tenant-a');
$this->service->logSecurityEvent($event); $this->service->logSecurityEvent($event);
} }
@@ -297,15 +296,15 @@ class FirewallServiceTest extends TestCase
&& $log->getMetadata()['detector'] === 'payload-signature'; && $log->getMetadata()['detector'] === 'payload-signature';
})) }))
->willReturnArgument(0); ->willReturnArgument(0);
$event = \KTXC\Security\Event\SecurityEvent::create( $event = \KTXF\Event\SecurityEvent::create(
\KTXC\Security\Event\SecurityEvent::SUSPICIOUS_ACTIVITY, \KTXF\Event\SecurityEvent::SUSPICIOUS_ACTIVITY,
'203.0.113.20', '203.0.113.20',
null, null,
['detector' => 'payload-signature'], ['detector' => 'payload-signature']
tenantId: 'tenant-a',
requestPath: '/admin',
requestMethod: 'POST',
); );
$event->setTenantId('tenant-a')
->setRequestPath('/admin')
->setRequestMethod('POST');
$this->service->logSecurityEvent($event); $this->service->logSecurityEvent($event);
} }
@@ -324,15 +323,15 @@ class FirewallServiceTest extends TestCase
&& $log->getIdentityId() === 'operator'; && $log->getIdentityId() === 'operator';
})) }))
->willReturnArgument(0); ->willReturnArgument(0);
$event = new \KTXC\Security\Event\SecurityEvent( $event = new \KTXF\Event\SecurityEvent(
\KTXC\Security\Event\SecurityEvent::FIREWALL_RULE_DISABLED, \KTXF\Event\SecurityEvent::FIREWALL_RULE_DISABLED,
[ [
'ruleId' => 'rule-123', 'ruleId' => 'rule-123',
'ruleScope' => FirewallRuleObject::SCOPE_SYSTEM, 'ruleScope' => FirewallRuleObject::SCOPE_SYSTEM,
'origin' => FirewallRuleManager::ORIGIN_MANUAL, 'origin' => FirewallRuleManager::ORIGIN_MANUAL,
], ]
identityId: 'operator',
); );
$event->setIdentityId('operator');
$this->service->logSecurityEvent($event); $this->service->logSecurityEvent($event);
} }
@@ -350,12 +349,11 @@ class FirewallServiceTest extends TestCase
&& $log->getMetadata()['changeReason'] === 'Tighten controls' && $log->getMetadata()['changeReason'] === 'Tighten controls'
)) ))
->willReturnArgument(0); ->willReturnArgument(0);
$event = new \KTXC\Security\Event\SecurityEvent( $event = new \KTXF\Event\SecurityEvent(
\KTXC\Security\Event\SecurityEvent::FIREWALL_SETTINGS_UPDATED, \KTXF\Event\SecurityEvent::FIREWALL_SETTINGS_UPDATED,
['changeReason' => 'Tighten controls'], ['changeReason' => 'Tighten controls']
tenantId: 'tenant-a',
identityId: 'operator',
); );
$event->setTenantId('tenant-a')->setIdentityId('operator');
$this->service->logSecurityEvent($event); $this->service->logSecurityEvent($event);
} }
@@ -378,10 +376,8 @@ class FirewallServiceTest extends TestCase
->willReturn(4); ->willReturn(4);
$this->events->expects($this->never())->method('dispatch'); $this->events->expects($this->never())->method('dispatch');
$event = new AuthenticationFailedEvent( $event = \KTXF\Event\SecurityEvent::authFailure('203.0.113.10');
'203.0.113.10', $event->setTenantId('tenant-a');
tenantId: 'tenant-a',
);
$this->service->handleAuthFailure($event); $this->service->handleAuthFailure($event);
self::assertSame(8, $this->currentConfiguration->firewall()->maxAuthFailures()); self::assertSame(8, $this->currentConfiguration->firewall()->maxAuthFailures());
@@ -404,10 +400,8 @@ class FirewallServiceTest extends TestCase
->with('tenant-a', '203.0.113.10', 300) ->with('tenant-a', '203.0.113.10', 300)
->willReturn(0); ->willReturn(0);
$event = new AuthenticationFailedEvent( $event = \KTXF\Event\SecurityEvent::authFailure('203.0.113.10');
'203.0.113.10', $event->setTenantId('tenant-a');
tenantId: 'tenant-a',
);
$this->service->handleAuthFailure($event); $this->service->handleAuthFailure($event);
} }
@@ -455,15 +449,13 @@ class FirewallServiceTest extends TestCase
&$lifecycleOrigin &$lifecycleOrigin
): void { ): void {
$publishedTenants[] = $event->getTenantId(); $publishedTenants[] = $event->getTenantId();
if ($event->getName() === \KTXC\Security\Event\SecurityEvent::FIREWALL_RULE_CREATED) { if ($event->getName() === \KTXF\Event\SecurityEvent::FIREWALL_RULE_CREATED) {
$lifecycleOrigin = $event->get('origin'); $lifecycleOrigin = $event->get('origin');
} }
}); });
$event = new AuthenticationFailedEvent( $event = \KTXF\Event\SecurityEvent::authFailure('203.0.113.10');
'203.0.113.10', $event->setTenantId('tenant-event');
tenantId: 'tenant-event',
);
$this->service->handleAuthFailure($event); $this->service->handleAuthFailure($event);
self::assertSame(['tenant-event', 'tenant-event', 'tenant-event'], $publishedTenants); self::assertSame(['tenant-event', 'tenant-event', 'tenant-event'], $publishedTenants);
@@ -486,7 +478,7 @@ class FirewallServiceTest extends TestCase
$this->events->expects($this->never())->method('dispatch'); $this->events->expects($this->never())->method('dispatch');
$this->service->handleAuthFailure( $this->service->handleAuthFailure(
new AuthenticationFailedEvent('203.0.113.10') \KTXF\Event\SecurityEvent::authFailure('203.0.113.10')
); );
} }
@@ -500,7 +492,7 @@ class FirewallServiceTest extends TestCase
->willReturn(0); ->willReturn(0);
$this->service->handleAuthFailure( $this->service->handleAuthFailure(
new AuthenticationFailedEvent('203.0.113.10') \KTXF\Event\SecurityEvent::authFailure('203.0.113.10')
); );
} }
@@ -512,7 +504,7 @@ class FirewallServiceTest extends TestCase
$this->store->expects($this->never())->method('depositRule'); $this->store->expects($this->never())->method('depositRule');
$this->service->handleAuthFailure( $this->service->handleAuthFailure(
new AuthenticationFailedEvent('203.0.113.10') \KTXF\Event\SecurityEvent::authFailure('203.0.113.10')
); );
} }
@@ -526,7 +518,7 @@ class FirewallServiceTest extends TestCase
->method('countRecentFailures') ->method('countRecentFailures')
->with('tenant-a', '203.0.113.10', 300) ->with('tenant-a', '203.0.113.10', 300)
->willReturn(1); ->willReturn(1);
$event = new AuthenticationFailedEvent('203.0.113.10'); $event = \KTXF\Event\SecurityEvent::authFailure('203.0.113.10');
$eventId = $event->getEventId(); $eventId = $event->getEventId();
$this->service->handleAuthFailure($event); $this->service->handleAuthFailure($event);
@@ -9,7 +9,7 @@ use KTXC\Models\Tenant\TenantObject;
use KTXC\Service\FirewallSettingsService; use KTXC\Service\FirewallSettingsService;
use KTXC\Service\TenantService; use KTXC\Service\TenantService;
use KTXF\Event\EventDispatcherInterface; use KTXF\Event\EventDispatcherInterface;
use KTXC\Security\Event\SecurityEvent; use KTXF\Event\SecurityEvent;
use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;