feat(firewall): audit rule lifecycle changes

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-07-30 22:57:25 -04:00
parent 5ada4c0c45
commit 7aa8a27b1b
11 changed files with 218 additions and 25 deletions
@@ -13,6 +13,7 @@ class FirewallLogObject implements \JsonSerializable, JsonDeserializable
{
public const RESULT_ALLOWED = 'allowed';
public const RESULT_BLOCKED = 'blocked';
public const RESULT_RECORDED = 'recorded';
public const EVENT_AUTH_FAILURE = 'auth_failure';
public const EVENT_RATE_LIMIT = 'rate_limit';
@@ -20,6 +21,9 @@ class FirewallLogObject implements \JsonSerializable, JsonDeserializable
public const EVENT_SUSPICIOUS = 'suspicious';
public const EVENT_RULE_MATCH = 'rule_match';
public const EVENT_ACCESS_CHECK = 'access_check';
public const EVENT_RULE_CREATED = 'rule_created';
public const EVENT_RULE_DISABLED = 'rule_disabled';
public const EVENT_RULE_REMOVED = 'rule_removed';
private ?string $id = null;
private ?string $tenantId = null;
+3
View File
@@ -41,6 +41,9 @@ class Module extends ModuleInstanceAbstract implements ModuleConsoleInterface, M
SecurityEvent::BRUTE_FORCE_DETECTED,
SecurityEvent::RATE_LIMIT_EXCEEDED,
SecurityEvent::SUSPICIOUS_ACTIVITY,
SecurityEvent::FIREWALL_RULE_CREATED,
SecurityEvent::FIREWALL_RULE_DISABLED,
SecurityEvent::FIREWALL_RULE_REMOVED,
] as $event) {
$this->events->listen(
'core',
+54 -11
View File
@@ -11,6 +11,9 @@ use KTXF\Event\SecurityEvent;
final class FirewallRuleManager
{
public const ORIGIN_MANUAL = 'manual';
public const ORIGIN_AUTOMATIC = 'automatic';
public function __construct(
private readonly FirewallStore $store,
private readonly FirewallRuleCache $cache,
@@ -30,7 +33,8 @@ final class FirewallRuleManager
string $ipAddress,
?string $reason,
?string $createdBy,
?int $durationSeconds = null
?int $durationSeconds = null,
string $origin = self::ORIGIN_MANUAL
): FirewallRuleObject {
$ipAddress = FirewallRuleValidator::ipAddress($ipAddress);
FirewallRuleValidator::duration($durationSeconds);
@@ -52,7 +56,8 @@ final class FirewallRuleManager
$ipAddress,
$reason ?? 'Blocked by administrator',
$createdBy,
$durationSeconds
$durationSeconds,
$origin
);
$this->publishIpEvent(SecurityEvent::IP_BLOCKED, $scope, $ipAddress, $reason);
@@ -63,7 +68,8 @@ final class FirewallRuleManager
FirewallRuleScope $scope,
string $ipAddress,
?string $reason,
?string $createdBy
?string $createdBy,
string $origin = self::ORIGIN_MANUAL
): FirewallRuleObject {
$ipAddress = FirewallRuleValidator::ipAddress($ipAddress);
$rule = $this->create(
@@ -72,7 +78,9 @@ final class FirewallRuleManager
FirewallRuleObject::ACTION_ALLOW,
$ipAddress,
$reason ?? 'Allowed by administrator',
$createdBy
$createdBy,
null,
$origin
);
$this->publishIpEvent(SecurityEvent::IP_ALLOWED, $scope, $ipAddress, $reason);
@@ -83,7 +91,8 @@ final class FirewallRuleManager
FirewallRuleScope $scope,
string $cidr,
?string $reason,
?string $createdBy
?string $createdBy,
string $origin = self::ORIGIN_MANUAL
): FirewallRuleObject {
return $this->create(
$scope,
@@ -91,7 +100,9 @@ final class FirewallRuleManager
FirewallRuleObject::ACTION_BLOCK,
FirewallRuleValidator::cidr($cidr),
$reason ?? 'Range blocked by administrator',
$createdBy
$createdBy,
null,
$origin
);
}
@@ -100,7 +111,8 @@ final class FirewallRuleManager
string $fingerprint,
?string $reason,
?string $createdBy,
?int $durationSeconds = null
?int $durationSeconds = null,
string $origin = self::ORIGIN_MANUAL
): FirewallRuleObject {
FirewallRuleValidator::duration($durationSeconds);
$fingerprint = FirewallRuleValidator::deviceFingerprint($fingerprint);
@@ -111,7 +123,8 @@ final class FirewallRuleManager
$fingerprint,
$reason ?? 'Device blocked by administrator',
$createdBy,
$durationSeconds
$durationSeconds,
$origin
);
$event = new SecurityEvent(SecurityEvent::DEVICE_BLOCKED, ['device' => $fingerprint, 'reason' => $reason]);
@@ -121,7 +134,7 @@ final class FirewallRuleManager
return $rule;
}
public function disable(FirewallRuleScope $scope, string $ruleId): bool
public function disable(FirewallRuleScope $scope, string $ruleId, ?string $actorId = null): bool
{
$rule = $this->ownedRule($scope, $ruleId);
if (!$rule) {
@@ -131,11 +144,12 @@ final class FirewallRuleManager
$rule->setEnabled(false);
$this->store->depositRule($rule);
$this->cache->invalidate();
$this->publishLifecycleEvent(SecurityEvent::FIREWALL_RULE_DISABLED, $rule, $actorId);
return true;
}
public function remove(FirewallRuleScope $scope, string $ruleId): bool
public function remove(FirewallRuleScope $scope, string $ruleId, ?string $actorId = null): bool
{
$rule = $this->ownedRule($scope, $ruleId);
if (!$rule) {
@@ -144,6 +158,7 @@ final class FirewallRuleManager
$this->store->destroyRule($rule);
$this->cache->invalidate();
$this->publishLifecycleEvent(SecurityEvent::FIREWALL_RULE_REMOVED, $rule, $actorId);
return true;
}
@@ -155,8 +170,13 @@ final class FirewallRuleManager
string $value,
string $reason,
?string $createdBy,
?int $durationSeconds = null
?int $durationSeconds = null,
string $origin = self::ORIGIN_MANUAL
): FirewallRuleObject {
if (!in_array($origin, [self::ORIGIN_MANUAL, self::ORIGIN_AUTOMATIC], true)) {
throw new \InvalidArgumentException("Invalid firewall rule origin: {$origin}");
}
$rule = (new FirewallRuleObject())
->setScope($scope->scope)
->setTenantId($scope->tenantId)
@@ -166,6 +186,7 @@ final class FirewallRuleManager
->setReason($reason)
->setCreatedBy($createdBy)
->setCreatedAt(new \DateTimeImmutable())
->setMetadata(['origin' => $origin])
->setEnabled(true);
if ($durationSeconds !== null) {
@@ -174,6 +195,7 @@ final class FirewallRuleManager
$this->store->depositRule($rule);
$this->cache->invalidate();
$this->publishLifecycleEvent(SecurityEvent::FIREWALL_RULE_CREATED, $rule);
return $rule;
}
@@ -195,4 +217,25 @@ final class FirewallRuleManager
$event->setIpAddress($ipAddress)->setReason($reason)->setTenantId($scope->tenantId);
$this->events->dispatch($event);
}
private function publishLifecycleEvent(
string $name,
FirewallRuleObject $rule,
?string $actorId = null
): void
{
$event = new SecurityEvent($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),
]);
$event->setTenantId($rule->getTenantId())
->setIdentityId($actorId ?? $rule->getCreatedBy());
$this->events->dispatch($event);
}
}
+12 -5
View File
@@ -191,7 +191,8 @@ class FirewallService
$ipAddress,
sprintf('Auto-blocked: %d failed auth attempts in %d seconds', $failureCount, $windowSeconds),
null, // System-created
$blockDuration
$blockDuration,
FirewallRuleManager::ORIGIN_AUTOMATIC
);
}
@@ -214,10 +215,10 @@ class FirewallService
->setRequestPath($event->getRequestPath())
->setRequestMethod($event->getRequestMethod())
->setEventType($this->mapEventToLogType($event->getName()))
->setResult($this->mapEventToResult($event->getName()))
->setResult($this->mapEventToResult($event))
->setRuleId($event->get('ruleId'))
->setRuleScope($ruleScope)
->setIdentityId($event->getUserId())
->setIdentityId($event->getUserId() ?? $event->getIdentityId())
->setTimestamp(new \DateTimeImmutable())
->setMetadata($event->getData());
@@ -236,6 +237,9 @@ class FirewallService
SecurityEvent::RATE_LIMIT_EXCEEDED => FirewallLogObject::EVENT_RATE_LIMIT,
SecurityEvent::ACCESS_DENIED => FirewallLogObject::EVENT_RULE_MATCH,
SecurityEvent::SUSPICIOUS_ACTIVITY => FirewallLogObject::EVENT_SUSPICIOUS,
SecurityEvent::FIREWALL_RULE_CREATED => FirewallLogObject::EVENT_RULE_CREATED,
SecurityEvent::FIREWALL_RULE_DISABLED => FirewallLogObject::EVENT_RULE_DISABLED,
SecurityEvent::FIREWALL_RULE_REMOVED => FirewallLogObject::EVENT_RULE_REMOVED,
default => FirewallLogObject::EVENT_ACCESS_CHECK,
};
}
@@ -243,11 +247,14 @@ class FirewallService
/**
* Map security event to result
*/
private function mapEventToResult(string $eventName): string
private function mapEventToResult(SecurityEvent $event): string
{
return match ($eventName) {
return match ($event->getName()) {
SecurityEvent::AUTH_SUCCESS,
SecurityEvent::ACCESS_GRANTED => FirewallLogObject::RESULT_ALLOWED,
SecurityEvent::FIREWALL_RULE_CREATED,
SecurityEvent::FIREWALL_RULE_DISABLED,
SecurityEvent::FIREWALL_RULE_REMOVED => FirewallLogObject::RESULT_RECORDED,
default => FirewallLogObject::RESULT_BLOCKED,
};
}
+10 -2
View File
@@ -66,13 +66,21 @@ final class SystemFirewallRuleService
public function disableRule(string $ruleId): bool
{
$this->requirePermission(self::PERMISSION_MANAGE);
return $this->rules->disable(FirewallRuleScope::system(), $ruleId);
return $this->rules->disable(
FirewallRuleScope::system(),
$ruleId,
$this->identity->identifier()
);
}
public function removeRule(string $ruleId): bool
{
$this->requirePermission(self::PERMISSION_MANAGE);
return $this->rules->remove(FirewallRuleScope::system(), $ruleId);
return $this->rules->remove(
FirewallRuleScope::system(),
$ruleId,
$this->identity->identifier()
);
}
private function requirePermission(string $permission): void
@@ -60,13 +60,13 @@ final class TenantFirewallRuleService
public function disableRule(string $ruleId): bool
{
$this->requirePermission(self::PERMISSION_MANAGE);
return $this->rules->disable($this->scope(), $ruleId);
return $this->rules->disable($this->scope(), $ruleId, $this->identity->identifier());
}
public function removeRule(string $ruleId): bool
{
$this->requirePermission(self::PERMISSION_MANAGE);
return $this->rules->remove($this->scope(), $ruleId);
return $this->rules->remove($this->scope(), $ruleId, $this->identity->identifier());
}
private function scope(): FirewallRuleScope