Bandit is a static analysis tool for finding security issues in Python code. pip install bandit. bandit -r src/ — scan recursively. bandit -r src/ -ll — only HIGH severity. bandit -r src/ -lll — only HIGH severity + HIGH confidence. bandit -r src/ -f json -o report.json. bandit -r src/ -x tests/,migrations/ — exclude paths. bandit -r src/ -t B101,B608 — run only specific tests. bandit -r src/ --skip B101 — skip specific tests. Configure bandit.yaml: skips: [B101], tests: [B102,B201], exclude_dirs: ['tests','migrations']. CI fail on HIGH: bandit -r src/ -ll --exit-zero (0 = success regardless) vs default (non-zero on any finding). Pre-commit: - repo: https://github.com/PyCQA/bandit; hooks: [{id: bandit, args: [-c, bandit.yaml]}]. Common tests: B101 assert_used, B102 exec_used, B103 setting_permissions, B104 hardcoded_bind_all, B105/106 hardcoded_password, B201 flask_debug_true, B301 pickle, B303 MD5, B311 random, B324 hashlib_new_insecure, B501 request_with_no_cert_validation, B601 paramiko_calls, B602/603 subprocess, B608 hardcoded_sql, B610/611 django_extra. Inline: result = md5(data).hexdigest() # nosec B324 — suppress specific finding. Baseline: bandit -r src/ -f json -o baseline.json && bandit -r src/ -b baseline.json — only new issues. Severity: CRITICAL(implied HIGH)/HIGH/MEDIUM/LOW. Claude Code generates Bandit CI pipelines, pre-commit security hooks, and remediation guides for common Python vulnerabilities.
CLAUDE.md for Bandit
## Bandit Stack
- Version: bandit >= 1.7
- Scan: bandit -r src/ -f json -o bandit.json
- Severity: -l (LOW+) | -ll (MED+) | -lll (HIGH only) — default: all
- Config: bandit.yaml with skips, tests, exclude_dirs
- CI: bandit -r src/ -ll --exit-zero (advisory) or without for blocking
- Inline: # nosec B101 — suppress specific finding on that line
- Baseline: -b baseline.json — report only new issues vs baseline
Bandit Security Scanning Pipeline
# security/bandit_examples.py
# This module demonstrates INSECURE patterns Bandit flags,
# alongside the SECURE alternatives you should use instead.
# Run: bandit -r security/ -f text
from __future__ import annotations
import hashlib
import hmac
import os
import secrets
import subprocess
from pathlib import Path
# ─────────────────────────────────────────────────────────────────────────────
# B101 — assert_used
# ─────────────────────────────────────────────────────────────────────────────
# INSECURE: assert is stripped by Python -O flag
def process_admin_action_bad(user_role: str) -> str:
assert user_role == "admin", "Must be admin" # B101
return "admin action executed"
# SECURE: use an explicit check that cannot be optimized away
def process_admin_action_good(user_role: str) -> str:
if user_role != "admin":
raise PermissionError(f"Admin role required, got {user_role!r}")
return "admin action executed"
# ─────────────────────────────────────────────────────────────────────────────
# B105/B106 — hardcoded_password
# ─────────────────────────────────────────────────────────────────────────────
# INSECURE: hardcoded credentials
DB_PASSWORD_BAD = "super_secret_password" # B105
def connect_db_bad():
return f"postgresql://admin:password123@localhost/db" # B105
# SECURE: read from environment or secrets manager
DB_PASSWORD_GOOD = os.environ.get("DB_PASSWORD")
def connect_db_good() -> str:
password = os.environ.get("DB_PASSWORD")
if not password:
raise EnvironmentError("DB_PASSWORD environment variable not set")
host = os.environ.get("DB_HOST", "localhost")
db = os.environ.get("DB_NAME", "mydb")
user = os.environ.get("DB_USER", "app")
return f"postgresql://{user}:{password}@{host}/{db}"
# ─────────────────────────────────────────────────────────────────────────────
# B303 / B324 — weak cryptography
# ─────────────────────────────────────────────────────────────────────────────
# INSECURE: MD5 and SHA1 are cryptographically broken
def hash_password_bad(password: str) -> str:
import hashlib
return hashlib.md5(password.encode()).hexdigest() # B303/B324
# SECURE: use bcrypt, argon2, or scrypt for passwords; SHA-256+ for data integrity
def hash_data_good(data: bytes) -> str:
"""SHA-256 for data integrity (not passwords)."""
return hashlib.sha256(data).hexdigest()
def hash_password_good(password: str, salt: bytes = None) -> tuple[str, bytes]:
"""Use scrypt (built-in since Python 3.6) for password hashing."""
salt = salt or os.urandom(16)
key = hashlib.scrypt(password.encode(), salt=salt, n=16384, r=8, p=1)
return key.hex(), salt
# ─────────────────────────────────────────────────────────────────────────────
# B311 — random module is not cryptographically secure
# ─────────────────────────────────────────────────────────────────────────────
import random
# INSECURE: predictable pseudo-random — do NOT use for tokens or keys
def generate_token_bad(length: int = 32) -> str:
chars = "abcdefghijklmnopqrstuvwxyz0123456789"
return "".join(random.choice(chars) for _ in range(length)) # B311
# SECURE: use secrets module for security-sensitive random values
def generate_token_good(nbytes: int = 32) -> str:
"""Generates a URL-safe random token suitable for session IDs or API keys."""
return secrets.token_urlsafe(nbytes)
def generate_otp_good(digits: int = 6) -> str:
"""6-digit OTP using cryptographically secure randomness."""
return str(secrets.randbelow(10 ** digits)).zfill(digits)
# ─────────────────────────────────────────────────────────────────────────────
# B501 — request_with_no_cert_validation
# ─────────────────────────────────────────────────────────────────────────────
import requests as req
# INSECURE: disabling TLS certificate validation exposes to MITM attacks
def fetch_bad(url: str) -> dict:
resp = req.get(url, verify=False) # B501 — never do this in production
return resp.json()
# SECURE: always verify TLS; supply custom CA bundle if needed
def fetch_good(url: str, ca_bundle: str = None) -> dict:
"""
Always verify TLS. Supply ca_bundle path when using private/internal CAs.
"""
resp = req.get(url, verify=ca_bundle or True, timeout=30)
resp.raise_for_status()
return resp.json()
# ─────────────────────────────────────────────────────────────────────────────
# B602/B603 — subprocess shell injection
# ─────────────────────────────────────────────────────────────────────────────
# INSECURE: shell=True with user input enables command injection
def run_convert_bad(input_file: str) -> None:
os.system(f"convert {input_file} output.png") # B605
subprocess.call(f"ffmpeg -i {input_file} out.mp3", shell=True) # B602
# SECURE: pass arguments as a list, never concatenate user input into a shell string
def run_convert_good(input_file: str) -> subprocess.CompletedProcess:
"""
Safe subprocess: arguments as list, shell=False (default), no user input injection.
Validate/sanitize input_file before passing to any subprocess.
"""
safe_path = Path(input_file).resolve()
if not safe_path.exists():
raise FileNotFoundError(f"Input file not found: {safe_path}")
return subprocess.run(
["convert", str(safe_path), "output.png"],
shell=False,
capture_output=True,
check=True,
timeout=60,
)
# ─────────────────────────────────────────────────────────────────────────────
# B608 — hardcoded_sql_expressions / SQL injection
# ─────────────────────────────────────────────────────────────────────────────
# INSECURE: string formatting in SQL is vulnerable to SQL injection
def get_user_bad(conn, username: str):
sql = f"SELECT * FROM users WHERE username = '{username}'" # B608
return conn.execute(sql).fetchall()
# SECURE: use parameterized queries — the DB driver handles escaping
def get_user_good(conn, username: str) -> list:
"""
Parameterized query prevents SQL injection.
Works with sqlite3, psycopg2, pymysql, and SQLAlchemy text().
"""
return conn.execute(
"SELECT id, name, email FROM users WHERE username = ?",
(username,),
).fetchall()
# SQLAlchemy ORM approach (also safe):
# user = session.execute(select(User).where(User.username == username)).scalar()
# ─────────────────────────────────────────────────────────────────────────────
# B301 — pickle — arbitrary code execution
# ─────────────────────────────────────────────────────────────────────────────
import pickle, json
# INSECURE: pickle.loads executes arbitrary Python during deserialization
def load_session_bad(data: bytes) -> dict:
return pickle.loads(data) # B301 — never unpickle untrusted data
# SECURE: use JSON or a schema-validated format for data you don't control
def load_session_good(data: str) -> dict:
"""Deserialize only JSON — no code execution possible."""
return json.loads(data)
# ─────────────────────────────────────────────────────────────────────────────
# HMAC for constant-time comparison (prevents timing attacks)
# ─────────────────────────────────────────────────────────────────────────────
# INSECURE: == comparison is not constant-time
def verify_token_bad(token: str, expected: str) -> bool:
return token == expected # timing oracle
# SECURE: hmac.compare_digest is constant-time
def verify_token_good(token: str, expected: str) -> bool:
"""
Constant-time comparison prevents timing side-channel attacks.
Always use this when comparing secrets, tokens, or HMACs.
"""
return hmac.compare_digest(
token.encode("utf-8"),
expected.encode("utf-8"),
)
# ─────────────────────────────────────────────────────────────────────────────
# Inline suppression (# nosec) — use sparingly with justification
# ─────────────────────────────────────────────────────────────────────────────
def hash_non_sensitive_data(data: bytes) -> str:
"""
MD5 for cache key generation — not security-sensitive.
# nosec suppresses Bandit's B324 for this specific line.
"""
return hashlib.md5(data).hexdigest() # nosec B303,B324
# ─────────────────────────────────────────────────────────────────────────────
# bandit.yaml configuration
# ─────────────────────────────────────────────────────────────────────────────
BANDIT_YAML = """
# bandit.yaml — project-level Bandit configuration
# Use with: bandit -r src/ -c bandit.yaml
# Tests to run (whitelist approach — omit to run all)
# tests:
# - B102 # exec_used
# - B108 # probable_permission_mask
# - B301 # pickle
# - B311 # random
# - B501 # request_with_no_cert_validation
# - B602 # subprocess_popen_with_shell_equals_true
# - B605 # start_process_with_a_shell
# - B608 # hardcoded_sql_expressions
# Tests to skip (blacklist approach — use for project-specific exceptions)
skips:
- B101 # assert_used — tests use assert intentionally
# Directories to exclude
exclude_dirs:
- tests
- migrations
- alembic
- ".venv"
- ".nox"
- build
- dist
"""
if __name__ == "__main__":
print("Bandit Security Analysis Examples")
print("=" * 50)
print("\nCommon Bandit checks:")
for test_id, desc, severity in [
("B101", "assert_used (optimized away by -O)", "LOW"),
("B105", "hardcoded_password_string", "MED"),
("B303", "MD5/SHA1 use", "MED"),
("B311", "random.choice for security", "LOW"),
("B501", "requests verify=False", "HIGH"),
("B602", "subprocess shell=True", "HIGH"),
("B608", "SQL string formatting", "MED"),
("B301", "pickle.loads", "MED"),
]:
print(f" {test_id} [{severity:4}] {desc}")
print("\nRun Bandit:")
print(" bandit -r src/ -f text # interactive scan")
print(" bandit -r src/ -ll -f json -o report.json # CI: HIGH+ only")
print(" bandit -r src/ -b baseline.json # only NEW findings")
print(" bandit -r src/ --exit-zero # advisory (never fails CI)")
For the manual code review alternative — security reviews miss patterns like random.choice for token generation (looks correct, is insecure) and subprocess(shell=True) in helper functions three layers deep while Bandit’s B311 and B602 find them in under a second, and running bandit -r src/ -ll -f json in CI produces a structured report that Slack/email alerting can parse, and a baseline file (bandit -b baseline.json) reports only new findings so existing technical debt doesn’t block PRs. For the pylint security checks alternative — pylint covers code quality and some security patterns but has no concept of severity levels (HIGH/MEDIUM/LOW) or confidence ratings, no B608 SQL hardcoded expression detection, no B303 weak cryptography check, and no OWASP mapping while Bandit’s test IDs map directly to CWE numbers and OWASP Top 10 categories, making audit trail generation for compliance (SOC 2, PCI-DSS) straightforward. The Claude Skills 360 bundle includes Bandit skill sets covering bandit -r recursive scan, severity and confidence filtering, bandit.yaml configuration, common test IDs B101/B105/B303/B311/B501/B602/B608, # nosec inline suppression, baseline file workflow, pre-commit hook setup, GitHub Actions integration, secure alternatives for each finding type, and hmac.compare_digest for timing-safe comparison. Start with the free tier to try security scanning code generation.