DevSecOps: Run SAST on Python Code with Bandit¶
Insecure Python patterns (eval(), os.system(), hardcoded credentials) end up in production repos. Bandit scans Python code to find them.
TL;DR: Install Bandit, configure your repository, run a scan, and add it to your CI pipeline to catch security issues before they reach production.
Install Bandit¶
The apt package is outdated at 1.6.2. Use pip for the current version (1.9.4).
Verify:
Extras¶
Bandit has optional extras for additional functionality:
# SARIF output formatter (for GitHub and other tools)
pip install bandit[sarif]
# Baseline comparison (ignore known-vulnerabilities)
pip install bandit[baseline]
# TOML configuration support
pip install bandit[toml]
These aren't needed to start. They become useful once you're past the basic scanning phase.
Understand the rule IDs¶
Bandit uses alphanumeric rule IDs. The letter prefix groups them by category:
| Prefix | Category | What it checks |
|---|---|---|
| B1xx | Assertion | assert statements in production code |
| B2xx | Exec/eval | eval(), exec(), pickle usage |
| B3xx | Blacklist calls | Dangerous function calls (shell injection, SSL bypass) |
| B4xx | Blacklist imports | Insecure imports (pickle, xml, subprocess with shell=True) |
| B5xx | Crypto | Weak crypto algorithms (MD5, DES, RC4) |
| B6xx | OS | os.system(), subprocess with shell=True, command injection |
| B7xx | Network | SSL/TLS misconfiguration, insecure protocols |
The most commonly skipped rules are B101 (assert statements) and B602 (os.system). Whether you skip them depends on your code. If you're running Bandit on test files or production code with assertions, they'll generate noise.
Scan a project¶
Basic usage:
# Single file
bandit single_file.py
# Entire directory
bandit -r /path/to/project
# Limit context lines shown in output
bandit examples/*.py -n 3 --severity-level high
Skip specific rules by ID:
Skip specific directories:
Scan from stdin:
You can filter by severity or confidence level:
# Medium security level
bandit -r . -ll
# High security + High confidence
bandit -r . -lll -iii
# By severity name
bandit -r . --severity-level high
# By confidence name
bandit -r . --confidence-level high
Output formats¶
Bandit supports several output formats. The default is plain text. For CI integration, use JSON or SARIF.
# Default: human-readable text
bandit -r .
# JSON (parseable in CI)
bandit -r . -f json -o findings.json
# SARIF (for GitHub and other tools)
bandit -r . -f sarif -o findings.sarif
Configuration files¶
You can configure Bandit with a YAML or TOML file, or an INI file called .bandit.
YAML¶
TOML¶
Config generator¶
Bandit ships bandit-config-generator which generates a full configuration file with all detected plugins:
This is useful for understanding what plugins are available and their default settings. Edit the output down to what your project actually needs.
Suppress individual findings¶
If a line triggers a finding that you've reviewed and determined is safe, add # nosec:
The whole line gets suppressed. If you only want to suppress specific checks on that line, name them:
You can use full test names instead of IDs:
Always add a comment explaining why you're suppressing the finding. Without context, future reviewers won't know if the suppression is intentional or lazy.
Baseline: ignore known vulnerabilities¶
If you have findings that are known and not currently actionable (e.g., a cleartext password in a unit test), generate a baseline and pass it to subsequent scans:
# Generate baseline
bandit -f json -o baseline.json -r .
# Subsequent scans ignore baseline findings
bandit -b baseline.json -f json -r .
This is cleaner than suppressing individual lines when you have dozens of known-vulnerabilities.
Add to local git hooks¶
Add Bandit as a pre-commit git hook:
Make it executable:
Add to CI/CD¶
# .github/workflows/bandit.yml
name: Bandit SAST
on: [pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Bandit
run: pip install bandit[sarif]
- name: Run Bandit
run: bandit -r . -f sarif -o bandit-results.sarif
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: bandit-results.sarif
The SARIF output format integrates directly with GitHub's code scanning UI, which is nicer than trying to parse plain-text results.
Takeaways¶
- Bandit catches the common things:
eval(),os.system(), weak crypto, hardcoded secrets - Use
--severity-leveland--confidence-levelto control finding noise # nosecsuppresses a line. Name the specific checks if you only want to suppress certain ones- Baseline files are cleaner than suppressing individual findings for known-vulnerabilities
- SARIF output integrates with GitHub code scanning
- Local git hooks catch findings before they reach CI
- The apt version is stale. Use pip for the current rule set.
- Always explain why when you skip a rule or add a
# noseccomment