Skip to content

funnypot-laravel

metrictower/funnypot-laravel is the Laravel adapter for funnypot: a thin service-provider + middleware layer over funnypot-policy, the position-blind decision engine. It normalises an incoming Illuminate\Http\Request, asks the policy engine's PolicyEngine for a Decision, and executes it — allow, log, block, or deceive. The package owns no decision logic itself; the same decision matrix runs identically here, in the WordPress adapter, and in the standalone app.

Reach for this package when you're protecting a Laravel application (8 through 12) and want either an automatic 404 → believable-fake upgrade, a before-the-router detection pass, or just a one-line detection call for an app that already owns its own response.

Install

composer require metrictower/funnypot-laravel
  • PHP: 8.0 or newer
  • Laravel: illuminate/* 8.0 – 12.0
  • The service provider (Funnypot\Laravel\FunnypotServiceProvider) is auto-discovered — nothing to register by hand.
  • Installing this package pulls in the three siblings it adapts: funnypot-core, funnypot-policy, and funnypot-mainnet-client.

Publish the config file:

php artisan vendor:publish --tag=funnypot-config

Add a funnypot log channel to config/logging.php so enforcement/telemetry logging has somewhere to go:

'funnypot' => ['driver' => 'single', 'path' => storage_path('logs/funnypot.log'), 'level' => 'debug'],

Usage

The package never forces itself into your app — you opt in per position.

NOT_FOUND position — upgrade your 404s

Add the responder as your fallback route, after your real routes, in routes/web.php:

use Funnypot\Laravel\FallbackResponder;

Route::fallback([FallbackResponder::class, 'handle']);

Every unmatched path (a scanner probing /wp-login.php, /.env, …) is now upgraded from a plain 404 to a believable fake — false-positive-free by construction, since the counterfactual was already a 404.

BEFORE position — evaluate real routes

Push the funnypot middleware onto a group or the global stack. In bootstrap/app.php (Laravel 11+):

->withMiddleware(function (Middleware $middleware) {
    $middleware->appendToGroup('web', \Funnypot\Laravel\HoneypotMiddleware::class);
    // or: $middleware->alias(['funnypot' => \Funnypot\Laravel\HoneypotMiddleware::class]);
})

Positions are a config choice

Which position(s) the engine actually evaluates is controlled by the posture config key, not by which of the snippets above you add — see Configuration below.

Detection only — for apps that own their response

If your app already has its own 404 handler or response pipeline, call detection directly through the facade instead of installing either position:

use Funnypot\Laravel\Facades\Funnypot;

return Funnypot::handleRequest($request) ?? $myOwn404;

handleRequest() returns funnypot's byte-exact fake (deceive) or an honest block for a probe, and null when the request looks clean — in which case you serve your own response. Always return the result in Laravel; only pass $die = true from a raw-PHP entry point with no framework to return into.

For more control, use inspectRequest() and act on the result yourself:

$result = Funnypot::inspectRequest($request);

if ($result->isSuspicious()) {
    return $result->toResponse();
}
// $result->action(), $result->reason(), $result->decision() are available too.

The facade is caller-decides

Funnypot::handleRequest() / toResponse() are the enforce action — they always serve the fake/block when the underlying decision calls for it. They are not gated by the enforcement config, which only governs the installed HoneypotMiddleware and FallbackResponder. For an observe-only integration, call inspectRequest() and act on isSuspicious() without calling toResponse().

A clean/allow verdict (or a detection fault) comes back as isClean() — treat that as "no opinion," never as proof the request is safe.

Enforcement modes

Independently of which position is wired in, enforcement decides — per position — whether the adapter performs funnypot's decision or only watches it. Modes are the string constants on Funnypot\Laravel\Enforcement:

Mode Constant Behaviour
enforce Enforcement::ENFORCE Serve the fake or the block.
observe Enforcement::OBSERVE Detect + report + log the withheld action, then pass through — your app owns the response.
off Enforcement::OFF Never evaluate (a per-position kill switch).
'enforcement' => [
    'before'    => Funnypot\Laravel\Enforcement::OBSERVE,  // default: watch, never block on install
    'not_found' => Funnypot\Laravel\Enforcement::ENFORCE,  // default: deceive 404s
],

Defaults are safe-by-default: a fresh install only watches + logs on the before position and never silently starts blocking real traffic. Reporting fires in every mode when the request was judged malicious — observe withholds only the response, never the report.

Configuration

config/funnypot.php is a Laravel front-end that produces the policy engine's config array. Highlights:

Key Purpose
posture honeypot (default, deceive on 404) · WAF (block on the before position) · both. Selects which position(s) the engine evaluates.
enforcement.before / enforcement.not_found Per position: enforce | observe | off — see above.
response_style realistic (default) | minimal | taunt — how a fake looks.
mainnet.base_url / mainnet.key The reputation/report service address + your operator-issued key. An empty key makes reporting, the reputation check, and mirror sync all inert.
check.enabled Off by default — the opt-in reputation gate (spends credits, sends the visitor IP to a third party).
mirror.enabled The local reputation mirror, kept warm by funnypot:mirror-sync.
reporting.self_ips Your own egress/test IPs — list these before enabling reporting from a host that also runs scans.
state.cache_store The Laravel cache store backing local state. Use a persistent, multi-node-safe store (redis / database / memcached) in production, not a per-node file.

The published file is fully commented — treat it as the reference for every key.

Artisan commands

Command Purpose
funnypot:rules-update Fetch, verify, and hot-swap a signed rules release.
funnypot:update <templates> Recompile the template index.
funnypot:mirror-sync Pull the reputation mirror artifact from the mainnet service.
funnypot:report-drain Deliver report rows parked when the queue connection is synchronous.

Typically scheduled in routes/console.php:

Schedule::command('funnypot:mirror-sync')->hourly();
Schedule::command('funnypot:report-drain')->everyFiveMinutes();
Schedule::command('funnypot:rules-update')->daily();

Public API

Everything a consumer calls lives under the Funnypot\Laravel\ namespace.

Class / facade Members Purpose
Funnypot\Laravel\Facades\Funnypot handleRequest(Request $request, bool $die = false): ?Response, inspectRequest(Request $request): InspectionResult, inspect(Request $request): ?Decision The detection facade for apps that own their own response.
Funnypot\Laravel\InspectionResult isSuspicious(): bool, isClean(): bool, action(): string, reason(): string, decision(): ?Decision, toResponse(): ?Response Result DTO from inspectRequest().
Funnypot\Laravel\HoneypotMiddleware handle(Request $request, Closure $next) The funnypot middleware alias — the BEFORE position.
Funnypot\Laravel\FallbackResponder handle(Request $request): Response Wired as Route::fallback() — the NOT_FOUND position.
Funnypot\Laravel\Enforcement OFF, OBSERVE, ENFORCE constants; values(), isValid(string $v), normalize($value) Per-position enforcement-mode constants used in config.
Funnypot\Laravel\FunnypotServiceProvider Auto-discovered; merges config, binds the policy engine + its ports, registers the middleware alias and Artisan commands.

InspectionResult::decision() and Funnypot::inspect() return a Funnypot\Policy\Decision from the underlying funnypot-policy engine.

Safety invariants

  • The engine only ever upgrades a 404 — any mapper/port/evaluate fault degrades to pass-through (or your app's own 404), never a 500.
  • The response mapper copies the fake's status, Content-Type, and headers verbatim; status is always app/engine-chosen, never model-chosen.
  • The reputation check fails open and is never a synchronous network call on the request path.