jayanta / laravel-threat-detection
Real-time threat detection and security logging for Laravel applications. Detects SQL injection, XSS, DDoS, scanner bots, and more.
Package info
github.com/jay123anta/laravel-threat-detection
pkg:composer/jayanta/laravel-threat-detection
Requires
- php: ^8.1
- illuminate/cache: ^10.0|^11.0|^12.0
- illuminate/database: ^10.0|^11.0|^12.0
- illuminate/http: ^10.0|^11.0|^12.0
- illuminate/notifications: ^10.0|^11.0|^12.0
- illuminate/routing: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^10.0|^11.0
Suggests
- laravel/sanctum: Recommended for API route authentication (^3.0|^4.0)
- laravel/slack-notification-channel: Required for Slack notifications on Laravel 11+ (^3.0)
README
Laravel Threat Detection
Know who's attacking your Laravel app — without changing a single line of application code.
A middleware-based threat detection and logging system for Laravel. Drop it in, and it starts scanning every HTTP request for SQL injection, XSS, RCE, scanner bots, DDoS patterns, and 40+ other attack types — logging everything to your database with full geo-enrichment and a built-in dashboard.
Extracted from a production application. Battle-tested with real traffic.
Important: This package never blocks any request. It only logs and alerts. Your application continues to handle every request normally, even when threats are detected.
Requirements
- PHP 8.1+
- Laravel 10.x, 11.x, or 12.x
- Any database supported by Laravel (MySQL, PostgreSQL, SQLite, SQL Server)
How It Works
- A middleware scans every incoming HTTP request
- The request is checked against 130+ regex patterns covering SQL injection, XSS, RCE, file traversal, SSRF, and more
- If a threat pattern matches, a record is written to your
threat_logsdatabase table with the IP, URL, threat type, severity level, and a confidence score - Optionally, a Slack alert is sent for high-severity threats
- The request proceeds normally — nothing is blocked
No internet connection is needed for detection.
Quick Start
1. Install the package
composer require jayanta/laravel-threat-detection
2. Publish migrations and run them
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).
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:
->withMiddleware(function (Middleware $middleware) { $middleware->web(append: [ \JayAnta\ThreatDetection\Http\Middleware\ThreatDetectionMiddleware::class, ]); })
How to check your Laravel version: Run
php artisan --versionin your terminal.
If you use Laravel 10 — open app/Http/Kernel.php:
protected $middlewareGroups = [ 'web' => [ // ... existing middleware \JayAnta\ThreatDetection\Http\Middleware\ThreatDetectionMiddleware::class, ], ];
4. (Optional) Publish the config file
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
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')
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):
php artisan threat-detection:stats
You should see a table with Total Threats, severity counts, and top IPs.
Option B — Tinker:
php artisan tinker
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
"I tested but threat-detection:stats shows zero threats"
| Check | How to verify |
|---|---|
| Migrations were run | Run php artisan migrate:status — look for threat_logs and threat_exclusion_rules tables |
| Middleware is registered | Confirm ThreatDetectionMiddleware is in your web middleware group (see Step 3 above) |
| IP is not whitelisted | If you added THREAT_DETECTION_WHITELISTED_IPS to .env, remove it during testing |
| Environment is enabled | Default enabled environments: production, staging, local. Check APP_ENV in .env |
| Used an existing route | The test URL must match a real route (e.g., /). |
| Dedup cache | Same IP + same attack type is cached for 5 minutes — try a different attack type |
"threat-detection:stats throws a database error"
The threat_logs table doesn't exist yet. Run:
php artisan vendor:publish --tag=threat-detection-migrations php artisan migrate
"API returns 401 Unauthorized"
See API Authentication below.
"Dashboard shows 404"
The dashboard is disabled by default. Add THREAT_DETECTION_DASHBOARD=true to .env and clear route cache:
php artisan route:clear
Features
- 130+ Detection Patterns — SQL injection, XSS, RCE, directory traversal, SSRF, XXE, Log4Shell, NoSQL injection, command injection, and more
- Scanner Detection — SQLMap, Nikto, Nmap, Burp Suite, Acunetix, WPScan, Nessus, Nuclei, Metasploit, and others
- Bot Detection — Suspicious user agents, automated scripts, headless browsers
- 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 — Payload normalization defeats SQL comment insertion (
UNION/**/SELECT), double URL encoding (%2527), and CHAR encoding bypasses - Context-Aware Detection — Patterns found in query strings score higher than those in POST body
- False Positive Reporting — Mark threats as false positives from the dashboard; auto-creates exclusion rules
- Three Detection Modes —
strict,balanced(default), andrelaxed— 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 + Tailwind CDN, zero build step)
- 15 API Endpoints — Full REST API for building custom Vue/React/mobile dashboards
- CSV Export — One-click threat log export (up to 10,000 rows)
- Correlation Analysis — Detect coordinated attacks and attack campaigns across IPs
- 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.
# 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 # 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 # 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) # 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
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 |
40 | Only high-severity patterns trigger. Best for content-heavy sites with frequent false positives. |
Enabled Environments
By default, detection runs in production, staging, and local. To change, publish the config and edit:
'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:
<env name="THREAT_DETECTION_ENABLED" value="false"/>
Config Reference
Publish the config file to see all available options:
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), context_weights (scoring multipliers), threat_levels (severity keyword mapping), api_route_filtering (suppress low/medium on API routes), queue (async processing), retention (auto-purge).
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.
// 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:
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.
Auto-Purge (Retention Policy)
Automatically delete old threat logs on a daily schedule:
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.
ThreatDetected Event
Every confirmed threat dispatches a ThreatDetected event that you can listen to:
// 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.
Slack Notifications
Slack alerts are disabled by default. To enable:
THREAT_DETECTION_NOTIFICATIONS=true THREAT_DETECTION_SLACK_WEBHOOK=https://hooks.slack.com/services/YOUR/WEBHOOK/URL THREAT_DETECTION_SLACK_CHANNEL=#threat-alerts
Only high-severity threats trigger notifications by default (configurable via notify_levels in the config).
Laravel 10: Uses the built-in SlackMessage notification class. No extra package needed.
Laravel 11+: The built-in Slack channel was removed. The package automatically detects this and sends raw HTTP POST webhooks to your Slack URL. No extra package needed. If you prefer the full notification channel, install:
composer require laravel/slack-notification-channel
Dashboard
The package ships with a built-in dark-mode dashboard (Alpine.js + Tailwind CDN — no build step required).
+-------------------------------------------------------------------------+
| Threat Detection Dashboard |
+-------------------------------------------------------------------------+
| Total: 847 | High: 23 | Med: 156 | Low: 668 | IPs: 94 |
+-------------------------------------------------------------------------+
| [Timeline Chart - 7 Day Stacked Bar] |
+-------------------------------------------------------------------------+
| Search: [___________] Level: [All] |
| Time IP Type Level Confidence Actions |
| Mar 2 14:02 185.220.101.4 SQL Injection HIGH 80% [FP] |
| Mar 2 13:58 45.33.32.156 XSS Script Tag HIGH 65% [FP] |
| Mar 2 13:45 192.168.1.10 Scanner: Nikto MED 35% [FP] |
+-------------------------------------------------------------------------+
| Top IPs | Threats by Country |
| 185.220.101.4 [23] | US 234 |
| 45.33.32.156 [18] | CN 156 |
| 103.152.220.1 [12] | RU 98 |
+-------------------------------------------------------------------------+
Enable the dashboard
Add to .env:
THREAT_DETECTION_DASHBOARD=true
Visit: http://your-app.test/threat-detection
Dashboard authentication
The dashboard uses ['web', 'auth'] middleware by default — users must be logged in.
If your app does not have authentication set up yet (e.g., during local development), temporarily change the middleware in config/threat-detection.php:
'dashboard' => [ 'enabled' => true, 'path' => 'threat-detection', 'middleware' => ['web'], // temporarily remove 'auth' ],
Restore
['web', 'auth']before deploying to production.
If the dashboard shows empty data, make sure the API endpoints are accessible. The dashboard fetches data from the API. See API Authentication for details.
API Endpoints
The package provides 15 REST endpoints for building custom dashboards or integrations.
API Authentication
API routes use auth:sanctum middleware by default. The package handles this gracefully:
- Sanctum installed: API requires authentication via Sanctum tokens or SPA session auth.
- Sanctum NOT installed: The package automatically detects that Sanctum is missing and falls back to
['api']only. The API works without authentication.
If you don't use Sanctum but want to protect your API, add your own auth guard in config/threat-detection.php:
'api' => [ 'enabled' => true, 'prefix' => 'api/threat-detection', 'middleware' => ['api', 'auth'], // or 'auth:your-guard' ],
For local testing (if Sanctum blocks access), temporarily change:
'middleware' => ['api'], // remove 'auth:sanctum'
Restore authentication before deploying to production.
Endpoint Reference
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/threat-detection/threats |
List threats (paginated, filterable) |
| GET | /api/threat-detection/threats/{id} |
Single threat details |
| POST | /api/threat-detection/threats/{id}/false-positive |
Mark threat as false positive |
| GET | /api/threat-detection/stats |
Overall statistics |
| GET | /api/threat-detection/summary |
Detailed breakdown by type, level, IP |
| GET | /api/threat-detection/live-count |
Threats in last hour |
| GET | /api/threat-detection/by-country |
Grouped by country |
| GET | /api/threat-detection/by-cloud-provider |
Grouped by cloud provider |
| GET | /api/threat-detection/top-ips |
Top offending IPs |
| GET | /api/threat-detection/timeline |
Threat timeline (for charts) |
| GET | /api/threat-detection/ip-stats?ip=x.x.x.x |
Stats for specific IP |
| GET | /api/threat-detection/correlation |
Correlation analysis |
| GET | /api/threat-detection/export |
Export to CSV |
| GET | /api/threat-detection/exclusion-rules |
List exclusion rules |
| DELETE | /api/threat-detection/exclusion-rules/{id} |
Delete an exclusion rule |
Query Parameters for /threats
| Parameter | Description |
|---|---|
keyword |
Search in IP, URL, type |
ip |
Filter by IP address |
level |
Filter by threat level (high, medium, low) |
type |
Filter by threat type |
country |
Filter by country code |
is_foreign |
Filter foreign IPs (true/false) |
cloud_provider |
Filter by cloud provider |
is_false_positive |
Filter by false positive status (true/false) |
date_from / date_to |
Date range filter |
per_page |
Items per page (default: 20, max: 100) |
Example API Response
GET /api/threat-detection/stats:
{
"success": true,
"data": {
"total_threats": 847,
"high_severity": 23,
"medium_severity": 156,
"low_severity": 668,
"unique_ips": 94,
"foreign_ips": 67,
"cloud_attacks": 12,
"today": 34,
"last_hour": 5
}
}
Building Custom Frontends
Vue.js:
async mounted() { const response = await fetch('/api/threat-detection/stats'); this.stats = await response.json(); const threats = await fetch('/api/threat-detection/threats?per_page=20'); this.threats = await threats.json(); }
React:
useEffect(() => { fetch('/api/threat-detection/stats') .then(res => res.json()) .then(data => setStats(data)); }, []);
If your API uses
auth:sanctum, include authentication headers or configure Sanctum SPA authentication for cookie-based requests.
Artisan Commands
# View threat stats summary in the terminal php artisan threat-detection:stats # Enrich existing logs with geo-data (country, city, ISP, cloud provider) # Uses the free ip-api.com service (rate-limited to 45 req/min, auto-throttled) php artisan threat-detection:enrich --days=7 # Purge old logs to keep the database clean php artisan threat-detection:purge --days=30
Custom Patterns
Add your own detection regex patterns in config/threat-detection.php:
'custom_patterns' => [ '/your-regex-here/i' => 'Your Threat Label', ],
Example — detect requests to a WordPress login page:
'/\/wp-login\.php/i' => 'WordPress Login Probe',
The threat level for each pattern is determined automatically by matching keywords in the label against the threat_levels config:
'threat_levels' => [ 'high' => ['XSS', 'SQL Injection', 'RCE', 'Aadhaar', 'PAN', 'Bank', 'Token', 'Password', 'JWT', 'Deserialization', 'Metadata Access', 'Evasion', 'Encoding'], 'medium' => ['Directory Traversal', 'LFI', 'SSRF', 'Sensitive', 'Config', 'Session', 'Command Chain', 'Recon Tool', 'Raw PHP'], 'low' => ['User-Agent', 'JS Redirect', 'SEO Bot', 'Empty', 'Rate', 'Command-line Downloader'], ],
If the label doesn't match any keyword, the threat defaults to low severity.
Invalid regex patterns are automatically skipped and logged as warnings — they won't crash your application.
Using the Facade
For programmatic access to threat data outside of the middleware:
use JayAnta\ThreatDetection\Facades\ThreatDetection; // Get attack statistics for a specific IP $stats = ThreatDetection::getIpStatistics('192.168.1.1'); // Detect coordinated attacks (multiple IPs targeting same URL within 15 minutes) $attacks = ThreatDetection::detectCoordinatedAttacks(15, 3); // Detect attack campaigns (same threat type from 5+ IPs in last 24 hours) $campaigns = ThreatDetection::detectAttackCampaigns(24); // Get a summary of all correlation data $summary = ThreatDetection::getCorrelationSummary();
Reducing False Positives
Content Path Suppression
If you have CMS editors, blog post forms, or comment sections where users submit rich content, those paths often trigger false positives (e.g., a blog post containing <script> code samples). Add those paths to suppress low/medium alerts:
// config/threat-detection.php 'content_paths' => [ 'admin/posts/*', 'admin/pages/*', 'blog/*/edit', 'comments', ],
On these paths, only high-severity threats are logged.
False Positive Reporting
Click the FP button on any threat in the dashboard to mark it as a false positive. This:
- Flags the threat as
is_false_positive = true - Auto-creates an exclusion rule so similar threats from the same URL/type are suppressed going forward
Manage exclusion rules via API:
GET /api/threat-detection/exclusion-rules
DELETE /api/threat-detection/exclusion-rules/{id}
Confidence Scoring
Every threat receives a confidence score (0-100) based on:
- Number of pattern matches in the same request
- Severity of the matched pattern
- Where the pattern was found (query string > headers > body)
- Whether the user-agent matches a known attack tool
- Current detection mode
Threats below the confidence threshold for your detection mode are not logged (see Detection Modes).
Detected Attack Types
| Category | Examples |
|---|---|
| Injection | SQL injection (UNION, boolean, time-based, CHAR encoding), NoSQL injection, command injection, LDAP injection |
| XSS | Script tags, event handlers, JavaScript URIs, DOM manipulation, encoded XSS |
| Code Execution | RCE, PHP deserialization, template injection (Blade, JSP, ASP), eval(), base64 decode |
| File Access | Directory traversal, LFI/RFI, sensitive file probes (.env, wp-config, composer.json, .git) |
| SSRF | Localhost access, AWS/GCP metadata endpoints, private IP ranges (10.x, 172.16-31.x, 192.168.x) |
| Authentication | Brute force detection, token leaks, password exposure, session ID exposure |
| Scanners | SQLMap, Nikto, Nmap, Burp Suite, Acunetix, WPScan, Nessus, Nuclei, Metasploit |
| Bots | Python scripts, Go HTTP clients, cURL, wget, empty user agents |
| DDoS | Rate-based excessive request detection |
| XXE | XML external entity attacks, DOCTYPE entity declarations |
| Log4Shell | JNDI injection attempts (LDAP, RMI, DNS) |
| Evasion | SQL comment insertion (UNION/**/SELECT), double URL encoding (%2527), CHAR encoding |
| Web Shells | c99, r57, b374k, WSO, FilesMan, encoded eval execution |
| Crypto Mining | Coinhive, CryptoNight, Monero script detection |
Running the Test Suite
composer test
The package includes 86 tests covering detection patterns, middleware behavior, API endpoints, confidence scoring, exclusion rules, DDoS detection, evasion resistance (full-cycle), queue support, event dispatch, and input validation.
License
MIT License. See LICENSE for details.
Contributing
Contributions are welcome! Please submit a Pull Request.