metrictower / funnypot-core
funnypot core engine: turn a scanner's own nuclei detection template into a matching fake-vulnerable HTTP response. Pure-PHP runtime, inert by default.
Requires
- php: >=8.0
Requires (Dev)
- nyholm/psr7: ^1.8
- phpunit/phpunit: ^9.5
- psr/http-factory: ^1.1
- psr/http-message: ^2.0
- psr/http-server-middleware: ^1.0
- symfony/yaml: ^5.4 || ^6.0
Suggests
- ext-openssl: Required only for the pure-PHP SSH honeypot server (Protocol\Ssh): the aes256-ctr transport cipher.
- ext-sodium: Required only for the pure-PHP SSH honeypot server (Protocol\Ssh): curve25519 key exchange + ed25519 host key.
- illuminate/support: Required only to use the Laravel bridge (Laravel\FunnypotServiceProvider).
- psr/http-factory: Required only to use the PSR-15 adapter (response/stream factories passed to Http\HoneypotMiddleware).
- psr/http-message: Required only to use the PSR-15 adapter (Http\PsrRequestMapper / Http\PsrResponseMapper).
- psr/http-server-middleware: Required only to use the PSR-15 adapter (Http\HoneypotMiddleware).
- symfony/yaml: Required only to compile templates (bin/funnypot). Runtime needs PHP alone.
This package is auto-updated.
Last update: 2026-08-20 10:15:15 UTC
README
Not sure you're in the right place?
- Want a ready-to-run honeypot box to deploy → funnypot
- Protecting a Laravel app → funnypot-laravel
- Protecting a WordPress site → funnypot-wordpress
- Embedding the deception/detection engine in your own PHP / PSR-15 app → funnypot-core ← you are here
- Querying / reporting to the IP-reputation service from code (the SDK) → funnypot-mainnet-client
- Building on the low-level decision/policy engine → funnypot-policy
The HTTP deception engine behind funnypot. It answers a scanner's probe with the fake-vulnerable response the scanner was fishing for. It is the inverse of a nuclei scan: instead of sending a probe and reading the reply to decide "this host is vulnerable", it reads an incoming probe and writes the reply that satisfies the scanner's own matcher. The scanner walks away with a full, coherent, wrong vulnerability report while you log every move.
This is the reusable PHP library. Drop it into any Laravel or PSR-15 app and its 404s start answering scanners with believable decoys. Runtime is pure PHP: no YAML, no extensions, no network. It is inert by default (detect only); respond mode is opt-in and gated by your own suspicion signal.
Want to run a honeypot, not embed one? The standalone app builds on this package and adds a live dashboard, a pure-PHP SSH server, a fake shell, and 18 TCP service emulators: github.com/metrictower/funnypot.
What it does
- Nuclei inversion. Compiles the upstream nuclei-templates
corpus and inverts each detection template into a response that satisfies its matcher. From 11,196
HTTP templates it indexes about 6,300 invertible ones into roughly 5,100
(method, path)route personas. - Attack-class emulators. Reflects LFI, SQLi, command injection, SSTI, XXE, shellshock, Struts
OGNL, open redirect, reflected XSS and cloud-IMDS probes on any path, with canned inert markers
(
root:x:0:0,uid=0(root)). - CRS-broadened coverage. Recall for the generic attack classes (SQLi/XSS/LFI/RCE) is widened
from the upstream OWASP CoreRuleSet: its portable
PL1 rules are aggregated into one broadened match per class, behind funnypot's SAME response
archetype. It never invents a per-rule response and never touches the nuclei corpus — see
docs/CRS.md. - Product and route decoys. Believable
.git/config,.env,wp-config,phpinfo,.htpasswd,server-status, SSH keys, SQL dumps, phpMyAdmin and more. - Anti-fingerprint. One coherent product persona per attacker (deterministic, spoof-proof seed)
instead of an impossible "vulnerable to everything" host. Consistent
X-Powered-By, tamper-evident honeytoken cookie.
Install
composer require metrictower/funnypot-core
Detect mode (always safe)
Detect never writes to the wire. It just tells you a request is a known scanner probe:
use Funnypot\Honeypot; use Funnypot\RequestContext; $funnypot = Honeypot::default(); // inert: detect-only, gate closed $detection = $funnypot->detect(RequestContext::fromGlobals()); if ($detection->matched) { logScannerProbe($detection->templateIds(), $detection->highestSeverity, $detection->tags()); }
Respond mode (opt-in, gated)
use Funnypot\Config; use Funnypot\Honeypot; use Funnypot\RequestContext; use Funnypot\Http\ResponseEmitter; $funnypot = Honeypot::default(new Config( mode: 'respond', gate: fn (RequestContext $r) => isSuspicious($r), // your suspicion predicate; null = closed responseStyle: 'realistic', // minimal | realistic | taunt attackEmulation: true, // also reflect LFI/SQLi and friends )); $response = $funnypot->respond(RequestContext::fromGlobals()); if ($response !== null) { ResponseEmitter::emit($response); // a matched probe gets an inert fake exit; } // nothing matched: serve your normal 404
Using Laravel?
Use funnypot-laravel
(composer require metrictower/funnypot-laravel) — the ServiceProvider + middleware drop-in. This
repo is the framework-agnostic engine.
Any other framework (PSR-15)
Wire the engine directly. A PSR-15 middleware (Funnypot\Http\HoneypotMiddleware) sends matched
probes an inert fake and passes everything else through, so your app serves its own 404 on a miss.
Start detect-only, watch the logs, then set mode = respond and supply a gate. Step-by-step
rollout: docs/INTEGRATION.md.
Response styles
Set at init with responseStyle:
| Style | What the attacker gets |
|---|---|
minimal |
Just the tokens the matcher needs. Smallest. The default. |
realistic |
A believable fake: a full .git/config, a plausible .env, a real XML-RPC methodResponse. All values inert. |
taunt |
Still satisfies the scanner, and carries a visible "honeypot, your scan was logged" marker. |
Rich content is validated against the matcher before use. If a richer body would not satisfy the scanner it falls back to minimal, so richness can never break the guarantee.
How it works
Templates are compiled once, at build time, into frozen PHP arrays (resources/compiled/*.php). The
app loads them into opcache and serves with a single O(1) lookup. A miss returns null so your app
serves its own 404. symfony/yaml is only needed by the compiler (bin/funnypot compile), which CI
runs weekly against the latest nuclei-templates release. See SPEC.md and
docs/PERSONA-CAP.md.
Response precedence
respond() decides what to serve in a fixed order — an earlier tier always wins:
- Nuclei-exact (tier 1). The request routes to a compiled nuclei template or route decoy → a byte-exact response derived from what that scanner probes for.
- CRS-generic / attack-class (tier 2). No route matched →
TemplateAttackEmulatoremulates a generic attack class (hand-authored rules first, then the CRS-broadened alternation) from a hand-authored response archetype. - LLM fake, then plain 404 (tiers 3–4, app layer).
So a request matching BOTH a nuclei template AND a CRS attack class always gets the nuclei-exact
response — nuclei-exact beats CRS-generic. CRS is a coverage multiplier for tier 2, never a
tier-1 source. Full detail, and how to regenerate (bin/funnypot compile-crs), in
docs/CRS.md.
Runtime rule updates (no composer update)
Rules move roughly weekly (nuclei-templates tags, CRS releases). Instead of a composer update
per change, a honeypot can fetch signed rule releases at runtime and hot-swap them:
funnypot rules:update --data-dir=/var/lib/funnypot/rules # fetch, verify, atomic swap funnypot rules:status --data-dir=/var/lib/funnypot/rules funnypot rules:rollback --data-dir=/var/lib/funnypot/rules # network-free, to a retained release
Laravel: schedule php artisan funnypot:rules-update (config block funnypot.rules). With no
data_dir configured, nothing changes — the engine loads only the bundled artifacts, exactly as
before. Because the compiled artifacts are required PHP, an update is verified in depth before
anything is loaded: an ed25519 signature against a public key vendored inside this package
(never fetched), a per-file sha256 cross-check, a pure-array-literal proof of every .php
(no code can execute on load), a ReDoS budget on every regex, a fingerprint-leak re-scan, and
an anti-blinding coverage floor. Any failure keeps the current rules — the honeypot never
serves empty. Full mechanism, trust model, and operator runbook:
docs/RULES-UPDATE.md.
Safety
funnypot can only mislead an attacker, never help one.
- Emulate output, never execute input. No
exec/eval, no real filesystem, no outbound socket. - Reflect, never harm. No bombs, no retaliation, no outbound requests. All responses size-capped
(
maxBodyBytes, default 64 KB). - Never reflects attacker input, never deserializes a request body. Every synthesized header is CRLF/NUL-safe.
- Inert by default. A fresh install is detect-only with the gate closed. A layered gate then guards respond mode: kill switch, mode, trusted bypass, suspicion gate, severity ceiling, coherent persona, body-size cap.
- Inert fakes only.
example.comhosts, RFC-5737 IPs, obviously-fake keys. Never a real secret.
Testing
composer install vendor/bin/phpunit # unit + compiler suite bash tests/acceptance/run.sh # real nuclei (Docker) vs a php -S server (golden test)
Licence
MIT, see LICENSE. Derived in part from
projectdiscovery/nuclei-templates
(MIT, © 2025 ProjectDiscovery, Inc.); the upstream notice is kept at
resources/UPSTREAM-LICENSE.md. The CRS-broadened attack
templates are derived from OWASP CoreRuleSet
(Apache-2.0); its separate notice and statement of changes are at
resources/UPSTREAM-LICENSE-CRS.md. A CI license gate
(scripts/ci/check-license.sh, SPDX allow-list) enforces this on every upstream refresh.