0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CodeMenderをCIに組み込む GitHub Actionsで検査結果を残す実装と注意点

0
Last updated at Posted at 2026-10-02

CodeMenderをCIに組み込む GitHub Actionsで検査結果を残す実装と注意点のアイキャッチ

はじめに

GoogleのCodeMenderは、コードの脆弱性を探し、検証し、修正を支援するAIエージェントです。では、開発の自動化にどう組み込むのでしょうか。

この記事では、GitHub Actionsを例に 検査を実行して結果を保存し、人が判断するところまで を組み立てます。「AIが問題なしと言ったから自動で本番へ出す」という構成ではありません。

先に大切な制約があります。

確認日は2026年10月1日です。公式文書上、CodeMenderは限られた顧客向けのPublic Previewで、限定的なテスト・評価用途に限られ、商用・本番用途には利用できません。本記事のコードも、利用許可を得た専用環境で評価するための例です。

出典:CodeMender公式概要・利用条件。実際に採用するときは、その時点の契約・提供条件を再確認してください。

この記事の範囲とシリーズ

記事 主な内容
入門編 CodeMenderとMantisの仕組み・違い・利用条件
CI実装編(本記事) 実行環境、認証、設定、結果保存、失敗時の扱い
学習・再実行編 モデル学習と知識蓄積、検出漏れ、実行回数の決め方

コマンドと設定は公式資料に照合し、周辺の収集処理は偽のCLIでテストしています。 CodeMender本体、Google Cloud接続、GitHub Actionsでの実行、sandboxの動作は未検証 です。利用可能な環境で結合テストする前の実装サンプルとして読んでください。

1. CI/CDとは何か。どこに検査を置くのか

CI(継続的インテグレーション) は、コードの変更を取り込みながら、ビルドやテストを継続的に実行する仕組みです。GitHub Actionsでは、実行する処理をYAMLファイルに定義します。

CD は継続的デリバリーまたは継続的デプロイを指します。前者は変更をリリースできる状態に保ち、後者は所定の条件を満たした変更を自動で配備する、という違いがあります。

今回のCodeMenderは、通常のテストとは別の 追加調査 として置きます。現時点の利用制限があるため、図のAI検査部分は評価用の独立経路です。本番のリリース承認条件へ組み込む実装ではありません。

図は筆者による運用設計例です。CodeMenderの内部構成図ではありません。CI/CDの用語は GitHubのCI解説、CD解説 を参照してください。

2. 最初に決めること

いきなりすべてのPRで動かさない

最初は次の条件で試すことを勧めます。これは公式の必須構成ではなく、筆者の導入案です。

  • 非公開の評価用リポジトリで、承認済みのmainから手動実行する。
  • 検査対象は秘密情報を含まない小さなサンプルにする。
  • 使い捨ての実行環境を使い、本番ネットワークや本番権限を与えない。
  • 担当者が結果とクラウド側のセッション停止を確認できる時間帯に実行する。
  • 自動修正、自動コミット、自動マージは行わない。

外部PRのコードやテストも、実行すればプログラムです。AIが読むコメントには、検査を妨げる指示が混ざる可能性もあります。承認なしの外部コードを、認証情報があるジョブで実行してはいけません。特に pull_request_target を安易に採用し、その中で外部PRのコードを取得・実行する構成は避けます。

検査対象を「数」だけで切らない

Googleは、効率のために関連する10〜50ファイル程度の小さな単位に絞ることを勧めています。ただし、これは精度保証でもファイル数の上限でもありません。

例えば注文APIなら、APIの入口だけでなく、認証処理、注文の所有者確認、データ取得処理、関連テストまでを同じ文脈で確認できる範囲にします。呼び出し関係を無視して機械的に分割すると、必要な情報を失います。

出典:Scan and verify code vulnerabilities。対象範囲の選び方は、同資料を踏まえた筆者の提案です。

3. Google CloudとGitHubの事前準備

CodeMender側の準備

