|claim|login
RepoCritics — Review. Share. Archive. Every open-source repo.

PyCQA/bandit

Wiki: PyCQA/bandit

Source: https://github.com/PyCQA/bandit

Last synced 2026-07-21 · 1068 words · Edit wiki on GitHub →

PyCQA/bandit

> An AST-based static analyzer that flags common security anti-patterns in Python source — fast, low-setup, and heuristic rather than sound.

GitHub repo · Official docs · License: Apache-2.0

Overview

Bandit scans Python code for a fixed catalog of known-insecure patterns: eval, pickle.load, subprocess(..., shell=True), hardcoded passwords, weak hashes (MD5/SHA1), disabled TLS verification, SQL built by string concatenation, and so on. It parses each file into a Python AST and runs a set of plugin checks against the nodes, then emits a report with a severity and confidence rating per finding. It began inside the OpenStack Security Project and was later rehomed to PyCQA, the group that also maintains flake8, pylint, and isort1.

The defining characteristic — and the source of most complaints — is that Bandit is pattern-matching, not taint analysis. It has no dataflow or inter-procedural reasoning: it cannot tell whether the string passed to a SQL query is attacker-controlled or a constant. So a check like B608 (hardcoded SQL) fires on the shape of the code, not on a proven vulnerability. This makes Bandit fast and dependency-light, but it also means findings are candidates to review, not confirmed bugs. Teams that treat its output as a security gate without triage tend to drown in false positives and end up sprinkling # nosec everywhere.

Its value is as a cheap first-pass linter that catches careless mistakes and enforces house rules (no assert in production paths, no yaml.load without SafeLoader) across a codebase or in CI, not as a substitute for a real SAST engine with taint tracking.

Getting Started

pip install bandit
# optional extras:
pip install "bandit[toml]"    # read config from pyproject.toml
pip install "bandit[sarif]"   # SARIF output for code-scanning dashboards
# recursively scan a package, medium+ severity only, medium+ confidence
bandit -r myproject/ -ll -ii

# emit machine-readable output for CI
bandit -r myproject/ -f sarif -o bandit.sarif
# example.py — Bandit flags line 3 as B602 (subprocess shell=True)
import subprocess

def run(user_cmd):
    subprocess.call(user_cmd, shell=True)   # >> Issue: [B602] ... Severity: High

    # suppress a reviewed finding inline:
    subprocess.call("ls -la", shell=True)   # nosec B602 - static command, reviewed

Architecture / How It Works

Bandit's pipeline is deliberately simple. For each target file it (1) reads the source, (2) compiles it with Python's built-in ast module, (3) walks the tree, and (4) dispatches each node to the plugins registered for that node type. Plugins are ordinary Python functions decorated with @test_id and a @checks(...) node-type filter; they return an Issue (with severity, confidence, CWE, and text) or nothing2.

Checks come in two flavors. Test plugins (the B1xxB7xx families) inspect call sites, imports, and expressions — e.g. B303 (weak MD5/SHA1), B301 (pickle), B501 (requests without cert validation), B701 (jinja2 autoescape off). Blacklist plugins are a lookup table of banned imports and function calls, so importing telnetlib or calling eval matches by name without bespoke logic. Every finding carries an ID (e.g. B602), a severity (LOW/MEDIUM/HIGH), and a confidence (LOW/MEDIUM/HIGH); the -l/-i flags filter on those two axes independently.

Because analysis is per-file and AST-only, Bandit has no view across module boundaries. It cannot follow a value from a web handler into a subprocess call, so it errs toward flagging syntactically suspicious constructs and leaving the human to judge reachability. Configuration lives in a .bandit INI file, a YAML file, or [tool.bandit] in pyproject.toml (with the toml extra); config controls which tests run (tests/skips), excluded paths, and per-test options. The plugin registry is exposed through setuptools entry points, so third parties can ship additional checks as installable packages.

Production Notes

  • False positives are the operating reality. B608 (SQL), B105–B107

(hardcoded password strings), and the assert check (B101) are the usual noise sources. Plan for a triage step, not a zero-findings gate. The common pattern is a baseline: run bandit -r . -f json -o baseline.json once, then bandit -b baseline.json in CI so only new findings fail the build.

  • # nosec is coarse. A bare # nosec silences every check on that line.

Prefer # nosec B602 to suppress only the specific test, or Bandit will also hide unrelated future findings on the same line. Suppressions are invisible in reports unless you audit for them.

  • B101 and -O. The assert_used check exists because Python's -O flag

strips assert statements, so asserts used for enforcement vanish in optimized builds. Test code uses assert everywhere; most teams skip B101 for their test directories.

  • Severity/confidence flags matter for signal. Plain bandit -r reports

everything; -ll (HIGH severity) or -ii (HIGH confidence) is what makes output actionable in CI. Tune before adopting, not after developers learn to ignore the job.

  • Scope is Python only, and only what the AST sees. It does not analyze

dependencies, templates, generated code, or exec'd strings. It is not an SCA/dependency scanner — pair it with pip-audit or Safety for CVEs in packages.

  • Ruff overlaps. If you already run Ruff, its S rule group reimplements

a large subset of Bandit's checks natively in Rust and runs far faster; many teams drop standalone Bandit once Ruff covers the checks they use3.

When to Use / When Not

Use when:

  • You want a zero-config first-pass security linter in CI for a Python

codebase.

  • You need to enforce simple house rules (no shell=True, no yaml.load, no

MD5 for security) mechanically across many files.

  • You want SARIF output feeding a code-scanning dashboard cheaply.

Avoid (or supplement) when:

  • You need to know whether a finding is actually reachable from untrusted

input — Bandit has no taint tracking; use Semgrep or CodeQL.

  • You want dependency-vulnerability (CVE) coverage — that is SCA, a different

tool class.

  • You already run Ruff and its S rules cover your checks — the standalone

tool may be redundant.

Alternatives

  • returntocorp/semgrep — pattern-based like Bandit but with dataflow/taint

modes and a large community rule registry; use when you need cross-function reasoning and multi-language coverage.

  • github/codeql — deep semantic taint analysis; use when soundness and low

false-positive rate justify heavier setup and query authoring.

  • astral-sh/ruff — reimplements much of Bandit's S rule set in Rust at a

fraction of the runtime; use when you already lint with Ruff and want one tool.

  • pyupio/safety — dependency CVE scanning, not source analysis; use alongside

Bandit to cover known-vulnerable packages, not code patterns.

  • pypa/pip-audit — audits installed packages against the PyPI advisory

database; the maintained, no-signup option for the SCA gap Bandit leaves.

History

VersionDateNotes
Origin~2014Started inside the OpenStack Security Project1.
Rehome2018-04Moved to the PyCQA organization (current repository)4.
1.xongoingDropped Python 2 support; added TOML config, SARIF output, and signed container images on ghcr.io2.

References

  1. ^ Bandit README, "Overview" — "Bandit was originally developed within the OpenStack Security Project and later rehomed to PyCQA." https://github.com/PyCQA/bandit#overview
  2. ^ Bandit documentation — plugin model, test/blacklist plugins, config, and container images. https://bandit.readthedocs.io/en/latest/
  3. ^ Ruff rules — the flake8-bandit (S) rule group. https://docs.astral.sh/ruff/rules/#flake8-bandit-s
  4. ^ PyCQA/bandit repository metadata — repository created 2018-04-26 under the PyCQA organization. https://github.com/PyCQA/bandit

Tags

python, security, static-analysis, sast, linter, ast, security-scanner, cli, pycqa, code-quality