package.json dependencies メンテの仕方 最短ルート


package.json の dependencies を最新に保って脆弱性を解消するために、どこから手を付ければいいのか。

「半年前に npm install で追加したっきり。パッケージのアップデートなんて考えたことなかった」という人や「GitHub から security alert が届いてるけども見て見ぬふりをしている」という人向けに、package.json の dependencies をメンテ方法について個人の考えを書いてみます。


Node.js を使っている人にはおなじみ package.json。

package.json の中で一番よく見るのが dependencies(個人の感想、次点で scripts)。
そして、依存パッケージが着々とバージョンアップしていくにも関わらず放置されてしまって後々問題になりがちなのも dependencies 。

npm install したときの dependencies

まずはおさらい。

$ npm install lodash

とすると、package.json の dependencies に以下の一行が追加されます。

    "lodash": "^4.17.19"

すこし古い記事とかだと「オプションに --save をつけること」と書いていたりしますが、npm 5.0.0 以降デフォルトで save オプションが付くようになったので書かなくて大丈夫。

依存パッケージをメンテせず放置すると

npm install したパッケージをアップデートせずにいると、新しく追加された機能が使えないのはもちろんのこと、新しいバージョンで修正された脆弱性がそのまま残ってしまう危険があります。

特に npm からインストールしたパッケージの場合、パッケージが依存しているパッケージの、さらにその中で依存しているパッケージに脆弱性が見つかった、みたいなことも多々。
今年7月に見つかった lodash の脆弱性(CVE-2020-82031)が起因で 500 万件以上の GitHub Dependabot のアラートが発生したとか。2
Node.js で書かれたパッケージは大体何かしらのパッケージに依存しているので、汎用的なパッケージに脆弱性が見つかると影響する可能性が非常に高いです。

そして、GitHub Dependabot からアラートが来たときに慌ててバージョンを一気にあげてしまうと、いままで使っていた機能がいつの間にか廃止されてアプリが動かなくなった、みたいなこともよく起こるので、出来れば日頃からこまめに上げていきたいところ。

npm outdated && npm update の利点・欠点

では、パッケージをアップデートしよう、となったときによく使われるコマンドが以下の 2 つ。

$ npm outdated
$ npm update

npm outdated で最新バージョンがあるかどうかを確認して、npm update で outdated なパッケージを一括更新します。

ただし注意点が。

npm update では package.json の更新はしません。package-lock.json と node_modules の更新だけ行います。
加えて、^ 付きのバージョン指定でもメジャーバージョンのアップデートまでは行いません。

これは npm update だけでなく npm audix fix も同様。

npm update では package.json の更新をしない

例えば package.json の dependencies に下のように書きかえてから、npm install してみましょう。

    "lodash": "^4.10.0"
$ npm install
added 1 package, and audited 1 package in 1s
found 0 vulnerabilities

$ npm list
test@1.0.0
└── [email protected]

lodash の 4.10.0 ではなく、4.17.19 がインストールされています。

そして、package-lock.json には以下のように記載されます。

  "packages": {
    "node_modules/lodash": {
      "version": "4.17.19",
      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
      "integrity": "sha512-xxxxxxxxxxxxx"
    }
  }

そして、この記事を書いている間に lodash が 4.17.19 から 4.17.20 へとアップデートされました。

$ npm outdated
Package  Current   Wanted   Latest  Location             Depended by
lodash   4.17.19  4.17.20  4.17.20  node_modules/lodash  test

$ npm update
changed 1 package, and audited 1 package in 974ms
found 0 vulnerabilities

$ npm list
test@1.0.0
└── [email protected]

無事アップデートできました。package-lock.json 内も "version": "4.17.20" となっています。
しかしながら、package.json の中は "lodash": "^4.10.0" のままです。

^ 付きのバージョン指定

そもそも、^ 付きのバージョン指定とは何か。
npm install から dependencies に追加したパッケージには "^4.17.19", のように ^(キャレット)がくっついています。

^ が付いていると最新版を取ってきてくれそうな感じを受けますが、微妙にニュアンスが違います。

