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.
Option 1 — downloadable zip (recommended for most WP hosts)¶
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"
}
}
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-Typematches 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.
Links¶
- Repository: github.com/metrictower/funnypot-wordpress
Packagist
Not yet published — see Install for the VCS-repository workaround. Check back or watch the repository for the Packagist listing.