最近、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 | カテゴリ | 検出例 |
|---|---|---|
| B101 | assert文の誤用 | セキュリティチェックに assert を使用 |
| B102 | exec() 使用 | 動的コード実行 |
| B105/B106/B107 | ハードコードパスワード | 変数名 password に固定文字列を代入 |
| B201 | Flaskデバッグモード | app.run(debug=True) の本番使用 |
| B301/B302 | pickle/marshal 逆シリアル化 | 任意コード実行につながる pickle.loads() |
| B303 | 弱い暗号アルゴリズム | MD5・SHA1・DES の使用 |
| B311 | 擬似乱数の誤用 | セキュリティ用途に random モジュールを使用 |
| B501–B507 | TLS/SSL設定不備 | 証明書検証無効、弱い TLS バージョン |
| B601/B602/B605/B607 | コマンドインジェクション | subprocess の shell=True、os.system() |
| B608 | SQLインジェクション | 文字列フォーマットで 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にバッジを入れることで、リポジトリを訪問した人に「このプロジェクトはセキュリティチェックをしている」と一目で伝えられます。
[](https://github.com/PyCQA/bandit)

bandit・safety・semgrepの使い分け
3ツールの比較表
bandit だけでは補えない部分があります。safety と semgrep を組み合わせることで、SAST・依存関係脆弱性・高度な解析の3つを網羅できます。
| 項目 | bandit | safety | semgrep |
|---|---|---|---|
| 検出対象 | ソースコードの危険パターン | 依存パッケージの既知脆弱性(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 以上にしておくのが安全です。