利用許可を得た評価用Google Cloudプロジェクトで、次のAPIを有効にします。

  • Vertex AI API:aiplatform.googleapis.com
  • Cloud Resource Manager API:cloudresourcemanager.googleapis.com

CLI実行用の公式推奨ロールは roles/aiplatform.user です。専用サービスアカウントに必要範囲で設定します。動かすためだけにOwnerやEditor、本番の管理権限を付けないでください。

これらはCodeMender側の要件です。後述のID連携にはIAM、Service Account Credentials、Security Token Serviceなど、別の準備も必要です。

出典:CLIのセットアップ、デプロイパイプラインとのWorkload Identity連携。

CIでは対話ログインの代わりにID連携を使う

ローカル開発では gcloud auth application-default login が公式の手順にあります。しかし、毎回人がログインする方法はCIには向きません。

ここでは Workload Identity Federation(WIF) を使います。GitHubが発行する短期の身分証明をGoogle Cloudが検証し、許可されたジョブに必要なアクセスを与える仕組みです。長期有効なサービスアカウントのJSON鍵をGitHubへ保存する必要を減らせます。

設定のポイントは次のとおりです。

  1. Cloud側のプロバイダで、対象の所有者ID・リポジトリID・mainブランチに信頼範囲を絞る。
  2. 条件で使うクレームの属性マッピングを設定する。名前の再利用を避けるため、repository_owner_id と repository_id のような数値IDを優先する。
  3. 専用サービスアカウントを借用する場合は、対象の連携IDだけに必要な roles/iam.workloadIdentityUser を設定する。
  4. GitHubのジョブには contents: read と id-token: write を与える。これは本番へのデプロイ権限ではない。
  5. GitHub Environmentに承認者と実行可能ブランチを設定する。YAMLにEnvironment名を書くだけでは承認制にならない。契約プランやリポジトリの公開範囲による機能制限も確認し、承認を強制できない構成のまま使わない。

GitHubの if: 条件だけでは、Cloud側の信頼制限の代わりになりません。

出典:WIF公式ガイド、google-github-actions/auth。

短期認証には有効期限があります。authのREADMEにはGitHub OIDCトークンの短い有効期限に関する注意があります。「ジョブの上限を30分にすれば、認証も30分続く」とは限りません。初回のCodeMenderリソース準備を済ませ、まず短い検査で接続を確認し、長時間実行・更新・再認証は別途検証してください。

4. CodeMenderの実行境界を設定する

CLIは通常 ~/.codemender/config.yaml を読みます。CM_HOME を指定すると、そのディレクトリの設定とローカル状態を使えます。

今回の例では実行ごとに新しい CM_HOME を使い、前回の結果を今回の結果と混ぜません。履歴を使い回す構成ではない点に注意してください。

ci/config.yaml
human_confirmation: true
confirm_writes: true
project_paths: []
model: gemini-3.8-flash
sandbox:
  enabled: true
  mounts:
    target_dir: "."
  network:
    profile: permissive-closed
build:
  command: "python3 -m unittest discover -s . -p '*_test.py'"

この設定はPython標準のunittestで検証する小さな教材用です。実プロジェクトでは build.command を、そのプロジェクトに必要なビルド・単体テスト・回帰テストへ置き換えます。

  • sandbox.enabled: true:ローカルで実行するツールの隔離を有効にします。
  • permissive-closed:sandbox内のツールの外向き通信を遮断します。CLIとGoogle Cloud間の通信が不要になるわけではありません。
  • project_paths: []:対象外への追加アクセスを与えません。周辺テストを参照する場合も、必要なパスだけを許可します。
  • human_confirmation と confirm_writes:ローカル操作での確認を残します。後述のCI実行は -y でその実行の確認を自動承認します。

-y は「読み取り専用にする」オプションではありません。 ツールの実行や書き込みを自動承認するため、使い捨ての評価環境とsandboxが前提です。

公式資料ではOSレベルのsandboxは完全に隔離されたVMより保護が弱いと説明されています。Linuxの権限制約などでsandboxが起動できない場合は検査失敗として止めます。通すためだけに --unrestricted を付けたり、--sandbox=false に変えたりしないでください。

