少人数チームでもできる——LLMエージェントによるCVE自動トリアージの実践

BLOG

はじめに

2025年12月3日、React Server Componentsに対する認証なしリモートコード実行(RCE)の脆弱性 CVE-2025-55182(React2Shell) が公開されました。CVSS 10.0。create-next-appで生成したデフォルト構成のNext.jsアプリが、たった1つのHTTPリクエストで任意のコード実行を許すという、Log4Shell級のインパクトでした。

公開から数時間以内に、中国の国家支援グループを含む複数の攻撃者による実際の攻撃が観測されています(AWS, Google, Microsoftがそれぞれ報告)。Webシェルの設置、暗号通貨マイナーの展開、バックドアの埋め込みが確認されました。

さらに厄介なのは、攻撃側もAIエージェントを活用し始めていることです。2025年後半から、LLMエージェントがゼロデイ脆弱性のエクスプロイトを自律的に作成できることが複数の研究で実証されています。従来は熟練した研究者が数週間〜数ヶ月かけていたエクスプロイト開発が、AIを使えば数時間に短縮されます。脆弱性の公開からエクスプロイトの実戦投入までのタイムラインが劇的に圧縮されている——つまり、防御側の対応にも同等のスピードが求められる時代に入りました。

私たちはWebサービスを本番運用している少人数の開発チームです。React2Shellの経験を経て、CVEの検知から影響判定・対策提案までを自動化するBotを構築しました。攻撃者がAIを使うなら、防御側もAIを使う。この記事では、その設計と実装を共有します。

背景と課題

従来のアプローチの限界

CVE監視には既存ツールがいくつかありますが、少人数チームには帯に短し襷に長しでした。

  • GitHub Dependabot / Snyk: リポジトリ内の依存パッケージは検知できるが、リバースプロキシ・DB・OSレベルの脆弱性はカバーしない。コンテナ内のランタイムも対象外
  • NVDのRSSフィード / メール通知: 全件届くので大量のノイズに埋もれる。自社に関係あるかの判断は人間がやる必要がある
  • Trivy / Grype: コンテナスキャンは強力だが、出力は「CVE一覧」であり、優先度付けや対策提案はしてくれない

つまり、「検知」はできても「判断」が自動化されていないのが問題でした。

私たちが欲しかったもの

  1. 自社スタックに関係するCVEだけを拾う
  2. 「このCVEは自社に影響するか?」を自動で判定する
  3. 影響ありなら対策案まで出す
  4. Slackに通知して、開発者が即座に判断できる状態にする

この「判断」のフェーズにLLMを使うというのが、今回のアプローチです。

アーキテクチャ

全体像はシンプルです。

┌────────────────────────────┐
│  本番サーバー                                          │
│  Docker コンテナ群(Webアプリケーション)              │
│                                                        │
│  syft(SBOM生成)→ keywords.txt / stack_context.txt   │
└───────────┬────────────────┘
                        │ ファイル転送
┌───────────▼─────────────────┐
│  監視サーバー(本番と分離)                              │
│                                                          │
│  bot.py(systemd常駐)                                   │
│  ├── NVD API 2.0 ─── 1時間毎にCVE取得              │
│  ├── キーワードマッチ(CPE構造 + description検索)    │
│  ├── OpenAI API ──── フルトリアージ               │
│  ├── Slack API ───── 結果通知                    │
│  └── SQLite ──────── 処理済み記録・重複排除   │
└─────────────────────────────┘
                        │
                        ▼
┌───────────────────────────┐
│  Slack                                               │
│  📋 影響なし → 日次サマリで一括通知(朝・夕の2回)  │
│  🟠 影響あり → 即時通知 + スレッドで対策詳細        │
│  🔴 緊急    → 即時 @メンション付き通知              │
└───────────────────────────┘

ポイントは監視サーバーを本番環境と分離していることです。Slack BotはSocket Modeで動作するため、固定IPもポート開放も不要で、監視サーバーから外向きのHTTPS通信だけで完結します。安価な小型マシンやクラウドの最小インスタンスで十分に動作します。

2段階トリアージ——なぜLLMを2回呼ぶのか

NVDから取得したCVEをすべてフルトリアージにかけると、API料金が跳ね上がります。キーワードマッチで絞り込んでも、1時間あたり数件〜十数件がヒットし、その大半は自社に無関係です。

そこで、軽量なスクリーニング(第1段階)で「影響なし」を高速に弾き、影響ありのものだけフルトリアージ(第2段階)にかけるという2段階方式を採用しました。

