refactor(security): type firewall rule lifecycle events
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -15,6 +15,11 @@ use KTXC\Security\Event\AccessDeniedEvent;
|
|||||||
use KTXC\Security\Event\AuthenticationFailedEvent;
|
use KTXC\Security\Event\AuthenticationFailedEvent;
|
||||||
use KTXC\Security\Event\AuthenticationSucceededEvent;
|
use KTXC\Security\Event\AuthenticationSucceededEvent;
|
||||||
use KTXC\Security\Event\BruteForceDetectedEvent;
|
use KTXC\Security\Event\BruteForceDetectedEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleCreatedEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleDisabledEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleEnabledEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleExtendedEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleRemovedEvent;
|
||||||
use KTXC\Security\Event\RateLimitExceededEvent;
|
use KTXC\Security\Event\RateLimitExceededEvent;
|
||||||
use KTXC\Security\Event\SuspiciousActivityEvent;
|
use KTXC\Security\Event\SuspiciousActivityEvent;
|
||||||
use KTXC\Security\Event\SecurityEvent;
|
use KTXC\Security\Event\SecurityEvent;
|
||||||
@@ -53,11 +58,11 @@ class Module extends ModuleInstanceAbstract implements ModuleConsoleInterface, M
|
|||||||
BruteForceDetectedEvent::class,
|
BruteForceDetectedEvent::class,
|
||||||
RateLimitExceededEvent::class,
|
RateLimitExceededEvent::class,
|
||||||
SuspiciousActivityEvent::class,
|
SuspiciousActivityEvent::class,
|
||||||
SecurityEvent::FIREWALL_RULE_CREATED,
|
FirewallRuleCreatedEvent::class,
|
||||||
SecurityEvent::FIREWALL_RULE_EXTENDED,
|
FirewallRuleExtendedEvent::class,
|
||||||
SecurityEvent::FIREWALL_RULE_ENABLED,
|
FirewallRuleEnabledEvent::class,
|
||||||
SecurityEvent::FIREWALL_RULE_DISABLED,
|
FirewallRuleDisabledEvent::class,
|
||||||
SecurityEvent::FIREWALL_RULE_REMOVED,
|
FirewallRuleRemovedEvent::class,
|
||||||
SecurityEvent::FIREWALL_SETTINGS_UPDATED,
|
SecurityEvent::FIREWALL_SETTINGS_UPDATED,
|
||||||
] as $event) {
|
] as $event) {
|
||||||
$this->events->listen(
|
$this->events->listen(
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace KTXC\Security\Event;
|
||||||
|
|
||||||
|
final class FirewallRuleCreatedEvent extends FirewallRuleEvent
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace KTXC\Security\Event;
|
||||||
|
|
||||||
|
final class FirewallRuleDisabledEvent extends FirewallRuleEvent
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace KTXC\Security\Event;
|
||||||
|
|
||||||
|
final class FirewallRuleEnabledEvent extends FirewallRuleEvent
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace KTXC\Security\Event;
|
||||||
|
|
||||||
|
use KTXC\Models\Firewall\FirewallRuleObject;
|
||||||
|
use KTXF\Event\Event;
|
||||||
|
|
||||||
|
abstract class FirewallRuleEvent extends Event implements SecurityEventInterface
|
||||||
|
{
|
||||||
|
final protected function __construct(
|
||||||
|
private readonly string $ruleId,
|
||||||
|
private readonly string $ruleScope,
|
||||||
|
private readonly string $ruleType,
|
||||||
|
private readonly string $ruleAction,
|
||||||
|
private readonly string $ruleValue,
|
||||||
|
private readonly ?string $reason,
|
||||||
|
private readonly string $origin,
|
||||||
|
private readonly ?string $expiresAt,
|
||||||
|
private readonly array $details,
|
||||||
|
?string $tenantId,
|
||||||
|
?string $identityId,
|
||||||
|
) {
|
||||||
|
if ($ruleId === '') {
|
||||||
|
throw new \InvalidArgumentException('Firewall rule events require a rule ID.');
|
||||||
|
}
|
||||||
|
foreach ([
|
||||||
|
'scope' => $ruleScope,
|
||||||
|
'type' => $ruleType,
|
||||||
|
'action' => $ruleAction,
|
||||||
|
'value' => $ruleValue,
|
||||||
|
'origin' => $origin,
|
||||||
|
] as $field => $value) {
|
||||||
|
if ($value === '') {
|
||||||
|
throw new \InvalidArgumentException("Firewall rule events require a rule {$field}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ([
|
||||||
|
'scope' => $ruleScope,
|
||||||
|
'type' => $ruleType,
|
||||||
|
'action' => $ruleAction,
|
||||||
|
'value' => $ruleValue,
|
||||||
|
'origin' => $origin,
|
||||||
|
] as $field => $value) {
|
||||||
|
if ($value === '') {
|
||||||
|
throw new \InvalidArgumentException("Firewall rule events require a rule {$field}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::__construct(
|
||||||
|
static::class,
|
||||||
|
[
|
||||||
|
'ruleId' => $ruleId,
|
||||||
|
'ruleScope' => $ruleScope,
|
||||||
|
'ruleType' => $ruleType,
|
||||||
|
'ruleAction' => $ruleAction,
|
||||||
|
'ruleValue' => $ruleValue,
|
||||||
|
'reason' => $reason,
|
||||||
|
'origin' => $origin,
|
||||||
|
'expiresAt' => $expiresAt,
|
||||||
|
...$details,
|
||||||
|
],
|
||||||
|
$tenantId,
|
||||||
|
$identityId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromRule(
|
||||||
|
FirewallRuleObject $rule,
|
||||||
|
?string $actorId = null,
|
||||||
|
array $change = [],
|
||||||
|
): static {
|
||||||
|
$metadata = $rule->getMetadata() ?? [];
|
||||||
|
$details = [...$metadata, ...$change];
|
||||||
|
foreach ([
|
||||||
|
'ruleId',
|
||||||
|
'ruleScope',
|
||||||
|
'ruleType',
|
||||||
|
'ruleAction',
|
||||||
|
'ruleValue',
|
||||||
|
'reason',
|
||||||
|
'origin',
|
||||||
|
'expiresAt',
|
||||||
|
] as $reservedKey) {
|
||||||
|
unset($details[$reservedKey]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new static(
|
||||||
|
ruleId: (string) $rule->getId(),
|
||||||
|
ruleScope: (string) $rule->getScope(),
|
||||||
|
ruleType: (string) $rule->getType(),
|
||||||
|
ruleAction: (string) $rule->getAction(),
|
||||||
|
ruleValue: (string) $rule->getValue(),
|
||||||
|
reason: $rule->getReason(),
|
||||||
|
origin: (string) ($metadata['origin'] ?? 'manual'),
|
||||||
|
expiresAt: $rule->getExpiresAt()?->format(\DateTimeInterface::ATOM),
|
||||||
|
details: $details,
|
||||||
|
tenantId: $rule->getTenantId(),
|
||||||
|
identityId: $actorId ?? $rule->getCreatedBy(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRuleId(): string
|
||||||
|
{
|
||||||
|
return $this->ruleId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRuleScope(): string
|
||||||
|
{
|
||||||
|
return $this->ruleScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRuleType(): string
|
||||||
|
{
|
||||||
|
return $this->ruleType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRuleAction(): string
|
||||||
|
{
|
||||||
|
return $this->ruleAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRuleValue(): string
|
||||||
|
{
|
||||||
|
return $this->ruleValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOrigin(): string
|
||||||
|
{
|
||||||
|
return $this->origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExpiresAt(): ?string
|
||||||
|
{
|
||||||
|
return $this->expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDetails(): array
|
||||||
|
{
|
||||||
|
return $this->details;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUserId(): ?string
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getReason(): ?string
|
||||||
|
{
|
||||||
|
return $this->reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSeverity(): int
|
||||||
|
{
|
||||||
|
return SecurityEvent::SEVERITY_INFO;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace KTXC\Security\Event;
|
||||||
|
|
||||||
|
final class FirewallRuleExtendedEvent extends FirewallRuleEvent
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace KTXC\Security\Event;
|
||||||
|
|
||||||
|
final class FirewallRuleRemovedEvent extends FirewallRuleEvent
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -21,11 +21,6 @@ final class SecurityEvent extends Event implements SecurityRequestEventInterface
|
|||||||
public const IP_BLOCKED = 'security.ip.blocked';
|
public const IP_BLOCKED = 'security.ip.blocked';
|
||||||
public const IP_ALLOWED = 'security.ip.allowed';
|
public const IP_ALLOWED = 'security.ip.allowed';
|
||||||
public const DEVICE_BLOCKED = 'security.device.blocked';
|
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';
|
public const FIREWALL_SETTINGS_UPDATED = 'security.firewall.settings.updated';
|
||||||
|
|
||||||
// Severity levels
|
// Severity levels
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ declare(strict_types=1);
|
|||||||
namespace KTXC\Service;
|
namespace KTXC\Service;
|
||||||
|
|
||||||
use KTXC\Models\Firewall\FirewallRuleObject;
|
use KTXC\Models\Firewall\FirewallRuleObject;
|
||||||
|
use KTXC\Security\Event\FirewallRuleCreatedEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleDisabledEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleEnabledEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleExtendedEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleRemovedEvent;
|
||||||
use KTXC\Stores\FirewallStore;
|
use KTXC\Stores\FirewallStore;
|
||||||
use KTXF\Event\EventDispatcherInterface;
|
use KTXF\Event\EventDispatcherInterface;
|
||||||
use KTXC\Security\Event\SecurityEvent;
|
use KTXC\Security\Event\SecurityEvent;
|
||||||
@@ -282,7 +288,7 @@ final class FirewallRuleManager
|
|||||||
$this->store->depositRule($rule);
|
$this->store->depositRule($rule);
|
||||||
$this->cache->invalidate();
|
$this->cache->invalidate();
|
||||||
$this->publishLifecycleEvent(
|
$this->publishLifecycleEvent(
|
||||||
SecurityEvent::FIREWALL_RULE_DISABLED,
|
FirewallRuleDisabledEvent::class,
|
||||||
$rule,
|
$rule,
|
||||||
$actorId,
|
$actorId,
|
||||||
['changeReason' => $reason, 'changeOrigin' => self::ORIGIN_MANUAL]
|
['changeReason' => $reason, 'changeOrigin' => self::ORIGIN_MANUAL]
|
||||||
@@ -321,7 +327,7 @@ final class FirewallRuleManager
|
|||||||
$this->store->depositRule($rule);
|
$this->store->depositRule($rule);
|
||||||
$this->cache->invalidate();
|
$this->cache->invalidate();
|
||||||
$this->publishLifecycleEvent(
|
$this->publishLifecycleEvent(
|
||||||
SecurityEvent::FIREWALL_RULE_ENABLED,
|
FirewallRuleEnabledEvent::class,
|
||||||
$rule,
|
$rule,
|
||||||
$actorId,
|
$actorId,
|
||||||
['changeReason' => $reason, 'changeOrigin' => self::ORIGIN_MANUAL]
|
['changeReason' => $reason, 'changeOrigin' => self::ORIGIN_MANUAL]
|
||||||
@@ -365,7 +371,7 @@ final class FirewallRuleManager
|
|||||||
$this->store->depositRule($rule);
|
$this->store->depositRule($rule);
|
||||||
$this->cache->invalidate();
|
$this->cache->invalidate();
|
||||||
$this->publishLifecycleEvent(
|
$this->publishLifecycleEvent(
|
||||||
SecurityEvent::FIREWALL_RULE_EXTENDED,
|
FirewallRuleExtendedEvent::class,
|
||||||
$rule,
|
$rule,
|
||||||
$actorId,
|
$actorId,
|
||||||
[
|
[
|
||||||
@@ -392,7 +398,7 @@ final class FirewallRuleManager
|
|||||||
$this->store->destroyRule($rule);
|
$this->store->destroyRule($rule);
|
||||||
$this->cache->invalidate();
|
$this->cache->invalidate();
|
||||||
$this->publishLifecycleEvent(
|
$this->publishLifecycleEvent(
|
||||||
SecurityEvent::FIREWALL_RULE_REMOVED,
|
FirewallRuleRemovedEvent::class,
|
||||||
$rule,
|
$rule,
|
||||||
$actorId,
|
$actorId,
|
||||||
['changeReason' => $reason, 'changeOrigin' => self::ORIGIN_MANUAL]
|
['changeReason' => $reason, 'changeOrigin' => self::ORIGIN_MANUAL]
|
||||||
@@ -447,9 +453,10 @@ final class FirewallRuleManager
|
|||||||
}
|
}
|
||||||
$rule->setMetadata($metadata);
|
$rule->setMetadata($metadata);
|
||||||
|
|
||||||
$this->store->depositRule($rule);
|
$rule = $this->store->depositRule($rule)
|
||||||
|
?? throw new \RuntimeException('Failed to persist firewall rule.');
|
||||||
$this->cache->invalidate();
|
$this->cache->invalidate();
|
||||||
$this->publishLifecycleEvent(SecurityEvent::FIREWALL_RULE_CREATED, $rule);
|
$this->publishLifecycleEvent(FirewallRuleCreatedEvent::class, $rule);
|
||||||
|
|
||||||
return $rule;
|
return $rule;
|
||||||
}
|
}
|
||||||
@@ -486,7 +493,7 @@ final class FirewallRuleManager
|
|||||||
|
|
||||||
$this->store->depositRule($rule);
|
$this->store->depositRule($rule);
|
||||||
$this->cache->invalidate();
|
$this->cache->invalidate();
|
||||||
$this->publishLifecycleEvent(SecurityEvent::FIREWALL_RULE_EXTENDED, $rule);
|
$this->publishLifecycleEvent(FirewallRuleExtendedEvent::class, $rule);
|
||||||
|
|
||||||
return $rule;
|
return $rule;
|
||||||
}
|
}
|
||||||
@@ -514,30 +521,17 @@ final class FirewallRuleManager
|
|||||||
$this->events->dispatch($event);
|
$this->events->dispatch($event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param class-string<FirewallRuleEvent> $eventClass
|
||||||
|
*/
|
||||||
private function publishLifecycleEvent(
|
private function publishLifecycleEvent(
|
||||||
string $name,
|
string $eventClass,
|
||||||
FirewallRuleObject $rule,
|
FirewallRuleObject $rule,
|
||||||
?string $actorId = null,
|
?string $actorId = null,
|
||||||
array $change = []
|
array $change = []
|
||||||
): void
|
): void
|
||||||
{
|
{
|
||||||
$event = new SecurityEvent(
|
$event = $eventClass::fromRule($rule, $actorId, $change);
|
||||||
$name,
|
|
||||||
[
|
|
||||||
'ruleId' => $rule->getId(),
|
|
||||||
'ruleScope' => $rule->getScope(),
|
|
||||||
'ruleType' => $rule->getType(),
|
|
||||||
'ruleAction' => $rule->getAction(),
|
|
||||||
'ruleValue' => $rule->getValue(),
|
|
||||||
'reason' => $rule->getReason(),
|
|
||||||
'origin' => $rule->getMetadata()['origin'] ?? self::ORIGIN_MANUAL,
|
|
||||||
'expiresAt' => $rule->getExpiresAt()?->format(\DateTimeInterface::ATOM),
|
|
||||||
...($rule->getMetadata() ?? []),
|
|
||||||
...$change,
|
|
||||||
],
|
|
||||||
tenantId: $rule->getTenantId(),
|
|
||||||
identityId: $actorId ?? $rule->getCreatedBy(),
|
|
||||||
);
|
|
||||||
$this->events->dispatch($event);
|
$this->events->dispatch($event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ use KTXC\Security\Event\AccessDeniedEvent;
|
|||||||
use KTXC\Security\Event\AuthenticationFailedEvent;
|
use KTXC\Security\Event\AuthenticationFailedEvent;
|
||||||
use KTXC\Security\Event\AuthenticationSucceededEvent;
|
use KTXC\Security\Event\AuthenticationSucceededEvent;
|
||||||
use KTXC\Security\Event\BruteForceDetectedEvent;
|
use KTXC\Security\Event\BruteForceDetectedEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleCreatedEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleDisabledEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleEnabledEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleExtendedEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleRemovedEvent;
|
||||||
use KTXC\Security\Event\RateLimitExceededEvent;
|
use KTXC\Security\Event\RateLimitExceededEvent;
|
||||||
use KTXC\Security\Event\SuspiciousActivityEvent;
|
use KTXC\Security\Event\SuspiciousActivityEvent;
|
||||||
use KTXC\Security\Event\SecurityEvent;
|
use KTXC\Security\Event\SecurityEvent;
|
||||||
@@ -289,11 +294,11 @@ class FirewallService
|
|||||||
RateLimitExceededEvent::class => FirewallLogObject::EVENT_RATE_LIMIT,
|
RateLimitExceededEvent::class => FirewallLogObject::EVENT_RATE_LIMIT,
|
||||||
AccessDeniedEvent::class => FirewallLogObject::EVENT_RULE_MATCH,
|
AccessDeniedEvent::class => FirewallLogObject::EVENT_RULE_MATCH,
|
||||||
SuspiciousActivityEvent::class => FirewallLogObject::EVENT_SUSPICIOUS,
|
SuspiciousActivityEvent::class => FirewallLogObject::EVENT_SUSPICIOUS,
|
||||||
SecurityEvent::FIREWALL_RULE_CREATED => FirewallLogObject::EVENT_RULE_CREATED,
|
FirewallRuleCreatedEvent::class => FirewallLogObject::EVENT_RULE_CREATED,
|
||||||
SecurityEvent::FIREWALL_RULE_EXTENDED => FirewallLogObject::EVENT_RULE_EXTENDED,
|
FirewallRuleExtendedEvent::class => FirewallLogObject::EVENT_RULE_EXTENDED,
|
||||||
SecurityEvent::FIREWALL_RULE_ENABLED => FirewallLogObject::EVENT_RULE_ENABLED,
|
FirewallRuleEnabledEvent::class => FirewallLogObject::EVENT_RULE_ENABLED,
|
||||||
SecurityEvent::FIREWALL_RULE_DISABLED => FirewallLogObject::EVENT_RULE_DISABLED,
|
FirewallRuleDisabledEvent::class => FirewallLogObject::EVENT_RULE_DISABLED,
|
||||||
SecurityEvent::FIREWALL_RULE_REMOVED => FirewallLogObject::EVENT_RULE_REMOVED,
|
FirewallRuleRemovedEvent::class => FirewallLogObject::EVENT_RULE_REMOVED,
|
||||||
SecurityEvent::FIREWALL_SETTINGS_UPDATED => FirewallLogObject::EVENT_SETTINGS_UPDATED,
|
SecurityEvent::FIREWALL_SETTINGS_UPDATED => FirewallLogObject::EVENT_SETTINGS_UPDATED,
|
||||||
default => FirewallLogObject::EVENT_ACCESS_CHECK,
|
default => FirewallLogObject::EVENT_ACCESS_CHECK,
|
||||||
};
|
};
|
||||||
@@ -307,11 +312,11 @@ class FirewallService
|
|||||||
return match ($event->getName()) {
|
return match ($event->getName()) {
|
||||||
AuthenticationSucceededEvent::class,
|
AuthenticationSucceededEvent::class,
|
||||||
SecurityEvent::ACCESS_GRANTED => FirewallLogObject::RESULT_ALLOWED,
|
SecurityEvent::ACCESS_GRANTED => FirewallLogObject::RESULT_ALLOWED,
|
||||||
SecurityEvent::FIREWALL_RULE_CREATED,
|
FirewallRuleCreatedEvent::class,
|
||||||
SecurityEvent::FIREWALL_RULE_EXTENDED,
|
FirewallRuleExtendedEvent::class,
|
||||||
SecurityEvent::FIREWALL_RULE_ENABLED,
|
FirewallRuleEnabledEvent::class,
|
||||||
SecurityEvent::FIREWALL_RULE_DISABLED,
|
FirewallRuleDisabledEvent::class,
|
||||||
SecurityEvent::FIREWALL_RULE_REMOVED,
|
FirewallRuleRemovedEvent::class,
|
||||||
SecurityEvent::FIREWALL_SETTINGS_UPDATED => FirewallLogObject::RESULT_RECORDED,
|
SecurityEvent::FIREWALL_SETTINGS_UPDATED => FirewallLogObject::RESULT_RECORDED,
|
||||||
default => FirewallLogObject::RESULT_BLOCKED,
|
default => FirewallLogObject::RESULT_BLOCKED,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace KTXT\Unit\Event;
|
||||||
|
|
||||||
|
use KTXC\Models\Firewall\FirewallRuleObject;
|
||||||
|
use KTXC\Security\Event\FirewallRuleCreatedEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleDisabledEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleEnabledEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleExtendedEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleRemovedEvent;
|
||||||
|
use KTXC\Security\Event\SecurityEvent;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use PHPUnit\Framework\Attributes\TestDox;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class FirewallRuleEventTest extends TestCase
|
||||||
|
{
|
||||||
|
#[Test]
|
||||||
|
#[TestDox('Firewall rule events capture immutable rule snapshots')]
|
||||||
|
public function capturesRuleSnapshot(): void
|
||||||
|
{
|
||||||
|
$expiresAt = new \DateTimeImmutable('+5 minutes');
|
||||||
|
$rule = (new FirewallRuleObject())
|
||||||
|
->setId('rule-a')
|
||||||
|
->setScope(FirewallRuleObject::SCOPE_TENANT)
|
||||||
|
->setTenantId('tenant-a')
|
||||||
|
->setType(FirewallRuleObject::TYPE_IP)
|
||||||
|
->setAction(FirewallRuleObject::ACTION_BLOCK)
|
||||||
|
->setValue('203.0.113.10')
|
||||||
|
->setReason('Repeated abuse')
|
||||||
|
->setCreatedBy('creator-a')
|
||||||
|
->setExpiresAt($expiresAt)
|
||||||
|
->setMetadata(['origin' => 'automatic', 'failureCount' => 5]);
|
||||||
|
|
||||||
|
$event = FirewallRuleExtendedEvent::fromRule(
|
||||||
|
$rule,
|
||||||
|
'operator-a',
|
||||||
|
['changeReason' => 'Continue monitoring'],
|
||||||
|
);
|
||||||
|
$rule->setReason('Mutated after publication');
|
||||||
|
|
||||||
|
self::assertSame(FirewallRuleExtendedEvent::class, $event->getName());
|
||||||
|
self::assertSame('rule-a', $event->getRuleId());
|
||||||
|
self::assertSame(FirewallRuleObject::SCOPE_TENANT, $event->getRuleScope());
|
||||||
|
self::assertSame(FirewallRuleObject::TYPE_IP, $event->getRuleType());
|
||||||
|
self::assertSame(FirewallRuleObject::ACTION_BLOCK, $event->getRuleAction());
|
||||||
|
self::assertSame('203.0.113.10', $event->getRuleValue());
|
||||||
|
self::assertSame('Repeated abuse', $event->getReason());
|
||||||
|
self::assertSame('automatic', $event->getOrigin());
|
||||||
|
self::assertSame($expiresAt->format(\DateTimeInterface::ATOM), $event->getExpiresAt());
|
||||||
|
self::assertSame('tenant-a', $event->getTenantId());
|
||||||
|
self::assertSame('operator-a', $event->getIdentityId());
|
||||||
|
self::assertSame(5, $event->get('failureCount'));
|
||||||
|
self::assertSame('Continue monitoring', $event->get('changeReason'));
|
||||||
|
self::assertSame(SecurityEvent::SEVERITY_INFO, $event->getSeverity());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
#[TestDox('Each firewall rule lifecycle operation has a dedicated event key')]
|
||||||
|
public function exposesDedicatedLifecycleKeys(): void
|
||||||
|
{
|
||||||
|
$rule = (new FirewallRuleObject())
|
||||||
|
->setId('rule-a')
|
||||||
|
->setScope(FirewallRuleObject::SCOPE_SYSTEM)
|
||||||
|
->setType(FirewallRuleObject::TYPE_IP)
|
||||||
|
->setAction(FirewallRuleObject::ACTION_BLOCK)
|
||||||
|
->setValue('203.0.113.10');
|
||||||
|
|
||||||
|
foreach ([
|
||||||
|
FirewallRuleCreatedEvent::class,
|
||||||
|
FirewallRuleExtendedEvent::class,
|
||||||
|
FirewallRuleEnabledEvent::class,
|
||||||
|
FirewallRuleDisabledEvent::class,
|
||||||
|
FirewallRuleRemovedEvent::class,
|
||||||
|
] as $eventClass) {
|
||||||
|
self::assertSame($eventClass, $eventClass::fromRule($rule)->getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ final class SecurityEventTest extends TestCase
|
|||||||
);
|
);
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
SecurityEvent::SEVERITY_INFO,
|
SecurityEvent::SEVERITY_INFO,
|
||||||
(new SecurityEvent(SecurityEvent::FIREWALL_RULE_CREATED))->getSeverity(),
|
(new SecurityEvent(SecurityEvent::IP_ALLOWED))->getSeverity(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ use KTXC\Security\Event\AccessDeniedEvent;
|
|||||||
use KTXC\Security\Event\AuthenticationFailedEvent;
|
use KTXC\Security\Event\AuthenticationFailedEvent;
|
||||||
use KTXC\Security\Event\AuthenticationSucceededEvent;
|
use KTXC\Security\Event\AuthenticationSucceededEvent;
|
||||||
use KTXC\Security\Event\BruteForceDetectedEvent;
|
use KTXC\Security\Event\BruteForceDetectedEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleCreatedEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleDisabledEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleEnabledEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleExtendedEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleRemovedEvent;
|
||||||
use KTXC\Security\Event\RateLimitExceededEvent;
|
use KTXC\Security\Event\RateLimitExceededEvent;
|
||||||
use KTXC\Security\Event\SuspiciousActivityEvent;
|
use KTXC\Security\Event\SuspiciousActivityEvent;
|
||||||
use KTXC\Security\Event\SecurityEvent;
|
use KTXC\Security\Event\SecurityEvent;
|
||||||
@@ -53,11 +58,11 @@ final class CoreModuleTest extends TestCase
|
|||||||
BruteForceDetectedEvent::class,
|
BruteForceDetectedEvent::class,
|
||||||
RateLimitExceededEvent::class,
|
RateLimitExceededEvent::class,
|
||||||
SuspiciousActivityEvent::class,
|
SuspiciousActivityEvent::class,
|
||||||
SecurityEvent::FIREWALL_RULE_CREATED,
|
FirewallRuleCreatedEvent::class,
|
||||||
SecurityEvent::FIREWALL_RULE_EXTENDED,
|
FirewallRuleExtendedEvent::class,
|
||||||
SecurityEvent::FIREWALL_RULE_DISABLED,
|
FirewallRuleDisabledEvent::class,
|
||||||
SecurityEvent::FIREWALL_RULE_ENABLED,
|
FirewallRuleEnabledEvent::class,
|
||||||
SecurityEvent::FIREWALL_RULE_REMOVED,
|
FirewallRuleRemovedEvent::class,
|
||||||
SecurityEvent::FIREWALL_SETTINGS_UPDATED,
|
SecurityEvent::FIREWALL_SETTINGS_UPDATED,
|
||||||
] as $event) {
|
] as $event) {
|
||||||
$listeners = $registry->listeners($event, DeliveryMode::Deferred);
|
$listeners = $registry->listeners($event, DeliveryMode::Deferred);
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ 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 KTXC\Security\Event\FirewallRuleCreatedEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleDisabledEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleEnabledEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleExtendedEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleRemovedEvent;
|
||||||
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;
|
||||||
@@ -41,7 +45,10 @@ class FirewallRuleManagerTest extends TestCase
|
|||||||
$this->store->method('findExactIpRule')->willReturn(null);
|
$this->store->method('findExactIpRule')->willReturn(null);
|
||||||
$this->store->expects($this->exactly(2))
|
$this->store->expects($this->exactly(2))
|
||||||
->method('depositRule')
|
->method('depositRule')
|
||||||
->willReturnArgument(0);
|
->willReturnCallback(static function (FirewallRuleObject $rule): FirewallRuleObject {
|
||||||
|
static $sequence = 0;
|
||||||
|
return $rule->setId('rule-' . ++$sequence);
|
||||||
|
});
|
||||||
|
|
||||||
$tenant = $this->manager->blockIp(
|
$tenant = $this->manager->blockIp(
|
||||||
FirewallRuleScope::tenant('tenant-a'), '203.0.113.10', null, 'admin-a'
|
FirewallRuleScope::tenant('tenant-a'), '203.0.113.10', null, 'admin-a'
|
||||||
@@ -135,7 +142,11 @@ class FirewallRuleManagerTest extends TestCase
|
|||||||
public function testConfirmedCurrentIpBlock(): void
|
public function testConfirmedCurrentIpBlock(): void
|
||||||
{
|
{
|
||||||
$this->store->method('findExactIpRule')->willReturn(null);
|
$this->store->method('findExactIpRule')->willReturn(null);
|
||||||
$this->store->expects(self::once())->method('depositRule')->willReturnArgument(0);
|
$this->store->expects(self::once())
|
||||||
|
->method('depositRule')
|
||||||
|
->willReturnCallback(static fn(FirewallRuleObject $rule): FirewallRuleObject =>
|
||||||
|
$rule->setId('rule-confirmed')
|
||||||
|
);
|
||||||
|
|
||||||
$rule = $this->manager->createManualRule(
|
$rule = $this->manager->createManualRule(
|
||||||
FirewallRuleScope::tenant('tenant-a'),
|
FirewallRuleScope::tenant('tenant-a'),
|
||||||
@@ -249,7 +260,9 @@ class FirewallRuleManagerTest extends TestCase
|
|||||||
$cache = new FirewallRuleCache($this->store);
|
$cache = new FirewallRuleCache($this->store);
|
||||||
$manager = new FirewallRuleManager($this->store, $cache, $this->events);
|
$manager = new FirewallRuleManager($this->store, $cache, $this->events);
|
||||||
$this->store->method('findExactIpRule')->willReturn(null);
|
$this->store->method('findExactIpRule')->willReturn(null);
|
||||||
$this->store->method('depositRule')->willReturnArgument(0);
|
$this->store->method('depositRule')->willReturnCallback(
|
||||||
|
static fn(FirewallRuleObject $rule): FirewallRuleObject => $rule->setId('rule-cache')
|
||||||
|
);
|
||||||
|
|
||||||
self::assertSame([], $cache->tenant('tenant-a'));
|
self::assertSame([], $cache->tenant('tenant-a'));
|
||||||
$manager->blockIp(FirewallRuleScope::tenant('tenant-a'), '203.0.113.10', null, 'admin');
|
$manager->blockIp(FirewallRuleScope::tenant('tenant-a'), '203.0.113.10', null, 'admin');
|
||||||
@@ -280,7 +293,7 @@ class FirewallRuleManagerTest extends TestCase
|
|||||||
300
|
300
|
||||||
);
|
);
|
||||||
|
|
||||||
$audit = $events[SecurityEvent::FIREWALL_RULE_CREATED];
|
$audit = $events[FirewallRuleCreatedEvent::class];
|
||||||
self::assertSame('rule-123', $audit->get('ruleId'));
|
self::assertSame('rule-123', $audit->get('ruleId'));
|
||||||
self::assertSame(FirewallRuleObject::SCOPE_TENANT, $audit->get('ruleScope'));
|
self::assertSame(FirewallRuleObject::SCOPE_TENANT, $audit->get('ruleScope'));
|
||||||
self::assertSame(FirewallRuleObject::TYPE_IP, $audit->get('ruleType'));
|
self::assertSame(FirewallRuleObject::TYPE_IP, $audit->get('ruleType'));
|
||||||
@@ -329,10 +342,10 @@ class FirewallRuleManagerTest extends TestCase
|
|||||||
self::assertGreaterThan($originalExpiry, $extended->getExpiresAt());
|
self::assertGreaterThan($originalExpiry, $extended->getExpiresAt());
|
||||||
self::assertCount(1, $extended->getMetadata()['extensions']);
|
self::assertCount(1, $extended->getMetadata()['extensions']);
|
||||||
self::assertSame([
|
self::assertSame([
|
||||||
SecurityEvent::FIREWALL_RULE_DISABLED,
|
FirewallRuleDisabledEvent::class,
|
||||||
SecurityEvent::FIREWALL_RULE_ENABLED,
|
FirewallRuleEnabledEvent::class,
|
||||||
SecurityEvent::FIREWALL_RULE_EXTENDED,
|
FirewallRuleExtendedEvent::class,
|
||||||
SecurityEvent::FIREWALL_RULE_REMOVED,
|
FirewallRuleRemovedEvent::class,
|
||||||
], array_map(static fn($event): string => $event->getName(), $audits));
|
], array_map(static fn($event): string => $event->getName(), $audits));
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
['Investigation', 'Threat confirmed', 'Continue monitoring', 'Case closed'],
|
['Investigation', 'Threat confirmed', 'Continue monitoring', 'Case closed'],
|
||||||
@@ -401,7 +414,7 @@ class FirewallRuleManagerTest extends TestCase
|
|||||||
$this->events->expects(self::once())
|
$this->events->expects(self::once())
|
||||||
->method('dispatch')
|
->method('dispatch')
|
||||||
->with(self::callback(static fn(\KTXF\Event\Event $event): bool =>
|
->with(self::callback(static fn(\KTXF\Event\Event $event): bool =>
|
||||||
$event->getName() === SecurityEvent::FIREWALL_RULE_EXTENDED
|
$event instanceof FirewallRuleExtendedEvent
|
||||||
&& $event->get('lastFailureCount') === 8
|
&& $event->get('lastFailureCount') === 8
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,9 @@ class FirewallRuleServicesTest extends TestCase
|
|||||||
->with(self::callback(static fn(FirewallRuleObject $rule): bool =>
|
->with(self::callback(static fn(FirewallRuleObject $rule): bool =>
|
||||||
$rule->isTenantScoped() && $rule->getTenantId() === 'tenant-a'
|
$rule->isTenantScoped() && $rule->getTenantId() === 'tenant-a'
|
||||||
))
|
))
|
||||||
->willReturnArgument(0);
|
->willReturnCallback(static fn(FirewallRuleObject $rule): FirewallRuleObject =>
|
||||||
|
$rule->setId('tenant-rule')
|
||||||
|
);
|
||||||
|
|
||||||
$this->tenantService()->blockIp('203.0.113.10');
|
$this->tenantService()->blockIp('203.0.113.10');
|
||||||
}
|
}
|
||||||
@@ -117,7 +119,9 @@ class FirewallRuleServicesTest extends TestCase
|
|||||||
->with(self::callback(static fn(FirewallRuleObject $rule): bool =>
|
->with(self::callback(static fn(FirewallRuleObject $rule): bool =>
|
||||||
$rule->isSystemScoped() && $rule->getTenantId() === null
|
$rule->isSystemScoped() && $rule->getTenantId() === null
|
||||||
))
|
))
|
||||||
->willReturnArgument(0);
|
->willReturnCallback(static fn(FirewallRuleObject $rule): FirewallRuleObject =>
|
||||||
|
$rule->setId('system-rule')
|
||||||
|
);
|
||||||
|
|
||||||
$this->systemService()->blockIp('203.0.113.10');
|
$this->systemService()->blockIp('203.0.113.10');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ use KTXC\Security\Event\AccessDeniedEvent;
|
|||||||
use KTXC\Security\Event\AuthenticationFailedEvent;
|
use KTXC\Security\Event\AuthenticationFailedEvent;
|
||||||
use KTXC\Security\Event\AuthenticationSucceededEvent;
|
use KTXC\Security\Event\AuthenticationSucceededEvent;
|
||||||
use KTXC\Security\Event\BruteForceDetectedEvent;
|
use KTXC\Security\Event\BruteForceDetectedEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleCreatedEvent;
|
||||||
|
use KTXC\Security\Event\FirewallRuleDisabledEvent;
|
||||||
use KTXC\Security\Event\RateLimitExceededEvent;
|
use KTXC\Security\Event\RateLimitExceededEvent;
|
||||||
use KTXC\Security\Event\SuspiciousActivityEvent;
|
use KTXC\Security\Event\SuspiciousActivityEvent;
|
||||||
use KTXC\Service\FirewallService;
|
use KTXC\Service\FirewallService;
|
||||||
@@ -367,14 +369,15 @@ class FirewallServiceTest extends TestCase
|
|||||||
&& $log->getIdentityId() === 'operator';
|
&& $log->getIdentityId() === 'operator';
|
||||||
}))
|
}))
|
||||||
->willReturnArgument(0);
|
->willReturnArgument(0);
|
||||||
$event = new \KTXC\Security\Event\SecurityEvent(
|
$event = FirewallRuleDisabledEvent::fromRule(
|
||||||
\KTXC\Security\Event\SecurityEvent::FIREWALL_RULE_DISABLED,
|
(new FirewallRuleObject())
|
||||||
[
|
->setId('rule-123')
|
||||||
'ruleId' => 'rule-123',
|
->setScope(FirewallRuleObject::SCOPE_SYSTEM)
|
||||||
'ruleScope' => FirewallRuleObject::SCOPE_SYSTEM,
|
->setType(FirewallRuleObject::TYPE_IP)
|
||||||
'origin' => FirewallRuleManager::ORIGIN_MANUAL,
|
->setAction(FirewallRuleObject::ACTION_BLOCK)
|
||||||
],
|
->setValue('203.0.113.10')
|
||||||
identityId: 'operator',
|
->setMetadata(['origin' => FirewallRuleManager::ORIGIN_MANUAL]),
|
||||||
|
'operator',
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->service->logSecurityEvent($event);
|
$this->service->logSecurityEvent($event);
|
||||||
@@ -521,7 +524,9 @@ class FirewallServiceTest extends TestCase
|
|||||||
&& $metadata['lastFailureCount'] === 5
|
&& $metadata['lastFailureCount'] === 5
|
||||||
&& $metadata['blockDurationSeconds'] === 3600;
|
&& $metadata['blockDurationSeconds'] === 3600;
|
||||||
}))
|
}))
|
||||||
->willReturnArgument(0);
|
->willReturnCallback(static fn(FirewallRuleObject $rule): FirewallRuleObject =>
|
||||||
|
$rule->setId('automatic-rule')
|
||||||
|
);
|
||||||
|
|
||||||
$publishedTenants = [];
|
$publishedTenants = [];
|
||||||
$lifecycleOrigin = null;
|
$lifecycleOrigin = null;
|
||||||
@@ -537,7 +542,7 @@ class FirewallServiceTest extends TestCase
|
|||||||
if ($event instanceof BruteForceDetectedEvent) {
|
if ($event instanceof BruteForceDetectedEvent) {
|
||||||
$bruteForceEvent = $event;
|
$bruteForceEvent = $event;
|
||||||
}
|
}
|
||||||
if ($event->getName() === \KTXC\Security\Event\SecurityEvent::FIREWALL_RULE_CREATED) {
|
if ($event instanceof FirewallRuleCreatedEvent) {
|
||||||
$lifecycleOrigin = $event->get('origin');
|
$lifecycleOrigin = $event->get('origin');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user