SmartHR Tech Blog

SmartHR 開発者ブログ

yarn auditで特定の不具合を無視する

こんにちは。年末調整機能の開発を担当している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落ちるのが一番気付きやすいんだよなぁ…とは思うので、面倒なくプロダクトを安全に保つ脆弱性との向き合い方は今後も考えていきたいです。

参考リンク

本稿を書くにあたって参考にさせていただいた情報です。インターネットは便利ですね。

We Are Hiring

こんな感じで、弊社では「この業務を自動化して楽にできないか」「あそこのチームがこんなことしてるらしいから パク 参考にしよう」といった具合に開発プロセスをハックしつつ楽しくプロダクトを作っております。楽しそうですね。

…ということで、SmartHRに興味が出てきたという方はぜひカジュアル面談でお話しましょう。

https://hello-world.smarthr.co.jp/

技術記事だけじゃ雰囲気がわからないよという方のためにエンジニアたちが会社についてあーだこーだ話しながらゲームする 謎の配信企画 もやっておりますので、そちらも是非観てみていただけたら嬉しいです。