
Passive Laravel middleware that detects and logs SQL injection, XSS, RCE, bot scanners, and 175+ attack patterns. Features a built-in dashboard, Slack alerts, REST API, and geo-enrichment. IDS, not WAF.
<p align="center">
<a href="https://packagist.org/packages/jayanta/laravel-threat-detection"><img src="https://img.shields.io/packagist/v/jayanta/laravel-threat-detection.svg?style=flat-square" alt="Latest Version"></a>
<a href="https://github.com/jay123anta/laravel-threat-detection/actions/workflows/tests.yml"><img src="https://img.shields.io/github/actions/workflow/status/jay123anta/laravel-threat-detection/tests.yml?branch=main&style=flat-square&label=tests" alt="Tests"></a>
<a href="https://github.com/jay123anta/laravel-threat-detection/actions/workflows/tests.yml"><img src="https://img.shields.io/badge/PHPStan-level%205-brightgreen?style=flat-square" alt="PHPStan Level 5"></a>
<a href="https://github.com/jay123anta/laravel-threat-detection/actions/workflows/tests.yml"><img src="https://img.shields.io/badge/code%20style-Pint-orange?style=flat-square" alt="Code Style Pint"></a>
<a href="https://packagist.org/packages/jayanta/laravel-threat-detection"><img src="https://img.shields.io/packagist/dt/jayanta/laravel-threat-detection.svg?style=flat-square" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/jayanta/laravel-threat-detection"><img src="https://img.shields.io/packagist/php-v/jayanta/laravel-threat-detection?style=flat-square" alt="PHP Version"></a>
<a href="https://github.com/jay123anta/laravel-threat-detection/blob/main/LICENSE"><img src="https://img.shields.io/packagist/l/jayanta/laravel-threat-detection.svg?style=flat-square" alt="License"></a>
</p>
# Laravel Threat Detection
**Security monitoring and attack logging for Laravel. Detect and log SQL injection,
XSS, RCE, directory traversal, bot scanners and `/wp-admin`-style recon probes —
every hostile request recorded to your database with full application context.
It's an IDS, not a WAF: it never blocks, filters, or modifies a request.**
<p align="center">
<img src="https://raw.githubusercontent.com/jay123anta/laravel-threat-detection/main/art/demo.gif" alt="Install the package, send three attacks — SQL injection, directory traversal, XSS — every one returns HTTP 200 because nothing is blocked, and all three are already counted in threat-detection:stats" width="100%">
</p>
### Are you here because you saw something like this?
```
GET /wp-admin/setup-config.php 404 — on a site that isn't WordPress
GET /.env 404 — someone wants your database password
GET /?id=1' UNION SELECT password FROM 200 — SQL injection against a real route
GET /phpmyadmin/index.php 404 — scanning for an admin panel
```
Those requests are already reaching your Laravel app. Your access log shows the URL
and the status code, and nothing else — not the decoded payload, not which of your
routes was targeted, not whether the same IP has tried forty other things this hour.
This package answers those questions. Drop it into any Laravel 10–13 app and it starts
scanning every HTTP request against 150+ attack patterns, scoring each match by
confidence and writing it to your database — with a built-in dashboard, Slack alerts,
geo-enrichment, and fail2ban/blocklist exports. No request is ever blocked. Think
security camera, not a lock: it shows you exactly who's probing your routes, how
often, and with what techniques.
> Extracted from a production app and battle-tested on real traffic. 2,400+ tests, no runtime
> dependencies beyond Laravel itself, and no internet connection required for detection.
>
> Upgrading? See [UPGRADING.md](https://github.com/jay123anta/laravel-threat-detection/blob/main/UPGRADING.md). Contributing? See [CONTRIBUTING.md](https://github.com/jay123anta/laravel-threat-detection/blob/main/CONTRIBUTING.md).
## Get started in under a minute
```bash
composer require jayanta/laravel-threat-detection
php artisan vendor:publish --tag=threat-detection-migrations
php artisan migrate
```
Then add the middleware to your `web` group (one line in `bootstrap/app.php` on Laravel 11+,
or `app/Http/Kernel.php` on Laravel 10) — full snippet in [Quick Start](#quick-start) below.
That's it; detection is live.
```bash
php artisan threat-detection:doctor # confirms it is actually recording
```
---
## Where it fits: IDS vs WAF vs edge
This package is a **passive, application-level IDS** — it watches and records, it doesn't
block. It's meant to sit *alongside* a WAF or edge service, not replace one. Each layer sees
something the others can't:
| | **This package** (app IDS) | **WAF** (ModSecurity + OWASP CRS, Cloudflare WAF) | **Edge / CDN** (Cloudflare) |
|---|:---:|:---:|:---:|
| Blocks malicious requests | ❌ logs only | ✅ | ✅ |
| Full app context (exact URL, decoded payload, authenticated user) | ✅ | ⚠️ partial | ❌ |
| Built-in dashboard + threat log in your DB | ✅ | ⚠️ varies | ⚠️ edge only |
| App-specific detections (e.g. Aadhaar / PAN / IFSC PII) | ✅ built in + custom | ⚠️ write your own rules | ⚠️ custom rules, plan-dependent |
| Works offline / no external service | ✅ | ⚠️ depends | ❌ |
| Stops traffic before it reaches your server | ❌ | ⚠️ cloud WAF only | ✅ |
| Setup | `composer require`, migrate, one middleware line | medium–high | low–medium |
| Cost | free, MIT | varies | free tier + paid |
**The short version:** an edge/WAF is your lock on the door; this is the security camera
*inside*, with the app context to tell you exactly what's being tried on which route, by
whom, and how often. Use it to feed real decisions — fail2ban bans, rate limits,
geo-blocking — with data your edge layer never sees.
### What it deliberately is NOT
- **Not a WAF.** It never blocks, filters, or modifies a request. Use Cloudflare,
mod_security, or a real WAF for enforcement. (No edge layer to hand off to? The
[operator-side helpers](#acting-on-the-data-operator-side-blocking) expose the
package's decisions so you can write your own five-line blocking middleware —
the enforcement code stays yours, not the package's.)
- **Not a replacement for secure coding.** Parameterized queries, input validation, and
output escaping are your actual defenses. This package assumes your code is already
secure and gives you *visibility*, not protection.
- **Not an edge service.** If you can put Cloudflare in front, do — then add this for the
application-level detail edge services can't see.
- **Not a complete detector, and it can't be.** Pattern matching catches attacks that
*look like* known attacks. A novel technique, or a familiar one rewritten enough, will
pass through unlogged — and you will not be told that it did. Silence here means
"nothing matched", never "nothing happened". Where it earns its place is the
high-volume, low-effort traffic that makes up most of what actually hits a public
Laravel app: scanners, recon probes, off-the-shelf injection strings, credential
sprays. Treat a quiet log as an absence of evidence, not evidence of absence.
### Expect it to flag your own content on day one
An untuned install fires on legitimate content, and you should know that before
you install rather than after. These are measured, not hypothetical. The rows
below are a sample of the floor pinned by
[`LegitimateTrafficCorpusTest`](https://github.com/jay123anta/laravel-threat-detection/blob/main/tests/Feature/LegitimateTrafficCorpusTest.php);
each one lists *everything* that request logs, and
[`ReadmeNoiseFloorTest`](https://github.com/jay123anta/laravel-threat-detection/blob/main/tests/Feature/ReadmeNoiseFloorTest.php) fails the
build if any row stops matching what the suite measures for it.
<!-- noise-floor:start -->
| Perfectly legitimate request | What an untuned install logs |
|---|---|
| `how to write a UNION SELECT in postgres` typed into a search box <!-- case: sql tutorial in a search box --> | `SQL Injection UNION` / high |
| A blog post containing `<script>window.dataLayer=[];</script>` <!-- case: javascript snippet in a blog post --> | `XSS Script Tag` / high · `Command Chain Injection` / medium |
| A support ticket with a pasted `SELECT * FROM users WHERE id = 1` error <!-- case: a pasted sql error in a support ticket --> | `SQLi Variant` / high · `SQL SELECT Query` / low |
| Documentation explaining that `../../etc/passwd` is the classic traversal payload <!-- case: a traversal explainer in documentation --> | `Directory Traversal` / medium · `Sensitive File Access` / medium |
| A profile form collecting a genuine Indian mobile number and PAN <!-- case: a profile form collecting genuine indian pii --> | `PAN Number Detected` / high · `Bank Account Number Detected` / high · `Mobile Number Detected` / low |
<!-- noise-floor:end -->
**None of these are bugs.** A blog post containing `<script>` is, byte for byte, a
stored-XSS payload; a search for `UNION SELECT` is indistinguishable from an attempt
at one; a ten-digit mobile number is also a plausible bank account number. Nothing
but application context separates them, and no pattern engine can supply that
context for you.
Supplying it is a one-line config change: name the field that legitimately carries
such content in `safe_fields` (or `safe_paths`, for one path inside nested JSON) and
it is no longer scanned. That is the switch that silences the rows above — and it is
a real trade, since an attack delivered through that field goes unseen too.
`content_paths` and `relaxed` mode are different tools. They drop low- and
medium-severity noise, and **deliberately keep recording high-severity matches** —
so they will not quiet the high rows in this table, by design. See
[Reducing False Positives](#reducing-false-positives) for which to reach for when.
**If your app accepts rich text, code samples, or search queries, do that before you
judge the output.** The default is deliberately noisy-but-honest rather than
quiet-and-incomplete: it is easier to silence a known match than to discover one that
never fired.
### So what do you actually do with it?
The most common question about a detector that never blocks. Four answers, in
increasing order of effort:
| You want to | Use | Effort |
|---|---|---|
| See what's hitting you | The [dashboard](#dashboard) or `threat-detection:stats` | none, it's already running |
| Ban repeat offenders at the firewall | [`threat-detection:export-fail2ban`](#artisan-commands) — pipe to a cron | one line |
| Deny at the web server | [`threat-detection:export-blocklist`](#artisan-commands) → nginx/apache directives | one line |
| Refuse requests in-app | [Operator-side helpers](#acting-on-the-data-operator-side-blocking) — `isBlocklisted()`, `isDdosThresholdExceeded()` | ~10 lines of your own middleware |
| React in real time | The [`ThreatDetected` event](#threatdetected-event) — Telegram, SIEM, PagerDuty | a listener |
The package supplies the intelligence; you supply the refusal. That split is
deliberate — enforcement code that lives in your app is code you can read,
test and turn off, and it means a detection bug can never take your site down.
### How it compares to other Laravel security packages
These solve different problems and compose well — the table is about picking the
right tool, not winning.
| Package | What it does | Blocks? | Use it when |
|---|---|:---:|---|
| **this package** | Scans every request against 150+ patterns, logs with full app context | ❌ | You want to *see* what's being tried on your app |
| `spatie/laravel-honeypot` | Hidden form field that catches spam bots | ✅ form only | You have public forms getting spammed |
| `graham-campbell/security` | Strips XSS-ish markup from input | ✅ mutates | You want naive input sanitising |
| `spatie/laravel-csp` | Sends Content-Security-Policy headers | ✅ browser | You want to constrain what the browser loads |
| `laravel/fortify` + rate limits | Auth throttling and lockout | ✅ | You need brute-force protection on login |
| Cloudflare / mod_security | Edge WAF, blocks before your app | ✅ | You want traffic stopped before it arrives |
The honest summary: a honeypot catches form spam, a WAF blocks known-bad traffic
at the edge, and CSP constrains the browser. **None of them tell you what an
attacker tried against your specific routes, with the payload decoded and the
authenticated user attached.** That gap is what this fills — and it's why the
package deliberately doesn't block: you can run it alongside all of the above
without any of them fighting each other.
---
## Requirements
- PHP 8.2+ (Laravel 13 requires PHP 8.3+)
- Laravel 10.x, 11.x, 12.x, or 13.x
- Any database supported by Laravel (MySQL, PostgreSQL, SQLite, SQL Server)
- Any cache driver - **no Redis or queue worker required**. Redis/Memcached is
only *recommended* to enable the optional DDoS check (which auto-disables on
non-atomic drivers). Queued writes are opt-in and off by default.
---
## How It Works
1. A middleware scans every incoming HTTP request
2. The request is checked against 158 regex patterns covering SQL injection, XSS, RCE, file traversal, SSRF, LDAP, XPath, SSTI, and more
3. If a threat pattern matches, a record is written to your `threat_logs` database table with the IP, URL, threat type, severity level, and a confidence score
4. Optionally, a Slack alert is sent for high-severity threats
5. The request proceeds normally - **nothing is blocked**
No internet connection is needed for detection.
---
## Quick Start
### 1. Install the package
```bash
composer require jayanta/laravel-threat-detection
```
### 2. Publish migrations and run them
> **This step is required.** Without it, the package will detect threats but cannot store them in the database. If you skip this step, your `threat_logs` table won't exist and all detections will be silently lost (you'll only see errors in `storage/logs/laravel.log`).
```bash
php artisan vendor:publish --tag=threat-detection-migrations
php artisan migrate
```
This creates two tables: `threat_logs` (stores detected threats) and `threat_exclusion_rules` (stores false positive rules).
**Verify tables were created:**
```bash
php artisan migrate:status
```
Look for `create_threat_logs_table`, `add_confidence_to_threat_logs_table`, and `create_threat_exclusion_rules_table` - all should show `Ran`.
### 3. Register the middleware
The middleware is what scans requests. You need to add it to your `web` middleware group.
**If you use Laravel 11 or 12** - open `bootstrap/app.php`:
```php
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
\JayAnta\ThreatDetection\Http\Middleware\ThreatDetectionMiddleware::class,
]);
})
```
> **How to check your Laravel version:** Run `php artisan --version` in your terminal.
**If you use Laravel 10** - open `app/Http/Kernel.php`:
```php
protected $middlewareGroups = [
'web' => [
// ... existing middleware
\JayAnta\ThreatDetection\Http\Middleware\ThreatDetectionMiddleware::class,
],
];
```
### 4. (Optional) Publish the config file
```bash
php artisan vendor:publish --tag=threat-detection-config
```
The package works with sensible defaults. Publishing the config lets you customize detection patterns, sensitivity modes, Slack notifications, and more. If you skip this step, everything still works.
**That's it.** Your app is now detecting threats.
---
## Verify It Works
After installation, trigger a test threat and confirm it was logged.
### Step 1: Start your app
```bash
php artisan serve
```
### Step 2: Open a test URL in your browser
Append a malicious query parameter to **any existing route** in your app (your homepage, a product page, etc.). For example:
**SQL Injection:**
```
http://localhost:8000/?q=' UNION SELECT * FROM users--
```
**XSS (Cross-Site Scripting):**
```
http://localhost:8000/?q=<script>alert(1)</script>
```
**Directory Traversal:**
```
http://localhost:8000/?file=../../etc/passwd
```
**RCE (Remote Code Execution):**
```
http://localhost:8000/?cmd=system('ls -la')
```
**Shellshock (CVE-2014-6271):**
```
http://localhost:8000/?cmd=() { :;}; /bin/bash
```
**Windows Command Injection:**
```
http://localhost:8000/?cmd=powershell -c whoami
```
**DROP TABLE (SQL DDL):**
```
http://localhost:8000/?q=DROP TABLE users
```
> Use a route that actually exists in your app (like `/`). If the URL returns a 404, the middleware may not have run.
### Step 3: Check that threats were logged
**Option A - Artisan command (quickest):**
```bash
php artisan threat-detection:stats
```
You should see a table with `Recorded Detections`, severity counts, and top
IPs. That number counts *rows*, not attempts: a detection is written once per
IP per threat type per five minutes, and repeats inside that window are
deduplicated rather than counted again.
**Option B - Tinker:**
```bash
php artisan tinker
```
```php
DB::table('threat_logs')->latest()->take(5)->get(['ip_address', 'type', 'threat_level', 'confidence_score']);
```
**Option C - Laravel log file:**
Each detected threat is written as a warning to `storage/logs/laravel.log`:
```
[high] Threat Detected: [middleware] SQL Injection UNION from 127.0.0.1 (http://localhost:8000/?q=...) [confidence: 50%]
```
### Things to know when testing
| Behavior | Explanation |
|----------|-------------|
| Same threat only logs once per 5 minutes | Deduplication: same IP + same threat type is cached for 5 minutes. Use **different attack types** for each test, or wait between tests. |
| `curl` requests trigger extra detection | Using `curl` also logs a "cURL Command" user-agent detection (low severity). This is expected - the package detects automated tools. |
| The package never blocks requests | Your app continues to function normally. Detection is passive. |
| No Slack setup needed | Notifications are off by default. |
| No internet connection needed | Core detection is 100% local. Only the optional `threat-detection:enrich` command calls an external API for geo-data. |
### Troubleshooting
**Start here — one command answers most of this:**
```bash
php artisan threat-detection:doctor
```
It checks the things that make detection fail *silently* — where the dashboard
stays empty, which looks identical to "no attacks" — and prints the exact fix
for each. It exits non-zero on a real failure, so it is safe to run in CI or a
deploy step.
```
Threat Detection — health check
PASS Detection is enabled for this environment
FAIL 'threat_logs' is missing confidence_label — EVERY threat is being discarded
Run: php artisan vendor:publish --tag=threat-detection-migrations && php artisan migrate
WARN 1 custom pattern(s) shadow a built-in: Localhost SSRF
Your copy runs instead of the maintained one, so later fixes to it never reach you.
```
What it covers: detection enabled for this environment; every column the writer
needs (a missing one discards **every** threat); dashboard/API columns; the
exclusion-rules table; whether the middleware is actually wired to a route or
group; published config that predates this version; custom patterns shadowing
built-in ones; a cache driver that cannot do DDoS counting; and a dashboard or
API left open without authentication.
**"I tested but `threat-detection:stats` shows zero threats" / "Threats are not stored in the database"**
If the doctor passed, the install is fine and the problem is the test request
itself. Three things it cannot check for you:
| Check | How to verify |
|-------|---------------|
| IP is not whitelisted | If you added `THREAT_DETECTION_WHITELISTED_IPS` to `.env`, remove it during testing |
| Used an existing route | The test URL must match a real route (e.g., `/`). A 404 means the middleware never ran |
| Dedup cache | Same IP + same attack type is cached for 5 minutes - try a different attack type |
> Running `php artisan migrate` alone is never enough: the migration files live
> inside the package and must be published to your app's `database/migrations/`
> first. The doctor prints the exact command when this is the problem.
**"API returns 401 Unauthorized"**
See [API Authentication](#api-authentication) below.
**"Dashboard shows 404"**
The dashboard is disabled by default. Add `THREAT_DETECTION_DASHBOARD=true` to `.env` and clear route cache:
```bash
php artisan route:clear
```
---
## Features
- **150+ Detection Patterns** - SQL injection (UNION, DDL, DML, file ops), XSS (script, SVG, CSS expression), RCE, directory traversal, SSRF, XXE, Log4Shell, NoSQL injection, command injection (Linux + Windows), LDAP injection, XPath injection, SSTI, CRLF injection, Java deserialization, and more
- **83 Bot/Scanner Signatures** - SQLMap, Nikto, Nmap, Burp Suite, FeroxBuster, FFUF, XSStrike, Dalfox, Netsparker, and 70+ other scanner and bot signatures
- **AI Scraper Detection** - GPTBot, ClaudeBot, ByteSpider, Common Crawl, and other AI training bots
- **Headless Browser Detection** - HeadlessChrome, PhantomJS, Selenium, Puppeteer, Playwright
- **404 Probe Tracking** - Detects reconnaissance probes hitting known vulnerable paths (`/wp-admin`, `/.env`, `/phpmyadmin`, `/actuator`, etc.) with 50+ default probe paths, plus an opt-in [AI-infrastructure pack](#ai-infrastructure-probes-opt-in) for exposed LLM gateways
- **DDoS Monitoring** - Rate-based threshold detection with configurable windows
- **Confidence Scoring** - Each threat gets a 0-100 confidence score based on pattern count, context, and signals
- **Evasion Resistance** - Normalization pipeline defeats SQL comment insertion, double URL encoding, HTML entity encoding, Unicode escapes, hex escapes, and IIS `%uXXXX` encoding before pattern matching — repeated until the payload stops changing, so stacking two techniques does not get past it
- **CVE Detection** - Shellshock (CVE-2014-6271), Spring4Shell (CVE-2022-22965), PHPUnit RCE (CVE-2017-9841), Drupalgeddon, Log4Shell
- **Context-Aware Detection** - Patterns found in query strings score higher than those in the request body
- **Request Body Scanning** - Both form-encoded and JSON (`application/json`) request bodies are inspected
- **Safe Fields** - Exclude specific form fields from scanning (for CMS editors, code inputs, search fields)
- **False Positive Reporting** - Mark threats as false positives from the dashboard; auto-creates exclusion rules
- **Three Detection Modes** - `strict`, `balanced` (default), and `relaxed` - tunable sensitivity
- **Content Path Suppression** - Whitelist CMS/blog paths to suppress low/medium alerts from rich content
- **PII Detection** - Sensitive data exposure patterns (configurable per region)
- **Geo-Enrichment** - Country, city, ISP, cloud provider identification via free API
- **Slack Alerts** - Real-time notifications for high-severity threats (works on Laravel 10 and 11+)
- **Built-in Dashboard** - Dark-mode Blade dashboard (Alpine.js and Tailwind, styles precompiled, zero build step)
- **Dashboard Auth Guard** - Configurable authentication for dashboard and API (none, auth, role, or IP-based)
- **15 API Endpoints** - Full REST API for building custom Vue/React/mobile dashboards
- **Fail2ban Export** - Export detected IPs in fail2ban-compatible format or plain blocklist
- **Blocklist Export** - Export IPs in nginx deny, Apache deny, CSV, or plain format
- **CSV Export** - One-click threat log export (up to 10,000 rows)
- **Correlation Analysis** - Detect coordinated attacks and attack campaigns across IPs
- **Performance Optimized** - Category-based lazy pattern loading (only runs regex for relevant attack categories), early bailout for clean requests, browser UA short-circuit (skips 70+ checks for normal browsers), probe path hash lookup, batch DB inserts, configurable max detections per request
- **Database Agnostic** - MySQL, PostgreSQL, SQLite, SQL Server
- **Zero Config** - Works out of the box with sensible defaults
- **Safe by Design** - The middleware catches its own errors. If detection fails, your app keeps running. Requests are never blocked.
---
## Configuration
The package works without any `.env` changes. All values below are optional - add them only if you want to override the defaults.
```env
# Enable/disable detection globally (default: true)
THREAT_DETECTION_ENABLED=true
# Detection sensitivity (default: balanced)
# Options: strict, balanced, relaxed
THREAT_DETECTION_MODE=balanced
# Custom table name (default: threat_logs)
# THREAT_DETECTION_TABLE=threat_logs
# Your ISO 3166-1 alpha-2 country code (default: IN)
# Drives the is_foreign flag on every enriched row — set this or every
# non-Indian address is reported as foreign.
# THREAT_DETECTION_HOME_COUNTRY=IN
# Geo-enrichment provider used by threat-detection:enrich (default shown).
# HTTPS since v1.8.0. A failed lookup is never retried over cleartext, and a
# run in which every lookup failed exits non-zero rather than reporting
# success. Enrichment is opt-in either way.
#
# ip-api.com's free tier rejects HTTPS, so on the free tier this command will
# now fail rather than quietly sending your visitors' IP addresses in the
# clear. Either point it at a provider you hold a key for, or set it back
# explicitly and accept the disclosure.
# THREAT_DETECTION_GEO_ENDPOINT=https://ip-api.com/json
# Dashboard URL path (default: threat-detection)
# THREAT_DETECTION_DASHBOARD_PATH=threat-detection
# API route prefix (default: api/threat-detection)
# THREAT_DETECTION_API_PREFIX=api/threat-detection
# Role required when the API guard is 'role' (default: admin)
# THREAT_DETECTION_API_ROLE=admin
# Allowed IPs when the API guard is 'ip'. Comma-separated, CIDR supported.
# THREAT_DETECTION_API_IPS=127.0.0.1,10.0.0.0/8
# Username shown on Slack alerts (default: ThreatBot)
# THREAT_DETECTION_SLACK_USERNAME=ThreatBot
# Whitelist IPs to skip detection entirely (default: empty)
# Supports CIDR notation. Comma-separated.
# THREAT_DETECTION_WHITELISTED_IPS=10.0.0.0/8,192.168.1.0/24
# Static operator denylist read by ThreatDetection::isBlocklisted() (default: empty)
# The package itself never blocks — see "Acting on the Data" for the
# enforcement recipe. Supports CIDR. Whitelist wins on overlap.
# THREAT_DETECTION_BLOCKLISTED_IPS=203.0.113.0/24,198.51.100.7
# DDoS detection thresholds (defaults shown)
# THREAT_DETECTION_DDOS_THRESHOLD=300
# THREAT_DETECTION_DDOS_WINDOW=60
# Minimum confidence score to log a threat (default: 0)
# Threats below this score are silently ignored.
# THREAT_DETECTION_MIN_CONFIDENCE=0
# Slack notifications (disabled by default)
# THREAT_DETECTION_NOTIFICATIONS=true
# THREAT_DETECTION_SLACK_WEBHOOK=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
# THREAT_DETECTION_SLACK_CHANNEL=#threat-alerts
# Dashboard (disabled by default)
# THREAT_DETECTION_DASHBOARD=true
# API endpoints (enabled by default)
# THREAT_DETECTION_API=true
# API rate limiting (default: 60 requests per minute)
# THREAT_DETECTION_API_THROTTLE=60,1
# Queue support - offload DB writes to a queue (disabled by default).
# OPTIONAL: only enable if your app already runs a queue worker. When false
# (default), threats are written synchronously with a plain DB insert - no
# Redis, no worker, nothing extra to run.
# THREAT_DETECTION_QUEUE=false
# THREAT_DETECTION_QUEUE_CONNECTION=redis
# THREAT_DETECTION_QUEUE_NAME=default
# Auto-purge old logs (disabled by default)
# Requires Laravel scheduler to be running.
# THREAT_DETECTION_RETENTION=false
# THREAT_DETECTION_RETENTION_DAYS=90
# 404 probe tracking (enabled by default)
# Detects bots hitting /wp-admin, /.env, /phpmyadmin, etc.
# THREAT_DETECTION_PROBE_TRACKING=true
# Max detections per request (default: 0 = unlimited)
# Stop scanning after N pattern matches per request.
# THREAT_DETECTION_MAX_DETECTIONS=0
# Dashboard auth guard (default: none)
# Options: none, auth, role, ip
# THREAT_DETECTION_DASHBOARD_GUARD=none
# THREAT_DETECTION_DASHBOARD_ROLE=admin
# THREAT_DETECTION_DASHBOARD_IPS=127.0.0.1
# API auth guard (default: none - uses existing middleware config)
# THREAT_DETECTION_API_GUARD=none
```
### Detection Modes
| Mode | Confidence Threshold | Behavior |
|------|---------------------|----------|
| `strict` | 0 (logs everything) | All patterns active, lowest thresholds. Catches everything but may flag legitimate traffic. |
| `balanced` | 10 | Default. Confidence scoring active, standard thresholds. Good for most apps. |
| `relaxed` | 25 | Only high-severity patterns run at all. Best for content-heavy sites with frequent false positives. (Was 40 before v1.8.0 — a floor no single detection could reach, so the mode silently logged nothing.) |
### Enabled Environments
By default, detection runs in `production`, `staging`, and `local`. To change, publish the config and edit:
```php
'enabled_environments' => ['production', 'staging', 'local'],
```
To disable detection in your test suite, set `APP_ENV=testing` (not in the list above) or add to your `phpunit.xml`:
```xml
<env name="THREAT_DETECTION_ENABLED" value="false"/>
```
### Config Reference
Publish the config file to see all available options:
```bash
php artisan vendor:publish --tag=threat-detection-config
```
Key config sections: `skip_paths` (paths to skip), `only_paths` (whitelist mode), `auth_paths` (smart detection for login routes), `content_paths` (suppress non-high alerts), `safe_fields` (exclude specific fields from scanning), `safe_paths` (path-aware field exclusion for nested JSON), `probe_tracking` (404 probe detection), `context_weights` (scoring multipliers), `threat_levels` (severity keyword mapping), `api_route_filtering` (suppress low/medium on API routes), `queue` (async processing), `retention` (auto-purge), `max_detections_per_request` (performance cap), `dashboard.guard` / `api.guard` (auth mode).
### Route Whitelisting (`only_paths`)
If your app has many routes but you only care about a few, use `only_paths` to scan **only** those routes. All other routes are automatically skipped - no middleware overhead at all.
```php
// config/threat-detection.php
'only_paths' => [
'admin/*',
'api/*',
'login',
'register',
],
```
Leave empty (default) to scan all routes (subject to `skip_paths`). When both are configured, `only_paths` is checked first, then `skip_paths` applies within the matched set.
### Queue Support
By default, threat logging happens synchronously in the request cycle. For high-traffic apps, you can offload DB writes and Slack notifications to a queue:
```env
THREAT_DETECTION_QUEUE=true
THREAT_DETECTION_QUEUE_CONNECTION=redis
THREAT_DETECTION_QUEUE_NAME=threat-logs
```
This dispatches a `StoreThreatLog` job (3 retries, backoff 10s/30s). Detection still happens in real-time - only the write is deferred. If the queue cannot take the job, the rows are written directly instead, with one warning in the log, so a queue outage does not lose detections.
### Auto-Purge (Retention Policy)
Automatically delete old threat logs on a daily schedule:
```env
THREAT_DETECTION_RETENTION=true
THREAT_DETECTION_RETENTION_DAYS=90
```
Requires Laravel's scheduler to be running (`php artisan schedule:run`). Runs daily at 02:00 via `threat-detection:purge`.
`THREAT_DETECTION_RETENTION_DAYS` must be a whole number, 1 or more; anything else
schedules no purge, and `threat-detection:doctor` reports it.
### ThreatDetected Event
Every confirmed threat dispatches a `ThreatDetected` event that you can listen to:
```php
// app/Providers/EventServiceProvider.php
use JayAnta\ThreatDetection\Events\ThreatDetected;
protected $listen = [
ThreatDetected::class => [
YourCustomListener::class,
],
];
```
The event carries `$threatLog` (full DB row array), `$ipAddress`, and `$threatLevel`. Use it to trigger custom actions - send Telegram alerts, update a blocklist, feed a SIEM, etc.
### DdosThresholdExceeded Event
When a client crosses the configured DDoS threshold (`ddos.threshold` requests within
`ddos.window` seconds), a `DdosThresholdExceeded` event is dispatched alongside the threat
log entry:
```php
use JayAnta\ThreatDetection\Events\DdosThresholdExceeded;
protected $listen = [
DdosThresholdExceeded::class => [
YourFloodListener::class,
],
];
```
The event carries `$ipAddress`, `$requestCount`, `$threshold`, and `$windowSeconds`. It is
throttled to once per IP per dedup window (same throttle as the log row), so a flood can't
drown your listeners. Use it for alerting or to feed an external ban store; to *refuse*
over-threshold clients, use `ThreatDetection::isDdosThresholdExceeded($ip)` from your own
---
[Read more](https://github.com/jay123anta/laravel-threat-detection)