依存パッケージは承認されたものを事前に用意します。通信が必要なテストを permissive-open で動かすと、特定サイトだけでなく外向き通信全体が許可されます。現行仕様ではドメイン単位の許可リストは使えません。

出典:CLI設定とsandbox、-yとsandboxの説明。

5. まずCLIで行う処理を理解する

CIに組み込む中心部分は、次のコマンドです。実行するとクラウド処理・ローカルのツール実行が発生します。説明を読んだだけで業務PC上の未知のコードへ実行しないでください。

CLIで行う処理の順序
# 承認済みの隔離評価環境で実行する
cm init --verify
cm find . -y --sandbox=true
cm session list
cm report --format json
cm report --format sarif

cm report --format sarif はSARIF 2.1.0形式の出力です。SARIFは静的解析などの結果を受け渡すための形式で、JSONは汎用的な構造化データ形式です。この段階ではどちらもファイルに保存し、人が確認する材料にします。

確認した公式ページには「重大な指摘があると cm find が必ず特定の終了コードになる」という契約はありません。したがって、コマンドの成功と安全判定を結びつけません。独自の --fail-on-high など、確認できないフラグも追加しません。

出典:検出と検証、セッション管理とJSON・SARIF出力。

6. GitHub Actionsの評価用サンプル

次のファイルを評価リポジトリに置きます。

配置するファイル
.github/workflows/codemender-evaluation.yml
ci/config.yaml
ci/collect.py
demo/orders.py
demo/orders_test.py

demo は秘密情報を含まない教材コードです。実際のプロジェクト全体を丸ごと送る例ではありません。

demoの小さな検査対象とテスト
demo/orders.py
def may_read_order(requester_id, owner_id):
    """概念説明用。本物の認証・認可APIではない。"""
    return requester_id is not None and requester_id == owner_id
demo/orders_test.py
import unittest
from orders import may_read_order


class OrderAccessTest(unittest.TestCase):
    def test_owner_can_read(self):
        self.assertTrue(may_read_order("alice", "alice"))

    def test_other_user_cannot_read(self):
        self.assertFalse(may_read_order("bob", "alice"))

    def test_anonymous_cannot_read(self):
        self.assertFalse(may_read_order(None, None))

この関数だけで実システムの認証・認可が完成するわけではありません。APIやDBを持たない、配置と実行を説明するための教材です。

GitHub Environment codemender-evaluation に、次のVariablesを設定します。

変数 設定する値
GCP_PROJECT_ID 評価用Google CloudプロジェクトID
GCP_WIF_PROVIDER 作成したWIFプロバイダの完全なリソース名
GCP_SERVICE_ACCOUNT 評価専用サービスアカウントのメールアドレス
CM_ZIP_SHA256 担当者が事前に公式配布元から取得・審査したCLI ZIPのSHA-256

ハッシュ値は「実行時にダウンロードしたファイルから、その場で期待値も作る」のでは意味がありません。承認した配布物の値を事前に管理します。公式の stable 配布物が更新されると、この例はハッシュ不一致で止まります。継続運用では、更新審査と固定した配布物の管理も必要です。

.github/workflows/codemender-evaluation.yml
name: CodeMender evaluation - human review required
on:
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: codemender-evaluation
  cancel-in-progress: false