NVD CVE取得
  │
  ├── キーワードマッチ(パッケージ名の機械的な突合)
  │     マッチしない → スキップ(LLM不使用)
  │
  ├── CVSSフィルタ
  │     4.0未満(LOW) → DB記録してスキップ
  │
  ├── 【第1段階】スクリーニングLLM
  │     「このCVEは当該構成に影響するか?」
  │     → JSON出力: {"affected": true/false, "reason": "..."}
  │     → affected: false → DB記録(日次サマリで通知)
  │     トークン消費: 第2段階の 1/10 以下
  │
  └── 【第2段階】フルトリアージLLM
        サービス仕様書 + SBOM + CVE情報を入力
        → 影響度・該当コンポーネント・攻撃条件・対策を出力
        → Slack即時投稿(サマリ + スレッドで対策詳細)

スクリーニングのプロンプトには自社のSBOM(パッケージ一覧とバージョン)とサービス仕様書を含めています。LLMは「このCVEはD-Linkルータのファームウェアの脆弱性であり、当該構成にD-Link製品は含まれない」といった判断を正確に行えます。

コスト感

1時間あたりのCVE取得数が50〜100件、キーワードマッチ後に残るのが0〜数件、そのうちスクリーニングで弾かれるのが大半なので、フルトリアージが走るのは1日に0〜2件程度です。月額のAPI料金は数ドルに収まっています。

キーワードマッチの工夫——CPE + description検索の併用

CVEの検知精度を上げるために、2つの方法を併用しています。

CPE構造マッチング

NVDのCVEデータにはCPE(Common Platform Enumeration)というフィールドがあり、影響を受ける製品名が構造化データとして格納されています。

cpe:2.3:a:ws_project:ws:*:*:*:*:*:node.js:*:*
              ↓
         product = "ws"  ← これとkeywords.txtを完全一致で突合

CPEマッチは正確ですが、問題があります。新しいCVEにはCPEが未登録のケースが大半です。実際に直近24時間のCVEを調べたところ、66件中CPEが付与されていたのは0件でした。CPEはNVDが手動で割り当てるため、公開から数日〜数週間のタイムラグがあります。

descriptionテキスト検索

CPEが未登録のCVEをカバーするため、descriptionフィールドのテキスト検索も行います。ここで問題になるのが短いパッケージ名の誤検知です。例えばws(Node.jsのWebSocketライブラリ)を検索すると、newsbrowseにもヒットしてしまいます。

対策として、5文字未満のキーワードには単語境界マッチ(\bws\b)を適用しています。これにより、wsは独立した単語としてのみヒットし、他の単語の一部としてのヒットを防ぎます。

SBOMからのキーワード自動生成

キーワードリストを手動で管理するのは現実的ではありません。Dockerコンテナからsyft(SBOMツール)でパッケージ一覧を抽出し、キーワードリストを自動生成しています。

syft <docker-image> -o syft-json
  → パッケージ名を抽出
  → OSパッケージを除外(debパッケージ等はノイズが多い)
  → 汎用語を除外(api, auth, core, util 等)
  → keywords.txt として出力

LLMに何を渡しているか

スクリーニングとフルトリアージでは、LLMに渡す情報量が異なります。

スクリーニング(軽量):

  • CVE-ID、CVSSスコア、CWE、概要テキスト
  • 自社スタックのパッケージ一覧(SBOM由来)
  • サービス仕様書(攻撃面の判断に使う)

フルトリアージ(詳細):

  • 上記に加えて、CVSSベクタ、参考URL、公開日
  • サービス仕様書の「外部入力を処理するコンポーネント」「認証不要エンドポイント」セクション

サービス仕様書をプロンプトに含めることで、LLMは「このエンドポイントは認証なしで外部からアクセスできるため、当該脆弱性の攻撃条件を満たす」といった具体的な判断ができるようになります。

Slack通知の設計

通知のノイズを減らすことにこだわりました。初期は影響なしCVEも1件ずつ通知していましたが、1日数件でもチャンネルが埋まり、本当に重要な通知が見落とされるリスクがありました。

影響なし → 日次サマリ(朝・夕の2回)

影響なしCVEは即時性が不要です。SQLiteに記録しておき、朝9時と夕方19時にまとめて1件の投稿として通知します。

📋 日次CVEサマリ(2026-02-25)— 影響なし 4件

• CVE-2026-XXXXX (5.3 MEDIUM) — 当該構成にApache HTTPDは使用していない
• CVE-2026-YYYYY (6.1 MEDIUM) — WordPress用プラグインの脆弱性。WordPressは未使用
• CVE-2026-ZZZZZ (4.3 MEDIUM) — Cisco IOS専用。該当機器なし
• CVE-2026-WWWWW (5.0 MEDIUM) — Android SDK固有の問題。サーバーサイドに影響なし

朝のサマリで前夜〜早朝の分を、夕方のサマリで日中の分を確認できます。1件もなければ投稿されません。

影響あり → 即時通知 + メンション

影響ありのCVEは即座にSlackに投稿され、影響度「高」「中」の場合はメンション付きで通知されます。

