こんにちは。年末調整機能の開発を担当しているzoshigayanと申します。気温が上がり半袖を着る季節になると年末の訪れを実感しますね。
弊社テックブログ、最近 めちゃくちゃイケてるRailsの記事 などが出て賑わいを感じているのですが、ここに来て驚くほど地味なシェルの記事を書くことになり震えています。
解決したい課題
SmartHRでは、全社的にフロントエンドのパッケージマネージャとして yarn を利用しています。
yarnには audit という依存パッケージの脆弱性を検知してアラートを出してくれるコマンドがあり、多くのチームがこれをCIに組み込むことでプロダクトに脆弱性を埋め込んでしまうことを防いでいます。
しかし、時々「自分たちの用途からすると関係ない不具合なのに修正PRが全然マージされなくてCIがpassしない (=デプロイできない)」といった事態が起きてしまうことがあります。困ってしまいますね。
対応の概要
対応の選択肢として「検査の対象とするseverity (深刻度) を引き上げる」とか「そもそもauditをやめる」といったものも考えられるのですが、本稿においては検査の対象とするセキュリティレベルを下げることなく 「既知の不具合だけを指定して無視する」 というものを考えていきたいと思います。
では早速、やっていきましょう〜。
作業
auditの結果を知っておく
audit コマンドは --json
オプションを付与することで結果をJSON形式にすることができます。こうすると jq
からクエリを書くだけで特定の情報のみを取り出すことができて便利です。
試しにSmartHR UIのリポジトリで脆弱性を検査してみましょう。実際にプロダクトの中で使うときには --groups dependencies
オプションをつけてランタイムスクリプトにバンドルされるパッケージのみに対象を絞ったほうがよいと思いますが、SmartHR UIはこれをやると何も脆弱性が検知されなかった (よかった) ので全パッケージを検査しています。
$ yarn audit --json | jq .
めちゃくちゃ巨大なJSONが生成されますので全部を載せることは控えますが、構造としては以下のような感じで「個別の脆弱性が "type": "auditAdvisory"
プロパティを持つオブジェクトとして並ぶ」「最後にauditの結果をまとめた要約が "type": "auditSummary"
プロパティを持つオブジェクトとして出力される」という感じになっています。
{ "type": "auditAdvisory", "data": { "resolution": {/* (略) */}, "advisory": {/* (略) */} } } { "type": "auditAdvisory", "data": { "resolution": {/* (略) */}, "advisory": {/* (略) */} } } // (略) { "type": "auditSummary", "data": { "vulnerabilities": { "info": 0, "low": 0, "moderate": 23, "high": 0, "critical": 0 }, "dependencies": 2275, "devDependencies": 0, "optionalDependencies": 0, "totalDependencies": 2275 } }
最後に添付されているサマリを見ると、どうやらmoderate (中くらい) の深刻度の脆弱性が23件検知されています。どんなものが検知されたのか内容が気になるところですが、このまま巨大なJSONを隅々まで見るのはあまりにもつらいので jq
で絞り込んで使っていきましょう。
CVEのリンクは、こんな感じで一覧化できます。
だいたい重複したものが沢山出るので sort -u
等のコマンドで絞り込んでおけるとよいです。
$ yarn audit --json | jq -r 'select(.type == "auditAdvisory").data.advisory.url' | sort -u https://github.com/advisories/GHSA-72xf-g2v4-qvf3 https://github.com/advisories/GHSA-c2qf-rxjj-qqgw https://github.com/advisories/GHSA-j8xg-fqg3-53r7
いい感じですね。
無視したい脆弱性情報を事前に書き出しておく
今回は一旦すべての脆弱性を無視してみることにしましょう。先ほどのクエリで取得したURLたちを yarn-audit-ignore
というファイルに書き出しておきます。
$ yarn audit --json | jq -r 'select(.type == "auditAdvisory").data.advisory.url' | sort -u >> yarn-audit-ignore $ cat yarn-audit-ignore https://github.com/advisories/GHSA-72xf-g2v4-qvf3 https://github.com/advisories/GHSA-c2qf-rxjj-qqgw https://github.com/advisories/GHSA-j8xg-fqg3-53r7
あとはCI上の脆弱性検査スクリプトがこのファイルを照会して記載されてる不具合を良い感じにスルーするようにするだけでOKです。ほぼ完成ですね。
脆弱性が検知されたら事前に書き出したファイル内容とJSONの差分を出す
yarn audit
の終了コードは検知された脆弱性によって変動する (検知されなかった場合のみ 0
) ので、終了コードに応じて動作を変えます。今回は深刻度に関わらず全部拾いたいので 0
ではなかった場合は既知の不具合かどうか判定を行うようにします。また、CI上での動作を前提とするので既知の不具合が見つかった場合は終了コードを上書きして 0
にする必要があります。
いきなりCIの設定を書いてしまう前に、こんな感じのシェルスクリプトで試してみましょう。
#!/bin/zsh function audit() { set +e yarn audit --json > audit-result.json result=$? set -e # 脆弱性が見つかった場合のみ実行 if [ "$result" != 0 ]; then # 既知の脆弱性の配列を作成 ignore_urls=($(cat yarn-audit-ignore)) # yarnが発見した脆弱性の配列を作成 eval "urls=($(cat audit-result.json | jq -r 'select(.type == "auditAdvisory").data.advisory.url' | sort -u))" # 既知ではない脆弱性を保持するための配列を作成 unknown_vulnerability_urls=() for url in $urls do # yarnが発見した脆弱性が既知の脆弱性配列に含まれていない場合 if [[ ! " ${ignore_urls[@]} " =~ " ${url} " ]]; then # 配列に記録しておく unknown_vulnerability_urls+=($url) fi done # 既知ではない脆弱性が存在する場合 if [ ${#unknown_vulnerability_urls[@]} != 0 ]; then echo '予期していない脆弱性が検知されました。' # URLを一覧で表示する for url in "${unknown_vulnerability_urls[@]}" do echo $url done echo '\nパッケージをアップデートするか、 yarn-audit-ignore に脆弱性URLを追加してください。' # 異常終了する exit 1 fi fi # 正常に終了する exit 0 } audit
手元で動かしてみます。把握しているすべての脆弱性が yarn-audit-ignore-ids
に格納されている場合は、特に何も表示されずにスッと処理が終了します。
$ chmod +x ./audit $ ./audit $ echo $? # 直前のコマンドの終了コードを表示してみる 0
試しに未知の脆弱性が発見された状況を想定して yarn-audit-ignore
の中身を減らしてみると、エラーを吐いて終了します。
$ ./audit 予期していない脆弱性が検知されました。 https://github.com/advisories/GHSA-c2qf-rxjj-qqgw パッケージをアップデートするか、 yarn-audit-ignore に脆弱性URLを追加してください。 $ echo $? # 直前のコマンドの終了コードを表示してみる 1
あとは、これをCircleCIなどのCIツールの設定に追加すれば完了です。
yarn-audit-ignore
をコミットするのも忘れないようにしましょう。
課題と展望
自分がこの対応をした2年前は一時しのぎのつもりだったのですが、どうやら一発で鮮やかに解決する方法は2023年7月現在も存在しないらしいです。「ほんまか…?」という気持ちはかなりあるので、もし情報をお持ちの方がいたらTwitterとかで教えていただけると助かります。
最近はGitHub上でdependabotがリポジトリに対して自動的にアラートを出してくれたりもするので、もしかするとCI上でパッケージマネージャからauditするというアプローチ自体があまりイケてないものになりつつあるのかもしれません。でもCI落ちるのが一番気付きやすいんだよなぁ…とは思うので、面倒なくプロダクトを安全に保つ脆弱性との向き合い方は今後も考えていきたいです。
参考リンク
本稿を書くにあたって参考にさせていただいた情報です。インターネットは便利ですね。
- yarn audit | Yarn
- auditコマンドのドキュメントです
- オプションの機能を調べる際に拝見しました
- jq Manual (development version)
- jqのドキュメントです
- 複雑なクエリを自力で書くのは非常にしんどいので大変助かりました
- Allow
yarn audit
issues to be suppressed · Issue #6669 · yarnpkg/yarn- 本稿と似たような課題への対処を話し合っているIssueです
- 実装例としてCircle CI orbのコードを参考にさせていただきました
We Are Hiring
こんな感じで、弊社では「この業務を自動化して楽にできないか」「あそこのチームがこんなことしてるらしいから パク 参考にしよう」といった具合に開発プロセスをハックしつつ楽しくプロダクトを作っております。楽しそうですね。
…ということで、SmartHRに興味が出てきたという方はぜひカジュアル面談でお話しましょう。
https://hello-world.smarthr.co.jp/
技術記事だけじゃ雰囲気がわからないよという方のためにエンジニアたちが会社についてあーだこーだ話しながらゲームする 謎の配信企画 もやっておりますので、そちらも是非観てみていただけたら嬉しいです。