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/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.5
ACTIONLINT_CHECKSUM: 3e6e0a832dfa0b5f027e6b8956aad2632d69b7cb778b1cff847b40279950a856
# NOTE: ghalint をアップデートする場合は、 GHALINT_VERSION, GHALINT_CHECKSUM を更新してください
GHALINT_OS: linux
GHALINT_ARCH: amd64
GHALINT_VERSION: 1.2.1
GHALINT_CHECKSUM: 3fafc8dac6fde1b74a5345764a9b19a94fdb2d374d3a3b076cee28b86449ae79
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- 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
.github/workflows/github-actions-lint-update.yml
---
name: GitHub Actions Lint Update
on:
schedule:
- cron: "0 0 * * 0"
workflow_dispatch:
env:
GHA_LINT_WORKFLOW_FILE: .github/workflows/github-actions-lint.yml
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
jobs:
update-binary-version:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
strategy:
matrix:
binary:
- actionlint
- ghalint
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Get Environment Names
id: get-env-names
run: |
case ${{ matrix.binary }} in
actionlint)
{
echo "os=ACTIONLINT_OS"
echo "arch=ACTIONLINT_ARCH"
echo "version=ACTIONLINT_VERSION"
echo "checksum=ACTIONLINT_CHECKSUM"
} >> "${GITHUB_OUTPUT}"
;;
ghalint)
{
echo "os=GHALINT_OS"
echo "arch=GHALINT_ARCH"
echo "version=GHALINT_VERSION"
echo "checksum=GHALINT_CHECKSUM"
} >> "${GITHUB_OUTPUT}"
;;
esac
- name: Get Binary Info
id: get-binary-info
run: |
os=$(grep -E "${{ steps.get-env-names.outputs.os }}: " "${{ env.GHA_LINT_WORKFLOW_FILE }}" | sed -e "s/ *${{ steps.get-env-names.outputs.os }}: //")
arch=$(grep -E "${{ steps.get-env-names.outputs.arch }}: " "${{ env.GHA_LINT_WORKFLOW_FILE }}" | sed -e "s/ *${{ steps.get-env-names.outputs.arch }}: //")
version=$(grep -E "${{ steps.get-env-names.outputs.version }}: " "${{ env.GHA_LINT_WORKFLOW_FILE }}" | sed -e "s/ *${{ steps.get-env-names.outputs.version }}: //")
checksum=$(grep -E "${{ steps.get-env-names.outputs.checksum }}: " "${{ env.GHA_LINT_WORKFLOW_FILE }}" | sed -e "s/ *${{ steps.get-env-names.outputs.checksum }}: //")
case ${{ matrix.binary }} in
actionlint)
name=actionlint
owner=rhysd
repo=actionlint
releases_url=https://github.com/rhysd/actionlint/releases
;;
ghalint)
name=ghalint
owner=suzuki-shunsuke
repo=ghalint
releases_url=https://github.com/suzuki-shunsuke/ghalint/releases
;;
esac
{
echo "name=${name}"
echo "owner=${owner}"
echo "repo=${repo}"
echo "releases_url=${releases_url}"
echo "os=${os}"
echo "arch=${arch}"
echo "version=${version}"
echo "checksum=${checksum}"
} >> "${GITHUB_OUTPUT}"
- name: Check Binary Version
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
id: check-binary-version
env:
BINARY_NAME: ${{ matrix.binary }}
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 }}
BINARY_OS: ${{ steps.get-binary-info.outputs.os }}
BINARY_OS_ENV_NAME: ${{ steps.get-env-names.outputs.os }}
BINARY_ARCH: ${{ steps.get-binary-info.outputs.arch }}
BINARY_ARCH_ENV_NAME: ${{ steps.get-env-names.outputs.arch }}
BINARY_VERSION: ${{ steps.get-binary-info.outputs.version }}
BINARY_VERSION_ENV_NAME: ${{ steps.get-env-names.outputs.version }}
BINARY_CHECKSUM: ${{ steps.get-binary-info.outputs.checksum }}
BINARY_CHECKSUM_ENV_NAME: ${{ steps.get-env-names.outputs.checksum }}
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);
const result = currentVersion === latestVersion ? 'true' : 'false';
if (result === 'false') {
const checkSumsTxtUrl = `https://github.com/${process.env.BINARY_OWNER}/${process.env.BINARY_REPO}/releases/download/v${latestVersion}/${process.env.BINARY_NAME}_${latestVersion}_checksums.txt`;
console.log('checkSumsTxtUrl:', checkSumsTxtUrl);
const response = await fetch(checkSumsTxtUrl);
const text = await response.text();
console.log('Checksums file content:', text);
const checksum = text.split("\n").find((line) => line.includes("linux_amd64")).split(" ")[0];
console.log('checksum:', checksum);
core.setOutput('checksum', checksum);
}
return result;
- name: Update env to github_actions_lint.yml
if: steps.check-binary-version.outputs.result == 'false'
env:
GHA_LINT_WORKFLOW_FILE: ${{ env.GHA_LINT_WORKFLOW_FILE }}
BINARY_NAME: ${{ matrix.binary }}
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 }}
BINARY_OS: ${{ steps.get-binary-info.outputs.os }}
BINARY_OS_ENV_NAME: ${{ steps.get-env-names.outputs.os }}
BINARY_ARCH: ${{ steps.get-binary-info.outputs.arch }}
BINARY_ARCH_ENV_NAME: ${{ steps.get-env-names.outputs.arch }}
BINARY_VERSION: ${{ steps.get-binary-info.outputs.version }}
BINARY_VERSION_ENV_NAME: ${{ steps.get-env-names.outputs.version }}
BINARY_CHECKSUM: ${{ steps.get-binary-info.outputs.checksum }}
BINARY_CHECKSUM_ENV_NAME: ${{ steps.get-env-names.outputs.checksum }}
LATEST_VERSION: ${{ steps.check-binary-version.outputs.latestVersion }}
LATEST_CHECKSUM: ${{ steps.check-binary-version.outputs.checksum }}
run: |
sed -i "s/${BINARY_VERSION_ENV_NAME}: .*/${BINARY_VERSION_ENV_NAME}: ${LATEST_VERSION}/g" "${GHA_LINT_WORKFLOW_FILE}"
sed -i "s/${BINARY_CHECKSUM_ENV_NAME}: .*/${BINARY_CHECKSUM_ENV_NAME}: ${LATEST_CHECKSUM}/g" "${GHA_LINT_WORKFLOW_FILE}"
- name: Create GitHub App Token
if: steps.check-binary-version.outputs.result == 'false'
uses: actions/create-github-app-token@0d564482f06ca65fa9e77e2510873638c82206f2 # v1.11.5
id: app-token
with:
app-id: ${{ vars.GH_APPS_CREATE_PULL_REQEST_BOT_APP_ID }}
private-key: ${{ secrets.GH_APPS_CREATE_PULL_REQEST_BOT_PRIVATE_KEY }}
- name: Create Pull Request
if: steps.check-binary-version.outputs.result == 'false'
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6
id: create-pull-request
with:
token: ${{ steps.app-token.outputs.token }}
commit-message: Update ${{ steps.get-binary-info.outputs.name }} from ${{ steps.get-binary-info.outputs.version }} to ${{ steps.check-binary-version.outputs.latestVersion }}
title: Update ${{ steps.get-binary-info.outputs.name }} from ${{ steps.get-binary-info.outputs.version }} to ${{ steps.check-binary-version.outputs.latestVersion }}
body: |
Update ${{ steps.get-binary-info.outputs.name }} from ${{ steps.get-binary-info.outputs.version }} to ${{ steps.check-binary-version.outputs.latestVersion }}
Release notes: ${{ steps.get-binary-info.outputs.releases_url }}
branch: update-${{ steps.get-binary-info.outputs.name }}-${{ steps.check-binary-version.outputs.latestVersion }}
まとめ
GitHub Actions ワークフローを静的解析するようにしました。
ワークフロー設定でセキュリティーを意識していたとしても、抜け漏れがあるので、静的解析も行うようにしてみてはいかがでしょうか。
最後まで読んでいただきありがとうございます。この記事がすこしでもよいと思ったら、Like♥ を押してもらえると励みになります。
Discussion