Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

GitHubにプッシュする前にシークレットスキャンを行う機構を自作した

こんにちは、技術本部 Eight Engineering Unit でエンジニアをやっている茂木(@shinnopo_)です。

今回は前回のブログで執筆した、ローカル環境で Lint をかける機構を利用して、pre-commit 時にシークレットスキャンを行うようにした話をしていきます。

前回のブログはこちら↓

buildersbox.corp-sansan.com

目次

背景

Eight では、コミットしたファイルに秘密鍵が含まれていた場合に該当箇所を Pull Request 上でコメントしてれる action-detect-secrets という GitHub Actions を導入しています。

しかし、これでは秘密鍵を GitHub にプッシュしてから気づくことになります。

プロダクトのコードはプライベートリポジトリにホスティングされているため、すぐに大きな問題になるわけではありませんが、できれば GitHub にプッシュする前に気づきたいです。

そこで、この問題を解決するために、pre-commit の仕組みを利用し、ローカル環境でシークレットスキャンを行うようにします。

方針

以前 Lint を行う pre-commit 機構を導入していたため、そこに乗っかる形にします。

今回はローカル環境のため、コメントを行う必要はありません。そのため、Reviewdog は利用せず、detect-secrets のみ利用します。

実装

既存の Git フックの流れは以下の通りです。

git commit
↓
[確認] front/ 以下に変更はあるか?
│
├─ はい → [実行] frontend hooks を呼ぶ
│
└─ いいえ → [確認] ruby関連ファイルに差分はあるか?
            │
            ├─ はい → [実行] backend hooks を呼ぶ
            │
            └─ いいえ → コミットする

新しい Git フックは、Lint を行う前にシークレットスキャンを行います。

git commit
↓
[確認] 変更に秘密鍵が含まれているか?
│
├─ はい → [実行] secret scan hooks を呼ぶ
│
└─ いいえ → [確認] front/以下に変更はあるか?
           │
           ├─ はい → [実行] frontend hooks を呼ぶ
           │
           └─ いいえ → [確認] ruby関連ファイルに差分はあるか?
                        │
                        ├─ はい → [実行] backend hooks を呼ぶ
                        │
                        └─ いいえ → コミットする

このフックを定義したシェルスクリプトのファイルは以下の通りです。

#!/bin/sh
set -eo pipefail

frontend_hook_run=false
backend_hook_run=false
secret_scan_hook_run=false

function call_frontend_hooks {
  if [ "$frontend_hook_run" = false ] ; then
    echo "Running frontend hooks..."
    front/script/hooks/pre-commit
    frontend_hook_run=true
  fi
}

function call_backend_hooks {
  if [ "$backend_hook_run" = false ] ; then
    echo "Running backend hooks..."
    .gitconfig/serverside_hooks/pre-commit
    backend_hook_run=true
  fi
}

function call_secret_scan_hooks {
  if [ "$secret_scan_hook_run" = false ] ; then
    echo "Running secret scan hooks..."
    ./.gitconfig/secret-scan
    secret_scan_hook_run=true
  fi
}

call_secret_scan_hooks

git diff --cached --quiet -- front || call_frontend_hooks
git diff --cached --quiet -- '*.rb' '*.rake' '*.ru' '*.jbuilder' Gemfile Guardfile Capfile .irbrc || call_backend_hooks

上記のシェルスクリプトでは、シークレットスキャンのフックを コミットした際に複数ファイルあった場合は、そのファイル数分実行されてしまうため、実行後 hoge_hook_run フラグを true にすることで重複実行を防いでいます。

次にフックの内部の挙動を紹介しますが、フロントエンド/バックエンドのフックの内容は割愛します。

気になる方は前回記事をご覧ください。

シークレットスキャンのフックは以下のように設定をしています。

#!/bin/sh
set -eo pipefail

# localにdetect-secretsが存在しない場合, secret scanを実行せずに終了する
if ! which detect-secrets &> /dev/null
then
  echo "detect-secrets could not be found. Please install it first by running: brew install detect-secrets"
  exit 1
fi

tmpfile=$(mktemp)
git diff --cached --name-only | xargs detect-secrets scan | jq .results > "$tmpfile"

line=$(cat "$tmpfile" | wc -w)
if [ $line -gt 2 ]; then
  echo "Private key detected. Please delete it."
  trap 'rm -f "$tmpfile"' EXIT INT TERM HUP
  exit 1;
fi

trap 'rm -f "$tmpfile"' EXIT INT TERM HUP

簡単にスクリプトの説明をします。 まず、ローカル環境に detect-secrets があるかを確認し、なければインストールするよう警告します。

detect-secrets が既に存在すれば、detect-secrets scan を実行し、実行結果を tmp ファイルに保存します。 tmp ファイルの単語数が 2 より大きい場合、つまり秘密鍵が検出された場合、警告をしてコミットをブロックします。

稀に誤検知されるケースもありますが、その場合は --exclude-lines を行うことで、その行をスキップすることが可能です。

導入後の効果

前提として、各々が秘密鍵をコミットしないよう気をつけていたため、目にみえる効果はありません。 しかし、万が一コミットしてしまいそうになった時に、未然に防ぐことができるようになったのは、大きい成果だと思います。

また、この仕組みによって CI からシークレットスキャンを削除することが可能になり、コスト削減や CI の高速化に期待できます。

まとめ

導入のインパクトはそこまで多くないものの、確実にセキュアになったため、やる意義は大きかったと思います。 また、自作の pre-commit 機構を導入していたことで、カスタマイズが容易なのも良かったです。

また、Eight ではエンジニアを募集中です。 少しでもご興味があれば、エントリー/DM お待ちしております!

open.talentio.com

© Sansan, Inc.