jobs:
  collect:
    if: github.ref == 'refs/heads/main' && github.event.repository.private == true
    runs-on: ubuntu-24.04
    timeout-minutes: 10
    environment: codemender-evaluation
    permissions:
      contents: read
      id-token: write
    env:
      CM_HOME: ${{ runner.temp }}/cm-home
      CM_TELEMETRY_OPT_OUT: "1"
      CM_ZIP_SHA256: ${{ vars.CM_ZIP_SHA256 }}
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
        with:
          persist-credentials: false
      - name: Prepare approved CLI and isolated source
        shell: bash
        run: |
          set -euo pipefail
          [[ "$CM_ZIP_SHA256" =~ ^[a-fA-F0-9]{64}$ ]]
          mkdir -p "$RUNNER_TEMP/cm-source" "$RUNNER_TEMP/cm-bin" "$CM_HOME"
          git archive HEAD demo | tar -x -C "$RUNNER_TEMP/cm-source"
          cp ci/config.yaml "$CM_HOME/config.yaml"
          curl --fail --location --max-time 90 \
            'https://artifactregistry.googleapis.com/download/v1/projects/cmoc-prod/locations/us/repositories/codemender-cli-production/files/cm%3Astable%3Acm-linux-amd64.zip:download?alt=media' \
            -o "$RUNNER_TEMP/cm-cli.zip"
          printf '%s  %s\n' "$CM_ZIP_SHA256" "$RUNNER_TEMP/cm-cli.zip" | sha256sum --check
          unzip -q "$RUNNER_TEMP/cm-cli.zip" -d "$RUNNER_TEMP/cm-bin"
          chmod +x "$RUNNER_TEMP/cm-bin/cm"
      - uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3
        with:
          project_id: ${{ vars.GCP_PROJECT_ID }}
          workload_identity_provider: ${{ vars.GCP_WIF_PROVIDER }}
          service_account: ${{ vars.GCP_SERVICE_ACCOUNT }}
      - name: Collect evidence - not a security approval
        shell: bash
        working-directory: ${{ runner.temp }}/cm-source/demo
        run: |
          python3 "$GITHUB_WORKSPACE/ci/collect.py" \
            --cm "$RUNNER_TEMP/cm-bin/cm" \
            --config "$CM_HOME/config.yaml" \
            --reports "$RUNNER_TEMP/cm-reports"
      - name: Save restricted evaluation evidence
        if: always()
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
        with:
          name: codemender-evaluation-${{ github.run_id }}-${{ github.run_attempt }}
          path: |
            ${{ runner.temp }}/cm-reports/collector-status.json
            ${{ runner.temp }}/cm-reports/findings.json
            ${{ runner.temp }}/cm-reports/findings.sarif
            ${{ runner.temp }}/cm-reports/sessions.txt
          if-no-files-found: error
          retention-days: 7

この例の要点は次のとおりです。

  • 手動実行・非公開リポジトリ・main限定。別ブランチではジョブがスキップされます。
  • git archive で認証前に demo だけを複製し、その複製を検査します。
  • authが元のworkspaceへ作る認証ファイルを、スキャン対象やアップロード対象に含めません。
  • Actionsは公式タグで確認した完全なコミットSHAへ固定しています。更新時には内容を確認して差し替えます。
  • CM_TELEMETRY_OPT_OUT=1 は匿名テレメトリの停止です。AI処理に必要なコード断片のクラウド送信は止まりません。
  • if: always() で失敗時も取得できた結果を保存します。ただし、強制終了・runner障害時まで保存を保証するものではありません。

ファイルの置き場所を分けるだけで、認証情報の完全な隔離を保証できるわけではありません。 CLI自身は認証を必要とします。ジョブの権限を絞り、sandboxの有効性と環境変数・ファイルのアクセス境界を実環境で確認してください。

出典:認証ファイルの生成場所と後片付け、CLI配布とテレメトリ。ワークフローの組み立ては筆者によるサンプルです。

失敗を「0件」にしない収集処理

ci/collect.py は、初期化失敗時にはスキャンを省略し、スキャン失敗時もレポート取得を試します。壊れたJSONやタイムアウトを、空の正常結果へ置き換えません。

ci/collect.pyの全体
ci/collect.py
"""Linux CI向けの評価用収集器。終了コード0は安全判定ではない。"""
import argparse
import hashlib
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def invoke(argv, output, seconds):
    """stdoutを保存。timeout時は自分が作ったprocess groupを止める。"""
    with output.open("w", encoding="utf-8") as stream:
        with subprocess.Popen(argv, stdout=stream, start_new_session=True) as proc:
            try:
                return proc.wait(timeout=seconds)
            except subprocess.TimeoutExpired:
                try:
                    os.killpg(proc.pid, signal.SIGKILL)
                except ProcessLookupError:
                    pass
                proc.wait()
                return 124


