📑

GitHub Actions ワークフローの静的解析

2024/10/16に公開

はじめに

GitHub Actions の設定は、設定を追加し動作すれば OK くらいの感覚で運用してました。
ただ、より良く運用するならセキュリティーを気にしたり、タイムアウトなども細かく設定したほうがよいと思ってます。
そこで、「GitHub CI/CD 実践ガイド」で紹介されていた actionlintghalint で静的解析するようにしてみました。

actionlint とは

https://github.com/rhysd/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 in steps.{id}.outputs are correct
  • Reusable 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 とは

https://github.com/suzuki-shunsuke/ghalint

README より引用

GitHub Actions linter for security best practices.

Policies

1. Workflow Policies

  1. job_permissions: All jobs should have permissions
  2. deny_read_all_permission: read-all permission should not be used
  3. deny_write_all_permission: write-all permission should not be used
  4. deny_inherit_secrets: secrets: inherit should not be used
  5. workflow_secrets: Workflow should not set secrets to environment variables
  6. job_secrets: Job should not set secrets to environment variables
  7. deny_job_container_latest_image: Job's container image tag should not be latest
  8. action_ref_should_be_full_length_commit_sha: action's ref should be full length commit SHA
  9. github_app_should_limit_repositories: GitHub Actions issueing GitHub Access tokens from GitHub Apps should limit repositories
  10. github_app_should_limit_permissions: GitHub Actions issueing GitHub Access tokens from GitHub Apps should limit permissions
  11. job_timeout_minutes_is_required: All jobs should set timeout-minutes

2. Action Policies

  1. action_ref_should_be_full_length_commit_sha: action's ref should be full length commit SHA
  2. github_app_should_limit_repositories: GitHub Actions issueing GitHub Access tokens from GitHub Apps should limit repositories
  3. github_app_should_limit_permissions: GitHub Actions issueing GitHub Access tokens from GitHub Apps should limit permissions
  4. action_shell_is_required: shell is required if run is set

セキュリティのベスト プラクティスのための GitHub Actions リンター。

ポリシー

1. ワークフローポリシー

  1. job_permissions: すべてのジョブには権限が必要です
  2. deny_read_all_permission: すべて読み取り権限を使用しないでください
  3. deny_write_all_permission: すべて書き込み権限を使用しないでください。
  4. deny_inherit_secrets: シークレット: 継承は使用しないでください
  5. workflow_secrets: ワークフローは環境変数にシークレットを設定しないでください
  6. job_secrets: ジョブは環境変数にシークレットを設定しないでください
  7. deny_job_container_latest_image: ジョブのコンテナー イメージ タグは最新であってはなりません
  8. action_ref_Should_be_full_length_commit_sha: アクションの参照は完全長のコミット SHA である必要があります。
  9. github_app_Should_limit_repositories: GitHub アプリから GitHub アクセス トークンを発行する GitHub アクションはリポジトリを制限する必要があります
  10. github_app_Should_limit_permissions: GitHub アプリから GitHub アクセス トークンを発行する GitHub アクションは権限を制限する必要があります
  11. jobtimeout minutes_is_required: すべてのジョブはタイムアウト分を設定する必要があります

2. 行動方針

  1. action_ref_Should_be_full_length_commit_sha: アクションの参照は完全長のコミット SHA である必要があります。
  2. github_app_Should_limit_repositories: GitHub アプリから GitHub アクセス トークンを発行する GitHub アクションはリポジトリを制限する必要があります
  3. github_app_Should_limit_permissions: GitHub アプリから GitHub アクセス トークンを発行する GitHub アクションは権限を制限する必要があります
  4. action_shell_is_required: run が設定されている場合はシェルが必要です

なぜ actionlint と ghalint を使用するのか

actionlint と ghalint はチェックする内容が異なります。
actionlint は構文チェック、 ghalint はセキュリティーチェックが主な内容になっています。
そのため、どちらかひとつよりも actionlint, ghalint の両方を使用するとよいと判断しました。

どこで静的解析を行うか

静的解析するタイミングは、以下のようなものがあります。

  1. ローカルで手動実行
  2. git pre-comitt で自動実行
  3. 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 へコメントを登録し、更新されたことを気づけるようにしました。

https://x.com/szkdash/status/1847237964872724491

まとめ

GitHub Actions ワークフローを静的解析するようにしました。
ワークフロー設定でセキュリティーを意識していたとしても、抜け漏れがあるので、静的解析も行うようにしてみてはいかがでしょうか。

Discussion