
Middleware pasivo de Laravel que detecta y registra inyección SQL, XSS, RCE, escáneres de bots y más de 175 patrones de ataque. Incluye un panel integrado, alertas de Slack, API REST y enriquecimiento geográfico. IDS, no WAF.
Detección pasiva de intrusiones para Laravel — observa cada inyección SQL, XSS, escáner y sonda de bot que golpea tu aplicación, registrada con contexto completo. Es un IDS, no un WAF: nunca bloquea, filtra ni modifica una solicitud.
Introdúcelo en cualquier aplicación Laravel 10–13 y empieza a escanear cada solicitud HTTP contra más de 150 patrones de ataque, puntuando cada coincidencia por confianza y registrándola en tu base de datos — con un panel integrado, alertas de Slack, geo-enriquecimiento y exportaciones para fail2ban/listas de bloqueo. Ninguna solicitud es bloqueada jamás. Piensa en una cámara de seguridad, no en un candado: te muestra exactamente quién está sondeando tus rutas, con qué frecuencia y con qué técnicas.
Extraído de una aplicación de producción y probado en tráfico real. 335 pruebas, sin dependencias en tiempo de ejecución más allá del propio Laravel, y no se requiere conexión a internet para la detección.
composer require jayanta/laravel-threat-detection php artisan vendor:publish --tag=threat-detection-migrations php artisan migrate
Luego añade el middleware a tu grupo `web` (una línea en `bootstrap/app.php` en Laravel 11+,
o `app/Http/Kernel.php` en Laravel 10) — fragmento completo en [Quick Start](#quick-start) más abajo.
Eso es todo; la detección ya está activa.```bash
php artisan threat-detection:doctor # confirms it is actually recording
Este paquete es un IDS pasivo a nivel de aplicación — observa y registra, no bloquea. Está pensado para situarse junto a un WAF o un servicio edge, no para sustituir a ninguno. Cada capa ve algo que las demás no pueden ver:
La versión corta: un edge/WAF es tu cerradura en la puerta; esto es la cámara de seguridad en el interior, con el contexto de la app para decirte exactamente qué se está intentando en cada ruta, quién lo intenta y con qué frecuencia. Úsalo para alimentar decisiones reales — bloqueos de fail2ban, límites de tasa, geobloqueos — con datos que tu capa edge nunca ve.
La pregunta más habitual sobre un detector que nunca bloquea. Cuatro respuestas, en orden creciente de esfuerzo:
El paquete aporta la inteligencia; tú aportas el rechazo. Esa separación es deliberada — el código de bloqueo que vive en tu app es código que puedes leer, probar y desactivar, y significa que un fallo de detección nunca puede tumbar tu sitio.
threat_logs de tu base de datos con la IP, la URL, el tipo de amenaza, el nivel de gravedad y una puntuación de confianzaNo se necesita conexión a internet para la detección.
composer require jayanta/laravel-threat-detection
### 2. Publicar migraciones y ejecutarlas
> **Este paso es obligatorio.** Sin él, el paquete detectará amenazas pero no podrá almacenarlas en la base de datos. Si omites este paso, tu tabla `threat_logs` no existirá y todas las detecciones se perderán silenciosamente (solo verás errores en `storage/logs/laravel.log`).```bash
php artisan vendor:publish --tag=threat-detection-migrations
php artisan migrate
Esto crea dos tablas: threat_logs (almacena amenazas detectadas) y threat_exclusion_rules (almacena reglas de falsos positivos).
Verificar que las tablas fueron creadas:```bash php artisan migrate:status
Busca `create_threat_logs_table`, `add_confidence_to_threat_logs_table` y `create_threat_exclusion_rules_table` - todas deberían mostrar `Ran`.
### 3. Registrar el middleware
El middleware es lo que analiza las solicitudes. Debes añadirlo a tu grupo de middleware `web`.
**Si usas Laravel 11 o 12** - abre `bootstrap/app.php`:```php
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
\JayAnta\ThreatDetection\Http\Middleware\ThreatDetectionMiddleware::class,
]);
})
Cómo comprobar tu versión de Laravel: Ejecuta
php artisan --versionen tu terminal.
Si usas Laravel 10 - abre app/Http/Kernel.php:```php
protected $middlewareGroups = [
'web' => [
// ... existing middleware
\JayAnta\ThreatDetection\Http\Middleware\ThreatDetectionMiddleware::class,
],
];
### 4. (Opcional) Publicar el archivo de configuración```bash
php artisan vendor:publish --tag=threat-detection-config
El paquete funciona con valores predeterminados sensatos. Publicar la configuración te permite personalizar los patrones de detección, los modos de sensibilidad, las notificaciones de Slack y más. Si te saltas este paso, todo sigue funcionando.
Eso es todo. Tu aplicación ahora está detectando amenazas.
Después de la instalación, activa una amenaza de prueba y confirma que se haya registrado.
php artisan serve
### Paso 2: Abre una URL de prueba en tu navegador
Añade un parámetro de consulta malicioso a **cualquier ruta existente** en tu aplicación (tu página de inicio, una página de producto, etc.). Por ejemplo:
**Inyección SQL:**```
http://localhost:8000/?q=' UNION SELECT * FROM users--
XSS (Cross-Site Scripting):``` http://localhost:8000/?q=
**Recorrido de directorios:**```
http://localhost:8000/?file=../../etc/passwd
RCE (Ejecución remota de código):``` http://localhost:8000/?cmd=system('ls -la')
**Shellshock (CVE-2014-6271):**```
http://localhost:8000/?cmd=() { :;}; /bin/bash
Inyección de comandos en Windows:``` http://localhost:8000/?cmd=powershell -c whoami
**DROP TABLE (SQL DDL):**```
http://localhost:8000/?q=DROP TABLE users
Usa una ruta que realmente exista en tu aplicación (como
/). Si la URL devuelve un 404, es posible que el middleware no se haya ejecutado.
Opción A - Comando de Artisan (la más rápida):```bash php artisan threat-detection:stats
Deberías ver una tabla con `Total Threats`, recuentos de severidad y las principales IPs.
**Opción B - Tinker:**```bash
php artisan tinker
DB::table('threat_logs')->latest()->take(5)->get(['ip_address', 'type', 'threat_level', 'confidence_score']);
Opción C - archivo de registro de Laravel:
Cada amenaza detectada se escribe como una advertencia en storage/logs/laravel.log:```
[high] Threat Detected: [middleware] SQL Injection UNION from 127.0.0.1 (http://localhost:8000/?q=...) [confidence: 50%]
### Cosas que debes saber al probar
| Comportamiento | Explicación |
|----------|-------------|
| La misma amenaza solo se registra una vez cada 5 minutos | Deduplicación: la misma IP + el mismo tipo de amenaza se almacenan en caché durante 5 minutos. Usa **diferentes tipos de ataque** para cada prueba, o espera entre pruebas. |
| Las peticiones con `curl` activan detección adicional | Usar `curl` también registra una detección de user-agent "cURL Command" (severidad baja). Esto es esperado: el paquete detecta herramientas automatizadas. |
| El paquete nunca bloquea peticiones | Tu aplicación sigue funcionando con normalidad. La detección es pasiva. |
| No se necesita configurar Slack | Las notificaciones están desactivadas por defecto. |
| No se necesita conexión a internet | La detección principal es 100% local. Solo el comando opcional `threat-detection:enrich` llama a una API externa para los datos geográficos. |
### Solución de problemas
**Empieza aquí: un comando responde la mayor parte de esto:**```bash
php artisan threat-detection:doctor
Comprueba las cosas que hacen que la detección falle silenciosamente — donde el panel permanece vacío, lo que parece idéntico a "sin ataques" — e imprime la solución exacta para cada una. Sale con un código distinto de cero ante un fallo real, por lo que es seguro ejecutarlo en CI o en un paso de despliegue.``` 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.
Qué cubre: la detección habilitada para este entorno; cada columna que el escritor necesita (una que falte descarta **todas** las amenazas); las columnas del panel/API; la tabla de reglas de exclusión; si el middleware está realmente conectado a una ruta o grupo; una configuración publicada que precede a esta versión; patrones personalizados que ocultan los integrados; un controlador de caché que no puede hacer el conteo de DDoS; y un panel o API que quedó abierto sin autenticación.
**"Probé pero `threat-detection:stats` muestra cero amenazas" / "Las amenazas no se almacenan en la base de datos"**
Si el doctor pasó, la instalación está bien y el problema es la propia solicitud de prueba. Tres cosas que no puede comprobar por ti:
| Comprobación | Cómo verificar |
|-------|---------------|
| La IP no está en la lista blanca | Si añadiste `THREAT_DETECTION_WHITELISTED_IPS` a `.env`, elimínala durante las pruebas |
| Se usó una ruta existente | La URL de prueba debe coincidir con una ruta real (p. ej., `/`). Un 404 significa que el middleware nunca se ejecutó |
| Caché de deduplicación | Misma IP + mismo tipo de ataque se almacena en caché durante 5 minutos - prueba con un tipo de ataque diferente |
> Ejecutar `php artisan migrate` por sí solo nunca es suficiente: los archivos de migración
> viven dentro del paquete y deben publicarse en `database/migrations/` de tu aplicación
> primero. El doctor imprime el comando exacto cuando este es el problema.
**"La API devuelve 401 Unauthorized"**
Consulta [Autenticación de API](#api-authentication) a continuación.
**"El panel muestra 404"**
El panel está deshabilitado por defecto. Añade `THREAT_DETECTION_DASHBOARD=true` a `.env` y limpia la caché de rutas:```bash
php artisan route:clear
/wp-admin, /.env, /phpmyadmin, /actuator, etc.) con más de 50 rutas de sonda predeterminadasapplication/json)El paquete funciona sin realizar cambios en .env. Todos los valores a continuación son opcionales: agrégalos solo si deseas anular los valores predeterminados.```env
THREAT_DETECTION_ENABLED=true
THREAT_DETECTION_MODE=balanced
### Modos de detección
| Mode | Confidence Threshold | Behavior |
|------|---------------------|----------|
| `strict` | 0 (registra todo) | Todos los patrones activos, umbrales más bajos. Detecta todo, pero puede marcar tráfico legítimo. |
| `balanced` | 10 | Predeterminado. Puntuación de confianza activa, umbrales estándar. Adecuado para la mayoría de las aplicaciones. |
| `relaxed` | 40 | Solo se activan patrones de alta gravedad. Mejor para sitios con mucho contenido y falsos positivos frecuentes. |
### Entornos habilitados
De forma predeterminada, la detección se ejecuta en `production`, `staging` y `local`. Para cambiarlo, publique la configuración y edite:```php
'enabled_environments' => ['production', 'staging', 'local'],
Para desactivar la detección en tu suite de pruebas, establece APP_ENV=testing (no en la lista anterior) o añade a tu phpunit.xml:```xml
### Referencia de configuración
Publica el archivo de configuración para ver todas las opciones disponibles:```bash
php artisan vendor:publish --tag=threat-detection-config
Secciones de configuración clave: skip_paths (rutas a omitir), only_paths (modo de lista blanca), auth_paths (detección inteligente de rutas de inicio de sesión), content_paths (suprimir alertas no altas), safe_fields (excluir campos específicos del escaneo), safe_paths (exclusión de campos basada en rutas para JSON anidado), probe_tracking (detección de sondas 404), context_weights (multiplicadores de puntuación), threat_levels (mapeo de palabras clave de severidad), api_route_filtering (suprimir baja/media en rutas de API), queue (procesamiento asíncrono), retention (purga automática), max_detections_per_request (límite de rendimiento), / (modo de autenticación).
only_paths)Si tu aplicación tiene muchas rutas pero solo te interesan unas pocas, usa only_paths para escanear solo esas rutas. Todas las demás rutas se omiten automáticamente - sin sobrecarga de middleware en absoluto.```php
// config/threat-detection.php
'only_paths' => [
'admin/',
'api/',
'login',
'register',
],
Déjalo vacío (predeterminado) para escanear todas las rutas (sujeto a `skip_paths`). Cuando ambos están configurados, `only_paths` se comprueba primero, luego `skip_paths` se aplica dentro del conjunto coincidente.
### Soporte de colas
Por defecto, el registro de amenazas se realiza sincrónicamente en el ciclo de solicitud. Para aplicaciones de alto tráfico, puedes descargar las escrituras de la base de datos y las notificaciones de Slack a una cola:```env
THREAT_DETECTION_QUEUE=true
THREAT_DETECTION_QUEUE_CONNECTION=redis
THREAT_DETECTION_QUEUE_NAME=threat-logs
Esto envía un trabajo StoreThreatLog (3 reintentos, backoff 10s/30s). La detección sigue ocurriendo en tiempo real - solo se difiere la escritura.
Eliminar automáticamente los registros de amenazas antiguos en un horario diario:```env THREAT_DETECTION_RETENTION=true THREAT_DETECTION_RETENTION_DAYS=90
Requiere que el programador de tareas de Laravel esté en ejecución (`php artisan schedule:run`). Se ejecuta diariamente a las 02:00 mediante `threat-detection:purge`.
### Evento ThreatDetected
Cada amenaza confirmada despacha un evento `ThreatDetected` que puedes escuchar:```php
// app/Providers/EventServiceProvider.php
use JayAnta\ThreatDetection\Events\ThreatDetected;
protected $listen = [
ThreatDetected::class => [
YourCustomListener::class,
],
];
El evento lleva $threatLog (array completo de la fila de la BD), $ipAddress y $threatLevel. Úsalo para desencadenar acciones personalizadas - enviar alertas de Telegram, actualizar una lista de bloqueo, alimentar un SIEM, etc.
Cuando un cliente supera el umbral DDoS configurado (ddos.threshold solicitudes en
ddos.window segundos), se despacha un evento DdosThresholdExceeded junto con la entrada
del registro de amenazas:```php
use JayAnta\ThreatDetection\Events\DdosThresholdExceeded;
protected $listen = [ DdosThresholdExceeded::class => [ YourFloodListener::class, ], ];
El evento lleva `$ipAddress`, `$requestCount`, `$threshold` y `$windowSeconds`. Se
limita a una vez por IP por ventana de deduplicación (el mismo límite que la fila de registro), para que una inundación no
ahogue a tus receptores. Úsalo para alertar o para alimentar un almacén de baneo externo; para *rechazar*
clientes por encima del umbral, usa `ThreatDetection::isDdosThresholdExceeded($ip)` desde tu propio
middleware en su lugar — consulta [Actuar sobre los datos](#acting-on-the-data-operator-side-blocking).
---
## Notificaciones de Slack
Las alertas de Slack están deshabilitadas por defecto. Para habilitarlas:```env
THREAT_DETECTION_NOTIFICATIONS=true
THREAT_DETECTION_SLACK_WEBHOOK=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
THREAT_DETECTION_SLACK_CHANNEL=#threat-alerts
Solo las amenazas de alta severidad activan notificaciones por defecto (configurable mediante notify_levels en la configuración).
Laravel 10: Utiliza la clase de notificación integrada SlackMessage. No se necesita ningún paquete adicional.
Laravel 11+: El canal integrado de Slack fue eliminado. El paquete detecta esto automáticamente y envía webhooks HTTP POST sin procesar a tu URL de Slack. No se necesita ningún paquete adicional. Si prefieres el canal de notificación completo, instala:```bash composer require laravel/slack-notification-channel
---
## Panel de control
El paquete incluye un panel de control integrado con modo oscuro (Alpine.js + Tailwind CDN - no se requiere compilación).```
+-------------------------------------------------------------------------+
| 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 |
+-------------------------------------------------------------------------+
Agrega a .env:```env
THREAT_DETECTION_DASHBOARD=true
Visita: `http://your-app.test/threat-detection`
### Acceso durante el desarrollo local
El panel usa el middleware `['web', 'auth']` por defecto, por lo que los usuarios deben iniciar sesión. Si tu aplicación aún no tiene autenticación, restríngelo a tu propia máquina:```env
THREAT_DETECTION_DASHBOARD_GUARD=ip
THREAT_DETECTION_DASHBOARD_IPS=127.0.0.1
Todas las opciones de guard, y el guard separado en los endpoints que desactivan las detecciones, se tratan en Dashboard y autenticación de API.
Si el dashboard muestra datos vacíos, la página cargó pero sus llamadas a la API no. Consulta Autenticación de API.
El paquete proporciona 15 endpoints REST para crear dashboards personalizados o integraciones.
Las rutas de la API utilizan el middleware auth:sanctum por defecto. El paquete maneja esto de forma elegante:
['api']. La API funciona sin autenticación.Si no utilizas Sanctum pero quieres proteger tu API, tienes dos opciones:
Opción 1 - Usa el guard de autenticación integrado:```env THREAT_DETECTION_API_GUARD=auth
**Opción 2 - Cambiar el middleware directamente:**```php
// config/threat-detection.php
'api' => [
'enabled' => true,
'prefix' => 'api/threat-detection',
'middleware' => ['api', 'auth'], // or 'auth:your-guard'
],
Para pruebas locales (si Sanctum bloquea el acceso), cambia temporalmente:```php 'middleware' => ['api'], // remove 'auth:sanctum'
> Restablezca la autenticación antes de implementar en producción.
### Referencia de Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/threat-detection/threats` | Listar amenazas (paginado, filtrable) |
| GET | `/api/threat-detection/threats/{id}` | Detalles de una amenaza individual |
| POST | `/api/threat-detection/threats/{id}/false-positive` | Marcar amenaza como falso positivo |
| GET | `/api/threat-detection/stats` | Estadísticas generales |
| GET | `/api/threat-detection/summary` | Desglose detallado por tipo, nivel, IP |
| GET | `/api/threat-detection/live-count` | Amenazas en la última hora |
| GET | `/api/threat-detection/by-country` | Agrupado por país |
| GET | `/api/threat-detection/by-cloud-provider` | Agrupado por proveedor de nube |
| GET | `/api/threat-detection/top-ips` | Principales IPs infractoras |
| GET | `/api/threat-detection/timeline` | Cronología de amenazas (para gráficos) |
| GET | `/api/threat-detection/ip-stats?ip=x.x.x.x` | Estadísticas para una IP específica |
| GET | `/api/threat-detection/correlation` | Análisis de correlación |
| GET | `/api/threat-detection/export` | Exportar a CSV |
| GET | `/api/threat-detection/exclusion-rules` | Listar reglas de exclusión |
| DELETE | `/api/threat-detection/exclusion-rules/{id}` | Eliminar una regla de exclusión |
### Parámetros de consulta para `/threats`
| Parameter | Description |
|-----------|-------------|
| `keyword` | Buscar en IP, URL, tipo |
| `ip` | Filtrar por dirección IP |
| `level` | Filtrar por nivel de amenaza (`high`, `medium`, `low`) |
| `type` | Filtrar por tipo de amenaza |
| `country` | Filtrar por código de país |
| `is_foreign` | Filtrar IPs extranjeras (`true`/`false`) |
| `cloud_provider` | Filtrar por proveedor de nube |
| `is_false_positive` | Filtrar por estado de falso positivo (`true`/`false`) |
| `date_from` / `date_to` | Filtro de rango de fechas |
| `per_page` | Elementos por página (predeterminado: 20, máximo: 100) |
### Ejemplo de respuesta de la API
**GET `/api/threat-detection/stats`:**```json
{
"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
}
}
Vue.js:```javascript 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:**```jsx
useEffect(() => {
fetch('/api/threat-detection/stats')
.then(res => res.json())
.then(data => setStats(data));
}, []);
Si tu API usa
auth:sanctum, incluye los encabezados de autenticación o configura la autenticación SPA de Sanctum para solicitudes basadas en cookies.
php artisan threat-detection:doctor
php artisan threat-detection:stats
php artisan threat-detection:enrich --days=7
php artisan threat-detection:purge --days=30
php artisan threat-detection:export-fail2ban --level=high --since=24h --min-hits=5 php artisan threat-detection:export-fail2ban --format=plain > /tmp/banlist.txt
php artisan threat-detection:export-blocklist --format=nginx > /etc/nginx/blocklist.conf php artisan threat-detection:export-blocklist --format=apache > .htaccess-deny php artisan threat-detection:export-blocklist --format=csv --since=7d
---
## Actuando sobre los datos (Bloqueo del lado del operador)
El paquete nunca bloquea una solicitud: esa es su identidad, no un valor predeterminado. Las exportaciones anteriores alimentan las capas de aplicación que ya ejecutas (fail2ban, nginx, un WAF perimetral). Pero algunos despliegues no tienen esa capa a la que alimentar: hosting compartido, PaaS, contenedores detrás de un balanceador de carga que no controlas. Para esos, el paquete expone sus *decisiones* como ayudantes, y tú escribes el middleware de aplicación tú mismo. Misma arquitectura que las exportaciones: **nosotros aportamos la inteligencia, tú aportas la denegación.**```php
// app/Http/Middleware/EnforceThreatDecisions.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use JayAnta\ThreatDetection\Facades\ThreatDetection;
class EnforceThreatDecisions
{
public function handle(Request $request, Closure $next)
{
$ip = (string) $request->ip();
// Static operator denylist (config: blocklisted_ips).
// CIDR supported; whitelisted_ips wins on overlap.
if (ThreatDetection::isBlocklisted($ip)) {
abort(403);
}
// Volumetric flood: refuse over-threshold clients until the window resets.
if (ThreatDetection::isDdosThresholdExceeded($ip)) {
return response('Too Many Requests', 429, [
'Retry-After' => (string) config('threat-detection.ddos.window', 60),
]);
}
return $next($request);
}
}
Antes de aplicar restricciones por IP, configura
TrustProxies.Todo lo anterior se basa en
$request->ip(). Detrás de un balanceador de carga, CDN o proxy inverso, eso devuelve la IP del cliente solo cuando Laravel sabe qué proxies debe confiar. Si no es así, dos cosas se rompen a la vez: cada solicitud parece venir del proxy, por lo que una entrada en la lista de denegación bloquea todo tu tráfico o nada — y peor aún, si la aplicación confía en una cabecera reenviada que no debería, un atacante estableceX-Forwarded-Fory atraviesa directamente la lista de bloqueo.Esto importa más aquí que para
whitelisted_ips. Una coincidencia incorrecta en la lista blanca solo significa que el paquete escanea una solicitud que podría haber omitido: falla de forma segura. Una lista de denegación usada para rechazar tráfico falla en abierto — crees que una dirección está bloqueada cuando no lo está. Revisaapp/Http/Middleware/TrustProxies.php(o la llamadatrustProxiesenbootstrap/app.phpen Laravel 11+) antes de confiar en cualquiera de las dos ayudas para imponer restricciones.
Regístralo globalmente (antes del middleware de detección está bien — las ayudas leen la configuración y la caché, no dependen del orden del middleware):```php // bootstrap/app.php (Laravel 11+) ->withMiddleware(function ($middleware) { $middleware->prepend(\App\Http\Middleware\EnforceThreatDecisions::class); })
Los helpers:
| Helper | Returns | Backed by |
|---|---|---|
| `ThreatDetection::isBlocklisted($ip)` | `bool` | config `blocklisted_ips` (CIDR mediante `IpUtils`; la lista blanca gana) |
| `ThreatDetection::isWhitelisted($ip)` | `bool` | config `whitelisted_ips` |
| `ThreatDetection::ddosRequestCount($ip)` | `int` | el contador de inundación que mantiene el middleware de detección |
| `ThreatDetection::isDdosThresholdExceeded($ip)` | `bool` | ese contador frente a `ddos.threshold` |
Notas:
- **La lista de denegación es estática y la mantiene el operador.** Nada en el paquete la modifica jamás: ejecuta la misma decisión que tomaría una jail de fail2ban («He leído el panel; este /24 es hostil»), solo dentro de la aplicación.
- El contador de DDoS solo cuenta las peticiones que llegaron a la detección (`skip_paths`, IPs en lista blanca y entornos deshabilitados nunca se cuentan), y permanece en 0 en los controladores de caché donde la detección de DDoS está deshabilitada (`file`, `database`, `null`).
- Cuando un cliente supera el umbral, también se despacha un [`DdosThresholdExceeded` evento](#ddosthresholdexceeded-event), útil para alertar o alimentar una lista de baneo externa. Sin embargo, no llames a `abort()` desde el listener: los listeners se ejecutan dentro del `try/catch` de fallo abierto del middleware de detección, por lo que el rechazo corresponde a tu propio middleware, como se indicó antes.
---
## Seguimiento de Sondas 404
El paquete detecta sondas de reconocimiento - bots que golpean rutas vulnerables conocidas como `/wp-admin`, `/.env` o `/phpmyadmin` en tu sitio que no es WordPress ni phpMyAdmin. Estas no tienen carga maliciosa; la propia ruta es la señal.
Se registran con una etiqueta de tipo `[probe]`, separada de la detección basada en cargas maliciosas. Si una petición de sonda también contiene una carga maliciosa, ambas se registran de forma independiente.
Habilitado por defecto con más de 50 rutas de sonda. Personalízalo en `config/threat-detection.php`:```php
'probe_tracking' => [
'enabled' => true,
'default_level' => 'medium',
'paths' => [
'/wp-admin' => 'WordPress Admin',
'/wp-admin/*' => 'WordPress Admin',
'/.env' => 'Environment File',
'/phpmyadmin' => 'phpMyAdmin',
'/actuator/*' => 'Spring Actuator',
// Add your own probe paths...
],
],
Desactivar con THREAT_DETECTION_PROBE_TRACKING=false.
Si campos de formulario específicos contienen legítimamente HTML, palabras clave SQL o código (por ejemplo, editores de CMS, entradas de fragmentos de código), puedes excluirlos del escaneo:```php // config/threat-detection.php 'safe_fields' => ['content', 'body', 'html', 'description', 'code'],
Los campos enumerados aquí se omiten de los parámetros de consulta y del cuerpo de la solicitud, tanto en formato `application/x-www-form-urlencoded` como JSON (`application/json`), antes de que se ejecute la detección. Los demás campos de la misma solicitud se siguen escaneando por completo.
### Rutas seguras (sensibles a la ruta, para API JSON anidadas)
`safe_fields` coincide con un nombre de clave **en cualquier lugar** donde aparezca. Para API JSON anidadas, eso suele ser demasiado amplio: puede que quieras eximir el valor de un campo específico sin eximir esa clave en todos lados. Usa `safe_paths`, que coincide mediante la **ruta** en notación de puntos y admite comodines `fnmatch`:```php
// config/threat-detection.php
'safe_paths' => ['search.query', 'filters.*.value'],
Por ejemplo, search.query exime el valor de {"search": {"query": "..."}} (un cuadro de búsqueda cuyo texto contiene legítimamente palabras como SELECT), mientras que un campo query en cualquier otro lugar de la solicitud sigue siendo escaneado. Todo lo que no esté listado se escanea exactamente igual que antes.
Una regex por sí sola no puede expresar todas las restricciones: cualquier secuencia de 12 dígitos coincide con el patrón de Aadhaar, pero un número Aadhaar real también pasa la suma de verificación de Verhoeff. Asigna una etiqueta de patrón (predeterminada o personalizada) a un validador con nombre, y una coincidencia de regex solo cuenta como detección cuando al menos un valor coincidente pasa el validador:```php // config/threat-detection.php 'pattern_validators' => [ 'Aadhaar Number Detected' => 'verhoeff', // shipped default ],
Validadores disponibles:
| Validador | Suma de verificación | Uso típico |
|------------|----------|-------------|
| `verhoeff` | Verhoeff | Números Aadhaar |
| `luhn` | Luhn | Números de tarjetas de crédito/débito |
Con el mapeo incluido, las marcas de tiempo, los IDs de pedidos y los códigos de barras que resultan tener 12 dígitos ya no se registran como PII — mientras que los números Aadhaar genuinos sí se siguen registrando. Si varios valores coinciden y solo uno pasa la suma de verificación, la detección igualmente se activa: un número real entre el ruido sigue siendo una fuga.
Combina un validador con tu propio patrón para la detección de tarjetas controlada por suma de verificación:```php
'custom_patterns' => ['/\b(?:\d[ -]?){13,19}\b/' => 'Card Number Detected'],
'pattern_validators' => ['Card Number Detected' => 'luhn'],
Un nombre de validador desconocido falla en abierto — la coincidencia se cuenta sin validar y se registra una advertencia una sola vez — de modo que un error tipográfico nunca puede desactivar silenciosamente un patrón de detección. Las configuraciones publicadas antes de esta función simplemente no tienen la clave y mantienen exactamente su comportamiento actual.
Detectar datos sensibles solía significar almacenarlos. Un formulario de perfil que llevara un número de móvil, PAN y cuenta bancaria activaría tres patrones de PII, y cada una de las tres filas escritas conservaba el cuerpo completo de la solicitud tal cual - retenido durante todo el período de retención, legible por cualquiera con acceso al panel o a la base de datos. Un valor en una cadena de consulta también acababa en la columna url. El detector se convertía en una segunda copia concentrada de exactamente aquello sobre lo que te advierte.
Activado por defecto desde v1.7.0. Cuando un patrón cuya etiqueta aparece en la lista se dispara, el valor que coincidió se enmascara en la carga útil almacenada y en la URL:``` BODY: {"name":"Jane Doe","mobile":"[REDACTED]","pan":"[REDACTED]","bank_account":"[REDACTED]"}
La alerta, el endpoint, los nombres de los campos y la IP atacante sobreviven - solo desaparece el valor. La redacción se ejecuta *después* de la detección, por lo que no se pierde nada.```php
// config/threat-detection.php
'redact' => [
'enabled' => env('THREAT_DETECTION_REDACT', true),
'mask' => '[REDACTED]',
'labels' => ['Aadhaar Number Detected', 'PAN Number Detected', /* ... */],
],
Los payloads de ataque se dejan intactos deliberadamente - una cadena de inyección es evidencia, no un secreto, y enmascararla destruiría la investigación. Solo se modifican las etiquetas que enumeres.
Esto no reemplaza a Safe Fields. Esos evitan que un campo sea escaneado; la redacción te permite seguir escaneando y dejar de almacenar. Establece
THREAT_DETECTION_REDACT=falsesi necesitas payloads completos para fines forenses.
El panel y la API admiten guardas de autenticación configurables a través de .env:```env
THREAT_DETECTION_DASHBOARD_GUARD=auth
THREAT_DETECTION_DASHBOARD_GUARD=role THREAT_DETECTION_DASHBOARD_ROLE=admin
THREAT_DETECTION_DASHBOARD_GUARD=ip THREAT_DETECTION_DASHBOARD_IPS=127.0.0.1,10.0.0.0/8
Las mismas opciones están disponibles para las rutas de API con `THREAT_DETECTION_API_GUARD`.
Cuando `guard=none` (por defecto), el paquete registra una advertencia una vez al día para recordarte que configures la autenticación.
La protección **falla en modo cerrado**: un valor de guard no reconocido (por ejemplo, un error tipográfico) se deniega con un 403 y una advertencia registrada en lugar de conceder acceso silenciosamente, y `guard=role` deniega (con una advertencia) cuando el modelo de usuario autenticado no tiene un método `hasRole()`.
### Deshabilitar una detección requiere algo más que acceso de lectura
Marcar una amenaza como falso positivo y eliminar una regla de exclusión silencian un tipo de detección para todos, lo cual es un privilegio distinto a leer el registro. Esos dos endpoints se comprueban contra una protección separada:```env
# Options: none, auth, role, ip. Default: role
THREAT_DETECTION_API_WRITE_GUARD=role
Se aplica solo a esas rutas, por lo que la lectura y el panel se comportan exactamente como indica THREAT_DETECTION_API_GUARD. Sin él, cualquier usuario autenticado de tu aplicación podría desactivar una detección.
Si tu modelo de usuario no tiene hasRole(), usa =auth. Para restaurar el comportamiento anterior a la 1.7.0 en el que cualquier usuario autenticado podía deshabilitar detecciones, usa =none; threat-detection:doctor mostrará una advertencia mientras eso esté configurado.
Nota sobre el panel ↔ API: el panel integrado obtiene sus datos de las rutas de la API utilizando la cookie de sesión del navegador. Si tus rutas de API están protegidas con
auth:sanctum, configura la autenticación stateful/SPA de Sanctum (o apunta el panel a un guard autenticado por cookie) para que esas llamadas AJAX estén autorizadas; de lo contrario, el panel se renderiza vacío.
Añade tus propios patrones de expresión regular de detección en config/threat-detection.php:```php
'custom_patterns' => [
'/your-regex-here/i' => 'Your Threat Label',
],
**Ejemplo - detectar una sonda de endpoint de administración personalizado:**```php
'/\/my-admin-panel/i' => 'Custom Admin Panel Probe',
Junto a la forma clásica de cadena, el valor de un patrón puede ser un array para tener control total:```php 'custom_patterns' => [ '/\b(?:\d[ -]?){13,19}\b/' => [ 'label' => 'Card Number Detected', // required 'level' => 'high', // low|medium|high — overrides keyword derivation 'contexts' => ['query', 'body'], // query|body|headers — default: all segments 'validator' => 'luhn', // post-match checksum, wins over pattern_validators ], ],
- **`level`** establece el nivel de amenaza directamente en lugar de derivarlo de las palabras clave de `threat_levels` en la etiqueta.
- **`contexts`** restringe el escaneo a segmentos específicos de la solicitud; por ejemplo, un patrón de tarjeta que solo tiene sentido en el cuerpo deja de coincidir con secuencias de dígitos en las cabeceras.
- **`validator`** designa una comprobación en línea posterior a la coincidencia (ver [Validadores posteriores a la coincidencia](#post-match-validators-checksum-aware-false-positive-reduction)); tiene prioridad sobre el mapa de etiquetas `pattern_validators`.
Las entradas de cadena y array se combinan libremente en la misma configuración. Las opciones malformadas **fail open** — el patrón sigue escaneando sin restricciones y se registra una advertencia —, por lo que un error de configuración nunca puede deshabilitar silenciosamente una detección.
> **Nota:** Las rutas de sonda comunes como `/wp-login.php`, `/.env`, `/phpmyadmin` ahora se manejan automáticamente mediante la función [Seguimiento de sondas 404](#404-probe-tracking). No necesitas patrones personalizados para ellas.
El nivel de amenaza de cada patrón se determina automáticamente comparando las palabras clave de la etiqueta con la configuración `threat_levels`:```php
'threat_levels' => [
'high' => ['XSS', 'SQL Injection', 'SQL DDL', 'SQL DML', 'SQL File', 'SQL Hex', 'RCE', ..., 'Shellshock', 'Spring4Shell', 'PowerShell', 'CRLF', 'Null Byte', 'SSTI', 'Java', 'LDAP', 'XPath', 'PHP assert', ...],
'medium' => ['Directory Traversal', 'LFI', 'SSRF', 'Sensitive', 'Config', ..., 'Open Redirect', 'LF Injection', 'GraphQL', 'Spring Boot Actuator', ...],
'low' => ['User-Agent', 'JS Redirect', 'SEO Bot', 'Empty', 'Rate', 'Command-line Downloader', 'DNS Rebinding'],
],
Si la etiqueta no coincide con ninguna palabra clave, la amenaza toma por defecto severidad low.
Los patrones de regex no válidos se omiten automáticamente y se registran como advertencias - no harán fallar tu aplicación.
Para acceso programático a los datos de amenazas fuera del middleware:```php 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();
// Operator-side decision helpers (see "Acting on the Data") $blocked = ThreatDetection::isBlocklisted('203.0.113.7'); // static denylist, CIDR, whitelist wins $trusted = ThreatDetection::isWhitelisted('10.0.0.5'); $count = ThreatDetection::ddosRequestCount('203.0.113.7'); // requests in the current DDoS window $flooded = ThreatDetection::isDdosThresholdExceeded('203.0.113.7');
---
## Puesta en Producción
El paquete es pasivo por diseño: nunca bloquea, rechaza ni altera una solicitud, y el middleware de detección envuelve todo su cuerpo en `try/catch`, por lo que un fallo de detección nunca puede romper tu aplicación. Incluye valores predeterminados sensatos y no necesita servicios externos para funcionar. Antes de salir a producción, merece la pena revisar esta breve lista:
1. **Protege el panel de control y la API.** Ambos usan por defecto `guard = none` para un primer arranque sin configuración, y registran una advertencia diaria mientras no estén protegidos. Antes de producción, establece un guard: `THREAT_DETECTION_DASHBOARD_GUARD` y `THREAT_DETECTION_API_GUARD` (`auth`, `role` o `ip`). Un valor no reconocido o un guard `role` en un modelo de usuario sin `hasRole()` ahora **falla de forma segura** (403), de modo que un error tipográfico no expondrá datos silenciosamente. La desactivación de una detección está controlada por separado mediante `THREAT_DETECTION_API_WRITE_GUARD`, que por defecto es `role`. Consulta [Autenticación del panel y la API](#dashboard-and-api-authentication).
2. **Ejecuta las migraciones** (`vendor:publish --tag=threat-detection-migrations && migrate`). Volver a publicar es seguro: las migraciones ya publicadas se omiten.
3. **Elige un modo de detección.** `balanced` (predeterminado) se adapta a la mayoría de las aplicaciones; usa `relaxed` para sitios con mucho contenido, `strict` para superficies de alta seguridad. Ajusta con `content_paths`, `safe_fields` y `min_confidence`; consulta [Reducción de falsos positivos](#reducing-false-positives).
4. **Revisa los patrones regionales de PII / patrones personalizados.** Los valores predeterminados están centrados en la India (Aadhaar, PAN, IFSC) y los patrones numéricos amplios (p. ej., cuentas bancarias) pueden coincidir con identificadores numéricos largos fuera de las rutas de autenticación. Reemplaza o recorta `custom_patterns` para tu región y aplicación, y añade rutas con mucho contenido a `auth_paths` / `content_paths`.
5. **Activa la retención** si esperas volumen: `THREAT_DETECTION_RETENTION=true` (purga automática mediante el programador). Requiere que el programador de Laravel (`schedule:run`) esté gestionado por cron.
6. **Extras opcionales, todos desactivados por defecto:** alertas de Slack (`THREAT_DETECTION_NOTIFICATIONS`), enriquecimiento geográfico (`php artisan threat-detection:enrich`, la única función que realiza una llamada saliente, al gratuito ip-api.com), y escrituras en cola (`THREAT_DETECTION_QUEUE`: actívalo solo si ya ejecutas un worker de colas; de lo contrario, las escrituras son síncronas y no necesitan Redis).
No se requieren Redis, ni worker de colas, ni llamadas de red salientes para la detección y el registro principales.
---
## Reducción de Falsos Positivos
El paquete ofrece varias herramientas para reducir los falsos positivos. Usa la que mejor se adapte a tu situación:
### Campos Seguros y Rutas Seguras
Excluye un campo del escaneo por completo, ya sea por nombre en todas partes (`safe_fields`) o mediante notación de puntos para JSON anidado (`safe_paths`). Es el enfoque más sencillo y el más contundente: el campo se omite, por lo que no se ejecuta ninguna detección sobre él.
Detalles completos y ejemplos: [Campos Seguros](#safe-fields-false-positive-reduction).
### Supresión por Rutas de Contenido
Si tienes editores CMS, formularios de entradas de blog o secciones de comentarios donde los usuarios envían contenido enriquecido, esas rutas suelen provocar falsos positivos (p. ej., una entrada de blog que contenga ejemplos de código `<script>`). Añade esas rutas para suprimir alertas de baja/media gravedad:```php
// config/threat-detection.php
'content_paths' => [
'admin/posts/*',
'admin/pages/*',
'blog/*/edit',
'comments',
],
En estas rutas, solo se registran las amenazas de alta severidad.
Haz clic en el botón FP en cualquier amenaza del panel para marcarla como falso positivo. Esto:
is_false_positive = trueAdministra las reglas de exclusión mediante la API:```bash GET /api/threat-detection/exclusion-rules DELETE /api/threat-detection/exclusion-rules/{id}
### Puntuación de Confianza
Cada amenaza recibe una puntuación de confianza (0-100) basada en:
- Número de coincidencias de patrones en la misma solicitud
- Gravedad del patrón coincidente
- Dónde se encontró el patrón (cadena de consulta > cabeceras > cuerpo)
- Si el user-agent coincide con una herramienta de ataque conocida
- Modo de detección actual
Las amenazas por debajo del umbral de confianza para tu modo de detección no se registran (consulta [Modos de Detección](#detection-modes)).
---
## Tipos de Ataque Detectados
| Categoría | Ejemplos |
|----------|---------|
| **Inyección SQL** | UNION, booleano, basado en tiempo, codificación CHAR, DDL (DROP/ALTER/CREATE), DML (INSERT/UPDATE/DELETE), operaciones de archivo (INTO OUTFILE, LOAD_FILE), enumeración ORDER BY, cadenas hexadecimales, UNHEX |
| **Inyección NoSQL** | Operadores MongoDB $ne, $gt, $regex, $where |
| **XSS** | Etiquetas script, manejadores de eventos SVG (`<svg onload=`), manejadores de eventos HTML (`<body onload=`, `<img onerror=`), expresiones CSS, URIs JavaScript, manipulación del DOM |
| **Ejecución de Código** | Funciones shell RCE, deserialización PHP, deserialización Java (base64 + bytes mágicos hex), inyección de plantillas (Blade, JSP, ASP, Jinja2, Velocity), eval(), decodificación base64, assert() de PHP, create_function(), preg_replace /e |
| **SSTI** | Sondas matemáticas (`{{7*7}}`), import/config de Jinja2, plantillas Velocity, Expression Language |
| **Inyección de Comandos** | Linux (funciones shell, cadenas de comandos, curl, wget, nc), Windows (cmd.exe, PowerShell, wscript, cscript, net user) |
| **Acceso a Archivos** | Directory traversal, protocolos LFI/RFI, sondeos de archivos sensibles (.env, .git, composer.json) |
| **SSRF** | Localhost (127.0.0.1, 0.0.0.0, ::1), metadatos AWS/GCP, IPs privadas, localhost codificado en hex/decimal, DNS rebinding (xip.io, nip.io, sslip.io) |
| **Inyección LDAP** | Manipulación de filtros LDAP, inyección OR |
| **Inyección XPath** | Selectores de atributos, funciones XPath (contains, substring) |
| **Inyección CRLF / Cabeceras** | CRLF codificado en URL (`%0d%0a`), inyección LF, inyección de byte nulo |
| **Ataques de Protocolo** | HTTP request smuggling (CL+TE), inyección SSI |
| **Explotación de CVEs** | Shellshock (CVE-2014-6271), Spring4Shell (CVE-2022-22965), RCE PHPUnit (CVE-2017-9841), Drupalgeddon, Log4Shell |
| **Seguimiento de Sondas** | WordPress (`/wp-admin`, `/wp-login.php`), archivos de configuración (`/.env`, `/.git`), herramientas de bases de datos (`/phpmyadmin`), sondas de tecnología (`.asp`, `.jsp`), Spring actuator, documentos Swagger/API - más de 50 rutas |
| **Escáneres** | SQLMap, Nikto, Nmap, Burp Suite, FeroxBuster, FFUF, XSStrike, Dalfox, Netsparker, Qualys, Nuclei y más de 20 otros (53 en total) |
| **Rastreadores IA** | GPTBot, ClaudeBot, ChatGPT, ByteSpider, Cohere, Common Crawl |
| **Navegadores Headless** | HeadlessChrome, PhantomJS, Selenium, Puppeteer, Playwright |
| **Bots** | Scripts Python, clientes HTTP Go, cURL, wget, AhrefsBot, SEMRushBot, user agents vacíos |
| **Autenticación** | Detección de fuerza bruta, fugas de tokens, exposición de contraseñas, exposición de IDs de sesión |
| **DDoS** | Detección de solicitudes excesivas basada en tasas |
| **Evasión** | Inserción de comentarios SQL, doble codificación URL, codificación de entidades HTML, escapes Unicode, Unicode IIS, escapes hexadecimales |
| **Otros** | Introspección GraphQL, contaminación de prototipos, redirección abierta, XXE, web shells, minería criptográfica, detección de PII |
---
## Ejecutar la Suite de Pruebas```bash
composer test
El paquete incluye 335 pruebas (856 comprobaciones) que cubren patrones de detección, comportamiento del middleware, endpoints de API, puntuación de confianza, reglas de exclusión, detección de DDoS, resistencia a la evasión, patrones CVE, inyección LDAP/XPath/SSTI, detección de bots/escáneres, seguimiento de sondas, comandos de exportación, autenticación del panel, campos seguros, optimizaciones de rendimiento y verificación completa del ciclo HTTP a BD.
Licencia MIT. Consulta LICENSE para más detalles.
¡Las contribuciones son bienvenidas! Envía una Pull Request.
| Este paquete (IDS de app) | WAF (mod_security, Cloudflare WAF) | Edge / CDN (Cloudflare) |
|---|
| Bloquea peticiones maliciosas | ❌ solo registra | ✅ | ✅ |
| Contexto completo de la app (ruta exacta, payload decodificado, usuario autenticado) | ✅ | ⚠️ parcial | ❌ |
| Panel integrado + registro de amenazas en tu BD | ✅ | ⚠️ varía | ⚠️ solo edge |
| Detecciones específicas de la app (p. ej. PII de Aadhaar / PAN / IFSC) | ✅ patrones personalizados | ❌ | ❌ |
| Funciona sin conexión / sin servicio externo | ✅ | ⚠️ depende | ❌ |
| Detiene el tráfico antes de que llegue a tu app | ❌ | ✅ edge | ✅ |
| Configuración | un composer require | media–alta | baja–media |
| Coste | gratis, MIT | varía | plan gratuito + de pago |
| Quieres | Usa | Esfuerzo |
|---|
| Ver qué te está atacando | El panel o threat-detection:stats | ninguno, ya está en marcha |
| Banear a los reincidentes en el cortafuegos | threat-detection:export-fail2ban — canalízalo a un cron | una línea |
| Denegar en el servidor web | threat-detection:export-blocklist → directivas de nginx/apache | una línea |
| Rechazar peticiones en la app | Ayudantes del lado del operador — isBlocklisted(), isDdosThresholdExceeded() | ~10 líneas de tu propio middleware |
| Reaccionar en tiempo real | El evento ThreatDetected — Telegram, SIEM, PagerDuty | un listener |
strict, balanced (predeterminado) y relaxed - sensibilidad ajustabledashboard.guardapi.guard