def collect(cm, report_dir, config, init_timeout=30, scan_timeout=120,
            report_timeout=15):
    report_dir.mkdir(parents=True, exist_ok=False)
    statuses = {}
    commands = [("init", ["init", "--verify"], "init.txt", init_timeout),
                ("find", ["find", ".", "-y", "--sandbox=true"], "scan.txt", scan_timeout),
                ("sessions", ["session", "list"], "sessions.txt", report_timeout),
                ("json", ["report", "--format", "json"], "findings.json", report_timeout),
                ("sarif", ["report", "--format", "sarif"], "findings.sarif", report_timeout)]
    for name, args, filename, seconds in commands:
        if name == "find" and statuses.get("init") != 0:
            statuses[name] = "SKIPPED_INIT_FAILED"
            continue
        try:
            statuses[name] = invoke([cm, *args], report_dir / filename, seconds)
        except OSError:
            statuses[name] = "EXEC_ERROR"

    formats = {}
    for filename in ("findings.json", "findings.sarif"):
        try:
            parsed = json.loads((report_dir / filename).read_text())
            valid = isinstance(parsed, (dict, list))
            if filename.endswith("sarif"):
                valid = (isinstance(parsed, dict) and parsed.get("version") == "2.1.0"
                         and isinstance(parsed.get("runs"), list))
            formats[filename] = valid
        except (OSError, ValueError):
            formats[filename] = False

    ok = all(value == 0 for value in statuses.values()) and all(formats.values())
    summary = {
        "collection_ok": ok,
        "security_decision": "REVIEW_REQUIRED",
        "remote_stop_confirmed": False,
        "command_exit_codes": statuses,
        "basic_format_checks": formats,
        "git_sha": os.environ.get("GITHUB_SHA", "local-test"),
        "cli_zip_sha256": os.environ.get("CM_ZIP_SHA256", "unrecorded"),
        "config_sha256": hashlib.sha256(config.read_bytes()).hexdigest(),
    }
    (report_dir / "collector-status.json").write_text(
        json.dumps(summary, indent=2) + "\n", encoding="utf-8")
    return 0 if ok else 1


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--cm", required=True)
    parser.add_argument("--reports", required=True, type=Path)
    parser.add_argument("--config", required=True, type=Path)
    args = parser.parse_args()
    sys.exit(collect(args.cm, args.reports.resolve(), args.config.resolve()))

collection_ok、security_decision、remote_stop_confirmed は この記事で作った管理項目 です。CodeMenderのレポートに同名のフィールドがあるという意味ではありません。

collection_ok: true でも、確認したのはコマンドの成功と最低限の形式だけです。セッションの COMPLETED、想定した検査範囲、レポートの意味を人が確認します。security_decision は常に REVIEW_REQUIRED、つまり人の確認待ちです。

この短時間評価例では初期化30秒、検査120秒、各結果取得15秒を上限にしています。CodeMenderの推奨値ではなく、精度評価に十分な時間でもありません。上限を延ばす前に、認証の有効期限・トークン費用・停止手順を確認してください。

7. タイムアウトしたらクラウド側も確認する

GitHub Actionsの停止、ローカルCLIの強制終了、ネットワーク切断は、クラウド側の処理停止と同じ意味ではありません。

公式のセッション管理手順 には、実行セッションを一覧表示し、IDを指定してキャンセルするコマンドがあります。

セッションを確認して停止する
cm session list
# 実際に一覧で確認した対象IDへ置き換える
cm session cancel SESSION_ID

SESSION_ID は説明用のプレースホルダーです。対象を確認せず、別の実行を止めないようにします。

今回のサンプルには、セッションIDを機械的に抽出してクラウド処理を必ずキャンセルする連携は入れていません。未確認のJSON項目や、表示文字列の推測に依存する処理を載せないためです。

