直接依存していない内部で利用しているライブラリが脆弱性を抱えているときにyarnで手動対応する方法について

タイトルが長い。しかもパッとわからない。

ので以下のストーリーを理解されたい。

ストーリー

f:id:hitonatsu:20200823014747p:plain
脆弱性があると言われている

GitHubでセキュリティアラートをONにしていると依存しているライブラリに脆弱性があったときにこんな感じに教えてくれる。

そしてDependabotをONにしていると基本的に自動で脆弱性に対応するためのPRを作成してくれる。

ただなんでかたまにPRを作ってくれないときがあって自分でなんとかしないといけない時があり、そういうときは自分で依存モジュールを上げないといけない。

まあライブラリが対応さえしてくれていれば上げるだけでOKなのだが。

だがしかし

難しいのは脆弱性があるdependencyが直接依存しているモジュールじゃないとき。

例えば C というライブラリのバージョン"1.0.8"に脆弱性があるとする。

しかし自分のプロジェクトのpackage.jsonC は書かれておらず直接の依存はしていなくて C は 直接依存している A というライブラリが利用している B というライブラリの中でさらに依存しているというケース。

言葉で書くとわからん。

図にするとこう

/node_modules
├── A
│   └── node_modules
│       └── B
│           └── node_modules
│               └── C
├── react
├── dateFns
└── ...

Aの孫にCがある

ここで問題が出る

素直に考えれば直接依存している A脆弱性対応してくれるのを待つのだが、全世界すべてのライブラリが活発にメンテされてるわけでもないし、メンテされていてもすぐに対応されるとは限らない。

たとえばこういう風に A が対応してくれなかったとする

/node_modules
├── A < ぼくはBの"3.3.7"に依存しているけど、Bを"3.3.8"に更新対応するつもりはない
│   └── node_modules
│       └── B < "3.3.7"でCの"1.0.8"を使ってたけど、"3.3.8"でCの"1.0.9"に上げたので脆弱性は対応済みです
│           └── node_modules
│               └── C < "1.0.8"で脆弱性あったけど、"1.0.9"で対応済みです
├── react
├── dateFns
└── ...

😇 詰んでしまった……。

どうすればよいか

Dependabotが脆弱性対応のPRを投げてくれるときのPRを思い出してみる。

そういえばlockファイルをなんか書き換えてくれていて対応版のバージョンを指定している。

f:id:hitonatsu:20200823015842p:plain
Dependabotがやってくれるこういうlockファイルの部分更新を手動でやりたい

Dependabotと同じようにlockファイルをなんとかして手動で更新できれば上述でいうライブラリ A が対応してくれなくても C脆弱性対応してくれているのでなんとかなりそう。

つまり自分がDependabotと同じことをできればOK。俺たちはDependabotになりたい。

手順

やっとこさ本題。

yarnでは resolutions というフィールドに書かれたバージョンで強制的に利用するバージョンを固定するという技ができるので、それを用いる。

https://classic.yarnpkg.com/ja/docs/selective-version-resolutions/

ここからは例として "lodash": "4.17.15"脆弱性があり "4.17.19" で 対応されたとする。先ほどからのツリー構造でみるとこう。

/node_modules
├── A
│   └── node_modules
│       └── B
│           └── node_modules
│               └── lodash // "4.17.15"を使っていて脆弱性がある
├── react
├── dateFns
└── ...

上述してきたライブラリ C = lodash ということ。

1 package.jsonに問題のあるパッケージを書く

dependencies or devDependencies と resolutions に書く。

{
  "name": "example-project",
  "version": "1.0.0",
  "dependencies": {
    "lodash": "4.17.19" // もともとこのプロジェクトでlodashは直接使ってないが一旦入れる
  },
  "resolutions": {
    "lodash": "4.17.19" // このバージョンで固定する
  }
}

node_modules内に複数のメジャーバージョンがある場合は

resolutionsにパスとglobを書けるので複数のバージョン指定をそれぞれに対して行えばOK

Selective dependency resolutions | Yarn

 "resolutions": {
    "d2/left-pad": "1.1.1",
    "c/**/left-pad": "^1.1.2"
  }
2 lockファイルを更新

そしてインストールする。

$ yarn install

するとこんな感じのyarn.lockのlodashの欄にDiffが出る。

.
.
.
  integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=

lodash@^4.11.1, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4:
  version "4.17.19"
  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
  integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==

loose-envify@^1.0.0, loose-envify@^1.4.0:
  version "1.4.0"
.
.
.

Dependabotがやってくれるやつだ!

3 package.jsonを元に戻す