この ^ は互換性のあるバージョンという意味。互換性のあるバージョン内、つまりメジャーバージョンアップを避けてアップデートを行います。jQuery 2.x.x を使っている場合、3.x.x に上げず 2.x.x の中で最新版をとってくるわけですね。

互換性のあるバージョンというのがまた微妙に複雑。

  • ^0.2.3 の場合、0.2.x 内で最新版を探す(0.3.x までは上げない)
  • ^0.0.3 の場合、0.0.3 内(-bata 付きとか)で最新版を探す(0.0.4 までは上げない)
  • ^0.2.3 -beta2 の場合、0.2.3 内(-bata3 とか)で最新版を探す(0.2.4 までは上げない)

などのルールがあるのですが、長くなるので詳しく知りたい方は公式3を。
メジャーバージョンアップができないというのが親切ながらも微妙に厄介なところ。
"lodash": "^3.10.1" のときにnpm update をしても 4.17.20 には上がらず、先の脆弱性 CVE-2020-8203 は解消しません。

メジャーバージョンのアップデートを行う @ latest

npm update ではメジャーバージョンアップができない。最新版にするにはどうすればいいか。

答えの1つとしては以下。

$ npm install <package-name>@latest

@latest を末尾に付けるとメジャーバージョン含めた最新版に更新してくれます。package.json も更新してくれます。

複数インストールするときは並べて書きます。

npm install <package-name1>@latest <package-name2>@latest

しかしながら、1 つ 1 つパッケージ名を打ち込んで @ latest をつけていくのは面倒です。

npm audit fix --force

npm install したときに脆弱性があると出てくる npm audit fix --forceも答えの1つ。
こちらも package.json 内の "lodash": "3.10.1""lodash": "^4.17.20" に書き換えます。
メジャーバージョンアップや ^ も無視して強制的にアップデートしてしまう、--force の付いた npm オプションです。

脆弱性が出てきたときの最終手段といった趣がありますね。

脆弱性があったときにしか使えないので日常の運用でこのコマンドを頼るのは悪手。
もちろんメジャーバージョンアップをまたぐので、作ったアプリが動かなくなる可能性も十分あります。
日頃からこまめにパッケージのバージョンアップを確認しつつ、メジャーバージョンアップに対応しておきたいところ。

アップデート可能なパッケージ一覧表示・更新する ncu

そこで便利なのが、npm-check-updates。略して ncu。

依存パッケージを見て、アップデート可能なものを一覧にしつつ、package.json の dependencies をメジャーバージョン含めて最新版に書き換えてくれる便利な子です。

使い方は簡単。

$ npm install -g npm-check-updates
$ ncu

とするだけ。そうすると、 アップデート可能なパッケージを一覧にしてくれます。

$ ncu
Checking xxx\package.json
[====================] 1/1 100%

 lodash  ^4.10.1  →  ^4.17.20

Run ncu -u to upgrade package.json

package.json の dependencies を最新版に書き換えたいときは以下。
ncu は package.json の更新だけするので、パッケージと package-lock.json を更新するために npm install はも忘れずに。

$ ncu -u
$ npm install

特定のパッケージだけ最新版に書き換えたいときは、-u の後にパッケージ名を指定すれば OK。

$ ncu -u lodash

ncu を覚えておけば、依存パッケージをさくっと最新バージョンにできます。

他アップデート確認に便利なもの

いくつかコメントで教えていただいたので追記。

Version Lens

Visual Studio Code を使っている場合は、Version Lens という拡張も便利そう。
package.json の dependencies を直接見て、latest かアップデートがあるかを確認できます。
https://marketplace.visualstudio.com/items?itemName=pflannery.vscode-versionlens

latestかどうか確認できると便利なときは意外と多々。
eslint が最新になったけども、eslint-plugin-* に最新でないものがあるな、とか。plugin や config 系のアップデート漏れを見たいときに、こういう拡張が入ってると助かります。

yarn upgrade-interactive --latest

yarnユーザの場合は yarn upgrade-interactive --latest も候補になりそう。

