Automate Security Linting with Bandit
Learn to automate security linting with Bandit in this secure development tutorial. Discover how Bandit scans Python code for common security issues, integrate it into your workflow, and understand its core concepts, practical steps, and troubleshooting tips. Perfect for developers progressing through the Secure Develo
Focus: automate security linting with bandit
You've written clean, tested, working Python — but is it secure? The painful truth is that security vulnerabilities like SQL injection, hardcoded secrets, and unsafe deserialization often slip into code long before a human reviewer ever sees it, and by then the damage is done. This lesson shows you how to automate security linting with Bandit, a static analysis tool that scans your Python code for common security issues before they reach production, turning a manual, error-prone review process into an automatic, repeatable part of your development workflow.
The Problem This Lesson Solves
Manual security review is slow, inconsistent, and — let's be honest — rarely happens for every commit. When a security flaw does make it to production, the cost of fixing it skyrockets: a bug that takes minutes to catch during development can take days to patch after deployment, not to mention the risk of a data breach. Even disciplined teams struggle to keep up with the sheer volume of new code and the ever-growing catalogue of common vulnerabilities. You need a security linter: a tool that runs on every change, checks for known dangerous patterns, and gives you immediate feedback — automate security linting with Bandit is exactly that.
Consider the typical developer workflow: black formats, flake8 checks style, mypy checks types — but without Bandit, nothing checks whether your code has a glaring security hole. Bandit fills that gap, catching issues that no amount of style checking or type hinting will ever reveal.
Pro tip: Security is not a stage at the end of development — it's a property of the code you write. Automated linting makes security part of your definition of done.
Core Concept / Mental Model
What is Bandit?
Bandit is a static security analyzer for Python, developed under the OpenStack security umbrella and now maintained as a PyPI project. It works by building an Abstract Syntax Tree (AST) of your code and walking through it looking for calls to dangerous functions, insecure configuration patterns, and suspicious code constructs. It's not a fuzzer, it doesn't run your code, and it doesn't find logic errors — it's a pattern-detection engine for security anti-patterns.
Think of Bandit as the security equivalent of a spell-checker: it flags words (code patterns) that are commonly misspelled (insecure), even if the sentence (your logic) is grammatically perfect. It doesn't understand the deeper meaning of your code, but it doesn't need to — it catches the low-hanging fruit that causes the most real-world breaches.
The Bandit rule set
Bandit ships with over 200 built-in tests (called 'plugins' or 'checks') grouped into categories like:
- Injection flaws — SQL injection, command injection via
os.system, shell injection viasubprocesswithshell=True. - Cryptography misuse — weak hashes (MD5, SHA1), using
randomfor security-sensitive purposes instead ofsecrets. - File system issues — unsafe temporary file creation, path traversal via
tarfile.extractall. - Deserialization — unsafe
pickleloads,yaml.loadwithout a safe loader. - Hardcoded passwords and secrets — detection of
password =assignments with literal values.
Each issue has an ID (e.g., B608 for SQL injection), a severity level (LOW, MEDIUM, HIGH), and a confidence level (LOW, MEDIUM, HIGH). Bandit outputs these findings in a readable report, which you can tailor and act upon.
How It Works Step by Step
Automating security linting with Bandit follows a predictable five-step cycle you can slot into any project:
- Install Bandit in your environment (or as a dev dependency).
- Run Bandit against your project to get a baseline report.
- Configure Bandit to match your project's risk profile (skip tests, adjust severity thresholds, exclude vendored code).
- Interpret the report — distinguish between true vulnerabilities, false positives, and acceptable risks.
- Integrate Bandit into your CI pipeline (or pre-commit hook) so every commit is automatically scanned.
The beauty is that after step 5, the tool runs itself — you've turned a manual process into an automated guardrail.
Hands-On Walkthrough
Let's put Bandit to work. We'll create a small, intentionally vulnerable Python file, run Bandit on it, then integrate it into a real project workflow.
Step 1: Install Bandit
pip install bandit
Verify the installation:
bandit --version
Step 2: Create a vulnerable sample file
Create vulnerable.py with the following code:
import subprocess
import hashlib
import os
import pickle
def run_command(user_input):
# B602: subprocess call with shell=True seems unsafe
subprocess.call('ls ' + user_input, shell=True)
def hash_password(password):
# B324: Use of weak MD5 hash for security
return hashlib.md5(password.encode()).hexdigest()
def load_data(filename):
# B301: Pickle and insecure deserialization
with open(filename, 'rb') as f:
return pickle.load(f)
def get_secret():
# B105: Possible hardcoded password
return "supersecret123"
if __name__ == '__main__':
run_command('--help')
print(hash_password('mypassword'))
load_data('data.pkl')
print(get_secret())
Step 3: Run Bandit
Now run Bandit against this file:
bandit vulnerable.py
You'll see output like this (abridged):
[main] INFO profile include tests: None
[main] INFO profile exclude tests: None
[...]
>> Issue: [B602: subprocess_popen_with_shell_equals_true] subprocess call with shell=True appears insecure
Severity: High Confidence: High
Location: vulnerable.py:4:0
More Info: https://bandit.readthedocs.io/en/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html
>> Issue: [B324: hashlib_insecure_functions] Use of weak MD5 hash for security purposes
Severity: Medium Confidence: High
Location: vulnerable.py:9:0
More Info: https://bandit.readthedocs.io/en/latest/plugins/b324_hashlib_insecure_functions.html
>> Issue: [B301: pickle] Pickle and modules that are blacklisted are considered unsafe
Severity: Medium Confidence: High
Location: vulnerable.py:14:0
>> Issue: [B105: hardcoded_password_string] Possible hardcoded password: 'supersecret123'
Severity: Low Confidence: Medium
Location: vulnerable.py:18:0
Bandit found all four issues we planted — this is exactly the kind of feedback you need before merging a pull request.
Step 4: Configure and exclude noise
Projects usually have files that shouldn't be scanned (test suites, migrations, vendored code). You can exclude them with the -x flag or through a configuration file. Create bandit.yaml:
exclude_dirs:
- tests
- migrations
- .venv
skips: ['B101'] # skip assert statements in tests
Run Bandit using the config:
bandit -c bandit.yaml -r .
Now Bandit only scans your production code, skipping the noisy test directory.
Step 5: Integrate into CI (GitHub Actions example)
Here's a minimal workflow file (.github/workflows/security.yml):
name: Security Lint
on: [push, pull_request]
jobs:
bandit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install bandit
- run: bandit -c bandit.yaml -r . -f json -o bandit-report.json
- run: bandit -c bandit.yaml -r . -ll
Now every push triggers a security scan — if Bandit finds anything at Medium severity or above (due to -ll), the pipeline fails, and the developer gets feedback before their code ever touches main.
Pro tip: The
-llflag includes LOW confidence but MEDIUM+ severity issues. Adjust the threshold to your team's risk tolerance: for most projects, High severity issues should always fail the build.
Compare Options / When to Choose What
Bandit isn't the only security tool in the Python ecosystem. Here's a comparison:
| Tool | Type | Focus | Best for |
|---|---|---|---|
| Bandit | Static analysis (AST) | Known Python security anti-patterns | Fast, default choice for most projects |
| Semgrep | Static analysis (pattern matching) | More expressive rules, multi-language | Teams needing custom rules or more coverage |
| pip-audit | Dependency scanner | Known vulnerabilities in installed packages | Checking third-party libraries, not your own code |
| Safety | Dependency scanner | Same as pip-audit, CLI-first | Simple CI integration |
Key differences: Bandit scans your code; pip-audit/Safety scans dependencies. Bandit is lightweight and Python-native; Semgrep is more powerful but requires YAML rule writing and a learning curve. Bandit is the recommended baseline for every Python project.
- Choose Bandit when you want zero-config scanning of common Python pitfalls.
- Choose Semgrep when you need custom, team-specific rules or cross-language support.
- Use Bandit + pip-audit together for a comprehensive security posture (your code + your dependencies).
Troubleshooting & Edge Cases
False positives everywhere
Bandit sometimes flags patterns that are safe in your context. For example, assert statements for input validation (B101) are fine if assertions are not stripped (python -O). You can suppress a specific line with a comment:
# nosec B101
assert x > 0
Or skip an entire rule in your config. But use # nosec sparingly — document why it's safe.
Bandit doesn't find anything
If Bandit returns zero issues and you know you have risky code, check:
- Are you using the latest version? (pip install -U bandit)
- Did you use -r . to scan recursively?
- Is your code in an excluded directory?
Performance on large codebases
Bandit can be slow on massive repositories. If it's too slow, run it only on files that changed in the last commit (using git diff), or split the scan into per-module jobs.
Bandit and Python 3.10+
Bandit supports modern Python syntax, but if you use very new language features (e.g., pattern matching), ensure Bandit is updated. Old versions may raise parse errors on new syntax.
Error: cannot import name 'walk_ert'
This is an internal error that occurs with mismatched dependencies. Fix: pip install --upgrade bandit or reinstall in a fresh virtual environment.
What You Learned & What's Next
You now understand the core idea behind automate security linting with Bandit: you can catch common security flaws automatically with a static analyzer that fits into any CI pipeline. You completed a hands-on exercise installing Bandit, scanning a vulnerable file, configuring exclusions, and adding a GitHub Actions step. You also learned how to compare Bandit with alternatives and handle common false-positive scenarios.
Your next lesson in the Secure development path builds on this foundation: Automate dependency scanning with pip-audit. Now that you've secured your own code, it's time to secure the code you use. With Bandit and pip-audit combined, you'll have a robust layer of automated security checks that runs on every commit — a safety net every professional developer should have.
Practice recap
Create a small Python project and add two intentionally vulnerable functions (e.g., using pickle and os.system). Run Bandit, then fix the issues using safe alternatives (json for loading data, subprocess.run with shell=False). Finally, add a bandit step to your CI config or a pre-commit hook, and practice interpreting the report to prioritize fixes.
Common mistakes
- Relying on Bandit alone for security: static analysis never catches logic flaws or design issues — combine with code review and dynamic testing.
- Suppressing findings with
# nosecwithout proper justification, which creates an audit trail problem and hides genuine issues. - Not updating Bandit regularly, causing misses on newer vulnerability patterns or parsing errors with modern Python syntax.
- Ignoring the confidence level of a finding: a High severity but Low confidence issue may be a false positive, while a Medium severity High confidence one deserves immediate action.
- Scanning test directories and vendored code, producing noisy reports that desensitize the team to real issues.
Variations
- Use Semgrep instead of Bandit when you need custom rules, multi-language support, or more expressive pattern matching.
- Pre-commit integration: run Bandit as a pre-commit hook (via the
pre-commitframework) for local feedback before code even reaches CI. - Bandit as a pre-commit hook: add
-rto scan everything, or combine with-f jsonto output a machine-readable report for your security dashboard.
Real-world use cases
- CI/CD pipeline gate: rejecting pull requests that introduce high-severity issues like SQL injection or shell injection.
- Pre-commit hook: catching hardcoded secrets before a developer accidentally commits them to a public repository.
- Compliance: generating a Bandit report as evidence for regulatory audits (e.g., SOC 2) showing automated security controls.
Key takeaways
- Bandit is a static analyzer that finds common security anti-patterns in Python code without executing it.
- You can automate security linting with Bandit by integrating it into CI (e.g., GitHub Actions) or pre-commit hooks.
- Understand Bandit's severity and confidence levels to prioritize fixes and manage false positives.
- Use configuration files to exclude tests/vendored code and skip benign rules to reduce noise.
- Combine Bandit with dependency scanners like pip-audit to cover both your code and your third-party libraries.
- Address issues with
# nosecannotations sparingly and always document the rationale.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.