1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GCP SCCの脆弱性JSONを丸投げしたら、Claude Codeが「npmごと削除」という解法を出してきた

1
Posted at

はじめに

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ページで操作する手順が延々と続く)",
    ...
  }
}

実際に必要な情報は vulnerabilityfiles の2ブロックだけです。しかしそれ以外に nextSteps(汎用的なGUI操作手順)、mitreAttackbackupDisasterRecoverycloudArmor など無数のフィールドが並んでいて、全部で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.jsonpicomatch を直接依存していれば 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

npxcorepack も一緒に削除しています。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コネクトではサービス開発支援や技術支援をはじめ、幅広い支援を行っておりますので、何かありましたらお気軽にお問合せください。

お問合せ:https://gmo-connect.jp/contactus/

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?