npmパッケージのvulnerability対応フロー

# 概要

- npmプロジェクトで利用しているnpmパッケージ(依存パッケージ)でvulnerability(脆弱性)が見つかったときの対処フローについて記載します。  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170905/102c8367-3dff-7ec9-61d6-a55d6ec510ef.png) 

（GitHub等が親切に"We found potential security vulnerabilities in your dependencies."のように通知してくれるので便利）

# 対応フロー
ざっくり[全体像](https://speakerd.s3.amazonaws.com/presentations/c5182e63c75e40c09e1371ab5549896e/npm_pj_vulnerabilities_flow_r2.pdf)は以下のとおり。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170905/bccab75e-d1f2-22ea-5818-100c3053ef15.png)


## ①最新のコードを取得する

いうまでもないが、手元のコードは最新のコードにしておく

```
git pull
```

最新であることが確認された

```
Already up to date.
```

ちなみに、今回は自作プロジェクト(https://github.com/riversun/simple-date-format) で実際にハンズオンした

## ②プロジェクトが使用しているnpmパッケージが最新かどうか確認する

脆弱性対応の前に、いま自分のプロジェクトが使っているnpmパッケージを最新のものにする。

プロジェクトが使っているnpmパッケージが最新かどうかは`npm outdated`コマンドで確認することができ、最新バージョンがある場合は、その情報を表示してくれる

```
npm outdated
```

するとこのように、最新版が存在するパッケージを一覧表示してくれる
いくつかのパッケージで最新版があるようだ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170905/702ddcc6-1749-dd61-b52f-8f25792d7127.png)


## パッケージをアップデートする

package.jsonに記載されるnpmパッケージ一覧は以下のようになっている。
(「^」がついた**キャレット表記**については[こちら](https://qiita.com/riversun/items/29d5264480dd06c7b9fb#packagejson%E3%81%AEdevdependencies%E6%84%8F%E5%91%B3%E3%81%A8%E3%81%AF)で説明)

```js:package.json抜粋
  "@babel/core": "^7.8.4",
  "@babel/preset-env": "^7.8.4",
  "babel-jest": "^25.1.0",
   ...
```

パッケージのバージョンの付け方[セマンティックバージョニング](https://semver.org/lang/ja/)に従っている。

セマンティックバージョニングにおいて、メジャーバージョン、マイナーバージョンは以下の意味をもっている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170905/5f60a8b9-1b6f-6cdf-4f6f-84d2c2a7b279.png)


これをふまえ、「**パッケージを最新にする**」には2種類の方法があるといえる。(パッチバージョンを上げるというのもいれれば3種類だが本筋にあまり影響ないので割愛)

- ②-1 **マイナーバージョンまで最新**にする
- ②-2 **メジャーバージョンまで最新**にする


 ②-1をとるか ②-2をとるかはポリシー次第だが<font color=red>メジャーバージョンを最新にする場合はAPIの後方互換が無い</font>ことを想定しておいたほうがいい。

### ②-1 マイナーバージョンまで最新にする場合

キャレット表記「＾」つきで定義されたパッケージのバージョンを、マイナーバージョンまでを最新にするには**[npm update](https://docs.npmjs.com/cli-commands/update.html)**コマンドで可能

```shell
npm update
```

以下のように、マイナーバージョンまでが最新になった
（ただし、メジャーバージョンは最新になっていない。）

```shell
+ jest@25.2.7
+ babel-loader@8.1.0
+ cross-env@7.0.2
+ @babel/core@7.9.0
+ babel-jest@25.2.6
+ @babel/preset-env@7.9.0
+ webpack@4.42.1
added 36 packages from 17 contributors, removed 6 packages, updated 140 packages, moved 1 package and audited 263490 packages in 18.399s
```

## ②-2 メジャーバージョンまで最新にする場合

`npm update`では、マイナーバージョンまでしかアップデートしてくれなかったが、ここではメジャーバージョンまで容赦なくアップデートしてくれるパッケージ**[npm-check-updates](https://github.com/tjunnone/npm-check-updates)**を導入する

以下のようにして**npm-check-updates**をインストールする

```
npm install -g npm-check-updates
```

**npm-check-updates**をインストールすると`ncu`というコマンドが使えるようになる。

ちなみに、**ncu**コマンドだけをたたくと現在のパッケージバージョンと最新のパッケージバージョンを表示してくれる**npm outdated**コマンドのような動作をする

```
ncu
```

を実行すると、以下のように現在のバージョンと最新バージョンが表示される。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170905/acba99a4-0443-a591-df78-544d54bbcef5.png)

つづいて、**ncu -u**を実行すれば、上でみたとおりパッケージが最新バージョンになるように<font color=blue>package.jsonを書き換えてくれる</font>

```
ncu -u
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170905/b40c8582-11e9-140c-4d5c-a8f5d0275565.png)


**npm update**と違って<font color=blue>パッケージの**インストールそのものはやってくれない**。package.jsonが書き換わるだけ</font>なので、**自分で npm install する必要がある**

```
npm install
```

これでパッケージは最新になった

## ③npm auditで脆弱性のある依存パッケージを確認する

今、最新のパッケージにした状態だが、この状態でも脆弱性(valnerability)のあるパッケージが含まれていることがある。
**[npm audit](https://docs.npmjs.com/cli/audit)**コマンドを使えば脆弱性のあるパッケージを洗い出すことができる。

```
npm audit
```

をやってみたら、**180個**の脆弱性がみつかった。レベルは**low**。

```
found 180 low severity vulnerabilities in 263397 scanned packages
  run `npm audit fix` to fix 174 of them.
  6 vulnerabilities require manual review. See the full report for details.
```

### ④npm audit fixで自動修復をこころみる

**npm audit fix**をすると、脆弱性のあるパッケージのバージョンを自動的に脆弱性の無いバージョンに置き換えてくれる（努力をしてくれる）

```
npm audit fix
```

すると以下のようになった。
180個の脆弱性のうち174個が修正された。

```
removed 1 package and updated 2 packages in 4.641s

31 packages are looking for funding
  run `npm fund` for details

fixed 174 of 180 vulnerabilities in 263397 scanned packages
  6 vulnerabilities required manual review and could not be updated
```

残り6個はマニュアルレビューしてくれとかいてある。

## ⑤npm dedupeで重複したパッケージの整理統合をこころみる

### npm dedupeを理解する

まず、**dedupe**の概念を理解するために以下の状態を考える

- <font color=purple>**自プロジェクト**</font>が<font color=blue>**npmパッケージA**</font>と<font color=green>**npmパッケージB**</font>に依存している
- <font color=blue>**npmパッケージA**</font>は<font color=orange>**npmパッケージC @2.1.1**</font>に依存している。
- <font color=green>**npmパッケージB**</font>は<font color=orange>**npmパッケージC @2.2.0**</font>に依存している。

この場合、**npm**を使っていると[^1]<font color=orange>**npmパッケージC @2.1.1**</font>も<font color=orange>**npmパッケージC @2.2.0**</font>もインストールされる。

パッケージとしては<font color=orange>**npmパッケージC**</font>で同じなのに、<font color=orange>**npmパッケージC @2.1.1**</font>と<font color=orange>**npmパッケージC @2.2.0**</font>でバージョンが違いがあるので両方保持されしまう。

だったら、バージョンを新しいほうの<font color=orange>**@2.2.0**</font>のほうに合わせて、<font color=orange>**npmパッケージC @2.2.0**</font>のほうを、<font color=blue>**npmパッケージA**</font>と<font color=green>**npmパッケージB**</font>で共通化して使いましょう、というのが**dedupe**の発想。

## npm dedupeは新しいバージョンをインストールしてくれない

npm update等で**dedupe**はひととおりのパッケージを最新にした後にやる。
なぜなら、**dedupe**自身はパッケージの重複排除などはやってくれるが、最新のパッケージを入れてくれるわけではないので古いパッケージバージョンで状態で**dedupe**してもあまり意味がない。

## dedupeする

さて、では**npm dedupe** (npm ddpでもOK)してみる

```
removed 10 packages and audited 263298 packages in 3.622s
found 0 vulnerabilities
```

脆弱性が0になった！

## なぜdedupeで脆弱性がゼロになった？

これは、dedupeが脆弱性を排除しているというより、古いパッケージバージョンに依存していたライブラリが新しいパッケージバージョンを参照するようにdedupeが変更してくれた効果のため。

上の例でいうと以下のようになる。


- <font color=purple>**自プロジェクト**</font>が<font color=blue>**npmパッケージA**</font>と<font color=green>**npmパッケージB**</font>に依存している
- <font color=blue>**npmパッケージA**</font>は<font color=orange>**npmパッケージC @2.1.1**</font><font color=red>**(脆弱性ありバージョン)**</font>に依存している。
- <font color=green>**npmパッケージB**</font>は<font color=orange>**npmパッケージC @2.2.0**</font>**<font color=darkgray>(安全バージョン)</font>**に依存している。

の状況を**dedupe**が↓のように変更してくれた効果

- <font color=purple>**自プロジェクト**</font>が<font color=blue>**npmパッケージA**</font>と<font color=green>**npmパッケージB**</font>に依存している
- <font color=blue>**npmパッケージA**</font>は<font color=orange>**npmパッケージC @2.2.0**</font>**<font color=darkgray>(安全バージョン)</font>**に依存している。
- <font color=green>**npmパッケージB**</font>は<font color=orange>**npmパッケージC @2.2.0**</font>**<font color=darkgray>(安全バージョン)</font>**に依存している。

[^1]: yarnは自動的にdedupeしてくれる(https://classic.yarnpkg.com/ja/docs/cli/dedupe/)

## ⑦回帰テストを実行する

さてここまでで、脆弱性の無い状態にできたら、最後に**テスト**をしてパッケージバージョンを変更したことによるプロジェクトのデグレが発生していないかどうかを確認する。

```
npm test
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170905/a8bfcd07-2c23-c05f-be27-97ebefe128ce.png)

テスト無事通過！

（といっても、今回はdevDependenciesの依存のみだったけど）

これで対応完了！

### 途中でつまずいた場合

上述のコマンドだけでうまくいかず、つまずくことも多々ある。そうした場合は、コマンドだけで楽に突破できない状況になっている可能性があるので、それなりの調査分析工数を覚悟するしかない。

つまずき例

- vulnerabilityが無くならなかった
 - 対応案：`npm audit`でvulnerabilityのあるパッケージに依存している上位のパッケージを特定する。そのパッケージがdeprecateになっていないか。ちゃんとメンテされているか確認する。deprecatedだったりメンテされていなければ使うのをやめたりPR送ってみたり、自分で直してみたり、代替パッケージを探したり。そういった工数を覚悟、確保する。

- 最後のテストでつまづいた
 - 対応案：手順を1手ずつロールバック（手順を戻す）しながら、都度`npm test`を実行し、どの段階でつまづいたのか特定する。ありがちなのは②-1メジャーアップデートでAPIに破壊的変更

# まとめ

- 依存パッケージにvulnerabilityがみつかった場合の対応フローの一例を紹介しました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170905/bccab75e-d1f2-22ea-5818-100c3053ef15.png)

- こういった関連サービスもあります
 - https://renovate.whitesourcesoftware.com/
 - https://snyk.io/