GitHub Actions ワークフローの静的解析
はじめに
GitHub Actions の設定は、設定を追加し動作すれば OK くらいの感覚で運用してました。
ただ、より良く運用するならセキュリティーを気にしたり、タイムアウトなども細かく設定したほうがよいと思ってます。
そこで、「GitHub CI/CD 実践ガイド」で紹介されていた actionlint や ghalint で静的解析するようにしてみました。
actionlint とは
README より引用
actionlint is a static checker for GitHub Actions workflow files. Try it online!
Features:
- Syntax check for workflow files to check unexpected or missing keys following workflow syntax
- Strong type check for
${{ }}
expressions to catch several semantic errors like access to not existing property, type mismatches, ...- Actions usage check to check that inputs at
with:
and outputs insteps.{id}.outputs
are correctReusable workflow check
to check inputs/outputs/secrets of reusable workflows and workflow calls- shellcheck and pyflakes integrations for scripts at run:
- Security checks; script injection by untrusted inputs, hard-coded credentials
- Other several useful checks; glob syntax validation, dependencies check for needs:, runner label validation, cron syntax validation, ...
actionlint は、GitHub Actions ワークフロー ファイルの静的チェッカーです。オンラインで試してみましょう!
特徴:
- ワークフローファイルの構文チェックにより、ワークフロー構文に従って予期しないキーまたは欠落しているキーをチェックします
- ${{ }} 式の強力な型チェックにより、存在しないプロパティへのアクセス、型の不一致などのいくつかのセマンティック エラーを検出します。
- アクションの使用状況チェックでは、with: での入力と、steps.{id}.outputs での出力が正しいことを確認します。
- 再利用可能なワークフロー チェックにより、再利用可能なワークフローとワークフロー呼び出しの入力/出力/秘密をチェックします。
- 実行時のスクリプトの shellcheck と pyflakes の統合:
- セキュリティチェック。信頼できない入力、ハードコーディングされた資格情報によるスクリプト インジェクション
- 他にもいくつかの便利なチェックがあります。 glob 構文の検証、依存関係のニーズのチェック、ランナー ラベルの検証、cron 構文の検証など
ghalint とは
README より引用
GitHub Actions linter for security best practices.
Policies
1. Workflow Policies
- job_permissions: All jobs should have
permissions
- deny_read_all_permission:
read-all
permission should not be used- deny_write_all_permission:
write-all
permission should not be used- deny_inherit_secrets:
secrets: inherit
should not be used- workflow_secrets: Workflow should not set secrets to environment variables
- job_secrets: Job should not set secrets to environment variables
- deny_job_container_latest_image: Job's container image tag should not be
latest
- action_ref_should_be_full_length_commit_sha: action's ref should be full length commit SHA
- github_app_should_limit_repositories: GitHub Actions issueing GitHub Access tokens from GitHub Apps should limit repositories
- github_app_should_limit_permissions: GitHub Actions issueing GitHub Access tokens from GitHub Apps should limit permissions
- job_timeout_minutes_is_required: All jobs should set timeout-minutes
2. Action Policies
- action_ref_should_be_full_length_commit_sha: action's ref should be full length commit SHA
- github_app_should_limit_repositories: GitHub Actions issueing GitHub Access tokens from GitHub Apps should limit repositories
- github_app_should_limit_permissions: GitHub Actions issueing GitHub Access tokens from GitHub Apps should limit permissions
- action_shell_is_required:
shell
is required ifrun
is set
セキュリティのベスト プラクティスのための GitHub Actions リンター。
ポリシー
1. ワークフローポリシー
- job_permissions: すべてのジョブには権限が必要です
- deny_read_all_permission: すべて読み取り権限を使用しないでください
- deny_write_all_permission: すべて書き込み権限を使用しないでください。
- deny_inherit_secrets: シークレット: 継承は使用しないでください
- workflow_secrets: ワークフローは環境変数にシークレットを設定しないでください
- job_secrets: ジョブは環境変数にシークレットを設定しないでください
- deny_job_container_latest_image: ジョブのコンテナー イメージ タグは最新であってはなりません
- action_ref_Should_be_full_length_commit_sha: アクションの参照は完全長のコミット SHA である必要があります。
- github_app_Should_limit_repositories: GitHub アプリから GitHub アクセス トークンを発行する GitHub アクションはリポジトリを制限する必要があります
- github_app_Should_limit_permissions: GitHub アプリから GitHub アクセス トークンを発行する GitHub アクションは権限を制限する必要があります
- jobtimeout minutes_is_required: すべてのジョブはタイムアウト分を設定する必要があります
2. 行動方針
- action_ref_Should_be_full_length_commit_sha: アクションの参照は完全長のコミット SHA である必要があります。
- github_app_Should_limit_repositories: GitHub アプリから GitHub アクセス トークンを発行する GitHub アクションはリポジトリを制限する必要があります
- github_app_Should_limit_permissions: GitHub アプリから GitHub アクセス トークンを発行する GitHub アクションは権限を制限する必要があります
- action_shell_is_required: run が設定されている場合はシェルが必要です
なぜ actionlint と ghalint を使用するのか
actionlint と ghalint はチェックする内容が異なります。
actionlint は構文チェック、 ghalint はセキュリティーチェックが主な内容になっています。
そのため、どちらかひとつよりも actionlint, ghalint の両方を使用するとよいと判断しました。
どこで静的解析を行うか
静的解析するタイミングは、以下のようなものがあります。
- ローカルで手動実行
- git pre-comitt で自動実行
- GitHub Actions で自動実行 (Pull Request)
GitHub Actions ワークフローの設定は、GitHub へ push すると動くものがあります。
その前に静的解析をおこなうように「ローカルで手動実行」や「git pre-comitt で自動実行」がタイミングとしてはよさそうです。
ただ、「ローカルで手動実行」や「git pre-comitt で自動実行」では、実行忘れなどで静的解析されない場合もあります。
そのため、「GitHub Actions で自動実行」で行うようにしてみました。
GitHub Actions で GitHub Actions ワークフローを静的解析
設定ファイル
.github/workflows/github_actions_lint.yml
---
# actionlint と ghalint を使って GitHub Actions の静的解析する
#
# - actionlint: https://github.com/rhysd/actionlint
# - ghalint: https://github.com/suzuki-shunsuke/ghalint
name: GitHub Actions Lint
on:
pull_request:
paths:
- .github/workflows/*.yaml
- .github/workflows/*.yml
env:
# NOTE: actionlint をアップデートする場合は、 ACTIONLINT_VERSION, ACTIONLINT_CHECKSUM を更新してください
ACTIONLINT_OS: linux
ACTIONLINT_ARCH: amd64
ACTIONLINT_VERSION: 1.7.3
ACTIONLINT_CHECKSUM: 37252b4d440b56374b0fc1726e05fd7452d30d6d774f6e9b52e65bb64475f9db
# NOTE: ghalint をアップデートする場合は、 GHALINT_VERSION, GHALINT_CHECKSUM を更新してください
GHALINT_OS: linux
GHALINT_ARCH: amd64
GHALINT_VERSION: 1.0.0
GHALINT_CHECKSUM: e9006ff212a3b27a99af43db687ded78173baa2f9816b2e2a9bed03a2ed2f954
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Download actionlint
run: |
curl -L -o actionlint.tar.gz "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_${ACTIONLINT_OS}_${ACTIONLINT_ARCH}.tar.gz"
if [ "$(shasum -a 256 actionlint.tar.gz)" != "${ACTIONLINT_CHECKSUM} actionlint.tar.gz" ]; then
echo "checksum mismatch"
echo "expected: ${ACTIONLINT_CHECKSUM} actionlint.tar.gz"
echo "actual: $(shasum -a 256 actionlint.tar.gz)"
exit 1
fi
tar xzf actionlint.tar.gz
./actionlint -version
- name: Run actionlint
run: ./actionlint -color
- name: Download ghalint
run: |
curl -L -o ghalint.tar.gz "https://github.com/suzuki-shunsuke/ghalint/releases/download/v${GHALINT_VERSION}/ghalint_${GHALINT_VERSION}_${GHALINT_OS}_${GHALINT_ARCH}.tar.gz"
if [ "$(shasum -a 256 ghalint.tar.gz)" != "${GHALINT_CHECKSUM} ghalint.tar.gz" ]; then
echo "checksum mismatch"
echo "expected: ${GHALINT_CHECKSUM} ghalint.tar.gz"
echo "actual: $(shasum -a 256 ghalint.tar.gz)"
exit 1
fi
tar xzf ghalint.tar.gz
./ghalint version
- name: Run ghalint
run: ./ghalint run
check-binary-version:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
pull-requests: write
strategy:
matrix:
binary:
- actionlint
- ghalint
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Get Binary Info
id: get-binary-info
run: |
case ${{ matrix.binary }} in
actionlint)
echo "name=${{ matrix.binary }}" >> "${GITHUB_OUTPUT}"
echo "version=${{ env.ACTIONLINT_VERSION }}" >> "${GITHUB_OUTPUT}"
echo "owner=rhysd" >> "${GITHUB_OUTPUT}"
echo "repo=actionlint" >> "${GITHUB_OUTPUT}"
echo "releases-url=https://github.com/rhysd/actionlint/releases" >> "${GITHUB_OUTPUT}"
;;
ghalint)
echo "name=${{ matrix.binary }}" >> "${GITHUB_OUTPUT}"
echo "version=${{ env.GHALINT_VERSION }}" >> "${GITHUB_OUTPUT}"
echo "owner=suzuki-shunsuke" >> "${GITHUB_OUTPUT}"
echo "repo=ghalint" >> "${GITHUB_OUTPUT}"
echo "releases-url=https://github.com/suzuki-shunsuke/ghalint/releases" >> "${GITHUB_OUTPUT}"
;;
esac
- name: Check Binary Version
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
id: check-binary-version
env:
BINARY_NAME: ${{ matrix.binary }}
BINARY_VERSION: ${{ steps.get-binary-info.outputs.version }}
BINARY_OWNER: ${{ steps.get-binary-info.outputs.owner }}
BINARY_REPO: ${{ steps.get-binary-info.outputs.repo }}
BINARY_RELEASES_URL: ${{ steps.get-binary-info.outputs.releases-url }}
with:
result-encoding: string
script: |
const currentVersion = process.env.BINARY_VERSION;
const latestVersion = await github.rest.repos.getLatestRelease({
owner: process.env.BINARY_OWNER,
repo: process.env.BINARY_REPO
}).then(({ data }) => {
return data.tag_name.replace(/^v/, '');
});
console.log('currentVersion:', currentVersion);
console.log('latestVersion:', latestVersion);
core.setOutput('currentVersion', currentVersion);
core.setOutput('latestVersion', latestVersion);
return currentVersion === latestVersion ? 'true' : 'false';
- name: Notify if actionlint version mismatch
if: steps.check-binary-version.outputs.result == 'false'
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
env:
MESSAGE: |
> [!WARNING]
> ${{ matrix.binary }} の新しいバージョンがリリースされています。更新してください。
> ${{ steps.get-binary-info.outputs.releases-url }}
>
> - 現在のバージョン: ${{ steps.check-binary-version.outputs.currentVersion }}
> - 最新のバージョン: ${{ steps.check-binary-version.outputs.latestVersion }}
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: process.env.MESSAGE
});
actionlint, ghalint をどのように実行するか
actionlint, ghalint はどちらも Go 言語で作成された CLI です。
インストール方法はいくつかありますが、共通してどのような環境でも実行できるように、バイナリーをダウンロードして実行するようにしました。
ただ、ダウンロードして実行していたら改ざんされたことに気付けないため、チェックサムによる確認もしています。
actionlint, ghalint が更新されたことに気づくために
actionlint, ghalint をバイナリーをダウンロードするようにしたが、これでは actionlint, ghalint が更新されたことに気づけません。
そのため、最新バージョン番号を取得し、現在使用しているバージョン番号と比較し、異なる場合は更新されたと判断しています。
最新バージョンが更新されている場合は、Pull Request へコメントを登録し、更新されたことを気づけるようにしました。
まとめ
GitHub Actions ワークフローを静的解析するようにしました。
ワークフロー設定でセキュリティーを意識していたとしても、抜け漏れがあるので、静的解析も行うようにしてみてはいかがでしょうか。
Discussion