python-bandit
Python Bandit Security Scanning
Bandit is a static analysis tool that finds common security issues in Python code. It processes each file, builds an AST, and runs security-focused plugins against AST nodes. Results are categorized by severity (LOW, MEDIUM, HIGH) and confidence (LOW, MEDIUM, HIGH).
Installation
Install the base package or add extras for specific features:
# Base installation
pip install bandit
# With TOML config support (pyproject.toml)
pip install "bandit[toml]"
# With SARIF output (for GitHub Advanced Security)
pip install "bandit[sarif]"
# With baseline support
pip install "bandit[baseline]"
Use the same Python version as the project under scan. Bandit relies on Python's ast module, which can only parse code valid for that interpreter version.
Core Usage
Scan a full project tree:
bandit -r path/to/project/
Scan with severity filter (report only HIGH):
bandit -r . --severity-level high
# or shorthand: -lll (high), -ll (medium+), -l (low+)
bandit -r . -lll
Scan with confidence filter:
bandit -r . --confidence-level high
# shorthand: -iii (high), -ii (medium+), -i (low+)
Target specific test IDs only:
bandit -r . -t B105,B106,B107 # hardcoded password checks only
Skip specific test IDs:
bandit -r . -s B101 # skip assert_used (common in tests)
Use a named profile:
bandit examples/*.py -p ShellInjection
Scan from stdin:
cat myfile.py | bandit -
Show N lines of context per finding:
bandit -r . -n 3
Configuration
pyproject.toml (Recommended)
Centralize Bandit settings alongside other tooling:
[tool.bandit]
exclude_dirs = ["tests", "migrations", "venv"]
skips = ["B101"] # assert_used — acceptable in test suites
tests = [] # empty = run all (minus skips)
Run with explicit config pointer:
bandit -c pyproject.toml -r .
.bandit (INI — auto-discovered with -r)
[bandit]
exclude = tests,migrations
skips = B101,B601
tests = B201,B301
Bandit auto-discovers .bandit when invoked with -r. No -c flag needed.
YAML Config
exclude_dirs: ['tests', 'path/to/file']
tests: ['B201', 'B301']
skips: ['B101', 'B601']
# Override plugin-specific defaults
try_except_pass:
check_typed_exception: true
Run: bandit -c bandit.yaml -r .
Generate a Config Template
bandit-config-generator > bandit.yaml
# Then edit — remove sections you don't need, adjust defaults
Suppressing False Positives
Mark individual lines with # nosec to suppress all findings:
self.process = subprocess.Popen('/bin/echo', shell=True) # nosec
Suppress specific test IDs only (preferred — avoids hiding future issues):
self.process = subprocess.Popen('/bin/ls *', shell=True) # nosec B602, B607
Use the full test name as an alternative to the ID:
assert yaml.load("{}") == [] # nosec assert_used
Always add a comment explaining why the suppression is justified.
Output Formats
bandit -r . -f json -o report.json # JSON (required for baseline)
bandit -r . -f sarif -o report.sarif # SARIF (GitHub Advanced Security)
bandit -r . -f csv -o report.csv # CSV
bandit -r . -f xml -o report.xml # XML
bandit -r . -f html -o report.html # HTML
bandit -r . -f screen # Terminal (default)
bandit -r . -f yaml -o report.yaml # YAML
Baseline Workflow
Use baselines to track only new issues, ignoring pre-existing findings:
# 1. Generate a baseline from the current state of the codebase
bandit -r . -f json -o .bandit-baseline.json
# 2. Commit the baseline to version control
git add .bandit-baseline.json
# 3. Future scans compare against the baseline
bandit -r . -b .bandit-baseline.json
Useful when adopting Bandit on an existing codebase — block only newly introduced issues.
Critical Plugin Categories
Bandit test IDs follow a group scheme:
| Range | Category |
|---|---|
| B1xx | Miscellaneous |
| B2xx | App/framework misconfiguration |
| B3xx | Blacklisted calls |
| B4xx | Blacklisted imports |
| B5xx | Cryptography |
| B6xx | Injection |
| B7xx | XSS |
High-Priority Checks to Always Enforce
Hardcoded secrets (B105, B106, B107) — passwords assigned to variables, passed as function arguments, or set as default parameters.
Injection (B602, B608) — shell injection via subprocess with shell=True, SQL injection via hardcoded SQL string construction.
Weak cryptography (B324, B501–B505) — MD5/SHA1 use, disabled TLS certificate validation, weak SSL versions, short cryptographic keys.
Unsafe deserialization (B301, B302, B303, B304) — pickle, marshal, yaml.load() without Loader.
Template injection (B701, B703, B704) — Jinja2 autoescape disabled, Django mark_safe, MarkupSafe XSS.
Common Findings and Fixes
B101 — assert_used
Asserts are stripped in optimized mode (python -O). Never use assert for security-critical checks.
# Bad
assert user.is_admin, "Not authorized"
# Good
if not user.is_admin:
raise PermissionError("Not authorized")
B105/B106/B107 — Hardcoded password
# Bad
password = "hunter2"
connect(password="secret")
# Good — read from environment or secrets manager
import os
password = os.environ["DB_PASSWORD"]
B324 — Weak hash (MD5/SHA1)
# Bad
import hashlib
hashlib.md5(data)
# Good — use SHA-256 or higher for security contexts
hashlib.sha256(data)
# If MD5 is for non-security use (checksums), suppress with comment:
hashlib.md5(data).hexdigest() # nosec B324 — used for cache key, not security
B506 — yaml.load()
# Bad — arbitrary code execution risk
import yaml
yaml.load(data)
# Good
yaml.safe_load(data)
# or
yaml.load(data, Loader=yaml.SafeLoader)
B602 — subprocess with shell=True
# Bad — shell injection vector
subprocess.Popen(user_input, shell=True)
# Good — pass args as a list, avoid shell
subprocess.Popen(["ls", "-l", path])
B608 — Hardcoded SQL
# Bad
query = "SELECT * FROM users WHERE name = '" + name + "'"
# Good — use parameterized queries
cursor.execute("SELECT * FROM users WHERE name = ?", (name,))
B501 — No certificate validation
# Bad
requests.get(url, verify=False)
# Good
requests.get(url) # verify=True by default
requests.get(url, verify="/path/to/ca-bundle.crt")
Severity Triage Workflow
- Run Bandit with JSON output format and save results to a file (see Output Formats section above).
- Fix all HIGH severity + HIGH confidence findings first — these are near-certain vulnerabilities.
- Evaluate MEDIUM severity findings for false positives; suppress with documented
# nosecif safe. - Decide team policy on LOW severity — consider skipping known false-positive-heavy tests via
skips. - Establish a baseline for legacy codebases to avoid alert fatigue during adoption.
Additional Resources
references/plugin-reference.md— Complete B-code listing with severity, description, and fix pattern per pluginreferences/ci-cd-integration.md— Pre-commit hooks, GitHub Actions, and baseline automation workflows