あとはさっき1で書いたpackage.jsonの内容を消す。

{
  "name": "example-project",
  "version": "1.0.0",
  "dependencies": {
  },
  "resolutions": {
  }
}

※ 実際にはこんな感じにまっさらではなく他のdepsは書かれている状況

なんで書いたものを消すかというと、あくまでlockファイルを更新するために書いたに過ぎないから。直接プロジェクトの依存として使っているわけではないし、resolutionsに書いたままにしておくと、あとになってなんでresolutionsに書かれて固定されているのかわからなくなる。

しかもresolutionsに書いてあると永遠に固定されるので、今回の例だと今後 "lodash": "4.17.19" 以降のバージョンが出ても永遠に "4.17.19" を利用しようとするからよくない。なのでpackage.jsonから記述は消してしまって、lockファイルだけを更新しておく。


以上、自分がDependabotになるにはどうすればよいかという話。長かったね。

Dependabotがやってくれないとか待ってられないときは自分たちがDependabotと同じことをするしかないので覚えておいて損はない気がする。

こういうのなかなか知見が書かれにくいし表現しにくいので書いた。

PullRequestに書かれたIssueやPRを自動でプロジェクトの指定したカラムに動かすやつ

というのを、そういえばこないだ作っていた。

意味わからんと思うので、Gifを見るほうがはやいはず。

https://raw.githubusercontent.com/1natsu172/github-action-auto-move-related-issue/b32ac32e466fe77b3442cc3e5cd348f5d87bd024/media/work-demo.gif

  1. PRの本文に Issue という段落を作る
  2. 段落内に #1234https://github.com/<Org名>/<Repository名>/issues/1234 という記述形式でIssueや別PRを書く
  3. PRがcloseされたりマージされたりしたら
  4. 書いておいたIssueやPRがプロジェクトの指定したカラムに自動で追加・移動される

というやつ。

リポジトリはここ。

github.com

ちなみにいまのバージョンでは GitHub自体のマージされたら自動でcloseされる fixed #1234 みたいなやつは除外するようにしている。プレフィックスキーワードがついてないIssue・PRのみ対象になる。

仕組み

PullRequestがマージされたりすると、GitHub Actionで得られる情報(Context)のなかに対象のPRのアレコレが入ったオブジェクトがある。

この中にbodyというプロパティがあり、ここにPRの本文が入っている。# Issue\n\n#1234\n\nhogefugapiyo\n\n# Summary\n\nsugoi pr desuという形式で乱暴に入っている。なのでこれをひらすら正規表現でパースしてIssueナンバーorURLを抜き取ってガンバリでやっている。

  1. bodyからIssue段落内に書かれたIssue・PRの短縮記法|URLだけを取り出す
  2. 各対象のナンバー #1234なら1234https://github.com/~~/999 なら999を取り出す
  3. GitHub APIを用いて、リポジトリ中の2.でとれたIssue/PRの内部IDを取ってくる(一緒にすでに配置してあるカンバン名やカードの内部IDもとる)
  4. GitHub APIを用いて、configに書かれた対象のカンバンとカラムの内部IDを取ってくる
  5. 3.で取った内部IDを元に4.の情報とカードの情報を突合する
  6. 対象カンバンの別カラムに既にあるカードはmove、カード未作成のものはaddの処理をGitHub APIを用いて行う

だいたいこういう具合。

めちゃくちゃ愚直だし、めちゃくちゃ地味。

所感

めちゃくちゃエッジニーズなActionのはずで、誰も彼もが使いてぇ〜〜〜ってなるやつじゃないと思っている。普通ならPRマージしたら該当のIssueはプレフィックスをつけておいて自動closeさせる、でOKだと思う。

なんでこんなものを作ったのかというと、closeしたあとに本当に正しく実装されているのかを確認する担当者がいて、そのチェックをパスしたらcloseするという運用をしたかったから。そのためにマージされたら、「このIssueに関するものが実装完了しましたよ」ということがわかる必要があり、それを一箇所に集めたいというニーズが生まれる。一箇所に集めさえすれば担当者は集まってきたものをさばくだけでOKという状態になれるので、そういうユースケースのためにこのアクションを作った。

こういう背景があるからおそらく汎用性はなくて、本当にエッジなアクションだと思っている。

正直OSSにしておくかどうか作っている最中もめちゃくちゃ悩んだのだけど、それでもまあもしかすると世界にはごくごく少数同じ気持ちの人間がいるかもしれないという気持ちになったので、一旦OSSにしておいた。

そういうわけなのでふとこういうことをやりたいなっていう気持ちになったら思い出してください、というスタンスでやっている。