From f1eca38fb68ad0d9408f97923e24aae1b53e96be Mon Sep 17 00:00:00 2001 From: Renn F Date: Thu, 8 Oct 2026 02:43:00 +0200 Subject: [PATCH] feat(config): muted_event_types and muted_metric_types as SecurityConfig fields The telemetry mute lists were only reachable through the EventFilter constructor seam; they are now SecurityConfig fields (reference default the empty set, exact identifier strings) that seed the event bus's derived filter, with an explicitly injected filter still winning over them. --- .github/coverage-unreachable.php | 6 +++--- bin/test_events_parity.php | 21 +++++++++++++++++++++ src/Config/SecurityConfig.php | 24 ++++++++++++++++++++++++ src/Events/EventBus.php | 8 +++++++- 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/.github/coverage-unreachable.php b/.github/coverage-unreachable.php index 7098652..a69f2a1 100644 --- a/.github/coverage-unreachable.php +++ b/.github/coverage-unreachable.php @@ -248,14 +248,14 @@ 85, 86, 88, ], - // The bare-string guards inside validateSensitiveSet (1067) and - // validateExclusionSet (1091) sit behind typed ?array constructor + // The bare-string guards inside validateSensitiveSet (1091) and + // validateExclusionSet (1115) sit behind typed ?array constructor // parameters: PHP raises the TypeError at the call boundary, so a // string can never reach the is_string($names) checks (the same shape // as the BehaviorRule entry above). The suites assert the boundary // TypeError instead (test_json_logging.php). 'src/Config/SecurityConfig.php' => [ - 1067, 1091, + 1091, 1115, ], // 219: the non-array/non-scalar json_decode guard - json_decode can diff --git a/bin/test_events_parity.php b/bin/test_events_parity.php index b339256..0d1c8bd 100644 --- a/bin/test_events_parity.php +++ b/bin/test_events_parity.php @@ -320,5 +320,26 @@ public function get(string $url, array $options = []): HttpResponse $t->same(1, count($sensitiveEvents), 'a non-empty sensitive set still emits the headers event'); $t->same('/redact-check', $sensitiveEvents[0]->metadata['path'] ?? null, 'the plain path survives the redaction pass'); +$t->section('config mute lists through the derived filter'); +$mutedAgent = new EvAgent(); +$mutedConfig = new SecurityConfig( + enableRedis: false, + mutedEventTypes: [EventTypes::EVENT_SECURITY_HEADERS_APPLIED], + mutedMetricTypes: [EventTypes::METRIC_RESPONSE_TIME] +); +$mutedEngine = new GuardEngine($mutedConfig); +$mutedEngine->setAgentHandler($mutedAgent); +$mutedEngine->initialize(); +$mutedEngine->execute(new SimpleGuardRequest(urlPath: '/muted-check', clientHost: '198.51.100.8')); +$t->truthy(!in_array(EventTypes::EVENT_SECURITY_HEADERS_APPLIED, $mutedAgent->types(), true), 'muted_event_types mutes the headers event through the derived filter'); +$unmutedEngine = new GuardEngine(new SecurityConfig(enableRedis: false)); +$unmutedEngine->setAgentHandler($mutedAgent); +$unmutedEngine->initialize(); +$unmutedEngine->execute(new SimpleGuardRequest(urlPath: '/unmuted-check', clientHost: '198.51.100.8')); +$t->truthy(in_array(EventTypes::EVENT_SECURITY_HEADERS_APPLIED, $mutedAgent->types(), true), 'an empty mute list keeps the headers event'); +$t->same([], (new SecurityConfig())->mutedEventTypes, 'muted_event_types defaults to the empty set'); +$t->same([], (new SecurityConfig())->mutedMetricTypes, 'muted_metric_types defaults to the empty set'); +$t->same(['ip_blocked'], (new SecurityConfig(mutedEventTypes: ['ip_blocked']))->mutedEventTypes, 'with() round-trips the mute lists via the constructor'); + $t->same(0, $t->failed, 'no failures above'); exit($t->finish('event emitters')); diff --git a/src/Config/SecurityConfig.php b/src/Config/SecurityConfig.php index 7dd887d..54b8648 100644 --- a/src/Config/SecurityConfig.php +++ b/src/Config/SecurityConfig.php @@ -262,6 +262,24 @@ final class SecurityConfig */ public readonly ?string $logCountryCheckLevel; + /** + * Event types muted from telemetry dispatch (muted_event_types, + * reference default the empty set): exact identifier strings, applied + * by the event bus's EventFilter. + * + * @var list + */ + public readonly array $mutedEventTypes; + + /** + * Metric types muted from telemetry dispatch (muted_metric_types, + * reference default the empty set): exact identifier strings, applied + * by the event bus's EventFilter. + * + * @var list + */ + public readonly array $mutedMetricTypes; + /** * Seconds between health checks on pooled connections * (redis_health_check_interval, reference default 30, ge 0): a socket @@ -415,6 +433,8 @@ public function __construct( ?string $logSuspiciousLevel = null, ?string $logRequestLevel = null, ?string $logCountryCheckLevel = 'INFO', + ?array $mutedEventTypes = null, + ?array $mutedMetricTypes = null, ?int $redisHealthCheckInterval = null, ?int $redisMaxConnections = null, ?int $redisRetries = null, @@ -549,6 +569,8 @@ public function __construct( // reference field's Optional level), so the parameter default // carries the reference default "INFO" and null passes through. $this->logCountryCheckLevel = $this->validateLogLevel($logCountryCheckLevel, 'log_country_check_level', null); + $this->mutedEventTypes = $this->validateStringList($mutedEventTypes ?? [], 'muted_event_types'); + $this->mutedMetricTypes = $this->validateStringList($mutedMetricTypes ?? [], 'muted_metric_types'); $this->redisHealthCheckInterval = $redisHealthCheckInterval ?? 30; if ($this->redisHealthCheckInterval < 0) { throw new \InvalidArgumentException('redis_health_check_interval must be >= 0'); @@ -866,6 +888,8 @@ private function constructorArgs(): array 'logSuspiciousLevel' => $this->logSuspiciousLevel, 'logRequestLevel' => $this->logRequestLevel, 'logCountryCheckLevel' => $this->logCountryCheckLevel, + 'mutedEventTypes' => $this->mutedEventTypes, + 'mutedMetricTypes' => $this->mutedMetricTypes, 'redisHealthCheckInterval' => $this->redisHealthCheckInterval, 'redisMaxConnections' => $this->redisMaxConnections, 'redisRetries' => $this->redisRetries, diff --git a/src/Events/EventBus.php b/src/Events/EventBus.php index 673ff23..375cda0 100644 --- a/src/Events/EventBus.php +++ b/src/Events/EventBus.php @@ -32,6 +32,8 @@ final class EventBus { private ?object $agentHandler; + private EventFilter $eventFilter; + /** @var list */ private array $queued = []; @@ -41,10 +43,14 @@ final class EventBus public function __construct( ?object $agentHandler, private readonly SecurityConfig $config, - private readonly EventFilter $eventFilter = new EventFilter(), + ?EventFilter $eventFilter = null, ?callable $countryResolver = null ) { $this->agentHandler = $agentHandler; + // The config's mute lists (muted_event_types / muted_metric_types) + // seed the filter; an explicitly injected filter wins over them. + $this->eventFilter = $eventFilter + ?? new EventFilter($config->mutedEventTypes, $config->mutedMetricTypes); $this->countryResolver = $countryResolver === null ? null : $countryResolver(...); }