Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 470961a955 |
@@ -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
@@ -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';
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace KTXC\Event;
|
namespace KTXF\Event;
|
||||||
|
|
||||||
interface DeferredEventProcessorInterface
|
interface DeferredEventProcessorInterface
|
||||||
{
|
{
|
||||||
+1
-1
@@ -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
@@ -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;
|
||||||
|
|
||||||
+1
-4
@@ -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
-5
@@ -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 = [];
|
||||||
@@ -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(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user