はじめに
npm audit signatures を CI に入れて「サプライチェーン対策は済み」と考えている Node.js アプリの開発者・運用担当者が対象読者です。
このコマンドは 2 種類の別物を同時に検証します。1 つは レジストリ署名(npm レジストリが全パッケージに付ける署名)、もう 1 つは provenance attestation(どのリポジトリのどのワークフローがビルドしたかを示す SLSA 証明)です。前者はほぼ 100% 付いていて、後者はパブリッシャー側がオプトインしないと付きません。ところが出力は両方を並べて表示するだけなので、「全部 verified」に見えたときに実際どこまで守られているのかが分かりません。
そこで 3 つの依存ツリーで実測しました。すべて 2026-09-08 JST、Linux コンテナ、Node.js v22.22.2 / npm 10.9.7 での実行結果です。
実測結果(3 ツリー比較)
| 依存ツリー | audited | レジストリ署名 verified | provenance attestation | attestation 率 | exit code |
|---|---|---|---|---|---|
| 本ブログ運用リポジトリ(既存・lockfileVersion 2) | 114 | 114 | 4 | 3.5% | 0 |
vite@^8.2.2 を新規 install |
17 | 17 | 10 | 58.8% | 0 |
express@^5.2.1 を新規 install |
68 | 68 | 0 | 0.0% | 0 |
3 ツリーとも レジストリ署名は 1 個の欠けもなく verified、そして 3 ツリーとも exit code は 0 でした。provenance が 1 個もない express のツリーでも、コマンドは何の警告も出さずに成功として終了します。
実際の出力はこうなります。
$ npm audit signatures # 本リポジトリ
audited 114 packages in 1s
114 packages have verified registry signatures
4 packages have verified attestations
$ npm audit signatures # express@5.2.1 のツリー
audited 68 packages in 0s
68 packages have verified registry signatures
express 側は「attestations」の行そのものが消えます。行が出ないことが 0 件の合図で、明示的な 0 packages とは表示されません。ログを目で追う運用だと、この差分はまず気づけません。
検証の流れ
CLI は件数しか教えてくれないので、https://registry.npmjs.org/-/npm/v1/attestations/{name}@{version} を依存ツリーの全パッケージに対して並列で叩き、predicateType に https://slsa.dev/provenance/v1 を含むものを数える独自スクリプトを併走させました。結果は CLI の件数と完全に一致しました(本リポジトリ 4 件、vite 10 件、express 0 件)。CLI の数字自体は信用してよい、ということです。
軸ごとの所見
軸 1: レジストリ署名は「差がつかない指標」
3 ツリーとも 100% verified でした。レジストリ署名は npm 側が全パッケージに機械的に付けるものなので、これが欠けるのは配信経路が壊れている場合か、レジストリをミラー・プロキシしている場合くらいです。この行が緑であることは、パッケージの出自について何も語りません。
軸 2: provenance 率はツリーの世代で 0% と 58.8% に割れる
vite のツリーは 17 個中 10 個が attested でした。内訳は vite・rolldown・@rolldown/binding-linux-x64-gnu・@rolldown/binding-linux-x64-musl・@rolldown/pluginutils・@oxc-project/types・postcss・nanoid・tinyglobby・detect-libc です。近年 GitHub Actions で publish している新しめのパッケージ群がまとまって対応しています。
一方 express のツリーは audited 68 個のうち attested が 1 つもありませんでした。express 5 系は依存が小さく枯れたユーティリティ中心で、そうしたパッケージほど publish フローが古いまま更新されていません。「依存を減らして枯れたものを選ぶ」という従来の安全策が、provenance の観点では逆に効いている のが実測から見えた点です。
本リポジトリの 4 件は zenn-cli・emoji-regex・package-manager-detector・readdirp でした。114 個中 4 個、3.5% です。
軸 3: provenance ゼロでも exit code は 0
これが運用上いちばん危ないところです。npm audit signatures が非ゼロで落ちるのは、レジストリ署名が invalid(署名が壊れている)か missing(署名がない)ときだけです。provenance attestation が 0 件であることは失敗条件に入っていません。
つまり CI に npm audit signatures を 1 行入れただけでは、「provenance 付きの依存が何割か」というゲートにはなりません。件数を見たいなら stdout をパースするか、後述のように自分で API を叩く必要があります。
軸 4: JSON 出力は件数すら返さない
機械可読で取りたくなるので --json を試しました。
$ npm audit signatures --json
{
"invalid": [],
"missing": []
}
$ npm audit signatures --json | wc -c
37
37 バイトです。公式ドキュメントに --include-attestations(sigstore の attestation bundle を JSON 出力に含める)というフラグの記載があるので、こちらも試しました。
$ npm audit signatures --json --include-attestations | wc -c
37
結果はバイト単位で同じでした。 JSON 出力は「失敗したもの」のリストであって、成功したものの内訳は入りません。全件パスしているツリーでは中身が空になり、何件が attested だったかも分かりません。人間向け stdout にしか出てこない情報がある、という構造です。
軸 5: lockfile だけでは監査できず、数も変わる
npm install --package-lock-only で lockfile だけ作った状態で実行すると、こうなります。
$ npm audit signatures
npm error found no dependencies to audit that were installed from a supported registry
exit code 1 で落ちます。実際に node_modules を展開していないと監査できません。 ロックファイルだけをレビューする CI ジョブでは使えない、ということです。
さらに、lockfile 基準と実インストール基準では母数が変わります。vite の lockfile には 40 個のユニークなパッケージがあり、そのうち 23 個(57.5%)が attested でした。一方 Linux x64 に実際にインストールされたのは 17 個で、attested は 10 個(58.8%)です。差分の 23 個は @rolldown/binding-darwin-arm64 や @rolldown/binding-win32-x64-msvc といった 他プラットフォーム向けの optional 依存 で、そのうち attested は 13 個(56.5%)でした。インストールされる側のほうがわずかに attested 率が高いため、lockfile 全体で数えると率は 58.8% から 57.5% へ少し下がります。差は 1.3 ポイントと小さいものの、母数が 40 と 17 で 2 倍以上違う ので、率を語るときは「どちらの母数か」を明示しないと比較になりません。
軸 6: attestation の中身は自分で読むしかない
npm の公式ドキュメントは、provenance について明確にこう書いています。
When a package in the npm registry has established provenance, it does not guarantee the package has no malicious code.
provenance が保証するのは「このパッケージは、この公開リポジトリのこのワークフローから作られた」という リンク だけです。そのリンク先が信頼できるかは利用者が判断する必要があります。ところが npm audit signatures はリンク先を表示しません。
実際に中身を読むには、attestation バンドルの DSSE envelope をデコードします。zenn-cli@0.4.5 で試した結果です(トップレベル await を使うので .mjs か node --input-type=module で実行してください)。
const r = await fetch('https://registry.npmjs.org/-/npm/v1/attestations/zenn-cli@0.4.5');
const j = await r.json();
for (const a of j.attestations) {
const payload = JSON.parse(
Buffer.from(a.bundle.dsseEnvelope.payload, 'base64').toString('utf8')
);
console.log(a.predicateType, payload.predicate?.buildDefinition?.externalParameters?.workflow);
}
出力から取れたのは次の値でした。
| 項目 | 値 |
|---|---|
predicateType |
https://slsa.dev/provenance/v1 |
subject |
pkg:npm/zenn-cli@0.4.5 |
buildType |
https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1 |
| ソースリポジトリ | https://github.com/zenn-dev/zenn-editor |
| ワークフロー | .github/workflows/publish.yml |
| builder | https://github.com/actions/runner/github-hosted |
ここまで取って初めて「意図したリポジトリから出ているか」を確認できます。パッケージ名に似せた別リポジトリから publish されていても、provenance 自体は正しく検証を通ります。検証が通ることと、出所が正しいことは別の話です。
なお predicateType は 2 種類返ってきます。https://slsa.dev/provenance/v1(ビルド来歴)と https://github.com/npm/attestation/tree/main/specs/publish/v0.1(レジストリが付ける publish attestation)で、ソースリポジトリ情報が入っているのは前者だけです。後者だけを見ても出所は分かりません。
共通していたパターン
3 ツリーを通して共通していたのは、「緑で終わる」という体験がツリーの実態と無関係に成立してしまう ことでした。provenance 3.5% のツリーも、0% のツリーも、58.8% のツリーも、コマンドの見た目上の結末は同じです。差は 1 行の有無と数字だけで、exit code にも JSON にも現れません。
もう 1 つは、率を上げようとしたときに打てる手が利用者側にほとんどない点です。provenance はパブリッシャーがオプトインするものなので、依存を差し替えるか、上流に対応を依頼するか、自前でミラーしてビルドするかしかありません。監査結果を「今どの依存に来歴があるか」の可視化として使い、来歴のない依存を優先的にレビュー対象へ回す、という使い方が現実的でした。
計測用のスクリプトは 40 行程度で書けます。attestations API は認証不要で、404 が「provenance なし」を意味します。CI に組み込むなら、npm audit signatures の exit code ではなく、この API で数えた比率を自前の閾値と比べるほうが実態に合います。
まとめ
-
npm audit signaturesの「verified registry signatures」は全パッケージに付く指標で、ツリーの安全性を区別しない(3 ツリーとも 100%) - provenance attestation の割合はツリーによって 0%〜58.8% と大きく割れる。本リポジトリは 114 個中 4 個(3.5%)
- provenance が 0 件でも exit code は 0。CI ゲートとしては provenance の有無を見ていない
-
--jsonは--include-attestationsを付けても失敗リストしか返さず、件数も内訳も取れない(37 バイト) - lockfile だけでは監査できず(exit 1)、lockfile 基準と実インストール基準で率が変わる(vite: 57.5% と 58.8%)
- 出所の確認は attestation の DSSE payload を自分でデコードして、ソースリポジトリとワークフローのパスを読むところまでやって初めて成立する
関連記事
- Codex Security CLIをSemgrepと比較、$1.28で所見0件だった
- Claude Codeのauto mode既定化、denyとhookは4モードとも効いた
- Claude Code archiveプラグイン、sha256改ざん検知を実機で確認