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

Sponsored

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 via subprocess with shell=True.
  • Cryptography misuse — weak hashes (MD5, SHA1), using random for security-sensitive purposes instead of secrets.
  • File system issues — unsafe temporary file creation, path traversal via tarfile.extractall.
  • Deserialization — unsafe pickle loads, yaml.load without 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:

  1. Install Bandit in your environment (or as a dev dependency).
  2. Run Bandit against your project to get a baseline report.
  3. Configure Bandit to match your project's risk profile (skip tests, adjust severity thresholds, exclude vendored code).
  4. Interpret the report — distinguish between true vulnerabilities, false positives, and acceptable risks.
  5. 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 -ll flag 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 # nosec without 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

  1. Use Semgrep instead of Bandit when you need custom rules, multi-language support, or more expressive pattern matching.
  2. Pre-commit integration: run Bandit as a pre-commit hook (via the pre-commit framework) for local feedback before code even reaches CI.
  3. Bandit as a pre-commit hook: add -r to scan everything, or combine with -f json to 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 # nosec annotations sparingly and always document the rationale.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.