actionlint v1.4 → v1.6 で実装した新機能の紹介

前回の actionlint の記事 GitHub Actions のワークフローをチェックする actionlint をつくった からちょうど1ヶ月経ち,今日 v1.6.0 をリリースしました.前回時点では v1.4.0 で,そこから v1.4.1, v1.4.2, v1.4.3, v1.5.0, v1.5.1, v1.5.2, v1.5.3, v1.6.0 とリリースを重ねています.git diff によると v1.4.0 〜 v1.6.0 で

155 files changed, 14816 insertions(+), 2981 deletions(-)

のコード変更を行ったようです.

github.com

この記事では大きめの機能追加や改善をいくつか紹介します.

  • 有名アクションの input/output チェック
  • スクリプトインジェクション脆弱性のチェック
  • -format オプションによる柔軟なエラー出力
  • ドキュメントの再構成
  • Windows でカレントディレクトリの実行ファイルが意図せず実行できてしまう脆弱性への対処
  • Playground の機能追加

変更の完全な履歴についてはチェンジログを確認してください.

有名アクションの input/output チェック

アクションには入力と出力があり,action.yml メタデータに定義されています.入力には必須のものと必須でないものがあります.

これらの input/output についての情報は,そのアクションの action.yml をフェッチしないと分かりません.ネットワークリクエストは遅いですし,ネットワークが制限された環境(コンテナ内など)でも使えるようにする必要があるため,フェッチするのは避けたいです.そのため,事前に有名なアクションの action.yml の情報をクロールしてコード生成で持っておくことで,input/output のチェックを実現しています.簡単な調査では,有名なアクションのチェックさえできれば全体アクション実行の9割以上はカバーできそうなので,とりあえずこれでヨシとしています.

on: push

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/cache@v2
        # ERROR: 必須の input "key" がない
        with:
          # ERROR: 定義されていない input
          keys: |
            ${{ hashFiles('**/*.lock') }}
            ${{ hashFiles('**/*.cache') }}
          path: ./packages
      - run: make

Playground

この例は actions/cache の input のチェックをしています.actions/cachekey という必須の input があるのでそれが指定されていないこと,action.yml に定義されていない keys input が指定されていることの2点がエラーとして報告されます.

on: push

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      # ERROR: id: cache のステップはまだ実行されていない
      - run: echo ${{ steps.cache.outputs.cache-hit }}
      - uses: actions/cache@v2
        id: cache
        with:
          key: ${{ hashFiles('**/*.lock') }}
          path: ./packages
      # OK
      - run: echo ${{ steps.cache.outputs.cache-hit }}
      # ERROR: cache_hit という output は存在しない
      - run: echo ${{ steps.cache.outputs.cache_hit }}

Playground

この例は actions/cache の output のチェックをしています.actions/cache はキャッシュがヒットしたかどうかを cache-hit という output にセットします.actionlint では steps.cache.outputs のオブジェクト型に action.yml に基づいてプロパティを定義することで,型チェックで正しい output を使えているかをチェックします.ここでは存在しない cache_hit プロパティにアクセスしようとしてエラーになっています.また,steps.cache.outputs の型は,id: cache のステップ以降でのみ定義されるので,id: cache より前のステップではアクセスできずエラーになります.

action.yml の情報はスクリプトを組んで go generate で更新するようにしていて,毎週 CI で実行され,更新があれば自動で pull request が生成される仕組みになっています.

スクリプトインジェクション脆弱性のチェック

GitHub Actions の ${{ }} は単に文字列として置換されます.例えば

- run: echo '${{ github.event.pull_request.title }}'

という step があると,ジョブ実行時に ${{ }} の中身が評価されて置換され,

- run: echo 'pull request のタイトル'

スクリプトとして実行されます.

ここで悪意のあるユーザが '; malicious_command ... というタイトルで pull request を作成するとどうなるでしょう?

- run: echo ''; malicious_command ...

と置換され,malicious_command が実行できてしまいます.ジョブ実行時には設定されたパーミッションで credential が生成されますし,フックによってはシークレットにアクセスすることもできますので,秘匿情報を盗むことができてしまいます.

これを防ぐためには

- run: echo "$TITLE"
  env:
    TITLE: ${{ github.event.pull_request.title }}

というように環境変数を通してアクセスする回避策があります.

ここでの問題は github.event.pull_request.title が信頼できない入力であるという点です.どの入力が信頼できないかは,GitHub Security Lab のブログ記事に一覧があります.

actionlint では,run: などのスクリプトインジェクションの危険がある箇所で,これらの信頼できない入力が ${{ }} で直に使われていないかをチェックします.

on: pull_request
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Print pull request title
        # ERROR: 信頼できない入力の使用
        run: echo '${{ github.event.pull_request.title }}'
      - uses: actions/stale@v4
        with:
          repo-token: ${{ secrets.TOKEN }}
          # OK: アクションの入力はスクリプトに直接渡されないので使って良い
          stale-pr-message: ${{ github.event.pull_request.title }} was closed
      - uses: actions/github-script@v4
        with:
          # ERROR: この入力はスクリプトで評価される
          script: console.log('${{ github.event.head_commit.author.name }}')