@engineer 🟠 新規CVE検知: CVE-2026-YYYYY
CVSS: 8.8 (HIGH) | CWE: CWE-79 | NVD
【影響度】 高
【該当コンポーネント】 フロントエンドフレームワーク
【判定理由】 ...
  └── スレッド: 暫定対策 / 恒久対策 / やってはいけないこと

影響度に応じてアイコンの色を変え(🔴高 / 🟠中 / 🟡低 / ⚪無関係)、低・無関係はメンションなしにしています。これにより、通知が来たときの「見るべきか?」の判断が一瞬で終わります。

メンションはSlack Blocksの先頭セクションに挿入しています。textパラメータのみにメンションを含めると、Blocks表示時にチャットUI上で見えない(プッシュ通知にしか反映されない)ため注意が必要です。

運用してわかったこと

LLMの判定精度

スクリーニングの偽陰性(影響ありを「なし」と判定)は、運用開始以降まだ確認されていません。LLMはCVEの説明文を読んで「影響を受ける製品」と「自社のパッケージ一覧」を照合する作業が得意で、特にD-Linkルータ、Cisco製品、WordPress プラグインなど明らかに無関係なCVEの除外は非常に正確です。

一方で、LLMの判定は一次判定であり、最終判断ではないという認識は重要です。影響度「高」のCVEは必ず人間が二次確認するルールにしています。

通知のノイズ削減

運用初期は影響なしCVEを1件ずつリアルタイムで通知していました。情報量としては有用ですが、チャンネルのS/N比が急速に悪化し、メンバーが通知を無視するようになるリスクがありました。

日次サマリ方式に切り替えた結果、チャンネルに流れるのは「影響ありの即時通知」と「1日2回のサマリ」だけになり、通知に対する信頼度が上がりました。影響なしの情報もサマリで確認できるため、見逃しはありません。

サマリの未通知管理にはSQLiteのsummary_notifiedフラグを使っています。メモリバッファではなくDBに永続化しているため、Bot再起動時に未通知分が失われることはありません。

キーワードマッチのチューニング

SBOMから自動生成されるキーワードリストには、bufferlinksendのような汎用英単語がパッケージ名として含まれることがあります。これらはdescriptionテキスト検索で大量のノイズを生みますが、スクリーニングLLMが弾くので実害はAPIコストだけです。除外リストで段階的に調整しています。

監視サーバーの安定性

systemdで常駐化しているため、サーバー再起動後も自動復旧します。CVE取得の時間窓を4時間に設定し、1時間毎の実行間隔との重複で再起動時の空白をカバーしています。SQLiteで重複排除しているので、同じCVEが二重に処理されることはありません。

応用——「LLMエージェント×防御」のパターン

今回構築したのはCVEトリアージの自動化ですが、その本質は**「大量の非構造化情報を読み、自社のコンテキストに照らして判断する」タスクをLLMに委任する**というパターンです。このパターンは他の領域にも応用できます。

セキュリティ領域:

  • WAFログの異常検知トリアージ — 大量のアラートから実際の攻撃を選別する
  • 依存パッケージのライセンス監査 — SBOM + ライセンスDBを突合し、GPL汚染リスクを自動判定する
  • インシデント対応の初動自動化 — アラート発生時にランブックを参照し、初動手順をSlackに投稿する

セキュリティ以外:

  • 法改正・規制変更の影響判定 — 自社サービスの仕様書と新しい規制文書を突合し、対応が必要な箇所を特定する
  • 競合プロダクトの変更検知 — リリースノートやchangelogを定期取得し、自社に影響する変更を要約する
  • 採用市場の技術トレンド監視 — 求人データから特定技術の需給変動を検知する

共通するのは、「情報ソース(NVD、ログ、法令DB等)」「自社コンテキスト(SBOM、仕様書、事業方針等)」「判断基準(影響あり/なし、対応要/不要)」の3要素をLLMに渡して判断させるという構造です。このフレームワークをテンプレート化すれば、新しい監視対象を追加するたびにゼロから作り直す必要はありません。

まとめ

React2Shellは、少人数チームにとって明確な警鐘でした。CVSS 10.0の脆弱性が公開され、数時間で実際の攻撃が始まる。しかもAIエージェントの活用により、エクスプロイト開発のコストは急速に下がっています。

この非対称性に対抗するには、防御側もAIを使うしかありません。CVEのトリアージは「大量の情報を読んで、自社に関係あるか判断する」という、まさにLLMが得意とするタスクです。

少人数チームのセキュリティ対策は、「専任がいないからできない」ではなく、「自動化すればできる」というのが今回の実践で得られた知見です。攻撃者がAIエージェントでエクスプロイトを量産する時代に、防御側が手作業でCVEを追いかけている場合ではありません。

今後はSBOM生成のCI/CD連携や、LLMの判定精度の定量評価に取り組む予定です。


本記事で紹介したシステムは、Python + OpenAI API + Slack で構成されています。特別なインフラは不要で、同様の構成は小規模チームでもすぐに始められます。

BLOG

アーカイブ