実行前に、runner消滅後も担当者が対象セッションを特定・停止できる権限と手順を、利用可能な環境で確認してください。確認できなければ実行しません。 無人の定期実行へ進める前には、利用するCLI/APIの仕様に合わせた自動停止と、その成功確認が必要です。

また、ジョブの実行時間上限は課金の厳密な上限ではありません。対象範囲、同時実行数、使用トークン、再実行回数、バックエンドの停止を別々に管理します。

出典:セッション管理・キャンセル、利用料金とトークン消費の説明。停止運用と費用管理の構成は筆者の提案です。

8. 指摘を検証・修正する工程は分ける

最初のCIに自動修正まで入れると、AIによる変更と検査の原因を追いにくくなります。人が確認した指摘だけを、別の隔離環境・変更単位で扱います。

指摘を検証して修正案を確認する
# FINDING_IDを実際の指摘IDへ置き換える
cm verify FINDING_ID
cm fix FINDING_ID
cm vcs diff

この段階では -y を付けず、ツール実行・書き込みの確認を行います。元の作業内容があるディレクトリは避け、状態とバックアップを確認した専用コピーを使います。

公式説明では cm fix は、パッチを作成し、設定したビルドとテスト、検証済みPoCの再実行を行います。PoCとは「その問題が起きることを確認するための再現コード」です。

ただし、設定したテストに他利用者へのアクセス制御やフロントエンドとの契約が含まれていなければ、その確認はできません。パッチの差分、業務仕様、権限境界、回帰テストを人がレビューし、通常の変更手順へ戻します。

出典:修正と差分の管理。

9. 常設する前のチェックリスト

  • 利用条件上、その用途で利用できるかを再確認した。
  • データ送信先、コード断片・ログ・指摘の保存先を関係者が承認した。
  • WIFの信頼条件をリポジトリとブランチまで絞った。
  • sandboxを有効にした実環境で、ファイル・通信の境界を確認した。
  • 対象拡張子、除外ディレクトリ、呼び出し元・先、テスト範囲を確認した。
  • コードのコミット、モデル設定、CLIハッシュ、設定、結果を対応づけた。
  • 検査失敗、未完了、指摘0件、検証済み修正を区別している。
  • 中断時にクラウド側の処理を停止し、停止を確認できる。
  • レポートの閲覧者・保存期間を制限した。認証情報やSQLite全体を公開しない。
  • 既存のテスト、静的解析、依存関係検査、人のレビューを残している。

検証履歴を保存する場合も、複数ジョブで同じ state.db を共有して同時書き込みする設計にしないでください。リポジトリや権限境界を越えるキャッシュの使い回しも避けます。

記事の例は 結果収集の評価まで です。SARIFをGitHub Code Scanningへ登録する処理、重大度に応じた承認制御、自動キャンセル、検証済みパッチのPR作成は含めていません。これらは権限や判定仕様を確認して段階的に追加します。

検証したこと・していないこと

対象 確認状況
CLI名・オプション・設定項目 2026-10-01の公式資料に照合
収集処理の正常・失敗・タイムアウト・破損出力 自作の偽CLIで検査。実際のCodeMender出力スキーマの再現ではない
教材のPythonテスト ローカルで実行
WIF、CodeMender、GitHub Actionsの結合 未実行
sandbox境界と検出精度 未実行

おわりに

CodeMenderをCIへ組み込むときは、まず 認証と実行環境を分け、検査結果と実行状態を残し、人が確認する ところから始めます。

「ジョブが緑」「指摘が0件」「修正済みという表示」のどれか一つを、安全の証明として使わないことが大切です。次の学習・再実行編では、繰り返し検査する意味と、検出漏れをどう評価するかを整理します。

Wealthy Designでは、Webシステム開発、クラウド活用、AIを使った業務改善に取り組んでいます。会社の取り組みは、会社サイトにまとめています。

AWS・Web開発・AI活用のご相談や、採用・カジュアル面談については、Wealthy Design公式LINEでもお問い合わせいただけます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?