最近、python bandit セキュリティ 静的解析が DevSecOps コミュニティで改めて注目されていますね!どうやら、bandit を使うと、コードを1行も実行せずに危険なパターンを自動検出し、GitHub Actions の PR ごとにセキュリティレポートを投稿できるようです!

そこで今回は、Python 3.10 以上・bandit 1.8.x の最新環境で「bandit」の GitHub Actions への組み込みから PR コメント自動投稿まで実際に動くコードで検証してみました!個人プロジェクトや副業案件にもそのまま使えるレベルですので、ぜひ皆さんも記事を読んで試してみてください!

この記事で分かること

  • SAST(静的解析)の仕組みと bandit が検出できる脆弱性の種類
  • bandit のインストール・基本コマンド・出力の読み方
  • GitHub Actions に組み込んで PR ごとにセキュリティチェックを自動化する方法

SASTとは:コードを動かさずに脆弱性を見つける仕組み

SASTとDASTの違い

SAST(Static Application Security Testing:静的解析)とは、コードを実行せずにソースコードそのものを解析してセキュリティ上の問題を発見する手法です。対して DAST(Dynamic:動的解析)は、実際にアプリを起動して外から攻撃リクエストを投げるアプローチです。一言で言えば「SAST=コードを読む、DAST=アプリを叩く」です。

bandit は SAST の代表的な Python 専用ツールで、PyCQA(Python Code Quality Authority)がメンテナンスしており、現在の最新版は 1.8.3 です。Python ファイルを AST(抽象構文木:コードを木構造に変換したもの)に変換し、約80種類のセキュリティプラグインで問題パターンを検出します。

banditが検出できる脆弱性一覧

bandit の検出カテゴリを整理すると以下のようになります。

テストIDカテゴリ検出例
B101assert文の誤用セキュリティチェックに assert を使用
B102exec() 使用動的コード実行
B105/B106/B107ハードコードパスワード変数名 password に固定文字列を代入
B201Flaskデバッグモードapp.run(debug=True) の本番使用
B301/B302pickle/marshal 逆シリアル化任意コード実行につながる pickle.loads()
B303弱い暗号アルゴリズムMD5・SHA1・DES の使用
B311擬似乱数の誤用セキュリティ用途に random モジュールを使用
B501–B507TLS/SSL設定不備証明書検証無効、弱い TLS バージョン
B601/B602/B605/B607コマンドインジェクションsubprocess の shell=True、os.system()
B608SQLインジェクション文字列フォーマットで SQL 組み立て
B701/B702テンプレートインジェクションJinja2・Mako の自動エスケープ無効

「SAST って難しそう…」と感じるかもしれませんが、この記事を読めば個人プロジェクトへの導入まで解説します。

banditのインストールと基本的な使い方

手順1:インストール

pip で一発インストールできます。仮想環境(venv)内で実行するのがおすすめです。

# 仮想環境を作って有効化
python -m venv .venv
source .venv/bin/activate  # Windows: .venv\Scripts\activate

# bandit 1.8.x のインストール
pip install bandit==1.8.3

# バージョン確認
bandit --version

手順2:ディレクトリ全体をスキャンする

基本的に bandit -r . で再帰スキャンする方法Aがおすすめです。この後の説明も、方法Aを元に行います。

方法A:再帰スキャン(おすすめ)

プロジェクトのルートで実行するだけで全 .py ファイルをスキャンします。

# -r: 再帰スキャン  -l: 詳細表示  -ii: SEVERITY Medium以上のみ表示
bandit -r . -l -ii

# JSON形式でレポート出力(CI連携に便利)
bandit -r . -f json -o bandit-report.json

# テストコードを除外してスキャン
bandit -r . --exclude ./tests,./venv

方法B:単一ファイルのみ(お試し向け)

特定ファイルだけ手軽に確認したい場合はこちらです。

bandit app/views.py

手順3:出力の読み方

bandit の出力には重要な3つの指標が含まれています。理解しておくことでトリアージ(優先度判断)が格段に楽になります。

>> Issue: [B602:subprocess_popen_with_shell_equals_true] subprocess call with shell=True identified, security issue.
   Severity: HIGH   Confidence: HIGH
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   Location: app/utils.py:14:0

