Techtouch Developers Blog

テックタッチ株式会社の開発チームによるテックブログです

GitHub API を使ったリポジトリの監査を始めました

こんにちは。SRE チームの izzii です。

つい先日、テックタッチでは GitHub リポジトリの利用ポリシーを定めました。創業から数年間、アクセル全開で開発して気がついたら、Owner 権限を持つ人間が増えてしまっていたことへの違和感を解消するためです。

ヒアリングを通して問題を分析し、リポジトリ利用ポリシーを定め、最終的には GitHub API を使って監査結果を Slack に通知する仕組みを作りました。

本記事はあくまで「リポジトリの利用ポリシー」の話に閉じます。GitHub の利用全般に及ぶ話に興味がある方は、Flatt Security さんが最近公開されたスライドが良さげなのでオススメしておきます。

https://blog.flatt.tech/entry/github-organization-best-practices

ヒアリングから見えた課題

私は2022年3月入社のほぼ新人ということもあり、過去の歴史的経緯を把握はできていませんでした。そこで数名のエンジニアに対して、現状の利用状況のヒアリングを実施しました。

すると、CI ツールの更新に手が回らなくなった保護されたブランチに対し、デプロイしてテストを急ぎたい場合などに、Owner/Admin 権限を使ったマージが実施されることがあると分かりました。

保護されたブランチ

GitHub では特定のブランチに保護ルールを設定できます。 例えばレビューの承認が済んでいない限り、コミットのマージを禁止するといった制限をかけられます。 https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/managing-a-branch-protection-rule

ただ恐ろしいことに、保護されたブランチに対して、保護ルールを無視してマージできるということは、保護ルールを無視してプッシュできることと権限としては一緒なのです。

今まで事故はなかったかもしれませんが、Owner/Admin 権限を持つエンジニアがうっかり保護されたブランチへ直接プッシュしても、エラーなど出ないのです。

定義したブランチ保護ルール

スクリーンショットを交えながら、上で挙げた問題に対処するためにテックタッチで機能を有効にすることを定めたオプションを紹介します。以下のような URL でブランチの保護ルール設定 UI にアクセスできます。

(https)://github.com/ORG_NAME/REPO_NAME/settings/branch_protection_rules/RULE_NAME

  • Require a pull request before merging
    • プルリクエストを必須とします。この設定を有効にしないと直接プッシュが可能になります。(この設定だけで Owner/Admin による直接のプッシュを防げる訳ではありません。)
  • Require approvals
    • レビュー承認者の数でマージ制約がかけられます。

  • Require status checks to pass before merging
    • Status Check(CI による検査など)を実施したい場合、設定してください。ここで重要なのは、マージに必須な Status Check を編集できることです。CI の更新が止まってしまったなら勇気を持って必須項目から外してしまいましょう。Owner/Admin による強制マージ常習化の原因になります。

  • Include administrators
    • この設定を有効にしないと Admin が保護ルールの制約の対象外になります。本当に必要な時だけ外してもよいが、すぐに元に戻すことをルールとしました。

GitHub API を利用した監査スクリプトの作成

https://docs.github.com/en/rest

上のドキュメントに API の説明があります。 まずは監査スクリプトを作る上で自分が利用した API とサンプルスニペットを紹介します。

# チームの一覧取得
# admin の人数による制限などかけたい場合必要になってきます。
https://api.github.com/orgs/{org}/teams

# チームの構成メンバーの一覧取得
# admin の人数による制限などかけたい場合必要になってきます。
https://api.github.com/teams/{team_id}/members

# 直接アサインされたメンバーの一覧取得
# 例えば、組織設定でメンバーのデフォルト権限を write にすると、全員取得される。
# affiliation=direct を指定することで直接日もづけられたメンバーだけ取得できる。
# admin の人数による制限などかけたい場合必要になってきます。
https://api.github.com/repos/{org}/{repo}/collaborators?affiliation=direct

# 直接アサインされたチームの一覧取得
# admin の人数による制限などかけたい場合必要になってきます。
https://api.github.com/repos/{org}/{repo}/teams

# リポジトリの情報取得
# private リポジトリか否か、デフォルトブランチの名前、といった重要な情報を含みます。
https://api.github.com/repos/{org}/{repo}

# ブランチの保護ルールを取得
https://api.github.com/repos/{org}/{repo}/branches/{branch}/protection

GitHub API は、Personal Access Token を使った Basic 認証でアクセスできます。Token の発行は以下を参考にしてください。

https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token

以下は Python を使ったリクエスト発行のサンプルスニペットです。特定リポジトリにおける、デフォルトブランチの保護ルールをチェックしています。

import requests

GITHUB_USER = ''
GITHUB_TOKEN = ''
ORG_NAME = ''
REPO_NAME = ''
BRANCH_NAME = ''

s = requests.Session()
s.auth = (GITHUB_USER, GITHUB_TOKEN)

r = s.get(f'https://api.github.com/repos/{ORG_NAME}/{REPO_NAME}/branches/{BRANCH_NAME}/protection')
if r.status_code != 200:
    print('API call failed.')
    return

# Accept ヘッダーを明示的に指定しなければ API v3 を利用することになります。
# 結果は json で返ってきます。
# GraphQL の API も存在します。
obj = json.loads(r.text)

print('private:', obj['private'])
br = obj['default_branch']
print('default branch:', br)

print('enforce admins:', obj['enforce_admins']['enabled'])
r = s.get(f'https://api.github.com/repos/{ORG_NAME}/{REPO_NAME}/branches/{br}/protection')
if r.status_code != 200:
    print('API call failed.')
    return

obj = json.loads(r.text)

print('enforce admins:', obj['enforce_admins']['enabled'])
print('allow force push:', obj['allow_force_pushes']['enabled'])
print('allow deletion:', obj['allow_deletions']['enabled'])
print('required conversation resolution:', obj['required_conversation_resolution']['enabled'])
if 'required_pull_request_reviews' in obj:
    obj = obj['required_pull_request_reviews']
    print('# of required reviews:', obj['dismiss_stale_reviews'])
    print('dissmiss stale reviews:', obj['required_approving_review_count'])
    print('required code owner reviews:', obj['require_code_owner_reviews'])

利用ポリシーを適用したいリポジトリの監査結果を、一括で管理者のいる Slack チャネルに投下するようにしました。

最終調整している際に仲間が暖かく見守ってくれています。

ちなみに上記の「リポジトリの安全性を監視しています。」というヘッダー文は実行時に与える形式にしています。無視されないように時々変化させながら投下していこうと思います。

終わりに

問題を見つけて利用ポリシーを定めたからといって、それをメンバーに準拠、継続してもらうのは非常に大変です。まずは自動化された監査の仕組みを作って、ローコストで利用ポリシーの浸透を図っていきたいと思います。

つい先日 SLO の設計に関するブログも書いたので、ぜひこちらもお目通しいただけると嬉しいです!

tech.techtouch.jp