Skip to content

Releases: Guard-Core/guard-core-php

v4.4.0

Choose a tag to compare

@rennf93 rennf93 released this 15 Oct 20:39
Immutable release. Only release title and notes can be modified.
2e3f829

v4.4.0 (2026-10-15)

Scan budget, client IP and IP list parity, dynamic rules over the base config, and a response modifier seam (v4.4.0)

A release in lockstep with the core engines at 4.4.0. It adds the per-request scan time budget, brings the client identity and the IP list checks to the reference behavior, applies dynamic rules over the base config, and adds GuardEngine::applyResponseModifier() for adapters.

Added

  • detectionMaxScanSeconds: a per-request cap on regex scan CPU time (default 10.0, range 0.1 to 60.0, PR #80). Each pattern scan's CPU time (getrusage() user plus system time, so time spent descheduled is not charged) is summed per request and checked between patterns. Once it is spent the remaining patterns and values are skipped and the request records a weighted pattern_timeout threat (pattern scan_time_budget_exhausted), so padding a request cannot skip detection. A direct detect() call outside a request scan is not capped.
  • excludedSemanticDetectionHeaders: an opt-in semantic-only header exclusion (default empty, PR #78). Listed headers skip the semantic analyzer only and the regex scan still runs. It exists on SecurityConfig and on RouteConfig (merged with the global set, also reachable as detectionExclusion(semanticHeaders: ...)).
  • GuardEngine::applyResponseModifier() and the customResponseModifier config field (PR #84). The modifier is applied once, after the security headers and before the CORS headers, to every response the engine returns. applyResponseModifier() is the seam adapters call to modify the pass-through responses they send, and an exception inside the modifier leaves the response unmodified.
  • dynamicRuleInterval (default 300, minimum 60, PR #84): execute() calls the dynamic rule manager's interval gate, so the first request fetches and then one fetch runs per interval.
  • Request metrics (PR #78): GuardEngine::processResponse emits response_time, request_count and error_rate once an agent handler with sendMetric is attached and agentEnableMetrics is on.

Changed

  • Behavior to know before upgrading. enableDynamicRules and enableEnrichment now require enableAgent, like the reference validate_agent_config: a config that sets either without enableAgent no longer validates. The client identity, the IP list matching and the IP list precedence follow the reference (see Fixed), so a ban, a rate limit or a list entry can now match a client that it did not match before. A configured whitelist decides alone and the blacklist applies only when no whitelist is configured. An ip_blacklist dynamic rule is now a timed ban instead of a merge into the static list. A dynamic rule version no longer carries over the previous version's changes. A passive-mode suspicious log line without a trigger has new wording, so update any match on the old text.
  • enableDynamicRules now lists exactly what a rule changes.

Fixed

  • Client identity resolution matches the reference (PR #86, issue #85). X-Forwarded-For entries lose their port, empty entries are kept (which changes the entry the depth selects), and a depth that over-counts the proxy hops falls back to the right to left walk. Zoned addresses parse and a mapped zoned address renders as ::ffff:1.2.3.4%eth0. A mapped peer inside an IPv4 CIDR in trustedProxies is no longer trusted and a malformed CIDR no longer stops the search. trustedProxies accepts the unix token, so a request with no peer address resolves through the chain. A forwarded header from an untrusted peer raises the spoofing_detected event and the spoof log once, with a redacted chain. List matching canonicalizes the entry (CanonicalIp::ipInList), so an IPv4 entry, an IPv4 CIDR, a mapped entry and a bracketed entry match a client that connects as IPv4-mapped IPv6.
  • IP list precedence matches the reference (PR #88, issue #87). The global stage runs the whitelist branch and consults the blacklist only when no whitelist is configured, a configured route whitelist skips the global lists for the request, and checkRouteIpAccess consults the route blacklist only when the route has no whitelist. The route country verdict still combines with the route IP verdict.
  • Dynamic rules apply over the base config (PR #84, issue #83). ip_blacklist bans through the engine ban manager for ip_ban_duration seconds and ip_whitelist unbans. blocked_countries, whitelist_countries, block_cloud_providers, blocked_user_agents and endpoint_rate_limits replace the base values. Each new version applies over the base config (guard-core #169): a failed apply keeps the previously active version, expiry restores the base, and patterns already in the pool are not added again. Emergency mode halves the auto-ban threshold in effect, once. The events match the reference envelopes, including emergency_mode_activated, and SecurityConfig::with() no longer drops enableAgent, enableDynamicRules, agentEnableEvents, agentEnableMetrics and dynamicRulesCachePath. GuardEngine::initialize() builds the DynamicRuleManager when enableDynamicRules is on and an agent handler with getDynamicRules() is attached.
  • The Cookie header is scanned one name=value pair at a time (PR #78, guard-core #162). The ; between cookies no longer counts as a command separator in the semantic score, so an ordinary logged-in header is not rejected, while XSS, SQLi and CRLF inside a single cookie are still detected.
  • Passive-mode suspicious logs keep the reason (PR #78, guard-core #165). A passive line without a trigger reads [PASSIVE MODE] Suspicious activity detected from <ip>: ... - Reason: <reason> - Headers: ....

Docs

  • The examples drop the stale logSensitiveHeaders address-header workaround, which skipped those headers from detection entirely (PR #82). docs/configuration.md and the agent docs describe the new options.

Chore

  • The sponsor button points at the maintainer's Sponsors page, the mkdocs site_url drops /latest/, and the README docs links point at the live site root (PR #76, #75, #73).

v4.3.3

Choose a tag to compare

@rennf93 rennf93 released this 12 Oct 22:52
Immutable release. Only release title and notes can be modified.
84da114

v4.3.3 (2026-10-12)

Parity release (v4.3.3)

A parity release with no functional change: the engine is tagged 4.3.3 so all five core engines (Python, TypeScript, Rust, Go, PHP) ship the same version.


v4.3.2

Choose a tag to compare

@rennf93 rennf93 released this 09 Oct 09:03
Immutable release. Only release title and notes can be modified.
v4.3.2
c63c2e1

v4.3.2 (2026-10-09)

The parity-completion release: guard-core-php 4.3.2 tracks the guard-core 4.3.2 release. The engine gains the decorator family (the per-endpoint SecurityDecorator surface with route identity, the shared revision cell, and the engine decorator-handler seam), the SusPatterns runtime pattern registry with the dynamic-rule suspicious_patterns application, the four missing event emitters (cloud_blocked, country_blocked, geo_lookup_failed, security_headers_applied) with the send_cloud_detection_events bus port, the mutedEventTypes/mutedMetricTypes config fields, and the seven previously missing config fields with their consumers.

The decorator family: SecurityDecorator, RouteDecoration, route identity (PR #60)

Added

  • src/Decorators/SecurityDecorator.php: the 25 config methods (requireIp, blockCountries, allowCountries, blockClouds, bypass, rateLimit, geoRateLimit, usageMonitor, returnMonitor, behaviorAnalysis, suspiciousFrequency, requireHttps, requireAuth, apiKeyAuth, requireAuthorizationHeader, requireHeaders, blockUserAgents, contentTypeFilter, maxRequestSize, requireReferrer, customValidation, detectionExclusion, timeWindow, suspiciousDetection, honeypotDetection) with reference semantics: uppercase country normalization, the block_clouds selector validity gate with the unknown-provider warning, the bypass union with decorator-time leniency, the auth/authorization-header mutual exclusion raising at application time, the ReDoS gate refusing unsafe user-agent patterns before decoration, the body-scan gate for body-reading return patterns, and the parse_qs-shaped honeypot validator with Python truthiness. Plus the base infra: config, routeConfigRevision, getRouteConfig, routeConfigs, initializeBehaviorTracking, initializeAgent, and the five send_*_event emitters over the event bus (decorator_type inside the metadata, the agent_enable_events gate honored), and the getRouteDecoratorConfig static.
  • src/Decorators/RouteDecoration.php: the immutable decoration chain, the port of the decorator closures the mixin methods return; decorate() is the application step. Chaining never mutates the handler or a shared chain, so two factories built from one decorator never bleed into each other (the per-function guarantee).
  • src/Decorators/DecoratedEndpoint.php: the stamped endpoint shell (invokable call-through; pattern shells throw LogicException) and src/Decorators/RouteConfigRevision.php: the shared revision cell.
  • Route identity (the _get_route_id port): the stamped id is reused only when this instance assigned it to this endpoint (stacked decorations share one config); otherwise the base id suffixes #2..#n. New routes seed enableSuspiciousDetection from enablePenetrationDetection like _ensure_route_config; every applied decoration bumps the shared cell.
  • GuardEngine::setDecoratorHandler (the set_decorator_handler port): requests stamped with a route id resolve their RouteConfig through the handler's registry (the get_route_decorator_config flow), and decorator mutations rebuild the pipeline through the revision staleness seam.
  • BehaviorRuleValidation::validateReturnPatternAgainstScanFlag: the single-pattern scan gate (the _validate_return_pattern_body_scan path), with the list validator refactored onto it. Decorated endpoints flow through the identical pipeline checks: adapters hand the registry (SecurityDecorator::routeConfigs()) to the existing path-pattern route maps, or stamp state->guardRouteId and let the engine resolve through the handler.

The runtime pattern registry and the seven config fields (PR #57)

Added

  • SusPatterns grows the runtime registry: addPattern/removePattern with the ReDoS safety gate, getDefaultPatterns/getCustomPatterns/getAllPatterns, catalog-row removal tracked as a scan-time skip, and detectPatternMatch (the reference detect_pattern_match projection). Registry additions scan on every view with no context filter and the custom category (the reference compiled, _CTX_ALL, "custom" tuple semantics), with the same per-pattern scan gates and monitor metrics as a catalog row.
  • The custom pool persists to Redis ("patterns:custom", comma-joined) and restores on engine initialize(); unsafe persisted rows are skipped with a warning. pattern_added / pattern_removed events flow through the event bus with handler_name sus_patterns, the system ip, and the redacted source, pool type and pool size. The engine owns the detection engine instance, so the registry survives applyDynamicConfig pipeline rebuilds; GuardEngine exposes susPatterns().
  • DynamicRuleManager applies suspicious_patterns through the registry (unsafe rows warned and rejected) instead of the skip warning; the reference's expiry semantics carry (registrations are runtime detection state, not config). SuspiciousActivityCheck exempts the custom category from the enabled-categories gate, mirroring _pattern_should_be_skipped.
  • The seven missing SecurityConfig fields with reference defaults, validation, and consumers: detection_threat_score_threshold (1.0, [0.0, 10.0], the detect() verdict gate on the regex-threat anomaly), detection_pattern_validation_cache_path (the disk-backed cost-verdict cache wired into the registry gate, the dynamic-rule user-agent validation and the CheckFactory fallback engine; cheap deterministic layers always re-run), log_country_check_level (INFO, nullable, the whitelisted and not-affected country verdict lines plus the reference debug lines for the loopback and no-geolocation skips), redis_health_check_interval (30, ge 0, the RESP idle-socket PING probe with recycle-on-failure), redis_max_connections (null, ge 1, the pool cap on the recycle path), redis_retries (1, ge 0, the capped-backoff retry wrapper over transient connect/write/read failures, never server-side error replies), and agent_strict (false, the strict agent attach: a handler without the sendEvent surface raises instead of degrading to agent-off).

The four event emitters and send_cloud_detection_events (PR #58)

Added

  • EventBus::sendCloudDetectionEvents, the reference middleware_events.py port: the cloud_blocked verdict event (handler cloud, action request_blocked or logged_only in passive mode, the provider and network metadata) plus, when a forwarded route blocks clouds, its access_control decorator_violation.
  • The cloud_provider check emits the bus cloud events on every block path and its own block_clouds decorator_violation for a route-level block (the reference check passes a null route to the bus for exactly that case). The ip_security check's country stage emits country_blocked (handler ipinfo, the reference allowlist and blocklist reasons with the country and rule_type metadata) and geo_lookup_failed (a failing resolver reports the lookup failure and a miss, never a raise, mirroring get_country's internal catch).
  • security_headers_applied emits behind the reference security-headers TTL cache (TTLCache(1000, 300) semantics: one event per config+path per window keyed cfg_{id}path{sha256 prefix}, empty paths gated, oldest-first trim matching the single-TTL eviction order), at the pipeline pass (the headers the adapter applies to the pass-through response) and on blocked responses. The events close the H3 and H6 event-deferral notes and give DynamicRuleManager.matchEvent a live cloud_blocked stream to correlate against.

Muted event and metric types as config fields (PR #59)

Added

  • SecurityConfig grows mutedEventTypes / mutedMetricTypes (validated string lists, with()-safe, the reference default of the empty set, exact identifier strings). EventBus derives its EventFilter from the config's mute lists when none is injected; an explicitly injected filter still wins (the AgentHandlerComposer wiring keeps precedence).

Coverage

Changed

  • The new surfaces join the 100%-line coverage gate: bin/test_decorators.php (210 assertions, mirroring the reference decorator tests: test_base, test_mixins, test_route_id_per_function, the access-control country/cloud/bypass arms, the advanced honeypot matrix, the detection-exclusion arms), bin/test_pattern_registry.php (80), bin/test_config_fields.php (69), and bin/test_events_parity.php (55) join the CI test steps, the coverage collect list, and the Makefile runners. The ip_ban initialization catch waiver moves with its shifted lines in .github/coverage-unreachable.php (same block, same proof).

v4.3.1

Choose a tag to compare

@rennf93 rennf93 released this 07 Oct 09:37
v4.3.1
0c8b0fd

v4.3.1 (2026-10-07)

The observability-seam release: guard-core-php 4.3.1 tracks the guard-core 4.3.1 release. The engine gains the event enrichment layer (ThreatScorer + EventEnricher stamping the reference guard.* metadata family), the CompositeAgentHandler multi-sink fan-out with the OTel (OTLP/HTTP JSON) and Logfire handlers, JSON structured logging with the log_format config switch, the GuardCoreError base exception, the setAgentHandler agent seam, and the per-component initialization status surface.

Event enrichment: ThreatScorer + EventEnricher (PR #55)

Added

  • The deterministic threat-score map (src/Events/ThreatScorer.php), same values and order as the reference _THREAT_SCORE_MAP: 90 penetration_attempt, then the attack-family events down to 10 for the lifecycle events, with default 20 for unmapped types. The map feeds the guard.threat_score enrichment key, so every enriched event carries its severity weight from the same table the reference scores with.
  • The enrichment layer (src/Events/EventEnricher.php + EnrichmentContext.php), the four reference steps in reference order, stamped into the event metadata under the exact ENRICHMENT_KEY_* strings added to EventTypes.php: guard.project_id, guard.service.name, guard.deployment.environment, guard.threat_score, guard.rule.id, guard.rule.version, guard.behavior.correlation_key, guard.behavior.recent_event_count. Identity (project id when configured, service name always, deployment environment from otel_resource_attributes["deployment.environment"]), threat score from the event type, dynamic-rule correlation via the duck-typed matchEvent ([ruleId, version]), and behavior correlation: BehaviorTracker.getRecentEventCount(ip, 300) (mirroring the reference get_recent_event_count) plus the sha256(ip|service|bucket)[:16] correlation key with bucket = floor(now/300). Failure semantics: any enrichment error logs and the event flows onward; the immutable PHP records make the failure path atomic (the original event is returned), matching the reference's stated intent (its in-place dict mutation can survive a later step's failure despite the "sent unenriched" log line, a divergence recorded in the PR). Enrichment lives in the composite only, as in the reference; the bare agent-handler path is untouched. New config fields with reference defaults: enableEnrichment=false, agentProjectId, otelServiceName="guard-core", otelExporterEndpoint, otelResourceAttributes={}, enableOtel=false, enableLogfire=false, logfireServiceName="guard-core".

Composite, OTel, and Logfire handlers (PR #55)

Added

  • src/Events/CompositeAgentHandler.php: the multi-sink fan-out port of the reference composite_handler.py, with the event/metric filter, one enrichment pass before the fan-out (all sinks observe the same enriched instance), per-sink exception isolation (degraded, not dead), the started()/degraded()/failedHandlers() lifecycle, best-effort stop()/flushBuffer()/initializeRedis(), first-non-null getDynamicRules() and all-answering healthCheck(). src/Events/AgentHandlerComposer.php mirrors the reference build_enricher + build_composite_handler: sinks [agent] + OtelHandler (enable_otel) + LogfireHandler (enable_logfire), enricher when enable_enrichment.
  • src/Events/OtelHandler.php (+ OtlpTransport.php/OtlpHttpTransport.php): the otel_handler.py port speaking OTLP/HTTP JSON through an injectable transport (production defaults to curl-or-streams with bounded timeouts, tests use a capturing fake). Faithful surface: the exact _otlp_signal_endpoint suffix logic, resource service.name plus extra attributes, span guard.event.<type> with the guard.* attribute family, enrichment forward (the guard.* metadata minus traceparent/tracestate), W3C traceparent extraction as the remote parent (malformed falls back to a generated traceId), the metrics mapping (response_time -> guard.request.duration histogram in seconds, request_count -> guard.request.count, error_rate -> guard.error.count, unknown types log and record nothing), and export failures logged never raised. Divergence: the reference's process-global tracer/meter provider claim-and-shutdown dance is inapplicable-idiom in PHP (no global OTel registry; each handler owns its transport, per-instance start() stays idempotent).
  • src/Events/LogfireHandler.php: the exact reference lifecycle over an injected duck-typed client (configured()/configure()/span()/info()/shutdown()): no client means disabled warning + no-op + unhealthy, an already-configured client means a warning naming the service name that will NOT be applied (configure is not called), and stop() shuts down only what guard configured. Emission: span guard.event.<type> with event fields plus the enrichment forward, info guard.metric.<type> with value/endpoint/safe tags.

JSON structured logging (PR #55)

Added

  • src/Logging/JsonFormatter.php (exactly four fields in reference order: timestamp in the python asctime shape, level uppercase, logger, message; malformed UTF-8 substitutes instead of failing the encode) and src/Logging/LogSetup.php (setupCustomLogging returning an engine-compatible RequestLogger, the text layout mirroring [%(name)s] %(asctime)s - %(levelname)s - %(message)s, the INFO+ gate, on-demand directory creation with console-only fallback, console emission writing bare lines to stderr). The config switch is log_format (text|json, validated at construction, with()-safe) with LogSetup::engineLogClosure() for adapters. The reference body_reader.py extras (read timeout, concurrency cap, straddle over-read) are documented as inapplicable-idiom for the SAPI model, not ported.

Engine seams: GuardCoreError, agent hook, initialization status (PR #54)

Added

  • GuardCoreError base exception: GuardRedisException, UnsupportedFeatureError and MmdbError now extend it, giving adapters one catch type for any guard-core failure, mirroring the Python engine.
  • The agent seam (W2): GuardEngine::setAgentHandler forwards to the event bus and queued events drain on attach. This is the hook the four adapter repos' agent integrations consume.
  • GuardEngine::initializationStatus(): per-component enabled/ok/error recorded across initialize(), the payload the adapters' status routes serve. Failure semantics unchanged: the original exception still throws, the status is recorded before the throw.

Coverage

Changed

  • The new surfaces are covered to the 100%-line gate: bin/test_enrichment.php (56 assertions), bin/test_composite_handlers.php (96), bin/test_json_logging.php (34), and bin/test_parity_seam.php (7) join the CI runner list, with the provably-unreachable inventory in .github/coverage-unreachable.php (composite sink-failure twins, the OTel attribute-type forward skips, the closed-stderr fallback, the double-enrich idempotence arms).

v4.3.0 (2026-10-01)

The safety-corpus release: guard-core-php 4.3.0 tracks the guard-core 4.3.0 release. The engine gains the spec 12 event bus with dynamic rules and Redis-backed metrics persistence, the spec 10 geo download and refresh lifecycle, the spec 04 ReDoS safety gates over PCRE plus the performance monitor, and the three new spec 4.1.0 conformance runners (pattern_safety, events, redis_interop) with fail-closed xfail baselines; the whole suite now sits behind a 100%-line coverage gate (every executable line covered or provably unreachable), and the hex-escape attack indicator carries the reference's parity backslash again, unbreaking PHP 8.5's stricter PCRE2.

Event bus, metrics collector, and dynamic rules (spec 12)

Added

  • The spec 12 parity port: EventBus, EventFilter, MetricsCollector, SecurityMetric, DynamicRuleManager and DynamicRules land engine-side, with the SecurityConfig agent gating (agent_enable_events / agent_enable_metrics, both default true) and the new dynamic_rules_cache_path field. Ported from the reference event-bus surface at the spec 12 parity commit (PR #43, cherry-pick of 136f35b); both new suites are registered in the CI test steps and the coverage collect list. Coverage lives in the new bin/test_event_bus.php honesty runner (136 checks at the port).
  • Metrics persist across requests via Redis, closing the last metrics gap: the spec 12 event bus is queueable, but MetricsCollector was in-memory only, so under the shared-nothing runtime model metrics emitted with no agent handler died with the request (PR #48). The collector's queueing ports onto the reference key semantics (spec 08 namespaced grammar, full_key = prefix + namespace + ':' + key): with no agent handler and a wired RedisHandler the pending backlog lives under {prefix}metrics:pending as a JSON list of SecurityMetric payloads (timestamp ATOM, metric_type, value, tags). setAgentHandler() dispatches the in-memory queue, then flushes the persisted backlog in order and clears the key, and drain() absorbs the persisted backlog too, so FPM adapters can deliver it themselves. Failure arms stay fail-closed and quiet: a corrupted or non-list payload is discarded with a log (empty backlog, never raised), malformed entries are skipped individually with a log, and Redis write failures log and never raise; agent_enable_metrics and the EventFilter gate before anything persists, and a disabled RedisHandler falls back to the in-memory queue. bin/test_event_bus.php grows the persistence arms (150 -> 174 assertions): backlog accumulation across two sends, flush-on-attach with valu...
Read more

v4.3.0

Choose a tag to compare

@rennf93 rennf93 released this 01 Oct 20:10
v4.3.0
fe1b6ba

v4.3.0 (2026-10-01)

The safety-corpus release: guard-core-php 4.3.0 tracks the guard-core 4.3.0 release. The engine gains the spec 12 event bus with dynamic rules and Redis-backed metrics persistence, the spec 10 geo download and refresh lifecycle, the spec 04 ReDoS safety gates over PCRE plus the performance monitor, and the three new spec 4.1.0 conformance runners (pattern_safety, events, redis_interop) with fail-closed xfail baselines; the whole suite now sits behind a 100%-line coverage gate (every executable line covered or provably unreachable), and the hex-escape attack indicator carries the reference's parity backslash again, unbreaking PHP 8.5's stricter PCRE2.

Event bus, metrics collector, and dynamic rules (spec 12)

Added

  • The spec 12 parity port: EventBus, EventFilter, MetricsCollector, SecurityMetric, DynamicRuleManager and DynamicRules land engine-side, with the SecurityConfig agent gating (agent_enable_events / agent_enable_metrics, both default true) and the new dynamic_rules_cache_path field. Ported from the reference event-bus surface at the spec 12 parity commit (PR #43, cherry-pick of 136f35b); both new suites are registered in the CI test steps and the coverage collect list. Coverage lives in the new bin/test_event_bus.php honesty runner (136 checks at the port).
  • Metrics persist across requests via Redis, closing the last metrics gap: the spec 12 event bus is queueable, but MetricsCollector was in-memory only, so under the shared-nothing runtime model metrics emitted with no agent handler died with the request (PR #48). The collector's queueing ports onto the reference key semantics (spec 08 namespaced grammar, full_key = prefix + namespace + ':' + key): with no agent handler and a wired RedisHandler the pending backlog lives under {prefix}metrics:pending as a JSON list of SecurityMetric payloads (timestamp ATOM, metric_type, value, tags). setAgentHandler() dispatches the in-memory queue, then flushes the persisted backlog in order and clears the key, and drain() absorbs the persisted backlog too, so FPM adapters can deliver it themselves. Failure arms stay fail-closed and quiet: a corrupted or non-list payload is discarded with a log (empty backlog, never raised), malformed entries are skipped individually with a log, and Redis write failures log and never raise; agent_enable_metrics and the EventFilter gate before anything persists, and a disabled RedisHandler falls back to the in-memory queue. bin/test_event_bus.php grows the persistence arms (150 -> 174 assertions): backlog accumulation across two sends, flush-on-attach with value/tags/timestamp round trip plus key clearing, drain absorption and empty-drain, redis write failure, corrupted/non-list/scalar pending payloads, malformed-entry skipping with tag coercion, the enable gate, and the disabled-redis fallback; every new MetricsCollector line is executed with a behavioral assertion.

Geo download and refresh lifecycle (spec 10)

Added

  • The spec 10 parity port: IpInfoManager owns the geo database lifecycle (PR #43, cherry-pick of 0fbbdb5). Token download (3 attempts, exponential backoff), atomic writes, mtime freshness, the Redis-cached database copy, never-raising get_country, check_country_access verdicts with country_blocked events, geo_lookup_failed events, and the Redis-backed single-flight refresh lock. Coverage lives in the new bin/test_geo_lifecycle.php honesty runner (100 checks after the #44 coverage arms, 92 at the port) over the new shared tests/MmdbFixture.php fixture; master's bin/test_geo_country.php keeps its own superset inline builder, and the shared fixture serves the geo-lifecycle suite exclusively (each suite runs in its own PHP process, so no redeclare).

Spec 04 ReDoS safety gates on PCRE, and the performance monitor

Added

  • The section 04 ReDoS safety gates port to the PHP engine's PCRE model, plus the performance monitor (spec 04) and the event constant table (PR #39, mandated by specs/impl/php.md's regex-engine section). Structural prefilters (the three reference dangerous-construct regexes verbatim, the compile check, and the nested-unbounded / adjacent-broad structural checks over a source-only scan, outputs pinned case-by-case against the Python functions); reach-probe synthesis (a subject that reaches every quantified region, null synthesizing a fail-closed reject; the reference's subprocess probe and slot/interval machinery adapted per the spec's open-questions note to the observable contract: reject slow, fail closed, bounded time); the cost arbiter (load factor floor 0.25 / ceiling 8.0 measured against the reference scan, scaled verdict deadline, the 4000..32000 ladder, the min16/min32 verdict rule with noise floor 0.001, growth per doubling with extrapolation to the cap against the 0.05 s budget, one confirming retry, the interactive probe at 0.05 s per string and 2.0 s overall fail-closed, stride-sampled probe sets, and the accept-with-disagreement log); the scan guard (per-scan pcre.backtrack_limit via ini_set with the previous limit saved/restored, PREG_BACKTRACK_LIMIT_ERROR/PREG_RECURSION_LIMIT_ERROR/PREG_JIT_STACKLIMIT_ERROR trips classified as scan timeouts that fail closed while other preg failures keep the engine's fail-secure 500, an hrtime deadline, the consecutive-timeout counter and pool-replacement warning, and a canary probe - the arbiter's verdict arithmetic over doubling subject prefixes - before plain-pattern scans of large subjects); and the SusPatterns classification and timeout semantics (scan-window bounded patterns unguarded, windowed finders under the full compiler timeout, plain patterns behind the canary plus the reference's 0.9x-timeout heuristic arm, and the exact reference timeout semantics: a scan timeout emits a pattern_timeout threats-logged-and-miss threat with the category weight, so an engine that could not finish a scan fails closed; detect() now also returns timeouts). PregFailure carries the preg error code captured at throw time so classification never reads stale process-global state. Coverage lives in the new bin/test_redos_gates.php (175 checks: allow and block paths, gate-fire assertions, and the catastrophic fixtures (a|a)*$, (.*a){20}$, (aa|a){20}$ over adversarial subjects tripping the engine budget and the deadline deterministically, hrtime-measured in the test) and bin/test_performance_monitor.php, both registered in the Makefile RUNNERS list, the CI test matrix and the coverage collection.

Spec 4.1.0 conformance runners: pattern_safety, events, redis_interop

Added

  • The three new reference corpus suite kinds are vendored and replayed with one runner per kind, mirroring the existing fail-closed xfail architecture (PR #46, fixes #45). The vendored corpus gains safety_gates.json, event_stream.json and redis_interop.json plus the updated index.json (the new comparison blocks: events envelopes with volatile-field dropping, redis interop byte-equality rules, pattern-safety record rules). bin/conformance_safety.php runs every case through the real Prefilters::validatePatternSafety gate chain and maps verdict reasons to the corpus reason classes with the corpus generator's CLASS_RULES (first-match order), comparing expected.safe + expected.reason_class only. bin/conformance_events.php drives the real PHP event seams (the spec 12 pipeline bus, the monitor anomaly emitters, the geo event sink, the dynamic rules manager, the ban manager) and compares full envelopes per the events_envelopes block: expected keys only, volatile fields dropped recursively, fields the PHP envelope model cannot observe recorded as null. bin/conformance_redis.php performs one live-Redis operation per key family under each case's prefix (flushed before and after) with byte equality for keys, static values and TTL semantics, shape pinning for engine-generated zset members, and parsed-then-canonicalized JSON comparison. Each kind carries a fail-closed xfail baseline (php_safety_xfail.json, php_events_xfail.json, php_redis_interop_xfail.json): a failing case not baselined is red, a baselined case that passes is red (stale), and a driverless case not baselined is red, every baseline entry naming the PHP source of the divergence. Baselines at the merge: conformance_safety 69 passed / 25 xfail (unported parse-slot structural rules - ambiguous tail, unreachable terminator, literal absorb - PCRE2 atomic-group compile acceptance and auto-possessification, and the two timed cost-arbiter disagreements), conformance_events 6 passed / 32 xfail plus 1 advisory corpus skip (handler-level emitters not ported: sus_patterns, ban envelopes, security headers, redis manager, ip extraction, decorators, dynamic-rule violations, the check-hook metadata shape, and the blockEventType default mapping), conformance_redis 7 passed / 7 xfail (unported security-headers / custom-pattern / cloud-ranges registries, the verbatim vs latin-1 GeoIP database cache, and the ATOM vs Z snapshot timestamp).

100% line coverage gate, and the process scaffold

Added

  • The suite sits behind a 100%-line coverage gate: every executable line covered or provably unreachable (PR #38 and the follow-up coverage campaign, closed together with #44's geo + events arms). .github/coverage-runner.php collects pcov line coverage per suite in --collect mode (extra suite args such as test_state's --integration forwarded) and a --merge pass aggregates every collection and gates. Coverage climbed 80.56% -> 89.19% -> 98.86% -> 99.08% across the campaign (~1650, then ~1400, then ~100 test LO...
Read more

v4.2.0

Choose a tag to compare

@rennf93 rennf93 released this 27 Sep 18:44
v4.2.0
388d4d1

v4.2.0 (2026-09-27)

The parity release: guard-core-php 4.2.0 tracks the guard-core engine 4.2.0 release. This train closes the remaining pipeline divergences from the spec 4.1.0 corpus (the on_block hook payloads now carry the reference log-format reasons and the route-level IP and country rules are enforced), and the fail-closed xfail registry is emptied.

Note on the 4.1.0 tags: the guard-core 4.1.0 family tags were a version-accuracy error and were yanked/unpublished (the 4.1.0 work itself is part of this 4.2.0 train; Packagist serves 4.2.0 from the v4.2.0 tag). The conformance corpus vendored here is the spec 4.1.0 corpus (219 cases across the detect and pipeline suites) and the pipeline gate runs green against it with an empty divergence registry.

on_block corrections and route-level IP enforcement

Fixed

  • The on_block hook payloads now carry the reference log-format reasons with an empty trigger_info (the reference checks pass no trigger_info to log_activity), and route-level IP rules are enforced for the first time. ip_security composes IP not allowed: {ip} - {access reason} over the access_control generic list block string and IP not allowed by route config: {ip} for route denials, rate_limit reports Rate limit exceeded for IP: {ip} ({count} requests in {window}s window) from the tripped tier, and suspicious_activity emits Suspicious activity detected for IP: {ip} - {trigger_info} active / Suspicious activity detected: {ip} passive with the detection trigger built from the reference component labels (Request body / Header / URL path / JSON key / Request body field) plus the first threat message; passive-mode dispatches fire the hook inline with a null status_code like the reference _dispatch_block_hook. The enforcement fix: route-level ip_whitelist (and ip_blacklist / route country rules) were absent from RouteConfig and the ip_security check entirely, so a non-whitelisted client passed where the reference denies 403. RouteConfig gains the ipWhitelist/ipBlacklist/blockedCountries/whitelistCountries fields with the route blacklist first, a configured route whitelist taking over the route verdict, the route country verdict combining with it, and a route IP whitelist clearing the identity flags without relaxing the global lists, with a regression test through GuardEngine::execute. The wildcard + credentials CORS combination is accepted at construction and downgraded at policy resolution (credentials dropped, wildcard answers without the allow-credentials header), and the CORS response surface composes methods/headers/3600 max-age like the reference _build_cors_headers. The 27 xfail entries are cleared leaving an empty fail-closed registry (pipeline gate: 35 passed, 0 failed, 0 xfail, 0 config divergences).

Verification

  • Full suite green on PHP 8.3 (Docker, php:8.3-cli + throwaway redis:7-alpine, no host php/composer): bin/conformance.php 184/184 (verdicts unchanged), bin/conformance_pipeline.php 35 passed / 0 failed / 0 xfail / 0 config divergences over the spec 4.1.0 corpus, and the full bin/ runner list per the CI workflow.

Per-route detection exclusions


Added

  • Per-route detection exclusion surface: the detection-exclusion fields of the reference RouteConfig are now honored by the suspicious_activity check instead of being absent. Ported from guard_core decorators/route_config.py (the detection-exclusion fields), _utils/detection_config.py (_resolve_excluded_params/_resolve_excluded_body_fields/_resolve_enabled_categories/_resolve_scan_body/_resolve_excluded_headers) and core/checks/helpers.py (_get_effective_penetration_setting), structurally following the guard-core-go detection-exclusion resolution: RouteConfig gains enableSuspiciousDetection (default true), excludedDetectionHeaders, excludedDetectionParams, excludedDetectionBodyFields (null = inherit the global config; a non-null set replaces the global one, the header set being the exception: it always merges the hardcoded proxy-identity defaults with the global set and the route set), enabledDetectionCategories (null = inherit; a non-null set replaces the global set, an empty list disables every category like the reference's empty frozenset) and detectionScanBody (null = inherit the new global detectionScanBody field, default true; a false skips the request-body surface only while the URL path, query and headers still scan). The per-route enable_suspicious_detection decorator wins over the global flag for routed requests (route true enables detection even with the global flag off, route false disables it even with the flag on, the reference's disabled_by_decorator miss), and the reference's route-aware applies_to gate now schedules the check when any registered route enables detection with the global flag off. Entries keep the repo's verbatim-exclusion convention (scan sites lower the scanned name or key and test membership), and route-excluded headers keep the excluded-header ssrf-only skip semantics (they suppress an ssrf address chain only; every other category still scans them). Coverage lives in the new bin/test_route_detection_exclusions.php honesty runner (37 assertions: the route kill switch, the factory-level scheduling gate, the per-request gate, each exclusion surface's replace/merge semantics, category narrowing, body-surface skips with the query surface still active, exclusion-scoping and bypass precedence, and config/route with() immutability) registered in the Makefile RUNNERS list and the CI workflow.

Deferred

  • Engine-level scheduling for route opt-in with the global flag off: the PHP pipeline is built without a route registry (adapters attach state->routeConfig per request), so CheckFactory::buildChecks is called without route configs and a route enabling detection alone cannot schedule the statically-built check. The route-aware appliesTo gate is implemented and pinned at the factory level, mirroring how the geo rate limit surface handled the same shape.

Verification

  • Full suite green on PHP 8.3 (Docker, php:8.3-cli + throwaway redis:7-alpine), mirroring the CI workflow step for step: composer install, make lint (LINT_OK + composer validate --strict), make test exit 0 over every runner, and the new bin/test_route_detection_exclusions.php 37/37.

Behavior rules

Added

  • Behavior rules: the engine now runs the reference behavioral surface instead of leaving it out. Ported from guard_core _security_config_fields.py (the behavior fields), _security_config_field_validators.py (BehaviorRuleConfig and the fail-closed scan validation), handlers/behavior_handler.py (sliding windows), handlers/_behavior_action_dispatch.py, handlers/_behavior_response_pattern.py, handlers/_behavior_json_pattern.py and core/behavioral/processor.py, structurally following guard-core-go #26. Config surface: globalBehaviorRules (rules applied to every route, in addition to any route-specific rules), behaviorScanResponseBody (default off: zero behavior change unless enabled) and behaviorMaxResponseBodyInspectBytes (default 262144, the reference ge/le bounds 1024..10485760, a configured 0 normalizes to the default). Rule validation mirrors the pydantic constraints fail-closed at construction: rule_type in usage/return_pattern/frequency, threshold >= 1, window default 3600 (a configured 0 falls back), action in ban/log/throttle/alert defaulting to log, ban_duration >= 1 or null, and the reference's fail-closed return_pattern check: a body-reading (non-status:) pattern with the scan flag off fails construction with the reference message, because the rule would silently never match; with() re-validates the copy. Route surface: RouteConfig accepts behaviorRules (the reference route_config.behavior_rules), entries failing the constraints silently dropped (decorator-time leniency like the other route maps). Request side: GuardEngine::execute runs usage/frequency rules on requests the pipeline allowed (the reference's process_usage_rules), tracking through sliding windows that are strictly-greater than the threshold, backed by Redis when enabled over the shared key layout (behavior_usage/behavior_returns namespaces with SHA-256 identity-segment hashing exactly like _hash_identity_segment, so the Python and PHP engines share keys) and by bounded local maps otherwise (10,000 endpoint buckets and client rows, oldest-tracked eviction standing in for Python's LRU), failing closed on Redis errors unless redisFailOpen. Response side: the new GuardEngine::processResponse($request, $response) runs the route's and the config's return_pattern rules for an adapter's outgoing pass-through response, mirroring the reference response factory's behavioral phase; return rules never modify the response. Pattern surface: status:<code> matches on status_code alone, json:<path>==<expected> walks decoded JSON with case-insensitive comparison and a [] segment matching any array element, regex:<pattern> matches case-insensitively, and a bare substring matches case-insensitively; body patterns read at most the leading behaviorMaxResponseBodyInspectBytes of the response body and are only evaluated when behaviorScanResponseBody is on. Action dispatch mirrors _behavior_action_dispatch: passive mode only logs the [PASSIVE MODE] lines, active mode bans with the rule's ban_duration or the 3600s fallback (reason behavioral_violation), alerts, or logs at the log_suspicious_level. correlate_with_detection halves the effective threshold for global return rules (min 1) while the IP has prior detection-category hits, driven by a new shared per-IP per-category suspicious-count store the suspicious_activity check writes (mirroring ...
Read more

v4.0.4: First stable release, full parity with guard-core 4.0.4

Choose a tag to compare

@rennf93 rennf93 released this 23 Sep 23:14
v4.0.4
87992d8

v4.0.4 (2026-09-24)

First stable release, full parity with guard-core 4.0.4 (v4.0.4)

Added

  • Full parity with the Python guard-core 4.0.4 engine: all 17 security checks ported and passing. The behavioral WAF engine now matches the reference engine check-for-check (route config, emergency mode, HTTPS enforcement, request logging, request size and content, required headers, authentication, referrer, custom validators, time window, cloud IP refresh, IP security, cloud provider, user agent, rate limit, suspicious activity, custom request).
  • Binary-noise gates over the detection engine, including the SQLi comment-terminator binary gate. Noise-prone pattern matches inside binary body content are discarded before reporting, and SQLi patterns that terminate on comment-like byte sequences are explicitly gated in binary bodies, closing the last detection divergence against the reference corpus.
  • Conformance corpus harmonized to spec 4.0.3. The fixture corpus (tests/Conformance/) carries the reference engine's expected values, including binary-body vectors, and must never be hand-edited.

Changed

  • First stable release on Packagist. The package installs as rennf93/guard-core-php (PHP 8.2+, platform-only dependencies: pcre, mbstring, json); the version is derived from the git tag, and this v4.0.4 tag is the first stable, non-pre-release publish. The earlier v0.1.0 tag was a burned pre-release snapshot and is superseded by this release.

Verification

  • Interop harness against the reference engine: 82/82 vectors passing.
  • Full suite: the eight bin/ runners plus bin/conformance.php across PHP 8.2, 8.3 and 8.4 (CI and the Release Gate workflow at the tag).