はじめに
GMOコネクトの平島です。
GCP Security Command Center(SCC)からコンテナスキャンのアラートが来るたびに、JSONを読み解くのに10〜15分かかっていました。nextSteps フィールドを読んでも汎用的なGCPコンソール操作の手順が並んでいるだけで、具体的にコードで何をすればいいかはわかりません😇
ある日、そのJSONをそのままClaude Codeに貼ったところ、「このケースはnpmパッケージの更新では解決できない」と判断した上で、Dockerfileへの5行追記まで完走しました。その記録です。
先にまとめ
投入したもの: SCCのCONTAINER_IMAGE_VULNERABILITY Finding JSON(約150行、コピペそのまま)
出てきたもの: 以下のDockerfile差分
# Remove npm (not needed at runtime; eliminates CVE-2026-33671 / picomatch 4.0.3)
RUN rm -rf /usr/local/lib/node_modules/npm \
/usr/local/bin/npm \
/usr/local/bin/npx \
/usr/local/bin/corepack
Claudeが判断したポイント:
-
files[0].pathが/usr/local/lib/node_modules/npm/node_modules/picomatch/→ npmのbuilt-in依存なのでアプリ側のpackage.json修正では解決不可 -
upstreamFixAvailable: false→ npmの上流(Node.jsイメージ)では修正版未取込み - マルチステージビルドの本番ステージにnpmは不要 → npmごと削除が正解
発端: SCCから飛んできた150行のJSON
GCP Artifact Registryへのpushをトリガーに、SCCがコンテナイメージの脆弱性を自動検知します。コンソールから「Export」するとJSONが得られますが、その構造はこうなっています。
{
"finding": {
"name": "organizations/YOUR_ORG_ID/sources/.../findings/...",
"state": "ACTIVE",
"category": "CONTAINER_IMAGE_VULNERABILITY",
"severity": "HIGH",
"vulnerability": {
"cve": {
"id": "CVE-2026-33671",
"cvssv3": {
"baseScore": 7.5,
"attackVector": "ATTACK_VECTOR_NETWORK",
"availabilityImpact": "IMPACT_HIGH"
},
"upstreamFixAvailable": false
},
"offendingPackage": {
"packageName": "picomatch",
"packageType": "NPM",
"packageVersion": "4.0.3"
},
"fixedPackage": {
"packageVersion": "4.0.4"
}
},
"files": [
{
"path": "/usr/local/lib/node_modules/npm/node_modules/picomatch/package.json"
}
],
"nextSteps": "1. Click on a resource type below to populate the query...\n(GCPコンソールのWorkloadsページで操作する手順が延々と続く)",
...
}
}
実際に必要な情報は vulnerability と files の2ブロックだけです。しかしそれ以外に nextSteps(汎用的なGUI操作手順)、mitreAttack、backupDisasterRecovery、cloudArmor など無数のフィールドが並んでいて、全部で150行を超えます。
しかも nextSteps に書かれているのは「Workloadsページで該当イメージのYAMLを編集して…」というGCPコンソール操作の説明で、コードで何をすべきかは一切書かれていません。
Claudeに生JSONを渡した
プロンプトはシンプルです。
GCPのArtifact RegistryにPUSHしたイメージで下記脆弱性が検知されているので修正してください。
{
"finding": { ...SCCからコピペしたJSON全文... }
}
肝は、JSONを要約も加工もせずそのまま渡したことです。「picomatchを4.0.4にアップデートして」と指示してしまうと、後述する「それでは解決しない理由」をClaudeが発見する機会を奪ってしまいます。
Claude Codeはリポジトリのファイルも読めるので、このプロンプトだけでDockerfileの構成まで確認しに行きます。
Claudeの解析: 2つのフィールドが鍵だった
Claudeがまず注目したのは files[0].path でした。
/usr/local/lib/node_modules/npm/node_modules/picomatch/package.json
このパスが示すのは「picomatchはアプリのnode_modulesではなく、npmというツール自体が内包しているパッケージ」ということです。
アプリの package.json で picomatch を直接依存していれば pnpm update picomatch で解決できます。しかしこのケースでは、picomatchは node:22-alpine ベースイメージに含まれるnpmの依存関係として存在しています。アプリ側のパッケージをいくら更新しても、このパスのファイルは変わりません。
加えて upstreamFixAvailable: false も確認しています。これは「npmの上流(Node.jsのベースイメージ)ではpicomatch 4.0.4がまだ取り込まれていない」という情報です。理論上はnpmのbuilt-in依存を差し替えれば解決しますが、通常の手順でできる操作ではありません。
解法: npmを本番イメージから削除する
Claudeが出した答えは、Dockerfileのマルチステージ構成から来ています。
#---- ビルドステージ ----
FROM node:22-alpine AS build-base
# pnpm install & build(npmもpnpmもcorepackも使う)
#---- 本番実行ステージ ----
FROM node:22-alpine AS production
# node dist/index.js を実行するだけ → npmは不要
本番ステージには node:22-alpine のベースイメージが持つnpmがそのまま残っていますが、アプリの実行には一切使いません。削除してもアプリの動作に影響がない、という判断です。
そこで production ステージの末尾に追加されたのがこれです。
# Remove npm (not needed at runtime; eliminates CVE-2026-33671 / picomatch 4.0.3)
RUN rm -rf /usr/local/lib/node_modules/npm \
/usr/local/bin/npm \
/usr/local/bin/npx \
/usr/local/bin/corepack
npx と corepack も一緒に削除しています。npmが消えると依存関係が壊れるため、セットで消すのが正しい対応です。また、これらも実行時には不要であり、攻撃対象面の削減にもつながります。
Before / After
| 項目 | Before | After |
|---|---|---|
| SCCのFindingステータス | ACTIVE (HIGH / CVSS 7.5) | Resolved |
| 本番イメージのnpm | 含まれる | 削除 |
| picomatch 4.0.3の所在 |
/usr/local/lib/node_modules/npm/node_modules/ に存在 |
消滅 |
| Dockerfileの変更量 | — | +5行(コメント1行 + rm -rf 4行) |
| 調査〜PR作成の所要時間 | 10〜15分 | 数分 |
コミットは1ファイル、6行の変更で完結しました。
ハマりポイント: packageのパスを精読していなかった
自分がJSONを手動で読んでいたら、おそらく最初に offendingPackage.packageName: "picomatch" と fixedPackage.packageVersion: "4.0.4" を見て「picomatchを4.0.4に上げれば解決」と結論づけていたと思います。
files[0].path はJSONの末尾付近にあり、「この脆弱性に関連するファイル一覧」程度の印象でパスを精読する動機が薄い。upstreamFixAvailable: false も「まだ修正版がない(つまり待つしかない)」という短絡的な読み方になりがちです。
Claudeはフィールド間の関係を横断的に解釈しています。files.path のディレクトリ構造、packageType: "NPM" という情報、そして実際のDockerfileのマルチステージ構成を組み合わせて「npmを消せばいい」という結論を導いています。
Trivy等の他スキャナーでも同じアプローチが使える
SCCはGCP固有ですが、JSONを投入するアプローチ自体は他のスキャナーにも応用できます。
Trivyの出力(trivy image --format json)の場合、対応するフィールドはこうなります。
{
"Results": [
{
"Target": "node:22-alpine (alpine 3.20.3)",
"Vulnerabilities": [
{
"VulnerabilityID": "CVE-2026-33671",
"PkgName": "picomatch",
"PkgPath": "usr/local/lib/node_modules/npm/node_modules/picomatch/package.json",
"InstalledVersion": "4.0.3",
"FixedVersion": "4.0.4"
}
]
}
]
}
Trivyの場合は PkgPath フィールドが「どのレイヤーのどのパスか」を示します。SCCの files[0].path と役割は同じです。このJSONをそのままClaudeに渡せば、同様の解析が動きます。
どのスキャナーでも共通しているのは、生のJSONをそのまま渡すことです。「picomatchを4.0.4に上げて」と要約した時点で、Claudeが推論に使えるコンテキストを人間側が削ってしまいます。
まとめ
- 脆弱性JSONは要約せずそのまま渡す。要約した時点でClaudeの推論材料を削ってしまう
-
files[0].pathが/usr/local/lib/node_modules/npm/...ならアプリのdeps更新では解決できない -
upstreamFixAvailable: falseは「パッケージマネージャーで解決できない」シグナル - 本番イメージにnpmが不要なら削除がベスト。攻撃対象面も減る
- Trivyなど他スキャナーのJSONでも同じアプローチが使える
最後に、GMOコネクトではサービス開発支援や技術支援をはじめ、幅広い支援を行っておりますので、何かありましたらお気軽にお問合せください。