Playground

このチェックは @azu さんに機能リクエストをいただいて実装しました.

-format オプションによる柔軟なエラー出力

Go テンプレート構文を使ってエラーメッセージの出力フォーマットを柔軟に指定できるようにしました.構文を知っている人向けに説明すると,. はエラーオブジェクトのスライスになっていて,各エラーオブジェクトの .Message.Line フィールドなどを利用して出力を整形します.

例えば,

actionlint -format '{{json .}}'

とすると json アクションを通してエラーメッセージの列が JSON 文字列化され

[{"message":"unexpected key \"branch\" for ...

のようにオブジェクトの配列で出力されます.出力を jq などで操作するのに便利です.

また,もう少し複雑な例として,

actionlint -format '{{range $err := .}}### Error at line {{$err.Line}}, col {{$err.Column}} of `{{$err.Filepath}}`\n\n{{$err.Message}}\n\n```\n{{$err.Snippet}}\n```\n\n{{end}}'

とすると Markdown 形式でエラーを出力でき

f:id:rhysd:20210811220522p:plain

のようにエラーが出力されます.

ドキュメントでは ::error コマンドを使ってエラーアノテーションを付ける例やフォーマットの使い方についても説明していますので,詳しくはそちらを参照してください.

この機能は @ybiquitous さんに機能リクエストをいただいて実装しました.

ドキュメントの再構成

以前はすべてのドキュメントをリポジトリ直下の README.md にすべて突っ込んでいたのですが,チェック項目やセクション数が増えてきて非常に長くなってしまっていました.長い README は読まれないので,ドキュメントの内容ごとにファイルを分けて整理しました.

  • README.md: イントロダクション,簡単なチュートリアル,他のドキュメントへのリンク,ライセンスについてのみ記載しています
  • docs/checks.md: actionlint が行うすべてのチェック項目のリストです.ワークフロー例とエラー出力例,Playground へのリンクをチェック項目ごとに記載しています
  • docs/install.md: インストール方法です.ビルド済みバイナリ,Homebrew,ダウンロードスクリプトgo install によるソースからのビルドのそれぞれについて説明しています
  • docs/usage.md: actionlint コマンドの使い方(特定のエラーを無視する方法,-format オプションの使い方,exit status の意味など),Playground の使い方について説明しています.また,reviewdog, Problem Matchers, super-linter との連携についても解説しています
  • docs/config.md: コンフィグファイルの使い方について説明しています
  • doc/api.md: actionlint を Go のライブラリとして使う方法について解説しています
  • doc/reference.md: 各種リソースへのリンク集です

Windows でカレントディレクトリの実行ファイルが意図せず実行できてしまう脆弱性への対処

Windows では foo というように絶対パスではないコマンドを実行すると,カレントディレクトリにある foo.exe を実行できてしまうという挙動があります.Go の exec.LookPath("foo") でもカレントディレクトリの foo.exe を返してしまいます.

golang/go#38736

actionlint でも shellcheck コマンドや pyflakes コマンドのパスを取得するのに exec.LookPath を使っていたので,カレントディレクトリに shellcheck.exepyflakes.exe があるとそちらを意図せず実行してしまう問題がありました.現在は execabs を使うことでこの問題を修正済みです.

Playground の機能追加

https://rhysd.github.io/actionlint/

actionlint を Wasm にビルドすることでブラウザ上でも動くようにした Playground ですが,より手軽に使えて共有できるようにいくつか改善を行いました.

f:id:rhysd:20210812212418p:plain

  • 'Permalink' ボタンを設置しました.これをクリックすると今のコードエディタのソースの状態を URL のハッシュにエンコードすることで永続化します.バグ報告時に Playground で再現させてそのパームリンクを提供してもらえると助かるなと思って実装しています.
  • URL の入力フォームを設置しました.URL を入力してから Check ボタンを押すと,URL 先のファイルをフェッチしてきます.もちろんですが GitHub Pages からフェッチできる URL のみ使えます.
    • https://github.com/owner/repo/tree/branch/...: GitHub 上の特定のファイルをブラウザで表示させた時の URL
    • https://raw.githubusercontent.com/owner/repo/branch/...: GitHub 上でホストされている生のファイルの URL
    • https://gist.github.com/owner/...: Gist 上の特定のファイルをブラウザで表示させた時の URL
    • https://gist.githubusercontent.com/owner/...: Gist 上でホストされている生のファイルの URL

まとめ

前回の記事以降も継続して改善・機能追加を行っていきました.もしよければ試しに使ってみて,何か改善点・問題点などあれば issue で教えてもらえるとありがたいです(日本語でも英語でも大歓迎です).