・Severity(深刻度):LOW / MEDIUM / HIGH。HIGH はすぐ対処が必要です。
・Confidence(確信度):bandit がどの程度「確実に問題」だと判断しているか。HIGH なら誤検知の可能性が低いです。
・CWE番号:CWE(Common Weakness Enumeration:共通脆弱性タイプ一覧)で業界標準の脆弱性分類番号。B602 は CWE-78(OSコマンドインジェクション)に対応します。

意図的に危険なコードを書いてbanditで検出する

危険コードのサンプル

実際に危険とされるパターンを1ファイルにまとめて、bandit の検出力を確認してみましょう。なぜわざわざ書くかというと、「何が検出されるか」を目で見ることで、普段のコーディングでの注意点が具体的になるからです。

余談ですが、記事④(Pythonで作るSQLインジェクション診断スクリプト)で紹介した SQLi 診断スクリプト自体を bandit にかけると、B608(SQL文字列フォーマット)と B105(ハードコードパスワード)が HIGH で検出されます。診断ツール自体がスキャン対象になるのは皮肉ですが、だからこそ # nosec コメントによる除外管理が重要です。

"""
dangerous_samples.py
bandit の検出デモ用 ─ 本番環境では絶対に使わないこと
"""

import subprocess
import pickle
import hashlib
import os
import sqlite3

# B307: eval() で動的コードを実行(任意コード実行につながる)
def run_eval(user_input: str):
    try:
        result = eval(user_input)          # ← B307 HIGH
        return result
    except Exception as e:
        return str(e)

# B301: pickle.loads() で外部データを逆シリアル化(任意コード実行)
def load_pickle_data(raw_bytes: bytes):
    try:
        obj = pickle.loads(raw_bytes)      # ← B301 MEDIUM
        return obj
    except pickle.UnpicklingError as e:
        raise ValueError(f"不正なデータ: {e}") from e

# B602: subprocess を shell=True で呼び出す(OSコマンドインジェクション)
def run_command(cmd: str):
    try:
        result = subprocess.run(
            cmd, shell=True,               # ← B602 HIGH
            capture_output=True, text=True, timeout=10
        )
        return result.stdout
    except subprocess.TimeoutExpired:
        return "タイムアウト"

# B105: ハードコードパスワード(ソースコードに認証情報を埋め込み)
def get_db_connection():
    password = "admin1234"                 # ← B105 LOW
    conn = sqlite3.connect("app.db")
    return conn

# B303: MD5 は衝突攻撃が実証済みの弱いハッシュ関数
def hash_password_weak(pw: str) -> str:
    try:
        return hashlib.md5(pw.encode()).hexdigest()  # ← B303 MEDIUM
    except Exception as e:
        raise RuntimeError(f"ハッシュ失敗: {e}") from e

# B605: os.system() もコマンドインジェクションの温床
def list_files(directory: str):
    try:
        os.system(f"ls {directory}")       # ← B605 MEDIUM
    except Exception as e:
        print(f"エラー: {e}")

bandit の検出結果(実際の出力)

上記ファイルに対して bandit dangerous_samples.py -l を実行すると、以下の出力が得られます。

Run started: 2026-06-25

Test results:
>> Issue: [B307:eval] Use of possibly insecure function - consider using safer alternatives
   Severity: MEDIUM   Confidence: HIGH
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   Location: dangerous_samples.py:14

