Skip to content

funnypot-wordpress

metrictower/funnypot-wordpress is a thin WordPress adapter over the funnypot-policy decision engine. It doesn't decide whether a request is an attack, whether to deceive, block, or report — that's the shared policy engine's job. The plugin only normalises the incoming WP request into a neutral RequestEvidence plus a WpSiteProfile (the real-route oracle that keeps a fake page from ever colliding with a genuine WordPress URL), asks the policy engine for a Decision, and executes it: allow/log lets WordPress proceed, deceive emits funnypot-core's byte-exact fake before the theme loads, and block emits an honest, app-chosen 403.

Reach for this package when you're protecting a WordPress site and want scanner traffic upgraded from a plain 404 into a believable fake — or optionally blocked and reported to the funnypot mainnet reputation service — without writing any detection logic yourself.

Inert by default

A fresh install decides nothing until an operator turns on a posture from Settings → Honeypot. Reputation checking and abuse reporting are each off, and both require a mainnet key.

Install

PHP 7.3 or newer (old WP hosts run old PHP), WordPress 5.5 or newer.

Not yet on Packagist

metrictower/funnypot-wordpress is public on GitHub but is not yet registered on Packagist — a plain composer require metrictower/funnypot-wordpress will not resolve. Use one of the two install paths below instead.

Clone or download the repository, then build the distributable zip (this bundles funnypot-policy, funnypot-core, and funnypot-mainnet-client into vendor/ for you):

git clone https://github.com/metrictower/funnypot-wordpress.git
cd funnypot-wordpress
bash bin/build.sh

Upload the resulting build/funnypot-wordpress.zip under Plugins → Add New → Upload Plugin in wp-admin, then activate it.

Option 2 — Composer, via a VCS repository

Point Composer straight at the GitHub repo and pin a tagged release, never a branch (see Versioning):

{
    "repositories": [
        { "type": "vcs", "url": "https://github.com/metrictower/funnypot-wordpress" }
    ],
    "require": {
        "metrictower/funnypot-wordpress": "^0.2"
    }
}
composer require metrictower/funnypot-wordpress

The package declares "type": "wordpress-plugin" but does not depend on composer/installers, so a plain composer require drops it under vendor/metrictower/funnypot-wordpress/ — it does not place or activate itself in wp-content/plugins/. See Usage below for wiring it up from a Composer-managed WordPress install (e.g. Bedrock-style layouts).

On activation (either path) the plugin creates its tables, generates a per-install sensor_id, and copies a must-use loader shim into wp-content/mu-plugins/ so its earliest hook runs before any theme or other plugin. If that directory isn't writable it falls back to the plugins_loaded hook and raises an admin notice instead of failing.

Usage

Once installed and activated, the plugin wires itself up — you don't call anything from this package in your own theme or plugin code for normal use. The two places you do touch code are a manual Composer bootstrap (if you skipped the zip's standard WP activation flow) and wp-config.php constants for the mainnet key.

Composer installs: a minimal bootstrap file

If you installed via Option 2 above and your WordPress setup doesn't auto-discover plugins under vendor/, add a small wrapper file under wp-content/plugins/funnypot-wordpress/ (a plugin header is required for WordPress to list and activate it) that hands off to the real bootstrap:

<?php
/**
 * Plugin Name: funnypot for WordPress
 */

require __DIR__ . '/vendor/autoload.php'; // or your project's shared autoloader

if (class_exists(\Funnypot\WordPress\Plugin::class)) {
    \Funnypot\WordPress\Plugin::register(__FILE__);
}

Activate it from Plugins in wp-admin like any other plugin. Plugin::register() is exactly what the zip's own plugin file calls on ordinary load — it registers the request hooks, the admin screen, WP-CLI (when running under wp), and the activation/deactivation callbacks.

Configure

Everything else is configured from Settings → Honeypot in wp-admin: posture (honeypot / WAF / both), response style, severity ceiling, reputation checking, country policy, and reporting. See the repository README for the full field list.

wp-config.php constants

Two constants override the stored settings for the mainnet reputation/reporting connection — handy for keeping a key out of the database, or for per-environment overrides:

define('HONEYPOT_WP_MAINNET_BASE_URL', 'https://mainnet.example'); // scheme + host only
define('HONEYPOT_WP_MAINNET_KEY', '...');                          // a sensor-tier key

Reporting and reputation checking are both inert without a key. The single key is a mainnet sensor-tier key carrying both report rights and a reputation-check quota.

WP-CLI

Activating the plugin under wp registers a wp honeypot command (implemented by Funnypot\WordPress\Cli\HoneypotCommand), including a status summary that reports the enabled state, posture, configured vs. verified request-hook mount (so a wiped loader shim is visible rather than silently demoting protection), response style, reputation/reporting state, and report queue depth. Run wp help honeypot after activation for the exact command set your installed version ships.

WP-Cron caveat

WP-Cron only fires on incoming traffic. On a low-traffic site the report queue drain and any reputation-mirror refresh can stall between visits — if you enable reporting or checking, disable WP-Cron (define('DISABLE_WP_CRON', true);) and drive the drain from a real system cron instead. See the repository README for the exact cron lines.

Public API

The plugin is designed to be configured, not coded against — most integrators never call any of these directly. They're documented for advanced setups (a custom Composer bootstrap, or code that needs to read the plugin's resolved settings).

Class Members Purpose
Funnypot\WordPress\Plugin register(string $file): void The bootstrap entry point. Wires the WordPress hooks, admin screen, WP-CLI, and activation/deactivation callbacks. Called automatically from the plugin's main file on ordinary load — call it yourself only from a custom bootstrap file (see Usage).
Funnypot\WordPress\Settings Settings::fromArray(array $raw): Settings; enabled(), posture(), responseStyle(), severityCeiling(), checkEnabled() / checkActive(), reportEnabled() / reportingActive(), mainnetKey(), mainnetBaseUrl(), countryPosture(), toArray() Immutable value object over the stored honeypot_wp_settings option (env constants win over the stored value). Constants: POSTURE_HONEYPOT, POSTURE_WAF, POSTURE_BOTH; CONST_BASE_URL / CONST_KEY name the two wp-config.php overrides above.
Funnypot\WordPress\Version Version::STRING The installed plugin version, for code that wants to pin behaviour to it.

Security invariants

  • Fail-safe to allow, never a 5xx. Any policy, evaluator, or storage fault degrades to "WordPress proceeds" — a 500 is itself a tell that gives away the honeypot.
  • Only ever upgrades a genuine 404. The fallback position fires only on is_404(), and the real-route oracle keeps a fake from colliding with an actual WordPress route.
  • Response Content-Type matches the request; status is always app-chosen, never model-chosen — so there's no model-driven redirect.
  • Reporting is key-gated and self-guarded: inert without a mainnet key, never reports the operator's own configured IPs, and only reports public-routable addresses.

Packagist

Not yet published — see Install for the VCS-repository workaround. Check back or watch the repository for the Packagist listing.