複数リポジトリ(Repo-A, B, C等)がGitHub Actionsの再利用可能なセキュリティCIワークフローに繋がり、SAST、シークレットスキャン、依存関係チェックが実行される様子。各リポジトリのセキュリティテストを一元化し、段階的に導入する設計が示されている。

はじめに

複数のリポジトリを運用していると、SAST・シークレットスキャン・依存脆弱性チェックといったセキュリティ系のCIを、リポジトリごとに個別実装してしまいがちです。
最初の1つはさっと作れますが、2つ目、3つ目と増えるうちにツールのバージョンや設定がじわじわとズレていき、「このリポジトリだけ古いルールセットのまま」「あのリポジトリにはそもそも入っていない」という状態に陥りやすくなります。

この記事では、GitHub Actionsのreusable workflowを使ってセキュリティCIを組織横断で共通化し、各リポジトリ側は薄い呼び出し(caller)だけを置く構成にリファクタリングした際の設計判断を整理します。ツールの選定やバージョン管理を1箇所に集約しつつ、いきなり全部を厳格化せず段階的に導入していく進め方が、実運用では現実的な落としどころになりそうです。

こんな人におすすめ

  • 複数リポジトリでSAST・シークレットスキャン・SCAをそれぞれ個別に実装していて、メンテナンスコストに悩んでいる方
  • GitHub Actionsのreusable workflow(workflow_call)をまだ本格的に使ったことがない方
  • 新しいセキュリティルールを導入する際、いきなり全部fail必須にして誤検知に振り回された経験がある方
  • 全履歴スキャンのような重い検査項目をCIにどう組み込むか悩んでいる方

各リポジトリがバラバラにセキュリティCIを実装する問題

同じ組織内の複数リポジトリで、それぞれ独自にシークレットスキャンのworkflowを書いているようなケースを考えます。

name: Secret Scan

on:
  pull_request:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  gitleaks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0
      - name: Install gitleaks
        run: |
          curl -sSfL https://github.com/gitleaks/gitleaks/releases/download/v8.22.1/gitleaks_8.22.1_linux_x64.tar.gz | tar -xz -C /usr/local/bin gitleaks
      - name: Run gitleaks
        run: gitleaks git --verbose --redact .

このworkflow自体は動きますが、バージョン番号がハードコードされているため、ツールを更新したいときは全リポジトリのYAMLを1つずつ書き換える必要があります。SAST・SCAも同様の構成にしていると、更新対象のファイルはリポジトリ数×ツール数だけ増えていきます。

Reusable Workflowで「呼び出すだけ」の構成にする

そこで、スキャンの中身(ツールの選定・バージョン・実行方法)を共通workflow側にまとめ、各リポジトリはuses:で呼び出すだけの薄いcaller workflowに置き換えます。

name: Security - PR

on:
  pull_request:
  push:
    branches: [main]
  workflow_dispatch: {}

jobs:
  security:
    uses: your-org/shared-workflows/.github/workflows/security-pr.yml@v1
    with:
      # baseline作成期間のため false。初回PR実行結果(真陽性/偽陽性)を確認してから
      # true へ切り替えるか判断する。
      semgrep-fail-on-findings: false
    permissions:
      contents: read

各リポジトリが管理するのは「何を有効にするか」というinput値だけになり、ツール更新やポリシー変更は共通workflow側の1箇所に集約できます。もともと独自実装していたシークレットスキャンのworkflowは、共通workflow側のGitleaks jobに機能が統合されるタイミングで削除しました。同じ範囲を二重に実行しているとCIが遅くなるだけでなく、どちらの結果を信じればよいか曖昧になるためです。

さらに、PR起動の軽量スキャンとNightlyの全量スキャンを分離し、後者では保持期間などの運用パラメータだけを渡す形にしています。

name: Security - Nightly

# full-history-scan は既定false のまま運用する。過去commitに既知の検出が
# 独自のignore設定で抑制されており、共通configはリポジトリ固有のignoreを
# 参照できないため、有効化すると既知findingsを誤って再検出する。
# 全履歴の走査は定期スキャンでローカル実行して担保する。

on:
  schedule:
    - cron: '0 18 * * *' # 03:00 JST
  workflow_dispatch: {}

jobs:
  security-nightly:
    uses: your-org/shared-workflows/.github/workflows/security-nightly.yml@v1
    with:
      report-retention-days: 14
    permissions:
      contents: read

fail条件はいきなり厳格化しない

新しいSASTルールセットを導入する際、初日から「findings 1件でもCI赤」にしてしまうと、誤検知の洗い出しに追われてルール自体への信頼が失われかねません。上のcaller workflowでsemgrep-fail-on-findings: falseとしているのは、まずは検出結果を観測する期間を設け、真陽性・偽陽性の内訳を確認してから厳格化するかどうかを判断するためです。

他リポジトリでの先行導入で真陽性0件という実績があった場合でも、新しく導入するリポジトリごとに同じ観測期間を踏むほうが、後から「なぜこの設定なのか」を追跡しやすくなります。

完全に自動化できない部分は割り切って手動運用で埋める

共通workflow側がリポジトリ固有の除外設定(ignoreファイル)を参照できない設計だと、Nightlyのような全量スキャンを有効化した瞬間に、既知の検出を大量に再検出してしまうことがあります。この非互換が解消されるまでは、該当スキャン項目を意図的にオフにし、四半期ごとの手動棚卸しのような代替運用で補うという判断も選択肢になります。

CIに載せられない検査項目を無理に自動化しようとせず、「いつ・誰が・何を基準に手動で確認するか」をIssueテンプレートなどの形で明文化しておくことで、抜け漏れを防ぎやすくなります。

つまづきやすいポイント

  • 共通workflow側がリポジトリ固有のignore設定を読み込めないと、全量スキャンを有効化した瞬間に既知の検出を大量に再検出してしまうことがあります
  • 独自実装のスキャンと共通CIの範囲が重なったまま両方残すと、二重実行でCIが遅くなり、どちらの結果を信頼すべきか曖昧になります
  • fail条件をいきなり厳格化すると、誤検知への対応に追われてルール導入そのものへの信頼が下がりやすくなります
  • 「なぜこのフラグをfalseにしたか」をworkflowのコメントだけでなく運用ドキュメントにも残しておかないと、後から見た人が設定意図を追いにくくなります

まとめ

セキュリティCIをリポジトリごとに個別実装していくと、ツールのバージョンや設定がじわじわとズレていき、更新のたびに全リポジトリを手で追いかける状況になりやすいです。
reusable workflowで「何を検査するか」を共通化し、各リポジトリは呼び出しとinputの調整だけに専念する構成にすると、更新箇所を1つに集約できます。
ただし、共通化した瞬間に全てを厳格化するのではなく、観測期間を設けて誤検知の傾向を見てから判断する、CIでカバーしきれない部分は手動運用で割り切って補う、といった段階的な進め方のほうが、実運用では現実的に回りやすいように見えます。