はじめに
npm audit
は、インストールされている npm パッケージに脆弱性が報告されているものがないかチェックする機能です。
クラウドサインでは、有志を募って、 npm audit
で検出された脆弱性をひとつずつ解消する活動をしています。
npm モジュールの依存性は木構造であるにもかかわらず、npm audit
はこれをフラットに表示しているため、読みづらいです。
この記事では、以下の流れで立ち向かっていきます。
- npm audit を読めるようにする
- 各パッケージの対応順序を決める
- 対象パッケージをアップデートする
おことわり
この記事は、npm audit fix
ですべてが解決するような環境は想定していません。npm audit fix
では大量のアップデートが作られてしまい、手に負えなくなるような環境を想定しています。
1. npm audit を読めるようにする: npm audit --json
を使う
標準出力では、大量の情報が列挙されて情報を読み取るのに苦労をします。そこで使えるのが --json
オプションです。
これによって、情報が JSON 形式で出力されるようになります。プログラムでフィルタリングするのに便利です。
我々のチームでは、 自前でツールを作って、対象のパッケージの情報を照会しやすくしました。
jq を使うなり、ファイルに書き込んで任意の言語で扱うなり、人力で読むなり、それぞれの環境に合わせて処理をするとよさそうです。
存在するパラメーター
npm audit
が出力する JSON の形式については公式ドキュメントが充実しておらず、我々のチームでも実際に出力された値から「どうやらこういうことだろう」とあたりをつけて取り組みました。
以下にご紹介します。
root
以下のようになっています。
{ "auditReportVersion": 2, "vulnerabilities": {...}, "metadata": {...} }
"vulnerabilities"
に各パッケージの脆弱性情報が、 "metadata"
には集計情報が入っています。
"vulnerabilities"
各パッケージ名と、その情報のオブジェクトをそれぞれ key/value とした構造が入っています。
サンプルを以下に示します。
"some-package-name": { "name": "some-package-name", "severity": "high", "isDirect": false, "via": [ "some-package-name-child", "some-package-name-grandchild" ], "effects": [ "some-package-name-parent" ], "range": "<=X.Y.Z", "nodes": [ "node_modules/some-package-name" ], "fixAvailable": true },
キー | バリュー |
---|---|
name |
パッケージ名です。 |
severity |
脆弱性のレベルです。取りうる値は、高い順に critical, high, moderate, low, info です。 |
isDirect |
package.jsonに直接指定されているかどうかを表す真偽値です。 |
via |
実際に脆弱性を発現している、このパッケージが依存するパッケージ名の配列です。要は子孫です。 |
effects |
このパッケージに依存しているパッケージ名の配列です。要は祖先です。 |
range |
脆弱性をもつバージョンの範囲です。パッケージによって記述がまちまちで、これをプログラムで扱うのは難しそうです。 |
nodes |
このパッケージにたどり着くパスの配列です。複数のeffectsを持っていたり、他の依存性の依存性でありつつ直接インストールしたりしている場合は複数表示されます。 |
fixAvailable |
npm audit fix もしくは npm audit fix --force で修正できるかどうかを表す真偽値です。 |
フィルタリングに便利なのは、 severity
プロパティと isDirect
プロパティです。
対象パッケージをどこで使っているかにもよりますが、一般には脆弱性のレベルの高いものから対処していくのが簡単です。 severity
プロパティでフィルタリングするとよいでしょう。
また最終的にアップデートする対象は isDirect
が true
のものです。お手軽に対処するなら、 "isDirect": false
のものは無視してしまっても、自然と解消されます。
via
と effects
は辿るのが大変です。基本的に via
方面(深いほう)には辿っていく必要はありません。effects
方面(浅いほう)に辿っていくケースは考えられます。
このときは、 npm ls <package-name>
を使用すると便利です。ルートから対象のパッケージまでどのような依存関係でインストールされているのかを示してくれます。
npm@11.0.0 /path/to/npm └─┬ init-package-json@0.0.4 └── promzard@0.1.5
npm-ls | npm Docs もご覧ください。
metadata
ここは集計情報が含まれます。「脆弱性のあるパッケージをアップデートしていく」という観点では見る必要はありません。
「いくつ減った」などをメトリクスとして取得する分には便利でしょう。
以下のようなものです。
"metadata": { "vulnerabilities": { "info": 0, "low": 0, "moderate": 0, "high": 0, "critical": 0, "total": 0 }, "dependencies": { "prod": 0, "dev": 0, "optional": 0, "peer": 0, "peerOptional": 0, "total": 0 } }
2. 各パッケージの対応順序を決める
npm audit は読めるようになりました。このセクションでは、それを読んだうえでどう立ち回っていくかの例をご紹介します。
置かれた環境に応じて決めるとよいでしょう。
直接インストールしているパッケージを、レベルの高いものからアップデートしていく
"isDirect": true
のものを抽出し、さらに severity
でソートして、上から順に対応していくという方針です。
最短かつ秩序を持って、リポジトリ内の脆弱性を駆逐できます。
レベルの高い脆弱性の祖先のパッケージをアップデートしていく
severity
でソートして、そのパッケージが直接インストールされていないものの場合、直接インストールしているパッケージまで辿って、辿り着いたパッケージをアップデートするという方針です。前述の npm ls
を使うと便利です。
高レベルの脆弱性に真っ先に対応していくことができます。
我々のチームではこの方針をとり、絶賛戦闘中です。
3. 対象パッケージをアップデートする (もしくは消す)
以下のような方針が考えられます。置かれた環境に応じて決めるとよいでしょう。
最新版にアップデートする
理想です。ただし、メジャーアップデートを伴う場合、アプリの動作が壊れないかしっかり確認する必要があります。
安定バージョンを明示して、 npm install <package-name>@X.Y.Z
を使うとよいでしょう。
乱暴にやるなら npm install <package-name>@latest
としてもよいですが、パッケージによってはベータ版など不安定なバージョンが降ってくる可能性があります。注意しましょう。
脆弱性が解消される最低限のバージョンにアップデートする
破壊的変更によるアプリへの影響を最小限に留められるかもしれません。
range
プロパティに示されたバージョンを超えるバージョンを指定して、npm install <package-name>@X.Y.Z
を行います。
上げた先のバージョンで別の脆弱性が報告されているかもしれません。あらためて npm audit
の内容を確認しましょう。
別の類似パッケージに変更する
対象パッケージがもはやメンテナンスされていないケースがあります。その場合、別の類似パッケージを探してきて、それに変更するしかないかもしれません。がんばりましょう。
アンインストールする
蓋を開けてみると、そのパッケージが使用されていない場合もあります。 npm rm <package-name>
で削除しましょう。
これは我々のチームで実際に多く体験したパターンです。
後述する depcheck は入っているのですが、その設定ファイルに ignore 指定されているケースがたびたびありました。息の長いサービスというのはいろいろあるものだなぁ。
そもそもこうならないようにするために
日頃からメンテナンスする
自分で書いていて耳が痛い話です。
未使用パッケージの検出には depcheck を使うと便利です。
アップデートには Renovate を使うと便利です。
Renovate は、クラウドサインでは一時的に停止していたのですが、稼働を再開させつつあります。
篠田の記事 フロントエンドの技術的負債と向き合っている話 に紹介がありますので、よろしければご覧ください。
依存性を減らす
自分で書いていて耳が痛い話です。
一般論としては、依存性を減らすほうがメンテナンスコストは下がります。
その一方で、依存を減らすということは、パッケージの使用を減らすということでもあります。パッケージを使用しないと、自身でコードベースをメンテナンスする必要が生じ、結果的にメンテナンスコストが上がってしまうかもしれません。
ここは両天秤になりますので、どちらが望ましいか適切に判断する必要があります。
言うのは簡単ですが、実際どうするかは難しいところでしょう。がんばりましょう。がんばります。
おわりに
npm audit
コマンドの説明記事はよく見るのですが、実際どうすればいいのかを解説した記事はあまり見当たりません。npm-audit | npm Docs にもそれらしい説明がありませんでした。これらが記事を書くモチベーションとなりました。
この記事が参考になれば幸いです。