npm outdated && npm update の上位互換が yarn upgrade-interactive という印象。
機能的にはほぼ同じで、インタラクティブに操作出来てわかりやすい。--latest を付けるとメジャーバージョンアップも可能。 npm updateと同様に package.json は書き換わらず、 node_modules と yarn.lock のみの反映であることは注意。

Renovate

Renovate というツールも便利そう。
https://github.com/renovatebot/renovate
依存ライブラリに更新があったときに、バージョン更新用の Pull Request をつくってくれます。
バージョンがどこからどこまであがるのかだけでなく、Release Notes もPRに追記してくれるので調査が行いやすくなりそう。

どのパッケージからアップデートしていくか

依存パッケージが全部最新版になったよ、良かったね。で、終わればラッキー。
長らく package.json を更新していない場合、動かしてみると何かしら不具合が出るのではないかと。

理由はおそらくいろいろと。いつのまにか webpack loader のオプションの渡し方が変わってただとか、使っていた function が unstable_function にリネームされているだとか、機能ごと廃止されているだとか。

長らく蓄積された度重なるアップデートに追いつくのは大変ですが、一度元に戻して、少しづつ切り崩していきましょう。

パッケージのナンバリング

幸いにも npm のパッケージバージョンは規則が設けられています。

例として先ほど追加した lodash を見てみましょう。

    "lodash": "^4.17.19"

一番左の 4 は大きな機能追加や仕様変更、根本から変更する場合などに数字が 1 増えるメジャーバージョンアップ。
メジャーバージョンアップのときに機能が廃止になって動かなくなったとか、いままでと動きが変わったとかいうことが起きやすいので、該当ライブラリをアップデートした後にちゃんとテストをしておきましょう。
メジャーバージョンアップしたパッケージは1つアップデートしたあとに動作確認しておくのが吉。横着してまとめてやろうとすると大変つらい思いをします(しました)。

真ん中の 17 がマイナーバージョンアップ。
便利オプションが増えたよとかちょっとした機能改善系のアップデート。これまでと動きが変わる、ということは起きにくいので、他のライブラリと一緒にまとめてアップデートして動作確認で十分かなという感じです。

一番右の 19 はパッチバージョンアップ。
基本的にバグ修正。こちらもまとめてアップデートで大丈夫という印象。

上で紹介した ncu を使うと、

  • パッチバージョンアップは緑
  • マイナーバージョンアップは水色
  • メジャーバージョンアップがある場合は赤

で、表示してくれます。

まずは水色・緑で表示されているパッケージをまとめてアップデートしてから動作確認して、いつでも戻れるように Git commit してからメジャーバージョンアップの赤色に取り掛かるのがよさそうだなという経験則があります。

また、メジャーバージョンアップは破壊的変更にあわせてコードの修正が入ることも多いので、バージョンを上げてうまく動作したら1パッケージごとに Git commit 打っていきましょう。commit 打ち忘れると切り戻したいときに大変です(n 敗)。

破壊的変更の確認

メジャーバージョンアップで何がどう変わったかは、基本的に各パッケージのお知らせを見るしかないです。

しかしながら、動作の変わるような大きい変更が入る場合はアナウンスもしっかりある場合が多いですし、親切なパッケージであれば代替手段の紹介やコードの自動修正ツールを用意してくれていることもあります。

お知らせやアナウンスといったようなものが見当たらないときは、残念ですが改めて README やコード、issue などを見ながら使い方を再確認しましょう。
いっそのこと別のパッケージを探すのも手。当時なかった便利パッケージや使いやすい loader が新たに生まれていることもあります。

まとめ

ncu でアップデートを確認して、マイナー・パッチバージョンアップを片付けてからメジャーバージョンアップを慎重に行う。

思い出したときに月一とかで徐々に上げておけば、GitHub から security alert が届いたときに冷静に対処できるのではないかと。

チームで脆弱性の管理などをしている場合、脆弱性の管理が追いつかなくなりがち。
npm パッケージだけでなくサーバの脆弱性の可視化やチケット管理を行える SaaS を Future から提供している4ので、ぜひ検討してみてください。