はじめに
セキュリティ勧告が「このバージョンのこのパッケージは危険」と名指ししたとき、確認したくなるのは自分の手元にそれが入っているかどうかである。ところが npm ls や pip show は package.json のスクリプトフックを経由することがあり、供給網攻撃の被害パッケージは「調べようとした瞬間に発火する」ように作られていることがある。読み取り専用で、パッケージマネージャーを一切呼ばずに調べる方法はあるのか。Perplexityが2026年5月にオープンソース化したBumblebeeは、この前提に立って作られたGo製CLIである。実際にインストールし、このリポジトリ(zenn-blog-automation)自身をスキャン対象にして、何が検出されるかを確かめた。
この記事で分かること
- Bumblebeeの
go installでの導入と、Go 1.25要求に対する自動ツールチェイン切り替えの挙動 - Bumblebeeが持つ
baseline/project/deepの3つのスキャンプロファイルのうち、baselineとprojectを実リポジトリと実行環境に対して回した結果(対象ファイル数・所要時間・検出パッケージ数) -
.mcp.jsonの MCP サーバー設定が、トークン値を漏らさずに検出される様子 - exposure catalog(既知の危険バージョン一覧)を自作して流したときに、同一バージョンのパッケージがリポジトリ内の複数箇所から見つかった実例
対象読者
- npmやPyPIの供給網攻撃(サプライチェーン攻撃)のインシデント対応を担当している、または担当する可能性がある人
- 自分のプロジェクトやCI環境に危険なパッケージバージョンが紛れ込んでいないか、パッケージマネージャーを起動せずに確認したい人
前提環境
- Go 1.24.7(クラウド実行環境にプリインストール済み。Bumblebee自体はGo 1.25以上を要求する)
- 検証対象: 本リポジトリ(zenn-blog-automation、Node.js/npm構成)
TL;DR
- Bumblebeeは
go install一発で導入でき、要求バージョン(Go 1.25+)を満たしていなくてもGoのツールチェイン自動切り替えで導入が完了する - このリポジトリを
projectプロファイルでスキャンすると、3,828ファイルを35ミリ秒で走査し342件のパッケージを検出した -
.mcp.jsonのMCPサーバー設定も検出対象で、サーバー名は拾うが環境変数の値(トークン)は読み取らない - 自作の exposure catalog(1件のみ登録)を流したところ、同一パッケージ・同一バージョンがリポジトリ内の3つの異なる場所(ルートのlockfile・直下のnode_modules・別パッケージにネストされたnode_modules)から見つかった
-
baselineプロファイルでは、Bumblebee自身のテスト用フィクスチャ(go installが$GOPATHに展開したもの)まで拾ってしまう。ホームディレクトリ全体を対象にする運用では、この種のノイズも想定しておく必要がある
背景 — なぜ「npmを呼ばない」スキャナーが必要か
2026年5月、TanStack・Mistral AI・UiPathなど複数のnpm/PyPIパッケージ名義を乗っ取った「Mini Shai-Hulud」と呼ばれる自己増殖型の供給網ワームが確認された。攻撃者はGitHub Actionsの pull_request_target を悪用して正規ワークフロー内で不正コードを実行し、開発者マシンやCI環境から窃取したシークレットで次のパッケージへ自身を再パブリッシュする手口を取った。このうちTanStackエコシステムの侵害(42パッケージ・84バージョン)はCVE-2026-45321(CVSSスコア9.6)として採番されるほど深刻と評価された(The Hacker News)。キャンペーン全体ではnpm・PyPI合わせて170以上のパッケージ・400以上の悪性バージョンが確認されている(The Hacker News、ArmorCode)。
Perplexityはこの種のキャンペーンを受け、自社の開発者マシン群のうち「どの端末に、どのバージョンの、どのパッケージが入っているか」を素早く把握する目的でBumblebeeを内製し、2026年5月22日にApache 2.0でオープンソース化した(MarkTechPost)。パッケージマネージャーのコマンドやインストールスクリプトは一切実行せず、lockfileとメタデータファイルを直接読むだけで済ませる。「調べるために npm install や npm ls を叩いた時点で、確認しようとしていた攻撃をこちらから起動しかねない」という供給網攻撃特有のリスクが設計の前提にある。対応エコシステムはnpm・pnpm・Yarn・Bun・PyPI・Go modules・RubyGems・Composerに加え、MCPサーバー設定やVS Code・Cursor・Windsurf・Chromium系ブラウザの拡張機能にも及ぶ。
導入 — Go 1.24環境での go install
クラウド実行環境のGoは1.24.7だが、Bumblebeeの go.mod はGo 1.25以上を要求する。バージョン指定インストールを実行すると、Goのツールチェイン自動切り替え機能が働き、必要なGo 1.25.12を自動取得してからビルドを完了させた。
$ go version
go version go1.24.7 linux/amd64
$ go install github.com/perplexityai/bumblebee/cmd/bumblebee@v0.1.1
go: downloading github.com/perplexityai/bumblebee v0.1.1
go: github.com/perplexityai/bumblebee@v0.1.1 requires go >= 1.25; switching to go1.25.12
go: downloading go1.25.12 (linux/amd64)
$ ~/go/bin/bumblebee version
bumblebee v0.1.1
commit: unknown
built: unknown
go: go1.25.12
手元のGoバージョンが要求に届いていなくても、明示的なアップグレード作業は不要だった。bumblebee --help はサブコマンドとして scan(NDJSON形式でレコードを出力するスキャン本体)・roots(解決されたスキャン対象ディレクトリの表示)・selftest(組み込みフィクスチャでの検出動作確認)・version の4つを持つ。
project プロファイルでリポジトリをスキャンする
project プロファイルは指定したディレクトリ配下だけを対象にする。本リポジトリのルートに対して実行した結果が以下である(... は本記事での省略で、実際の出力ではフィールドは省略されずすべて出力される)。
$ bumblebee scan -profile project -root . -ecosystem npm,pypi,go
(342件のpackageレコードの末尾)
{"record_type":"scan_summary", ...,"profile":"project","status":"complete",
"roots":[{"path":".","kind":"project_root"}],
"counts":{"finding":0,"package":342},
"package_records_emitted":342,"files_considered":3828,
"timed_out":false,"duration_ms":35}
3,828ファイルを走査して342件のnpmパッケージを35ミリ秒で検出した。出力はレコードごとに独立したNDJSON(1行1JSON)で、record_type に応じて package(検出したパッケージ)・finding(既知の危険パッケージとの一致)・scan_summary(実行全体の集計)が並ぶ。package レコードには source_file・direct_dependency(直接依存かどうか)・has_lifecycle_scripts(インストール時に実行されるスクリプトの有無)まで含まれ、単なるパッケージ一覧よりインシデント対応向けの文脈が付く。
続いて -ecosystem mcp を指定すると、本リポジトリのルートにある .mcp.json が検出された。
$ bumblebee scan -profile project -root . -ecosystem mcp
{"record_type":"package", ...,"ecosystem":"mcp",
"package_name":"kinako-mocchi","version":"",
"package_manager":"mcp","source_type":"mcp-config",
"source_file":".mcp.json","confidence":"low",
"server_name":"kinako-mocchi"}
.mcp.json には GEMINI_MCP_AUTH_TOKEN を参照する環境変数展開の記述があるが、レコードに含まれるのはサーバー名(kinako-mocchi)だけで、トークンの値そのものは一切出力されない。読み取り専用という設計方針が、シークレットを含みやすいMCP設定に対しても守られていることを確認できた。
baseline プロファイルで見えたノイズ
baseline プロファイルは -root を指定せず、言語ツールチェーンのグローバルパッケージ置き場やユーザーのホーム配下の既定ディレクトリを自動で対象にする。このクラウド実行環境に対して実行すると、10個のルート(/root/go・/root/.cargo・/root/.local/lib/python3.11・/root/.claude を含む)から519件のパッケージを18,108ファイル・223ミリ秒で検出した。
この実行で -ecosystem mcp を付けると、本物のMCPサーバー設定に混じって ghcr.io/bumblebee-selftest/evil-mcp という名前のレコードが出てきた。source_file を辿ると ~/go/pkg/mod/github.com/perplexityai/bumblebee@v0.1.1/cmd/bumblebee/selftest/fixtures/mcp-fixture/mcp.json という、go install がGoモジュールキャッシュ配下に展開したパスにあるBumblebee自身のテスト用フィクスチャだった。baseline プロファイルは $GOPATH 配下をユーザーパッケージ置き場として無条件にスキャン対象へ含むため、テストデータまで「検出結果」として混ざり込む。ツール自体の欠陥ではなく対象範囲の広さに起因する挙動だが、開発機のホームディレクトリ全体を対象にする運用では、この種のノイズも想定しておいたほうがよい。
exposure catalogで危険バージョンの検出を試す
Bumblebeeは既知の危険パッケージ一覧(exposure catalog)をJSONで渡すと、スキャン結果と突き合わせて finding レコードを出す機能を持つ。公式リポジトリの説明ではスキーマ例に schema_version: "0.2.0" が示されている場合があるが、実際に手元のv0.1.1バイナリへ "0.2.0" を渡すと次のエラーで止まった。
unsupported exposure catalog schema_version "0.2.0" (supported: "0.1.0")
schema_version を "0.1.0" に直すと通過した。ドキュメントの記載とインストール済みバイナリが対応するスキーマのバージョンで食い違うことがあるため、エラーメッセージが示す対応バージョンを実際の基準にする必要がある。
動作確認のため、本リポジトリの package-lock.json に実在するパッケージ(dotenv@17.3.1)を1件だけ「危険バージョン」として登録した最小のカタログを作成し、-findings-only を付けて再スキャンした(このバージョンが実際に危険という意味ではなく、検出の仕組みを確かめるためのデモである)。
{
"schema_version": "0.1.0",
"entries": [
{
"id": "demo-0001",
"name": "dotenv 17.3.1 (demonstration entry, not a real advisory)",
"ecosystem": "npm",
"package": "dotenv",
"versions": ["17.3.1"],
"severity": "critical"
}
]
}
$ bumblebee scan -profile project -root . -ecosystem npm \
-exposure-catalog demo-catalog.json -findings-only
結果、同じ dotenv@17.3.1 が3つの異なる場所から finding として検出された。
| 検出箇所 | source_file | confidence |
|---|---|---|
| リポジトリ直下のlockfile | package-lock.json |
high |
| 直下のnode_modules | node_modules/.package-lock.json |
high |
| 別パッケージ内にネストされたnode_modules | node_modules/@qiita/qiita-cli/node_modules/dotenv/package.json |
medium |
3件目は、@qiita/qiita-cli というnpmパッケージが自身の依存として dotenv を別バージョン管理の都合でネストして持ち込んでいたケースである。トップレベルの package-lock.json だけを人間の目で確認すると1箇所しか見えないが、実際には同じ危険バージョンが依存ツリーの奥に複製されて存在しうる。インシデント対応で「このバージョンは駆除した」と判断する前に、ネストされたコピーまで含めて洗い出せるかどうかは、対応の完了条件そのものに関わる。
著者視点の発見ポイント
exposure catalogのスキーマバージョン不一致(ドキュメント上の 0.2.0 例に対し、バイナリが実際に受け付けるのは 0.1.0)と、baseline プロファイルが $GOPATH を無条件に対象へ含みBumblebee自身のテストフィクスチャまで「検出結果」として拾ってしまう挙動は、いずれも公式の説明文だけを読んでいては見えず、実際にコマンドを叩いて出力を読むことでしか確認できなかった。ネストされたnode_modules内の複製を検出できる点は謳い文句どおりだったが、その適用範囲(どのプロファイルで何が対象になるか)は手元で動かして初めて輪郭がはっきりした。
まとめ
- Bumblebeeは
go installで導入でき、要求Goバージョンに手元の環境が届いていなくてもツールチェイン自動切り替えで解決する -
projectプロファイルは指定ディレクトリのみを対象にし、本リポジトリでは3,828ファイル・342パッケージを35ミリ秒で検出した - MCPサーバー設定(
.mcp.json)も検出対象だが、環境変数で参照されるトークンの値は出力に含まれない - exposure catalogを使うと、同一の危険バージョンがリポジトリ内の複数箇所(直下・ネストされたnode_modules)に存在することを機械的に洗い出せる
-
baselineプロファイルはホームディレクトリ配下を広く対象にするため、ツール自身のインストール副産物まで拾いうる。対象範囲を絞りたい場合はprojectプロファイルと-rootの明示が有効
関連記事
- Copilot CLIがPAT不要になったが、forkPRには罠があった
- MCPサーバーとの通信を「盗聴」できる mcpsnoop を動かしてみた
- Agent設定をnpmのように配布するAPM入門 — 実機で見えた2つの罠
参考リンク
- perplexityai/bumblebee(GitHubリポジトリ) — インストール手順・対応エコシステム・ライセンスの引用元
- Perplexity Open-Sources Bumblebee(MarkTechPost) — 公開の背景・設計思想の引用元
- Mini Shai-Hulud Worm Compromises TanStack, Mistral AI, Guardrails AI & More Packages(The Hacker News) — 供給網ワームキャンペーンの詳細・CVE-2026-45321
- Mini Shai-Hulud: The NPM Supply Chain Worm Hitting TanStack, Mistral, UiPath, and More(ArmorCode) — 被害範囲の詳細