
SQLインジェクション、XSS、RCE、ボットスキャナー、175以上の攻撃パターンを検出・記録するパッシブなLaravelミドルウェア。内蔵ダッシュボード、Slackアラート、REST API、地理情報エンリッチメントを搭載。WAFではなくIDS。
Laravel向けの受動的侵入検知 — SQLインジェクション、XSS、スキャナー、ボットのプローブなど、アプリに届くすべての攻撃をフルコンテキスト付きで記録します。これはIDSでありWAFではありません。リクエストをブロック、フィルタリング、変更することは一切ありません。
Laravel 10〜13の任意のアプリに導入するだけで、すべてのHTTPリクエストを150以上の攻撃パターンに対してスキャンし、マッチごとに信頼度をスコアリングしてデータベースに記録します。組み込みのダッシュボード、Slackアラート、ジオエンリッチメント、fail2ban/ブロックリストのエクスポートも備えています。リクエストがブロックされることは決してありません。鍵ではなく防犯カメラと考えてください。誰が、どれだけの頻度で、どのような手法でルートを調べているかを正確に示します。
本番アプリから抽出し、実際のトラフィックで実証済みです。335のテスト、Laravel自体以外のランタイム依存関係なし、検知にインターネット接続も不要です。
composer require jayanta/laravel-threat-detection php artisan vendor:publish --tag=threat-detection-migrations php artisan migrate
次に、`web` グループにミドルウェアを追加します(Laravel 11+ では `bootstrap/app.php`、Laravel 10 では `app/Http/Kernel.php` に1行追加)— 完全なスニペットは下記の [クイックスタート](#quick-start) にあります。これで完了です。検出が有効になりました。```bash
php artisan threat-detection:doctor # confirms it is actually recording
このパッケージは 受動的でアプリケーションレベルの IDS です — 監視して記録はしますが、ブロックはしません。WAF やエッジサービスの 横に 置くことを想定しており、置き換えるものではありません。各レイヤーは他の層には見えないものをそれぞれ見ています:
短い説明: エッジ/WAF はドアの鍵です。これは 内部 の防犯カメラで、アプリのコンテキストを使って、どのルートに対して誰がどのくらいの頻度で何を試しているのかを正確に教えてくれます。エッジ層が見ることのないデータを、fail2ban の ban、レート制限、geo-blocking といった実際の判断に活用してください。
ブロックしない検出器に関する最も一般的な質問です。労力の増加順に4つの答えがあります:
パッケージはインテリジェンスを提供し、拒否はあなたが提供します。この分割は意図的です — アプリ内に存在する強制コードは、読み取り、テスト、無効化が可能なコードであり、検出のバグがサイトをダウンさせることが決してないことを意味します。
threat_logs データベーステーブルにレコードが書き込まれます検出にインターネット接続は必要ありません。
composer require jayanta/laravel-threat-detection
### 2. マイグレーションを公開して実行する
> **この手順は必須です。** これを実行しないと、パッケージは脅威を検出してもデータベースに保存できません。この手順をスキップすると、`threat_logs` テーブルが存在せず、すべての検出結果が通知なしに失われます(`storage/logs/laravel.log` にエラーのみが表示されます)。```bash
php artisan vendor:publish --tag=threat-detection-migrations
php artisan migrate
これにより、threat_logs(検出された脅威を保存)とthreat_exclusion_rules(誤検知ルールを保存)の2つのテーブルが作成されます。
テーブルが作成されたことを確認します:```bash php artisan migrate:status
`create_threat_logs_table`、`add_confidence_to_threat_logs_table`、`create_threat_exclusion_rules_table` を探してください - すべて `Ran` と表示されるはずです。
### 3. ミドルウェアを登録する
ミドルウェアはリクエストをスキャンするものです。これを `web` ミドルウェアグループに追加する必要があります。
**Laravel 11 または 12 を使用している場合** - `bootstrap/app.php` を開きます:```php
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
\JayAnta\ThreatDetection\Http\Middleware\ThreatDetectionMiddleware::class,
]);
})
Laravelのバージョンを確認する方法: ターミナルで
php artisan --versionを実行してください。
Laravel 10 を使用している場合 - app/Http/Kernel.php を開いてください:```php
protected $middlewareGroups = [
'web' => [
// ... existing middleware
\JayAnta\ThreatDetection\Http\Middleware\ThreatDetectionMiddleware::class,
],
];
### 4. (オプション) 設定ファイルを公開```bash
php artisan vendor:publish --tag=threat-detection-config
パッケージは適切なデフォルト設定で動作します。設定を公開すると、検出パターン、感度モード、Slack通知などをカスタマイズできます。この手順をスキップしても、すべて正常に動作します。
これだけです。 これでアプリは脅威を検出しています。
インストール後、テスト用の脅威をトリガーし、ログに記録されることを確認します。
php artisan serve
### ステップ 2: ブラウザでテストURLを開く
アプリ内の**既存の任意のルート**(ホームページ、製品ページなど)に悪意のあるクエリパラメータを追加してください。例:
**SQLインジェクション:**```
http://localhost:8000/?q=' UNION SELECT * FROM users--
XSS(クロスサイトスクリプティング):``` http://localhost:8000/?q=
**ディレクトリトラバーサル:**```
http://localhost:8000/?file=../../etc/passwd
RCE(リモートコード実行):``` http://localhost:8000/?cmd=system('ls -la')
**Shellshock (CVE-2014-6271):**```
http://localhost:8000/?cmd=() { :;}; /bin/bash
Windows コマンドインジェクション:``` http://localhost:8000/?cmd=powershell -c whoami
**DROP TABLE (SQL DDL):**```
http://localhost:8000/?q=DROP TABLE users
アプリ内に実際に存在するルートを使用してください(例:
/)。URLが404を返す場合、ミドルウェアが実行されていない可能性があります。
オプションA - Artisanコマンド(最も速い):```bash php artisan threat-detection:stats
`Total Threats`、重大度のカウント、および上位 IP を含むテーブルが表示されるはずです。
**オプション B - Tinker:**```bash
php artisan tinker
DB::table('threat_logs')->latest()->take(5)->get(['ip_address', 'type', 'threat_level', 'confidence_score']);
オプションC - Laravel ログファイル:
検出された各脅威は、警告として storage/logs/laravel.log に書き込まれます:```
[high] Threat Detected: [middleware] SQL Injection UNION from 127.0.0.1 (http://localhost:8000/?q=...) [confidence: 50%]
| 動作 | 説明 |
|----------|-------------|
| 同じ脅威は5分間に1回のみログに記録 | 重複排除:同じIP + 同じ脅威タイプは5分間キャッシュされます。各テストには**異なる攻撃タイプ**を使用するか、テストの間隔を空けてください。 |
| `curl` リクエストは追加の検出をトリガーします | `curl` を使用すると、"cURL Command" ユーザーエージェント検出(低重要度)もログに記録されます。これは想定された動作です - このパッケージは自動化ツールを検出します。 |
| このパッケージはリクエストをブロックしません | アプリは通常どおり機能し続けます。検出はパッシブです。 |
| Slack のセットアップは不要 | 通知はデフォルトでオフになっています。 |
| インターネット接続は不要 | コア検出は100%ローカルです。任意の `threat-detection:enrich` コマンドのみが地理位置情報データのために外部APIを呼び出します。 |
### トラブルシューティング
**ここから始める — これに関するほとんどは1つのコマンドで解決します:**```bash
php artisan threat-detection:doctor
検出をサイレントに失敗させる要素 — ダッシュボードが空のままで 「攻撃なし」と見分けがつかない状態 — をチェックし、それぞれの正確な修正を 出力します。実際の障害では非ゼロで終了するため、CIや デプロイステップで安全に実行できます。``` 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: この環境で検出が有効になっているか; ライターが必要とするすべてのカラム(欠けているものは**すべての**脅威を破棄します); ダッシュボード/APIのカラム; 除外ルールのテーブル; ミドルウェアが実際にルートまたはグループに接続されているか; このバージョンより前に公開された設定; ビルトインのパターンをシャドウするカスタムパターン; DDoSカウントを実行できないキャッシュドライバ; そして認証なしで公開されたダッシュボードまたはAPI。
**"テストしたのに `threat-detection:stats` はゼロ件と表示される" / "脅威がデータベースに保存されない"**
ドクターが合格した場合、インストールは正常であり、問題はテストリクエスト自体にあります。ドクターが代わりに確認できない3つの点:
| チェック | 確認方法 |
|-------|---------------|
| IPがホワイトリストに含まれていない | `THREAT_DETECTION_WHITELISTED_IPS` を `.env` に追加した場合は、テスト中は削除してください |
| 既存のルートを使用した | テストURLは実際のルート(例: `/`)と一致する必要があります。404が返る場合はミドルウェアが実行されていません |
| 重複排除キャッシュ | 同じIP + 同じ攻撃タイプは5分間キャッシュされます - 別の攻撃タイプを試してください |
> `php artisan migrate` の実行だけでは決して十分ではありません。マイグレーションファイルは
> パッケージ内にあり、まずアプリの `database/migrations/` に公開する必要があります。
> この問題が原因の場合、ドクターは正確なコマンドを出力します。
**"APIが401 Unauthorizedを返す"**
下記の [API認証](#api-authentication) を参照してください。
**"ダッシュボードに404が表示される"**
ダッシュボードはデフォルトで無効です。`THREAT_DETECTION_DASHBOARD=true` を `.env` に追加し、ルートキャッシュをクリアしてください:```bash
php artisan route:clear
/wp-admin、/.env、/phpmyadmin、/actuator など)への偵察プローブを検出。デフォルトのプローブパスは50以上application/json)の両方のリクエストボディを検査strict、balanced(デフォルト)、 - 感度を調整可能このパッケージは .env を変更しなくても動作します。以下の値はすべて任意です - デフォルトを上書きしたい場合のみ追加してください。```env
THREAT_DETECTION_ENABLED=true
THREAT_DETECTION_MODE=balanced
### 検出モード
| モード | 信頼度しきい値 | 動作 |
|------|---------------------|----------|
| `strict` | 0(すべてをログ記録) | すべてのパターンが有効で、しきい値は最低。すべてを検出するが、正当なトラフィックを誤検知する可能性がある。 |
| `balanced` | 10 | デフォルト。信頼度スコアリングが有効で、標準的なしきい値。ほとんどのアプリに適しています。 |
| `relaxed` | 40 | 高重要度のパターンのみがトリガーされます。誤検知が多いコンテンツ重視のサイトに最適です。 |
### 有効な環境
デフォルトでは、検出は`production`、`staging`、`local`で実行されます。変更するには、設定を公開して編集します:```php
'enabled_environments' => ['production', 'staging', 'local'],
テストスイートで検出を無効にするには、APP_ENV=testing を設定するか(上記のリストにはありません)、phpunit.xml に追加します:```xml
### 設定リファレンス
設定ファイルを公開して、利用可能なすべてのオプションを確認してください:```bash
php artisan vendor:publish --tag=threat-detection-config
主要な設定セクション: skip_paths (スキップするパス), only_paths (ホワイトリストモード), auth_paths (ログインルートのスマート検出), content_paths (高以外のアラートを抑制), safe_fields (スキャンから特定フィールドを除外), safe_paths (ネストされたJSONのパス対応フィールド除外), probe_tracking (404プローブ検出), context_weights (スコアリング乗数), threat_levels (重要度キーワードマッピング), api_route_filtering (APIルートでの低/中を抑制), queue (非同期処理), retention (自動パージ), max_detections_per_request (パフォーマンス上限), dashboard.guard / (認証モード)。
only_paths)アプリに多数のルートがあるが、一部だけを対象にしたい場合は、only_paths を使用してそれらのルートのみをスキャンします。他のすべてのルートは自動的にスキップされます - ミドルウェアのオーバーヘッドはまったくありません。```php
// config/threat-detection.php
'only_paths' => [
'admin/',
'api/',
'login',
'register',
],
空のまま(デフォルト)にすると、すべてのルートをスキャンします(`skip_paths` の対象となります)。両方が設定されている場合、最初に `only_paths` がチェックされ、次に一致したセット内で `skip_paths` が適用されます。
### キューサポート
デフォルトでは、脅威ログはリクエストサイクル内で同期的に記録されます。高トラフィックのアプリケーションでは、DB 書き込みと Slack 通知をキューにオフロードできます。```env
THREAT_DETECTION_QUEUE=true
THREAT_DETECTION_QUEUE_CONNECTION=redis
THREAT_DETECTION_QUEUE_NAME=threat-logs
これは StoreThreatLog ジョブをディスパッチします (リトライ3回、バックオフ10s/30s)。検出は依然としてリアルタイムで行われます - 遅延されるのは書き込みのみです。
古い脅威ログを毎日のスケジュールで自動的に削除します:```env THREAT_DETECTION_RETENTION=true THREAT_DETECTION_RETENTION_DAYS=90
Laravelのスケジューラーが実行されている必要があります(`php artisan schedule:run`)。`threat-detection:purge` によって毎日02:00に実行されます。
### ThreatDetected Event
確認されたすべての脅威は、リッスンできる `ThreatDetected` イベントをディスパッチします:```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.
クライアントが設定されたDDoS閾値(ddos.window 秒以内に ddos.threshold リクエスト)を超えると、脅威ログエントリとともに DdosThresholdExceeded イベントがディスパッチされます:```php
use JayAnta\ThreatDetection\Events\DdosThresholdExceeded;
protected $listen = [ DdosThresholdExceeded::class => [ YourFloodListener::class, ], ];
このイベントは `$ipAddress`、`$requestCount`、`$threshold`、`$windowSeconds` を保持し、ログ行と同じスロットルで
重複排除ウィンドウごとに IP ごとに 1 回に制限されるため、フラッドが発生しても
リスナーが埋もれることはありません。アラート発報や外部のBANストアへのフィードに使用してください。しきい値を超えたクライアントを*拒否*するには、
代わりに独自のミドルウェアから `ThreatDetection::isDdosThresholdExceeded($ip)` を使用してください —
[データへの対応](#acting-on-the-data-operator-side-blocking) を参照してください。
---
## Slack 通知
Slack アラートはデフォルトでは無効です。有効にするには:```env
THREAT_DETECTION_NOTIFICATIONS=true
THREAT_DETECTION_SLACK_WEBHOOK=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
THREAT_DETECTION_SLACK_CHANNEL=#threat-alerts
デフォルトでは、高重大度の脅威のみが通知をトリガーします(設定ファイル内の notify_levels で設定可能)。
Laravel 10: 組み込みの SlackMessage 通知クラスを使用します。追加のパッケージは不要です。
Laravel 11+: 組み込みの Slack チャンネルは削除されました。このパッケージはこれを自動的に検出し、Slack URL に生の HTTP POST ウェブフックを送信します。追加のパッケージは不要です。完全な通知チャンネルを希望する場合は、以下をインストールしてください:```bash composer require laravel/slack-notification-channel
---
## ダッシュボード
このパッケージには、ダークモード対応のダッシュボード(Alpine.js + Tailwind CDN - ビルドステップ不要)が同梱されています。```
+-------------------------------------------------------------------------+
| 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 |
+-------------------------------------------------------------------------+
.env に追加します:```env
THREAT_DETECTION_DASHBOARD=true
アクセス: `http://your-app.test/threat-detection`
### ローカル開発中にアクセスするには
ダッシュボードはデフォルトで `['web', 'auth']` ミドルウェアを使用するため、ユーザーはログインしている必要があります。アプリにまだ認証がない場合は、代わりに自分のマシンにのみ制限してください:```env
THREAT_DETECTION_DASHBOARD_GUARD=ip
THREAT_DETECTION_DASHBOARD_IPS=127.0.0.1
すべてのガードオプションと、検出を無効化するエンドポイント上の個別のガードについては、ダッシュボードとAPI認証で説明されています。
ダッシュボードに空のデータが表示される場合、ページは読み込まれましたが、そのAPI呼び出しは実行されませんでした。API認証を参照してください。
このパッケージは、カスタムダッシュボードや統合を構築するための15のRESTエンドポイントを提供します。
APIルートはデフォルトで auth:sanctum ミドルウェアを使用します。パッケージはこれを自動的に処理します:
['api'] のみにフォールバックします。APIは認証なしで動作します。Sanctumを使用しないがAPIを保護したい場合、2つのオプションがあります:
オプション1 - 内蔵の認証ガードを使用する:```env THREAT_DETECTION_API_GUARD=auth
**オプション 2 - ミドルウェアを直接変更する:**```php
// config/threat-detection.php
'api' => [
'enabled' => true,
'prefix' => 'api/threat-detection',
'middleware' => ['api', 'auth'], // or 'auth:your-guard'
],
ローカルテストでは(Sanctum がアクセスをブロックする場合)、一時的に変更:```php 'middleware' => ['api'], // remove 'auth:sanctum'
> 本番環境へデプロイする前に認証を復元してください。
### エンドポイントリファレンス
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/threat-detection/threats` | 脅威の一覧(ページング・フィルタリング対応) |
| GET | `/api/threat-detection/threats/{id}` | 単一脅威の詳細 |
| POST | `/api/threat-detection/threats/{id}/false-positive` | 脅威をフォールス・ポジティブとしてマーク |
| GET | `/api/threat-detection/stats` | 全体統計 |
| GET | `/api/threat-detection/summary` | タイプ、レベル、IP別の詳細内訳 |
| GET | `/api/threat-detection/live-count` | 直近1時間の脅威 |
| GET | `/api/threat-detection/by-country` | 国別にグループ化 |
| GET | `/api/threat-detection/by-cloud-provider` | クラウドプロバイダー別にグループ化 |
| GET | `/api/threat-detection/top-ips` | 上位の迷惑IP |
| GET | `/api/threat-detection/timeline` | 脅威のタイムライン(チャート用) |
| GET | `/api/threat-detection/ip-stats?ip=x.x.x.x` | 特定IPの統計 |
| GET | `/api/threat-detection/correlation` | 相関分析 |
| GET | `/api/threat-detection/export` | CSVエクスポート |
| GET | `/api/threat-detection/exclusion-rules` | 除外ルールの一覧 |
| DELETE | `/api/threat-detection/exclusion-rules/{id}` | 除外ルールの削除 |
### `/threats` のクエリパラメータ
| Parameter | Description |
|-----------|-------------|
| `keyword` | IP、URL、タイプで検索 |
| `ip` | IPアドレスでフィルタリング |
| `level` | 脅威レベルでフィルタリング(`high`、`medium`、`low`) |
| `type` | 脅威タイプでフィルタリング |
| `country` | 国コードでフィルタリング |
| `is_foreign` | 外国IPをフィルタリング(`true`/`false`) |
| `cloud_provider` | クラウドプロバイダーでフィルタリング |
| `is_false_positive` | フォールス・ポジティブ状態でフィルタリング(`true`/`false`) |
| `date_from` / `date_to` | 日付範囲フィルター |
| `per_page` | 1ページあたりの項目数(デフォルト: 20、最大: 100) |
### 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));
}, []);
APIが
auth:sanctumを使用している場合は、認証ヘッダーを含めるか、Cookieベースのリクエスト用にSanctum SPA認証を設定してください。
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
---
## データへの対応(オペレーター側でのブロッキング)
このパッケージがリクエストをブロックすることは決してありません — それがこのパッケージのアイデンティティであり、デフォルト動作ではないのです。上記のエクスポートは
すでに稼働中の強制レイヤー(fail2ban、nginx、エッジWAF)に供給されます。しかし一部のデプロイ環境には
そのような供給先となるレイヤーがありません — 共有ホスティング、PaaS、制御不能なロードバランサーの背後にあるコンテナなどです。
そのような環境向けに、パッケージは*決定事項*をヘルパーとして公開しており、強制ミドルウェアは
自分で記述します。エクスポートと同じアーキテクチャです:**我々はインテリジェンスを供給し、
あなたは拒否を供給します。**```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);
}
}
IP で強制する前に、
TrustProxiesを設定してください。上記はすべて
$request->ip()を基にしています。ロードバランサー、CDN、またはリバースプロキシの背後では、Laravel がどのプロキシを信頼するかを指定されている場合にのみ、これはクライアント IP を返します。指定されていない場合、同時に2つの問題が発生します。すべてのリクエストがプロキシから来たように見えるため、拒否リストのエントリがすべてのトラフィックをブロックするか、まったくブロックしないかのどちらかになります。さらに悪いことに、アプリが信頼すべきでない転送ヘッダーを信頼する場合、攻撃者はX-Forwarded-Forを設定してブロックリストをそのまま通過します。これは
whitelisted_ipsの場合よりもここで重要です。ホワイトリストの誤一致は、パッケージがスキップしたかもしれないリクエストをスキャンするだけであり、フェイルセーフです。トラフィックを拒否するために使用される拒否リストはopen に失敗します — アドレスがブロックされていると思っていても、実際にはブロックされていません。いずれかのヘルパーを強制に使用する前に、app/Http/Middleware/TrustProxies.php(または Laravel 11+ のbootstrap/app.php内のtrustProxies呼び出し)を確認してください。
グローバルに登録します(検出ミドルウェアの前で問題ありません — ヘルパーは設定とキャッシュを読み取るだけで、ミドルウェアの順序に依存しません):```php // bootstrap/app.php (Laravel 11+) ->withMiddleware(function ($middleware) { $middleware->prepend(\App\Http\Middleware\EnforceThreatDecisions::class); })
ヘルパー:
| ヘルパー | 戻り値 | 基づくもの |
|---|---|---|
| `ThreatDetection::isBlocklisted($ip)` | `bool` | `blocklisted_ips` 設定(`IpUtils` による CIDR 対応、ホワイトリスト優先) |
| `ThreatDetection::isWhitelisted($ip)` | `bool` | `whitelisted_ips` 設定 |
| `ThreatDetection::ddosRequestCount($ip)` | `int` | 検出ミドルウェアが維持するフラッドカウンター |
| `ThreatDetection::isDdosThresholdExceeded($ip)` | `bool` | そのカウンターと `ddos.threshold` の比較 |
注記:
- **拒否リストは静的であり、運用者が管理します。** パッケージは決してこれに追加することは
ありません。fail2ban の jail と同じ判断(「ダッシュボードを見ると、この /24 は
敵対的だ」)をアプリ内で実行するだけです。
- DDoS カウンターは、検出に到達したリクエストのみをカウントします(`skip_paths`、ホワイトリスト
登録済み IP、および無効な環境は一切カウントされません)。また、DDoS 検出が無効なキャッシュ
ドライバ(`file`、`database`、`null`)では 0 のままです。
- クライアントがしきい値を超えると、[`DdosThresholdExceeded` イベント](#ddosthresholdexceeded-event)
もディスパッチされます。アラートや外部の禁止リストへの登録に便利です。ただし、`abort()` を
リスナー内で呼び出さないでください。リスナーは検出ミドルウェアのフェイルオープン
`try/catch` 内で実行されるため、拒否は上記のように独自のミドルウェアで行ってください。
---
## 404 プローブ追跡
このパッケージは偵察プローブを検出します。WordPress でも phpMyAdmin でもないサイトで、`/wp-admin`、`/.env`、`/phpmyadmin` などの既知の脆弱なパスにアクセスするボットです。これらには悪意のあるペイロードはありません。パス自体がシグナルです。
`[probe]` タイプのタグでログに記録され、ペイロードベースの検出とは区別されます。プローブリクエストに悪意のあるペイロードも含まれている場合、両者は独立してログに記録されます。
デフォルトでは、50 以上のプローブパスが有効になっています。カスタマイズは `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...
],
],
THREAT_DETECTION_PROBE_TRACKING=false で無効化します。
特定のフォームフィールドに正当なHTML、SQLキーワード、またはコードが含まれる場合(例: CMSエディタ、コードスニペット入力)、それらをスキャン対象から除外できます:```php // config/threat-detection.php 'safe_fields' => ['content', 'body', 'html', 'description', 'code'],
ここにリストされたフィールドは、検出が実行される前に、クエリパラメータとリクエストボディ(フォームエンコードおよびJSON(`application/json`)の両方)から除去されます。同じリクエスト内の他のフィールドは引き続き完全にスキャンされます。
### セーフパス(パス対応、ネストされたJSON API向け)
`safe_fields` はキー名が**どこに**現れても一致します。ネストされたJSON APIでは、それはしばしば広すぎます。特定のキー全体を除外せずに、特定のフィールドの値だけを除外したい場合もあるでしょう。その場合は、ドット記法の**パス**で一致し、`fnmatch` ワイルドカードをサポートする `safe_paths` を使用してください。```php
// config/threat-detection.php
'safe_paths' => ['search.query', 'filters.*.value'],
例えば、search.query は {"search": {"query": "..."}} の値を除外します(テキストに SELECT のような単語が正当に含まれる検索ボックス)。一方、リクエスト内の他の場所にある query フィールドは引き続きスキャンされます。リストにないものはすべて以前とまったく同じようにスキャンされます。
正規表現だけではすべての制約を表現できません。任意の 12桁の連続はAadhaarパターンに一致しますが、実際のAadhaar番号はVerhoeffチェックサムも通過します。パターンラベル(デフォルトまたはカスタム)を名前付きバリデータにマッピングすると、正規表現のヒットは、一致した値の少なくとも1つがそれを通過した場合にのみ検出としてカウントされます:```php // config/threat-detection.php 'pattern_validators' => [ 'Aadhaar Number Detected' => 'verhoeff', // shipped default ],
利用可能なバリデータ:
| バリデータ | チェックサム | 一般的な用途 |
|------------|----------|-------------|
| `verhoeff` | Verhoeff | Aadhaar番号 |
| `luhn` | Luhn | クレジット/デビットカード番号 |
同梱のマッピングにより、たまたま12桁の長さを持つタイムスタンプ、注文ID、バーコードはPIIとして記録されなくなります — 一方、本物のAadhaar番号は依然として記録されます。複数の値が一致し、そのうちの1つだけがチェックサムを通過する場合でも、検出は依然として作動します: ノイズの中の実在する番号は依然として漏洩です。
チェックサムで制御されるカード検出のために、バリデータを独自のパターンと組み合わせてください:```php
'custom_patterns' => ['/\b(?:\d[ -]?){13,19}\b/' => 'Card Number Detected'],
'pattern_validators' => ['Card Number Detected' => 'luhn'],
未知のバリデータ名は フェイルオープン します — マッチは未検証のままカウントされ、警告は一度だけログに記録されます — そのため、タイプミスが検出パターンを静かに無効化することは絶対にありません。この機能より前に公開された設定には単にそのキーがなく、現在の正確な動作が維持されます。
機密データを検出することは、かつてそれを保存することを意味していました。携帯電話番号、PAN、銀行口座を含むプロフィールフォームは3つのPIIパターンを発火させ、書き込まれた3つの行それぞれが、リクエストボディ全体を逐語的に保持していました - 完全な保持期間中保持され、ダッシュボードやデータベースにアクセスできる人なら誰でも読める状態でした。クエリ文字列内の値も url 列に含まれていました。検出器は、まさに警告対象となるデータの2番目の集中コピーになっていました。
v1.7.0 以降デフォルトで有効です。 ラベルがリストに含まれるパターンが発火すると、マッチした値は保存されるペイロードとURL内でマスクされます:``` BODY: {"name":"Jane Doe","mobile":"[REDACTED]","pan":"[REDACTED]","bank_account":"[REDACTED]"}
アラート、エンドポイント、フィールド名、攻撃元IPはすべて残ります - 消えるのは値だけです。リダクションは検出の*後*に実行されるため、何も見逃されません。```php
// config/threat-detection.php
'redact' => [
'enabled' => env('THREAT_DETECTION_REDACT', true),
'mask' => '[REDACTED]',
'labels' => ['Aadhaar Number Detected', 'PAN Number Detected', /* ... */],
],
攻撃ペイロードは意図的にそのまま残されます。インジェクション文字列は証拠であって秘密ではなく、マスキングすると調査が台無しになるからです。リストに挙げたラベルのみが対象となります。
これは安全なフィールドの代替にはなりません。これらはフィールドのスキャンを停止しますが、リダクションではスキャンを継続しつつ保存を停止できます。フォレンジックで完全なペイロードが必要な場合は、
THREAT_DETECTION_REDACT=falseを設定してください。
ダッシュボードとAPIは、.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
同じオプションは、`THREAT_DETECTION_API_GUARD` を伴う API ルートでも利用できます。
`guard=none`(デフォルト)の場合、パッケージは認証を設定するよう促す警告を1日1回ログに記録します。
このガードは **フェイルクローズ** です:認識されないガード値(例:タイプミス)は、黙ってアクセスを許可するのではなく、403とログに記録される警告で拒否されます。また、`guard=role` の場合、認証済みユーザーモデルに `hasRole()` メソッドがないときは(警告付きで)拒否されます。
### 検出の無効化には読み取りアクセス以上の権限が必要
脅威を誤検知としてマークすることと除外ルールを削除することは、どちらも全ユーザーに対して検出タイプを無効化する操作であり、ログの読み取りとは異なる権限です。これら2つのエンドポイントは、別のガードに対してチェックされます:```env
# Options: none, auth, role, ip. Default: role
THREAT_DETECTION_API_WRITE_GUARD=role
これはそれらのルートにのみ適用されるため、読み取りとダッシュボードは THREAT_DETECTION_API_GUARD が指定するとおりに動作します。これがないと、アプリケーションの認証済みユーザーなら誰でも検出をオフにできてしまいます。
ユーザーモデルに hasRole() がない場合は、=auth を使用してください。認証済みユーザーなら誰でも検出を無効化できた pre-1.7.0 の動作に戻すには、=none を使用します。それが設定されている間は threat-detection:doctor が警告を出します。
ダッシュボード ↔ API 注記: 組み込みのダッシュボードは、ブラウザのセッション Cookie を使用して API ルートからデータを取得します。API ルートが
auth:sanctumで保護されている場合は、Sanctum の stateful/SPA 認証を設定して(またはダッシュボードを Cookie 認証ガードに向けて)、それらの AJAX 呼び出しが承認されるようにしてください。そうしないと、ダッシュボードは空で表示されます。
独自の検出用正規表現パターンを config/threat-detection.php に追加します:```php
'custom_patterns' => [
'/your-regex-here/i' => 'Your Threat Label',
],
**例 - カスタム管理エンドポイントのプローブを検出する:**```php
'/\/my-admin-panel/i' => 'Custom Admin Panel Probe',
従来の文字列形式に加えて、パターンの値は完全に制御するための配列にすることができます:```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`** は、ラベル内の `threat_levels` キーワードから導出する代わりに、脅威レベルを直接設定します。
- **`contexts`** はスキャンを特定のリクエストセグメントに制限します — 例えば、ボディ内でのみ意味を持つカードパターンは、ヘッダー内の数字の連なりにマッチしなくなります。
- **`validator`** はインラインのポストマッチチェックを指定します([ポストマッチバリデータ](#post-match-validators-checksum-aware-false-positive-reduction) を参照)。これは `pattern_validators` ラベルマップよりも優先されます。
文字列と配列のエントリは同じ設定内で自由に混在できます。不正なオプションは**フェイルオープン**します — パターンは引き続き制限なくスキャンされ、警告がログに記録されます — そのため、設定ミスによって検出が静かに無効化または縮小されることは決してありません。
> **注記:** `/wp-login.php`、`/.env`、`/phpmyadmin` などの一般的なプローブパスは、[404プローブトラッキング](#404-probe-tracking) 機能によって自動的に処理されるようになりました。これらのためにカスタムパターンを用意する必要はありません。
各パターンの脅威レベルは、ラベル内のキーワードを `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'],
],
ラベルがどのキーワードにも一致しない場合、脅威の重大度はデフォルトで low になります。
無効な正規表現パターンは自動的にスキップされ、警告としてログに記録されます - アプリケーションがクラッシュすることはありません。
ミドルウェアの外部で脅威データにプログラムからアクセスするには:```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');
---
## 本番環境への移行
このパッケージは設計上パッシブです - リクエストをブロック・拒否・変更することは一切なく、検出ミドルウェアは本体全体を `try/catch` でラップしているため、検出が失敗してもアプリが壊れることはありません。適切なデフォルト設定が同梱されており、実行に外部サービスは不要です。本番稼働前に、以下の短いチェックリストをご確認ください:
1. **ダッシュボードとAPIを保護する。** どちらも初回はゼロ設定で動くようデフォルトで `guard = none` になっており、保護されていない間は毎日警告をログに記録します。本番環境の前に、ガードを設定してください - `THREAT_DETECTION_DASHBOARD_GUARD` と `THREAT_DETECTION_API_GUARD`(`auth`、`role`、`ip`)。認識できない値や、`hasRole()` を持たないユーザーモデルに対する `role` ガードは、**フェイルクローズ**(403)となるため、タイポが静かにデータを晒すことはありません。検出の無効化は別途 `THREAT_DETECTION_API_WRITE_GUARD` によって制御され、デフォルトは `role` です。[ダッシュボードとAPI認証](#dashboard-and-api-authentication) を参照してください。
2. **マイグレーションを実行する**(`vendor:publish --tag=threat-detection-migrations && migrate`)。再公開は安全です - 既に公開済みのマイグレーションはスキップされます。
3. **検出モードを選択する。** `balanced`(デフォルト)はほとんどのアプリに適しています。コンテンツ中心のサイトでは `relaxed`、高セキュリティ領域では `strict` を使用します。`content_paths`、`safe_fields`、`min_confidence` で調整します - [誤検知の削減](#reducing-false-positives) を参照してください。
4. **地域別のPII / カスタムパターンを確認する。** デフォルトはインド中心(Aadhaar、PAN、IFSC)で、幅広い数値パターン(例: bank-account)は認証ルート外の長い数値IDに一致する可能性があります。地域とアプリに合わせて `custom_patterns` を置き換えたり削ったりし、コンテンツ量の多いルートを `auth_paths` / `content_paths` に追加してください。
5. **データ保持を有効にする** 大量のデータを予想する場合: `THREAT_DETECTION_RETENTION=true`(スケジューラー経由で自動削除)。Laravelのスケジューラー(`schedule:run`)をcronで駆動させる必要があります。
6. **オプションの追加機能(すべてデフォルトでオフ):** Slackアラート(`THREAT_DETECTION_NOTIFICATIONS`)、地理位置情報エンリッチメント(`php artisan threat-detection:enrich` - 無料のip-api.comへの外部呼び出しを行う唯一の機能)、およびキュー書き込み(`THREAT_DETECTION_QUEUE` - すでにキューワーカーを実行している場合のみ有効にしてください。それ以外の場合は書き込みは同期であり、Redisは不要です)。
コアの検出とロギングには、Redisもキューワーカーも、外部ネットワーク呼び出しも必要ありません。
---
## 誤検知の削減
このパッケージは誤検知を減らすための複数のツールを提供します。状況に合ったものを使用してください:
### セーフフィールドとセーフパス
フィールドをスキャンから完全に除外します。これは全箇所でのフィールド名による指定(`safe_fields`)またはネストされたJSONのドット記法パス(`safe_paths`)のいずれかで行います。最もシンプルで、最も乱暴な方法です - フィールドはスキップされるため、そのフィールドでは検出は一切実行されません。
全文と例: [セーフフィールド](#safe-fields-false-positive-reduction)。
### コンテンツパスの抑制
CMSエディター、ブログ投稿フォーム、ユーザーがリッチコンテンツを投稿するコメント欄がある場合、それらのパスはしばしば誤検知を引き起こします(例: `<script>` コードサンプルを含むブログ投稿)。それらのパスを追加すると、低/中レベルのアラートを抑制できます:```php
// config/threat-detection.php
'content_paths' => [
'admin/posts/*',
'admin/pages/*',
'blog/*/edit',
'comments',
],
これらのパスでは、高深刻度の脅威のみが記録されます。
ダッシュボードで任意の脅威のFPボタンをクリックすると、その脅威を誤検知としてマークできます。この操作により:
is_false_positive = true としてフラグ付けされますAPI経由で除外ルールを管理します:```bash GET /api/threat-detection/exclusion-rules DELETE /api/threat-detection/exclusion-rules/{id}
### 信頼度スコアリング
すべての脅威は、以下に基づいて信頼度スコア(0〜100)を受け取ります:
- 同じリクエスト内のパターンマッチ数
- マッチしたパターンの深刻度
- パターンが検出された場所(クエリ文字列 > ヘッダー > ボディ)
- ユーザーエージェントが既知の攻撃ツールと一致するかどうか
- 現在の検出モード
検出モードの信頼度しきい値を下回る脅威はログに記録されません([検出モード](#detection-modes)を参照)。
---
## 検出される攻撃タイプ
| カテゴリ | 例 |
|----------|---------|
| **SQLインジェクション** | UNION、ブール型、時間ベース、CHARエンコーディング、DDL(DROP/ALTER/CREATE)、DML(INSERT/UPDATE/DELETE)、ファイル操作(INTO OUTFILE、LOAD_FILE)、ORDER BY列挙、16進文字列、UNHEX |
| **NoSQLインジェクション** | MongoDBの$ne、$gt、$regex、$where演算子 |
| **XSS** | スクリプトタグ、SVGイベントハンドラー(`<svg onload=`)、HTMLイベントハンドラー(`<body onload=`、`<img onerror=`)、CSS式、JavaScript URI、DOM操作 |
| **コード実行** | RCEシェル関数、PHP逆シリアル化、Java逆シリアル化(base64 + 16進マジックバイト)、テンプレートインジェクション(Blade、JSP、ASP、Jinja2、Velocity)、eval()、base64デコード、PHP assert()、create_function()、preg_replace /e |
| **SSTI** | 数学的プローブ(`{{7*7}}`)、Jinja2のimport/config、Velocityテンプレート、式言語 |
| **コマンドインジェクション** | Linux(シェル関数、コマンドチェーン、curl、wget、nc)、Windows(cmd.exe、PowerShell、wscript、cscript、net user) |
| **ファイルアクセス** | ディレクトリトラバーサル、LFI/RFIプロトコル、機密ファイルのプローブ(.env、.git、composer.json) |
| **SSRF** | ローカルホスト(127.0.0.1、0.0.0.0、::1)、AWS/GCPメタデータ、プライベートIP、16進/10進エンコードされたローカルホスト、DNSリバインディング(xip.io、nip.io、sslip.io) |
| **LDAPインジェクション** | LDAPフィルター操作、ORインジェクション |
| **XPathインジェクション** | 属性セレクター、XPath関数(contains、substring) |
| **CRLF / ヘッダーインジェクション** | URLエンコードされたCRLF(`%0d%0a`)、LFインジェクション、ヌルバイトインジェクション |
| **プロトコル攻撃** | HTTPリクエストスマグリング(CL+TE)、SSIインジェクション |
| **CVEエクスプロイト** | Shellshock(CVE-2014-6271)、Spring4Shell(CVE-2022-22965)、PHPUnit RCE(CVE-2017-9841)、Drupalgeddon、Log4Shell |
| **プローブトラッキング** | WordPress(`/wp-admin`、`/wp-login.php`)、設定ファイル(`/.env`、`/.git`)、データベースツール(`/phpmyadmin`)、テクノロジープローブ(`.asp`、`.jsp`)、Spring actuator、Swagger/APIドキュメント - 50以上のパス |
| **スキャナー** | SQLMap、Nikto、Nmap、Burp Suite、FeroxBuster、FFUF、XSStrike、Dalfox、Netsparker、Qualys、Nuclei、および20以上のその他(合計53) |
| **AIスクレイパー** | GPTBot、ClaudeBot、ChatGPT、ByteSpider、Cohere、Common Crawl |
| **ヘッドレスブラウザー** | HeadlessChrome、PhantomJS、Selenium、Puppeteer、Playwright |
| **ボット** | Pythonスクリプト、Go HTTPクライアント、cURL、wget、AhrefsBot、SEMRushBot、空のユーザーエージェント |
| **認証** | ブルートフォース検出、トークン漏洩、パスワード露出、セッションID露出 |
| **DDoS** | レートベースの過剰リクエスト検出 |
| **回避** | SQLコメント挿入、二重URLエンコーディング、HTMLエンティティエンコーディング、Unicodeエスケープ、IIS Unicode、16進エスケープ |
| **その他** | GraphQLイントロスペクション、プロトタイプ汚染、オープンリダイレクト、XXE、Webシェル、暗号通貨マイニング、PII検出 |
---
## テストスイートの実行```bash
composer test
The package includes 335 tests (856 assertions) covering detection patterns, middleware behavior, API endpoints, confidence scoring, exclusion rules, DDoS detection, evasion resistance, CVE patterns, LDAP/XPath/SSTI injection, bot/scanner detection, probe tracking, export commands, dashboard auth, safe fields, performance optimizations, and full-cycle HTTP-to-DB verification.
MIT License. See LICENSE for details.
Contributions are welcome! Please submit a Pull Request.
| このパッケージ (アプリIDS) | WAF (mod_security, Cloudflare WAF) | エッジ / CDN (Cloudflare) |
|---|
| 悪意のあるリクエストをブロックする | ❌ ログのみ | ✅ | ✅ |
| 完全なアプリコンテキスト (正確なルート、デコードされたペイロード、認証済みユーザー) | ✅ | ⚠️ 一部 | ❌ |
| 組み込みダッシュボード + DB 内の脅威ログ | ✅ | ⚠️ さまざま | ⚠️ エッジのみ |
| アプリ固有の検出 (例: Aadhaar / PAN / IFSC PII) | ✅ カスタムパターン | ❌ | ❌ |
| オフライン/外部サービス不要で動作 | ✅ | ⚠️ 依存 | ❌ |
| アプリに到達する前にトラフィックを止める | ❌ | ✅ エッジ | ✅ |
| セットアップ | composer require 1回 | 中〜高 | 低〜中 |
| コスト | 無料、MIT | さまざま | 無料ティア + 有料 |
| やりたいこと | 使用 | 労力 |
|---|
| 何がヒットしているか確認する | ダッシュボード または threat-detection:stats | なし、すでに実行中 |
| ファイアウォールで常習者を BAN する | threat-detection:export-fail2ban — cron にパイプ | 1行 |
| Web サーバーで拒否する | threat-detection:export-blocklist → nginx/apache ディレクティブ | 1行 |
| アプリ内でリクエストを拒否する | オペレーター側ヘルパー — isBlocklisted()、isDdosThresholdExceeded() | 独自ミドルウェア約10行 |
| リアルタイムで対応する | ThreatDetected イベント — Telegram、SIEM、PagerDuty | リスナー1つ |
relaxedapi.guard