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(-)
のコード変更を行ったようです.
この記事では大きめの機能追加や改善をいくつか紹介します.
- 有名アクションの 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
この例は actions/cache
の input のチェックをしています.actions/cache
は key
という必須の 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 }}
この例は 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 }}')
このチェックは @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 形式でエラーを出力でき
のようにエラーが出力されます.
ドキュメントでは ::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
を返してしまいます.
actionlint でも shellcheck
コマンドや pyflakes
コマンドのパスを取得するのに exec.LookPath
を使っていたので,カレントディレクトリに shellcheck.exe
や pyflakes.exe
があるとそちらを意図せず実行してしまう問題がありました.現在は execabs を使うことでこの問題を修正済みです.
Playground の機能追加
https://rhysd.github.io/actionlint/
actionlint を Wasm にビルドすることでブラウザ上でも動くようにした Playground ですが,より手軽に使えて共有できるようにいくつか改善を行いました.
- 'Permalink' ボタンを設置しました.これをクリックすると今のコードエディタのソースの状態を URL のハッシュにエンコードすることで永続化します.バグ報告時に Playground で再現させてそのパームリンクを提供してもらえると助かるなと思って実装しています.
- URL の入力フォームを設置しました.URL を入力してから Check ボタンを押すと,URL 先のファイルをフェッチしてきます.もちろんですが GitHub Pages からフェッチできる URL のみ使えます.
https://github.com/owner/repo/tree/branch/...
: GitHub 上の特定のファイルをブラウザで表示させた時の URLhttps://raw.githubusercontent.com/owner/repo/branch/...
: GitHub 上でホストされている生のファイルの URLhttps://gist.github.com/owner/...
: Gist 上の特定のファイルをブラウザで表示させた時の URLhttps://gist.githubusercontent.com/owner/...
: Gist 上でホストされている生のファイルの URL
まとめ
前回の記事以降も継続して改善・機能追加を行っていきました.もしよければ試しに使ってみて,何か改善点・問題点などあれば issue で教えてもらえるとありがたいです(日本語でも英語でも大歓迎です).