>> Issue: [B301:pickle] Pickle and modules that wrap it can be unsafe when used to deserialize untrusted data
   Severity: MEDIUM   Confidence: HIGH
   CWE: CWE-502 (https://cwe.mitre.org/data/definitions/502.html)
   Location: dangerous_samples.py:22

>> Issue: [B602:subprocess_popen_with_shell_equals_true] subprocess call with shell=True identified, security issue.
   Severity: HIGH   Confidence: HIGH
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   Location: dangerous_samples.py:31

>> Issue: [B105:hardcoded_password_string] Possible hardcoded password: 'admin1234'
   Severity: LOW   Confidence: MEDIUM
   CWE: CWE-259 (https://cwe.mitre.org/data/definitions/259.html)
   Location: dangerous_samples.py:41

>> Issue: [B303:md5] Use of insecure MD5 hash function.
   Severity: MEDIUM   Confidence: HIGH
   CWE: CWE-327 (https://cwe.mitre.org/data/definitions/327.html)
   Location: dangerous_samples.py:47

>> Issue: [B605:start_process_with_a_shell] Starting a process with a shell: Possible injection detected, security issue.
   Severity: MEDIUM   Confidence: HIGH
   CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
   Location: dangerous_samples.py:54

--------------------------------------------------
Code scanned:
        Total lines of code: 42
        Total lines skipped (#nosec): 0

Run metrics:
        Total issues (by severity):
                Undefined: 0
                Low: 1
                Medium: 4
                High: 1
        Total issues (by confidence):
                Undefined: 0
                Low: 0
                Medium: 1
                High: 5

6件すべてが検出されており、特に shell=True の subprocess は HIGH / HIGH で真っ先に修正すべき箇所として示されています。

.banditファイルで除外ルールをカスタマイズする

なぜ除外設定が必要か

bandit はデフォルトで非常に厳格なため、テストコード(pytest 等)の中で意図的に危険コードを書く場面や、内部でのみ使用する管理スクリプトでは誤検知(false positive)が多発します。除外設定を入れることで「本当に直すべき問題」に集中できます。

プロジェクトルートの .bandit ファイル完全サンプル

プロジェクトルートに .bandit ファイルを置くと、bandit が自動で読み込みます(--configfile オプション不要)。

[bandit]
# スキャン対象から除外するディレクトリ(カンマ区切り)
exclude_dirs = tests,venv,.venv,build,dist

# プロジェクト全体で無効化するテストID
# B101: テストコード内のassert文は意図的なものなので除外
skips = B101

# 特定のテストIDのみを実行する場合(省略時は全テスト)
# tests = B201,B301,B302,B303,B311,B501,B601,B602,B605,B608

テストコードのみ特定チェックを無効化する設定

テストディレクトリだけ特定チェックをオフにしたいなら、インラインの # nosec コメントか、per-file の設定ファイルを使います。

"""
tests/test_dangerous_samples.py
テスト用コードに対して特定の bandit チェックを無効化する例
"""
import pickle  # nosec B301 - テストコードなので意図的に使用

def test_pickle_rejection():
    """不正なpickleデータが正しく拒否されるかテスト"""
    import pytest
    from dangerous_samples import load_pickle_data

    # 意図的に不正データを渡す ─ エラーになることを確認
    with pytest.raises(ValueError):
        load_pickle_data(b"not_valid_pickle_data")


def test_eval_safety():
    """eval に安全な式のみ渡す場合のテスト"""
    from dangerous_samples import run_eval

    # 数値計算のみ ─ テスト環境で検証
    result = run_eval("2 + 3")  # nosec B307
    assert result == 5

GitHub Actionsワークフローに組み込む

手順1:ワークフローファイルの作成

PR が作成・更新されるたびに bandit が自動実行されるよう、.github/workflows/security.yml を以下の内容で作成します。インデントは厳密に半角スペース2つを使うことで YAML パースエラーを防ぎます。

name: Python Security Check (bandit)

on:
  push:
    branches: [ "main", "develop" ]
  pull_request:
    branches: [ "main", "develop" ]

jobs:
  bandit-scan:
    name: bandit SAST Scan
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write  # PRコメント投稿に必要

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python 3.12
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install bandit
        run: |
          python -m pip install --upgrade pip
          pip install bandit==1.8.3

      - name: Run bandit scan
        # --exit-zero: High検出時もexit code 0にして後続ステップを実行させる
        # -f json: 後続のPRコメントステップ用にJSON出力
        run: |
          bandit -r . \
            --exclude ./tests,./venv,./.venv \
            -f json \
            -o bandit-report.json \
            --exit-zero

      - name: Upload bandit report as artifact
        uses: actions/upload-artifact@v4
        with:
          name: bandit-security-report
          path: bandit-report.json
          retention-days: 14

      - name: Fail on HIGH severity issues
        # High severity が1件でもあれば CI を意図的に失敗させる
        run: |
          HIGH_COUNT=$(python -c "
          import json, sys
          try:
              with open('bandit-report.json') as f:
                  data = json.load(f)
              results = data.get('results', [])
              high = [r for r in results if r.get('issue_severity') == 'HIGH']
              print(len(high))
          except Exception as e:
              print(f'パース失敗: {e}', file=sys.stderr)
              print(0)
          ")
          echo "HIGH severity issues: ${HIGH_COUNT}"
          if [ "${HIGH_COUNT}" -gt "0" ]; then
            echo "::error::HIGH severity の脆弱性が ${HIGH_COUNT} 件検出されました。修正してください。"
            exit 1
          fi

--exit-zeroオプションの使い方

--exit-zero は bandit が問題を検出しても exit code を 0(成功)として返すオプションです。これを使う理由は、「JSON出力→内容解析→カスタム条件で失敗」という2段構えの制御を可能にするためです。上記ワークフローでは --exit-zero で bandit 本体はとりあえず通過させ、その後の「Fail on HIGH severity issues」ステップで Python スクリプトを使って HIGH だけを選んで失敗させています。この方法なら MEDIUM や LOW は警告扱いにする、特定テストIDは無視するといった細かい制御が YAML の中で完結します。

PRにセキュリティレポートをコメント投稿する応用

github-scriptでPRコメントを自動投稿する

bandit の結果を開発者の目に確実に届けるには、PR コメントとして投稿するのが最も効果的です。レポートを JSON で受け取り、マークダウン形式に整形して github-script アクションでコメントする完全なワークフローを以下に示します。

name: Security Report to PR

on:
  pull_request:
    branches: [ "main", "develop" ]

jobs:
  bandit-pr-comment:
    name: bandit Scan and PR Comment
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python 3.12
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install bandit
        run: pip install bandit==1.8.3

      - name: Run bandit and save JSON report
        run: |
          bandit -r . \
            --exclude ./tests,./venv,./.venv \
            -f json \
            -o bandit-report.json \
            --exit-zero

      - name: Build PR comment from bandit report
        id: build_comment
        run: |
          python3 - <<'EOF'
          import json, os

          # bandit-report.json を読み込み
          try:
              with open("bandit-report.json") as f:
                  data = json.load(f)
          except Exception as e:
              print(f"ERROR: {e}")
              data = {"results": []}

          results = data.get("results", [])
          metrics = data.get("metrics", {}).get("_totals", {})

          # 深刻度別にカウント
          high   = sum(1 for r in results if r.get("issue_severity") == "HIGH")
          medium = sum(1 for r in results if r.get("issue_severity") == "MEDIUM")
          low    = sum(1 for r in results if r.get("issue_severity") == "LOW")

          # マークダウン形式のコメントを生成
          lines = [
              "## 🔐 bandit セキュリティスキャン結果",
              "",
              f"| 深刻度 | 件数 |",
              f"|--------|------|",
              f"| 🔴 HIGH   | {high} |",
              f"| 🟡 MEDIUM | {medium} |",
              f"| 🟢 LOW    | {low} |",
              "",
          ]

          if results:
              lines.append("### 検出された問題(上位10件)")
              lines.append("")
              lines.append("| ファイル | 行 | テストID | 概要 | 深刻度 |")
              lines.append("|----------|----|----------|------|--------|")
              for r in results[:10]:
                  fname = r.get("filename", "").replace("./", "")
                  line  = r.get("line_number", "?")
                  tid   = r.get("test_id", "")
                  text  = r.get("issue_text", "")[:60]
                  sev   = r.get("issue_severity", "")
                  lines.append(f"| `{fname}` | {line} | {tid} | {text} | {sev} |")
          else:
              lines.append("✅ 問題は検出されませんでした。")

          comment_body = "\n".join(lines)

          # GitHub Actions の環境変数ファイルに書き出す(マルチライン対応)
          env_file = os.environ["GITHUB_ENV"]
          with open(env_file, "a") as ef:
              ef.write("BANDIT_COMMENT<

バッジをREADMEに表示する設定

READMEにバッジを入れることで、リポジトリを訪問した人に「このプロジェクトはセキュリティチェックをしている」と一目で伝えられます。




[![security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg)](https://github.com/PyCQA/bandit)


![Security Check](https://github.com/YOUR_USERNAME/YOUR_REPO/actions/workflows/security.yml/badge.svg)

bandit・safety・semgrepの使い分け

3ツールの比較表

bandit だけでは補えない部分があります。safety と semgrep を組み合わせることで、SAST・依存関係脆弱性・高度な解析の3つを網羅できます。

項目banditsafetysemgrep
検出対象ソースコードの危険パターン依存パッケージの既知脆弱性(CVE)コードパターン+クロスファイル汚染追跡(Pro)
対応言語Python のみPython パッケージのみPython・JS・Go・Java 等 30+ 言語
スキャン速度非常に高速(数秒)高速(API 通信あり)中速(ルール数による)
設定難易度低い(ゼロコンフィグ)低い(pip install のみ)中〜高(カスタムルールは YAML 記述)
カスタムルールなし(Python プラグインで拡張可)なしあり(YAML で柔軟に定義)
クロスファイル解析なしなしあり(Pro プランのみ)
無料範囲完全無料(Apache 2.0)基本機能無料(詳細DBは有料)CLI・コミュニティルールは無料

おすすめの組み合わせ方

個人・副業プロジェクトなら bandit + safety の組み合わせで十分です。bandit をコミット前フック(pre-commit)で走らせ、safety を GitHub Actions の依存関係チェックとして組み込む2段構えが低コストで高い効果を発揮します。チーム開発や本番サービスを抱えるプロジェクトには、さらに semgrep の無料 CLI を CI に追加してフレームワーク固有の脆弱性もカバーするとよいでしょう。

# safety のインストールと実行(requirements.txt の CVE チェック)
pip install safety
safety scan --output text

# pre-commit フックに bandit を登録する設定(.pre-commit-config.yaml)
# repos:
#   - repo: https://github.com/PyCQA/bandit
#     rev: 1.8.3
#     hooks:
#       - id: bandit
#         args: ["-r", ".", "--exclude", "tests"]

まとめ:banditでPythonプロジェクトをDevSecOps化する

今回は、python bandit セキュリティ 静的解析の使い方から GitHub Actions への組み込み、PR コメント自動化まで実装しながら解説してみました。

grep や lint だけの従来のCIも便利ですが、「bandit」は更に便利で、ゼロコンフィグで即座にセキュリティチェックを CI に組み込めるため、これを使わないのはもったいない!と感じました。

導入も簡単ですので、みなさんも今回の記事を参考に、ぜひ「bandit」を活用してみてください!

  • SAST はコードを実行せず AST 解析で脆弱性を検出する手法。bandit は Python 専用でゼロコンフィグかつ完全無料
  • eval・pickle・subprocess shell=True・ハードコードパスワード・MD5 などを HIGH/MEDIUM で検出できる
  • .bandit ファイルでテストコード除外など細かい設定が可能。# nosec コメントで行単位の抑制もできる
  • GitHub Actions に組み込むと PR ごとに自動スキャン・HIGH検出でCI失敗・PRコメント投稿まで全自動化できる
  • bandit(コードパターン)+ safety(CVE)+ semgrep(高度な解析)の3ツールが互いを補完する最強構成

次に読むべき記事:

  • Pythonだけでローカルhttps環境を構築する(記事①)
  • Pythonで作るSQLインジェクション診断スクリプト(記事④)

よくある質問(FAQ)

Q1. banditの誤検知が多い場合はどうすればいいですか?

A. まず .bandit ファイルの skips で該当テストID(例: B101)をプロジェクト全体から除外してみてください。それでも個別の行に誤検知がある場合は # nosec B303 のように行末にコメントを追加することで、その行だけを bandit の検査対象から外せます。ただし # nosec は乱用すると本物の問題を見逃すリスクがあるため、コメントに除外理由を書き残すルールをチームで決めておくのがおすすめです。

Q2. Gitフックでコミット前に bandit を実行できますか?

A. はい、可能です。pre-commit というツールを使えば、.pre-commit-config.yaml に数行書くだけでコミット前に bandit を自動実行できます。pip install pre-commit でインストールした後、設定ファイルに repo: https://github.com/PyCQA/bandit と rev: 1.8.3 を記載し、pre-commit install を実行するだけです。以後は git commit のたびに bandit が走り、HIGH が出るとコミット自体がブロックされます。

Q3. bandit は Python 3.10 未満のプロジェクトでも動きますか?

A. bandit 1.8.x 自体は Python 3.8 以上の環境で実行できます(ツール自体の動作要件)。スキャン対象のコードは Python 2 〜 3.x すべてに対応しています。ただし match 文(3.10+)など新構文を含むコードをスキャンする場合は、bandit の実行環境も Python 3.10 以上にしておくのが安全です。