diff --git a/.github/ghas.yml b/.github/ghas.yml new file mode 100644 index 0000000..5a1c78a --- /dev/null +++ b/.github/ghas.yml @@ -0,0 +1,46 @@ +# GHAS / Actions セキュリティ設定(declarative policy) +# +# engine: ghas-setup がこのファイルを読んで `gh api` に適用する。 +# ポリシー値(どの scanner を有効にするか・enforcement・Actions 権限)はすべてここに集約し、 +# engine 側はロジックだけを持つ。git diff だけでポリシー変更意図が追えるようにインラインで文書化する。 +# 各項目の意味は https://docs.github.com/en/rest/code-security/configurations を参照。 +# +# これは devtools 自身の dogfood config 兼・利用側がコピーする書式例。利用側は org を自分の org に変える。 + +org: airs + +configuration: + name: airs-default + description: airs org default code security configuration. Managed by ghas-setup. + # enforced: repo 側で設定をオーバーライド不可に強制する(org ポリシーを全 repo へ確実に適用) + enforcement: enforced + scanners: + # GHAS 本体。code scanning / secret scanning 等の前提となるため有効化 + advanced_security: enabled + # 依存グラフ。Dependabot alerts / security updates の前提 + dependency_graph: enabled + # 既知 CVE を含む依存を検知してアラート + dependabot_alerts: enabled + # CVE 修正版への PR を自動生成(サプライチェーン防御の中核) + dependabot_security_updates: enabled + # コミット履歴・push 内のシークレット検知 + secret_scanning: enabled + # シークレットを含む push をブロック(漏洩を未然に防ぐ) + secret_scanning_push_protection: enabled + # 検知したシークレットが有効(生きている)かを発行元 API で検証 + secret_scanning_validity_checks: enabled + # 既知パターン外の汎用シークレット(独自トークン等)も検知 + secret_scanning_non_provider_patterns: enabled + # 脆弱性の非公開報告(private vulnerability reporting)受付を有効化 + private_vulnerability_reporting: enabled + +# 新規 repo の default configuration に本 configuration を指定(all = public/private 問わず) +default_for_new_repos: all +# 既存 repo 全件に attach(all = org 内すべて) +attach_existing_repos: all + +actions: + # GITHUB_TOKEN の既定権限を read に最小化(書き込みは workflow 側で明示 grant) + default_workflow_permissions: read + # workflow から PR レビュー承認を不可に(権限昇格経路を塞ぐ) + can_approve_pull_request_reviews: false diff --git a/README.md b/README.md index 9506019..744cdef 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,14 @@ Nix flake × devbox。言語非依存でツールを PATH へ配布し、`flake. flake.nix # packages / checks (test・lint) / formatter を公開 devbox.json # 開発環境(Nix の test/lint ツール)と自己 dogfooding .env.template # env-init 用テンプレートの書式例(本 repo の dogfood でも使用) +.github/ghas.yml # ghas-setup 用ポリシー(本 repo の dogfood 兼・書式例) pkgs/ - env-init/ - package.nix # env-init を makeWrapper で wrap する派生 - env-init # エンジン本体(単体実行可能な生スクリプト) - tests/ # bats テスト + env-init/ # 各 pkg の構成は pkg 内 README を参照 + ghas-setup/ ``` +各パッケージの内部構成は [ツール](#ツール) の各 README を参照。 + ## 開発 devbox 経由で開発する。 @@ -52,31 +53,11 @@ SemVer を上げて release する。CLI 契約を変えない依存更新は ** ## ツール -### env-init - -現在の git worktree 用に `.env` を生成する汎用エンジン。worktree 番号 N を計算し、リポジトリルートの -`.env.template`(bash として 1 回評価される)から `.env` を書き出す。プロジェクト非依存設計で、実行時依存は -`bash` / `git` / `gawk` / `gnused` + coreutils。 - -**利用側 repo での使い方**: - -1. `devbox.json` の `packages` に flake 参照を足し、init_hook で起動する。 - - ```jsonc - { - "packages": ["github:airs/devtools/#env-init"], - "shell": { "init_hook": ["[ -f .env ] || env-init"] } - } - ``` - - `` は次の 2 パターンから選ぶ。 - - - 低摩擦(メジャー追従): `github:airs/devtools/v1#env-init` — 移動タグ。`devbox update` で最新の 1.x を取得する。 - - 厳密 pin: `github:airs/devtools/v1.2.0#env-init` — 不変タグ。版を固定し、利用側 Renovate でメジャー更新を抑止できる。 +各ツールの使い方・前提・設定書式は各パッケージの README を参照。 - 本リポジトリ自身の `devbox.json` は、ローカル flake を dogfood するため `path:.#env-init` を使う点だけが利用側と - 異なる。Nix を使わない環境では `pkgs/env-init/env-init` を直接実行できる(生スクリプトは単体実行可能なまま)。 +- [env-init](pkgs/env-init/README.md) — 現在の git worktree 用に `.env` を生成する汎用エンジン。 +- [ghas-setup](pkgs/ghas-setup/README.md) — GitHub org の GHAS / セキュリティ設定を `gh api` で一括適用する汎用エンジン。 -2. **利用側 repo がルートに `.env.template` を用意する**(中央には持ち込まない repo 固有ファイル)。各 worktree で - `N` を参照し、ポートを `$((BASE + N))` でずらし、secret を `openssl rand` / `op read` 等で書く。書式は本リポジトリ - ルートの [`.env.template`](.env.template) を参照(コピー用ではなく書式例)。 +利用側は `devbox.json` の `packages` に flake 参照(`github:airs/devtools/#`)を足す。`` の選び方は +[バージョニング](#バージョニング)を参照。本リポジトリ自身は dogfood のため `path:.#` を使う。Nix を使わない環境では +`pkgs//` を直接実行できる(生スクリプトは単体実行可能なまま)。 diff --git a/devbox.json b/devbox.json index c79b8aa..3b3095c 100644 --- a/devbox.json +++ b/devbox.json @@ -8,7 +8,10 @@ "deadnix@1", "nixfmt@1", "bats@1", - "path:.#env-init" + "gh@2", + "yq-go@4", + "path:.#env-init", + "path:.#ghas-setup" ], "env": { "NIX_CONFIG": "experimental-features = nix-command flakes" diff --git a/flake.nix b/flake.nix index a93538d..1f1309a 100644 --- a/flake.nix +++ b/flake.nix @@ -20,6 +20,7 @@ # 複数ツールが入るため default は設けない。利用側は常に `#` を明示する。 packages = forAllSystems (pkgs: { env-init = pkgs.callPackage ./pkgs/env-init/package.nix { }; + ghas-setup = pkgs.callPackage ./pkgs/ghas-setup/package.nix { }; }); # test / lint の単一ソース。`nix flake check` で全て走る。 @@ -31,9 +32,10 @@ { # パッケージ build が通ること。 env-init = self.packages.${system}.env-init; + ghas-setup = self.packages.${system}.ghas-setup; shellcheck = pkgs.runCommand "shellcheck" { nativeBuildInputs = [ pkgs.shellcheck ]; } '' - shellcheck ${./pkgs/env-init/env-init} + shellcheck ${./pkgs/env-init/env-init} ${./pkgs/ghas-setup/ghas-setup} touch "$out" ''; @@ -58,6 +60,33 @@ touch "$out" ''; + # ghas-setup の bats。引数パース・pre-flight・--dry-run は wrap 済みパッケージ + # ($GHAS_SETUP) で、実適用パス(create/update 分岐・API 呼び出し列)は gh をスタブして + # 生スクリプト ($GHAS_SETUP_RAW) を bash で直起動して検証する(wrapper は gh を PATH + # 先頭に prefix するため stub で上書きできない。bash 直起動なら shebang も回避でき、 + # PATH 上の stub gh が使われる)。wrap 済みパッケージの closure には gh が入るが、 + # --dry-run テストでは gh auth チェックより手前で exit するため起動はしない。 + ghas-setup-bats = + pkgs.runCommand "ghas-setup-bats" + { + nativeBuildInputs = [ + pkgs.bats + pkgs.bash + pkgs.git + pkgs.coreutils + pkgs.gnugrep + pkgs.yq-go + self.packages.${system}.ghas-setup + ]; + } + '' + cp -r ${./pkgs/ghas-setup/tests} tests + export GHAS_SETUP_RAW=${./pkgs/ghas-setup/ghas-setup} + export HOME="$TMPDIR" + bats tests + touch "$out" + ''; + statix = pkgs.runCommand "statix-check" { nativeBuildInputs = [ pkgs.statix ]; } '' statix check ${./.} touch "$out" @@ -69,7 +98,7 @@ ''; nixfmt = pkgs.runCommand "nixfmt-check" { nativeBuildInputs = [ pkgs.nixfmt ]; } '' - nixfmt --check ${./flake.nix} ${./pkgs/env-init/package.nix} + nixfmt --check ${./flake.nix} ${./pkgs/env-init/package.nix} ${./pkgs/ghas-setup/package.nix} touch "$out" ''; } diff --git a/pkgs/env-init/README.md b/pkgs/env-init/README.md new file mode 100644 index 0000000..b3da366 --- /dev/null +++ b/pkgs/env-init/README.md @@ -0,0 +1,33 @@ +# env-init + +現在の git worktree 用に `.env` を生成する汎用エンジン。worktree 番号 N を計算し、リポジトリルートの +`.env.template`(bash として 1 回評価される)から `.env` を書き出す。プロジェクト非依存設計で、実行時依存は +`bash` / `git` / `gawk` / `gnused` + coreutils。 + +## 構成 + +``` +package.nix # env-init を makeWrapper で wrap する派生 +env-init # エンジン本体(単体実行可能な生スクリプト) +tests/ # bats テスト +``` + +## 利用側 repo での使い方 + +1. `devbox.json` の `packages` に flake 参照を足し、init_hook で起動する。 + + ```jsonc + { + "packages": ["github:airs/devtools/#env-init"], + "shell": { "init_hook": ["[ -f .env ] || env-init"] } + } + ``` + + `` の選び方(メジャー追従/厳密 pin)はルート [README の「バージョニング」](../../README.md#バージョニング) を参照。 + + 本リポジトリ自身の `devbox.json` は、ローカル flake を dogfood するため `path:.#env-init` を使う点だけが利用側と + 異なる。Nix を使わない環境では `pkgs/env-init/env-init` を直接実行できる(生スクリプトは単体実行可能なまま)。 + +2. **利用側 repo がルートに `.env.template` を用意する**(中央には持ち込まない repo 固有ファイル)。各 worktree で + `N` を参照し、ポートを `$((BASE + N))` でずらし、secret を `openssl rand` / `op read` 等で書く。書式は本リポジトリ + ルートの [`.env.template`](../../.env.template) を参照(コピー用ではなく書式例)。 diff --git a/pkgs/ghas-setup/README.md b/pkgs/ghas-setup/README.md new file mode 100644 index 0000000..b0fa309 --- /dev/null +++ b/pkgs/ghas-setup/README.md @@ -0,0 +1,62 @@ +# ghas-setup + +GitHub **org 単位**で GHAS / Actions セキュリティ設定を `gh api`(code-security-configurations)で一括適用する汎用エンジン。 +適用対象は repo 単位ではなく org 全体で、`default_for_new_repos` / `attach_existing_repos` により新規・既存 repo の両方へ反映される。 +engine(本スクリプト)はロジックだけを持ち、org 名・scanner・enforcement・Actions 権限などのポリシー値はすべて config(`.github/ghas.yml`)由来。 + +## 構成 + +``` +package.nix # ghas-setup を makeWrapper で wrap する派生 +ghas-setup # エンジン本体(単体実行可能な生スクリプト) +tests/ # bats テスト +``` + +## 前提 + +- 対象 org で **GitHub Advanced Security (GHAS) が有効**であること。GHAS が有効でないと `advanced_security` 等の scanner 適用が失敗する。 +- gh CLI が認証済みで **`admin:org` scope** を持つこと(org レベル設定の変更に必要)。 + + ```sh + gh auth login + gh auth refresh -h github.com -s admin:org + ``` +- `yq`(mikefarah 版 = yq-go)。本パッケージは PATH に同梱する。 + +## 使い方 + +1. `devbox.json` の `packages` に flake 参照を足す。 + + ```jsonc + { "packages": ["github:airs/devtools/#ghas-setup"] } + ``` + + `` の選び方(メジャー追従/厳密 pin)はルート [README の「バージョニング」](../../README.md#バージョニング) を参照。 + +2. **利用側 repo がルートに `.github/ghas.yml` を用意する**(ポリシーの単一ソース。書式は下記)。 + +3. 適用する。org を変更する前に必ず `--dry-run` で解決値と叩く API を確認する。 + + ```sh + ghas-setup --dry-run # org を変更せず適用予定を表示 + ghas-setup # .github/ghas.yml を org へ適用 + ghas-setup --config # 別の yaml を渡す(複数 org 用) + ``` + + config は呼び出した git worktree のルートの `.github/ghas.yml`(cwd 基準で解決)。`--config` で上書きできる。 + +## config(`.github/ghas.yml`) + +書式は本リポジトリ実物の [`.github/ghas.yml`](../../.github/ghas.yml) を参照(コピー用の書式例)。利用側はこれをコピーして自 repo の +`.github/ghas.yml` に置き、`org` を自分の org 名に変える。各項目の意味は +[code security configurations の REST API ドキュメント](https://docs.github.com/en/rest/code-security/configurations) を参照。 + +主な項目: + +- `org` — 適用対象の org 名。 +- `configuration.name` / `description` / `enforcement` — code security configuration の識別名・説明・強制方法(`enforced` で repo 側のオーバーライドを禁止)。 +- `configuration.scanners` — 有効化する scanner(`advanced_security` / `dependency_graph` / `dependabot_alerts` / `secret_scanning` ほか)と値。 +- `default_for_new_repos` — 新規 repo の default configuration に指定する範囲(`all` = public/private 問わず)。 +- `attach_existing_repos` — 既存 repo へ attach する範囲(`all` = org 内すべて)。 +- `actions.default_workflow_permissions` — `GITHUB_TOKEN` の既定権限(`read` で最小化)。 +- `actions.can_approve_pull_request_reviews` — workflow から PR レビュー承認を許すか(`false` で権限昇格経路を塞ぐ)。 diff --git a/pkgs/ghas-setup/ghas-setup b/pkgs/ghas-setup/ghas-setup new file mode 100755 index 0000000..78369fd --- /dev/null +++ b/pkgs/ghas-setup/ghas-setup @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +# GitHub org の GHAS / Actions セキュリティ設定を code-security-configurations API で一括有効化する汎用エンジン。 +# engine(本スクリプト)とポリシー値(config yaml)を分離する。 +# engine: config を読む / gh api を叩く / 適用後の状態を読み戻して表示する だけを知る。 +# org 名・scanner 名・enable 値・enforcement・Actions 設定はすべて config 由来。 +# config は呼び出した git worktree の .github/ghas.yml(--config で別 yaml を渡せる)。 +# 詳細は README.md を参照。 +set -euo pipefail + +# リポジトリルート(このスクリプトを呼んだ git worktree のルート)を cwd 基準で解決する。 +# git 管理外で実行された場合は git の生エラーではなく、何をすべきか分かるメッセージで終了する。 +if ! REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null); then + echo "git worktree 内で実行してください(git リポジトリのルートから .github/ghas.yml を解決します)。" >&2 + exit 1 +fi + +CONFIG="$REPO_ROOT/.github/ghas.yml" +DRY_RUN=false + +# 引数パース: --config (別 org 用 yaml を渡す)/ --dry-run(org を変更せず適用予定を表示) +while [ $# -gt 0 ]; do + case "$1" in + --config) + if [ $# -lt 2 ]; then + echo "--config に値がありません" >&2 + echo "usage: ghas-setup [--config ] [--dry-run]" >&2 + exit 1 + fi + CONFIG="$2"; shift 2 ;; + --config=*) + CONFIG="${1#*=}"; shift ;; + --dry-run) + DRY_RUN=true; shift ;; + *) + echo "不明な引数: $1" >&2 + echo "usage: ghas-setup [--config ] [--dry-run]" >&2 + exit 1 ;; + esac +done + +# pre-flight: yq(mikefarah 版 = yq-go) +command -v yq >/dev/null 2>&1 || { + echo "yq が見つかりません。mikefarah 版 yq(yq-go)が必要です。wrapper 経由(nix run .#ghas-setup や devbox)なら同梱されます。生スクリプトを直接使う場合は yq-go をインストールしてください。" >&2 + exit 1 +} + +# pre-flight: config file +[ -f "$CONFIG" ] || { + echo "config file が見つかりません: $CONFIG" >&2 + exit 1 +} + +# config 読み出し(ポリシー値の単一ソース) +ORG=$(yq '.org' "$CONFIG") +CONFIG_NAME=$(yq '.configuration.name' "$CONFIG") +CONFIG_DESC=$(yq '.configuration.description' "$CONFIG") +ENFORCEMENT=$(yq '.configuration.enforcement' "$CONFIG") +DEFAULT_FOR_NEW_REPOS=$(yq '.default_for_new_repos' "$CONFIG") +ATTACH_SCOPE=$(yq '.attach_existing_repos' "$CONFIG") +ACTIONS_DEFAULT_PERMS=$(yq '.actions.default_workflow_permissions' "$CONFIG") +ACTIONS_APPROVE_PR=$(yq '.actions.can_approve_pull_request_reviews' "$CONFIG") + +# configuration の body フィールドを config 由来で動的生成 +CONFIG_BODY_FIELDS=( + -f "name=$CONFIG_NAME" + -f "description=$CONFIG_DESC" + -f "enforcement=$ENFORCEMENT" +) +while IFS= read -r entry; do + CONFIG_BODY_FIELDS+=(-f "$entry") +done < <(yq '.configuration.scanners | to_entries | .[] | .key + "=" + .value' "$CONFIG") + +# --dry-run: 解決済みの値とこれから叩く API を表示して org を変更せず exit +if [ "$DRY_RUN" = true ]; then + echo "==> dry-run: config=$CONFIG" + echo "org: $ORG" + echo "configuration:" + echo " name: $CONFIG_NAME" + echo " enforcement: $ENFORCEMENT" + echo " scanners:" + yq '.configuration.scanners | to_entries | .[] | " " + .key + ": " + .value' "$CONFIG" + echo "default_for_new_repos: $DEFAULT_FOR_NEW_REPOS" + echo "attach_existing_repos: $ATTACH_SCOPE" + echo "actions.default_workflow_permissions: $ACTIONS_DEFAULT_PERMS" + echo "actions.can_approve_pull_request_reviews: $ACTIONS_APPROVE_PR" + echo + echo "==> これから叩く API:" + echo " POST /orgs/$ORG/code-security/configurations (既存が無ければ新規作成)" + echo " PATCH /orgs/$ORG/code-security/configurations/ (既存があれば更新) name / description / enforcement / scanners" + echo " PUT /orgs/$ORG/code-security/configurations//defaults default_for_new_repos=$DEFAULT_FOR_NEW_REPOS" + echo " POST /orgs/$ORG/code-security/configurations//attach scope=$ATTACH_SCOPE" + echo " PUT /orgs/$ORG/actions/permissions/workflow default_workflow_permissions=$ACTIONS_DEFAULT_PERMS can_approve_pull_request_reviews=$ACTIONS_APPROVE_PR" + exit 0 +fi + +# pre-flight: gh CLI 認証 +gh auth status -h github.com >/dev/null 2>&1 || { + echo "gh CLI が未認証です。'gh auth login' を実行してください。" >&2 + exit 1 +} + +# pre-flight: admin:org scope の有無 +gh auth status -h github.com 2>&1 | grep -q "admin:org" || { + echo "gh CLI に admin:org scope がありません。'gh auth refresh -h github.com -s admin:org' を実行してください。" >&2 + exit 1 +} + +echo "==> $ORG org の code security configuration を有効化します" + +# 既存 configuration を name で lookup(冪等化のため)。configuration が 30 件(既定 per_page)を +# 超えても取りこぼさないよう --paginate で全件取得する。--slurp で全ページを 1 つの配列にまとめ、 +# jq 側で最初の一致 id を取り出す(`| head -n1` だと pagination 途中で stdout が閉じ、gh が SIGPIPE +# で非 0 終了して set -o pipefail がスクリプト全体を落としうるため使わない)。 +# name は config 由来の文字列なので jq 式に直埋めせず env 経由で渡す(" や \ を含んでも壊れない)。 +CONFIG_ID=$(GHAS_CONFIG_NAME="$CONFIG_NAME" gh api --paginate --slurp "/orgs/$ORG/code-security/configurations" \ + --jq '[.[][] | select(.name == env.GHAS_CONFIG_NAME) | .id] | .[0] // ""') + +if [ -z "$CONFIG_ID" ]; then + echo " - configuration を新規作成: $CONFIG_NAME" + CONFIG_ID=$(gh api --method POST "/orgs/$ORG/code-security/configurations" \ + "${CONFIG_BODY_FIELDS[@]}" --jq '.id') +else + echo " - 既存 configuration を更新: $CONFIG_NAME (id=$CONFIG_ID)" + gh api --method PATCH "/orgs/$ORG/code-security/configurations/$CONFIG_ID" \ + "${CONFIG_BODY_FIELDS[@]}" --silent +fi + +# 新規 repo の default に指定 +echo "==> 新規 repo の default configuration に指定" +gh api --method PUT "/orgs/$ORG/code-security/configurations/$CONFIG_ID/defaults" \ + -f "default_for_new_repos=$DEFAULT_FOR_NEW_REPOS" --silent + +# 既存 repo に attach +echo "==> 既存 repo に attach (scope=$ATTACH_SCOPE)" +gh api --method POST "/orgs/$ORG/code-security/configurations/$CONFIG_ID/attach" \ + -f "scope=$ATTACH_SCOPE" --silent + +# Actions: GITHUB_TOKEN default permission を config 値に +echo "==> Actions GITHUB_TOKEN default permission = $ACTIONS_DEFAULT_PERMS" +gh api --method PUT "/orgs/$ORG/actions/permissions/workflow" \ + -F "default_workflow_permissions=$ACTIONS_DEFAULT_PERMS" \ + -F "can_approve_pull_request_reviews=$ACTIONS_APPROVE_PR" \ + --silent + +# 検証: 適用後の状態を読み戻して表示 +echo "==> 適用後の状態:" +echo "--- configuration ---" +gh api "/orgs/$ORG/code-security/configurations/$CONFIG_ID" --jq '{ + id, name, enforcement, + advanced_security, dependency_graph, + dependabot_alerts, dependabot_security_updates, + secret_scanning, secret_scanning_push_protection, + secret_scanning_validity_checks, secret_scanning_non_provider_patterns, + private_vulnerability_reporting +}' +echo "--- default for new repos ---" +gh api "/orgs/$ORG/code-security/configurations/defaults" --jq '.[] | {default_for_new_repos, configuration_name: .configuration.name}' +echo "--- Actions permissions ---" +gh api "/orgs/$ORG/actions/permissions/workflow" + +echo "完了しました。" diff --git a/pkgs/ghas-setup/package.nix b/pkgs/ghas-setup/package.nix new file mode 100644 index 0000000..a8a62dc --- /dev/null +++ b/pkgs/ghas-setup/package.nix @@ -0,0 +1,43 @@ +# ghas-setup を makeWrapper で wrap する。生スクリプト (./ghas-setup) は libexec にそのまま置き、 +# 外側の wrapper で runtimeInputs を PATH 先頭に prefix して exec するだけ(ロジックは無改変。 +# shebang のみビルド時に絶対 bash へ差し替える=下記 substituteInPlace)。 +# 設計の理由は pkgs/env-init/package.nix のコメントを参照(writeShellApplication を避ける理由・ +# shebang を nix store の bash に固定する理由・PATH prefix で既存 PATH を suffix に残す理由は同じ)。 +# +# 同梱する runtimeInputs: スクリプトが使う git (rev-parse) / gh (api) / yq (yq-go, config 読み) / +# gnugrep (admin:org scope 判定の grep。coreutils には grep が入らない) / coreutils。 +# gh は closure が重いが、利用側の ~/.config/gh 認証状態を読むため動作上の問題はない。 +{ + lib, + runCommand, + makeWrapper, + bashNonInteractive, + coreutils, + git, + gh, + gnugrep, + yq-go, +}: +runCommand "ghas-setup" + { + nativeBuildInputs = [ makeWrapper ]; + meta = { + description = "GitHub org の GHAS / セキュリティ設定を gh api で一括適用する汎用エンジン"; + mainProgram = "ghas-setup"; + }; + } + '' + install -Dm755 ${./ghas-setup} "$out/libexec/ghas-setup" + substituteInPlace "$out/libexec/ghas-setup" \ + --replace-fail '#!/usr/bin/env bash' '#!${bashNonInteractive}/bin/bash' + makeWrapper "$out/libexec/ghas-setup" "$out/bin/ghas-setup" \ + --prefix PATH : ${ + lib.makeBinPath [ + coreutils + git + gh + gnugrep + yq-go + ] + } + '' diff --git a/pkgs/ghas-setup/tests/ghas-setup.bats b/pkgs/ghas-setup/tests/ghas-setup.bats new file mode 100644 index 0000000..3f20e45 --- /dev/null +++ b/pkgs/ghas-setup/tests/ghas-setup.bats @@ -0,0 +1,162 @@ +#!/usr/bin/env bats +# ghas-setup のテスト。引数パース・pre-flight・--dry-run は wrap 済みの ghas-setup(GHAS_SETUP、 +# 既定は PATH 上の ghas-setup)で検証する。実適用パス(create/update 分岐・gh api 呼び出し列)は +# gh をスタブし、生スクリプト(GHAS_SETUP_RAW)を bash で直起動して検証する。wrapper は gh を +# PATH 先頭に prefix するため stub で上書きできないが、bash 直起動なら shebang を回避でき PATH 上の +# stub gh が使われる。 + +GHAS_SETUP="${GHAS_SETUP:-ghas-setup}" +# 実適用パス用の生スクリプト。flake check では nix store のパスが渡る。未設定時(repo で +# `bats pkgs/ghas-setup/tests` を直接実行)は隣の生スクリプトを既定にする。 +GHAS_SETUP_RAW="${GHAS_SETUP_RAW:-$BATS_TEST_DIRNAME/../ghas-setup}" + +setup() { + BASE="$BATS_TEST_TMPDIR" + REPO="$BASE/repo" + mkdir -p "$REPO" + cd "$REPO" + git init -q -b main + git config user.email test@example.com + git config user.name test + git commit -q --allow-empty -m init +} + +# .github/ghas.yml を書く(dry-run が読む全項目を含む)。 +write_config() { + mkdir -p "$1/.github" + cat > "$1/.github/ghas.yml" <<'EOF' +org: example-org +configuration: + name: example-default + description: Test configuration. + enforcement: enforced + scanners: + advanced_security: enabled + secret_scanning: enabled +default_for_new_repos: all +attach_existing_repos: all +actions: + default_workflow_permissions: read + can_approve_pull_request_reviews: false +EOF +} + +# gh をスタブする。呼び出し引数を $GH_LOG に記録し、種別ごとに想定 stdout を返す: +# - auth status : "admin:org" を出力(scope 判定の grep 用)/ exit 0 +# - api(--slurp あり) : 既存 lookup。$1(""=新規, それ以外=既存 id)を返す +# - api(POST で末尾が /configurations): 新規作成。固定 id 123 を返す +# - その他 api : 読み戻し等。{} を返す +make_gh_stub() { + STUB_DIR="$BASE/stubbin" + GH_LOG="$BASE/gh-calls.log" + mkdir -p "$STUB_DIR" + : > "$GH_LOG" + { + printf '#!%s\n' "$(command -v bash)" + cat <> "$GH_LOG" +case "\$1" in + auth) echo "admin:org"; exit 0 ;; + api) + for a in "\$@"; do [ "\$a" = "--slurp" ] && { printf '%s' "$1"; exit 0; }; done + case "\$*" in + *"--method POST"*"/code-security/configurations "*) printf '123'; exit 0 ;; + esac + echo '{}'; exit 0 ;; +esac +exit 0 +EOF + } > "$STUB_DIR/gh" + chmod +x "$STUB_DIR/gh" + PATH="$STUB_DIR:$PATH" +} + +@test "不明な引数は usage を出して exit 1" { + write_config "$REPO" + run "$GHAS_SETUP" --nope + [ "$status" -eq 1 ] + [[ "$output" == *"usage: ghas-setup"* ]] +} + +@test "--config に値が無いと usage を出して exit 1" { + write_config "$REPO" + run "$GHAS_SETUP" --config + [ "$status" -eq 1 ] + [[ "$output" == *"--config に値がありません"* ]] +} + +@test "git 管理外で実行すると明確なエラーで exit 1" { + mkdir -p "$BASE/nongit" + cd "$BASE/nongit" + run "$GHAS_SETUP" --dry-run + [ "$status" -eq 1 ] + [[ "$output" == *"git worktree"* ]] +} + +@test "config が無ければ exit 1" { + run "$GHAS_SETUP" --dry-run + [ "$status" -eq 1 ] + [[ "$output" == *"config file が見つかりません"* ]] +} + +@test "--dry-run は解決値と叩く API を出して exit 0(gh は呼ばない)" { + write_config "$REPO" + run "$GHAS_SETUP" --dry-run + [ "$status" -eq 0 ] + [[ "$output" == *"org: example-org"* ]] + [[ "$output" == *"name: example-default"* ]] + [[ "$output" == *"これから叩く API"* ]] + [[ "$output" == *"/orgs/example-org/actions/permissions/workflow"* ]] +} + +@test "--config で別 yaml を渡せる" { + mkdir -p "$BASE/alt" + cat > "$BASE/alt/other.yml" <<'EOF' +org: alt-org +configuration: + name: alt-default + description: Alt. + enforcement: unenforced + scanners: + advanced_security: enabled +default_for_new_repos: none +attach_existing_repos: none +actions: + default_workflow_permissions: read + can_approve_pull_request_reviews: false +EOF + run "$GHAS_SETUP" --config "$BASE/alt/other.yml" --dry-run + [ "$status" -eq 0 ] + [[ "$output" == *"org: alt-org"* ]] +} + +@test "適用(新規): 既存 config 無し → POST 作成 + defaults/attach/actions を config 値で叩く" { + write_config "$REPO" + make_gh_stub "" # lookup は空 → 新規作成分岐 + run bash "$GHAS_SETUP_RAW" + [ "$status" -eq 0 ] + [[ "$output" == *"configuration を新規作成"* ]] + # 新規作成 POST と、作成された id=123 への defaults/attach/actions 適用 + grep -q -- "--method POST /orgs/example-org/code-security/configurations " "$GH_LOG" + grep -q -- "/code-security/configurations/123/defaults" "$GH_LOG" + grep -q -- "/code-security/configurations/123/attach" "$GH_LOG" + grep -q -- "--method PUT /orgs/example-org/actions/permissions/workflow" "$GH_LOG" + # config 値が field として渡る + grep -q -- "default_for_new_repos=all" "$GH_LOG" + grep -q -- "scope=all" "$GH_LOG" + grep -q -- "can_approve_pull_request_reviews=false" "$GH_LOG" + # 更新分岐は呼ばれない + ! grep -q -- "--method PATCH" "$GH_LOG" +} + +@test "適用(更新): 既存 config 有り → PATCH 更新し新規作成しない" { + write_config "$REPO" + make_gh_stub "999" # lookup が既存 id を返す → 更新分岐 + run bash "$GHAS_SETUP_RAW" + [ "$status" -eq 0 ] + [[ "$output" == *"既存 configuration を更新"* ]] + grep -q -- "--method PATCH /orgs/example-org/code-security/configurations/999" "$GH_LOG" + grep -q -- "/code-security/configurations/999/defaults" "$GH_LOG" + # 新規作成 POST は叩かない + ! grep -q -- "--method POST /orgs/example-org/code-security/configurations " "$GH_LOG" +}