Bandit traverses Python syntax trees and reports patterns associated with risk, including unsafe subprocess usage, weak hashes, and possible secrets. A finding starts an investigation; it does not automatically prove exploitability.
This category of tool is known as static application security testing, or SAST. Working from the syntax tree lets Bandit recognize language constructs more accurately than a plain text search. It still cannot understand every trust boundary, deployment control, or source of a value. A human must connect the finding to the application's actual data flow.
Run the scanner
python -m pip install "bandit[toml]"
bandit -r src
bandit -r src --severity-level high
Review the complete result before setting a CI threshold. Do not select only high severity merely to hide relevant medium findings.
A subprocess warning may be acceptable with constant arguments and shell=False, but dangerous when user input reaches a shell. Trace data from source to sink. The Python API security guide explains related boundaries.
Each result includes a test identifier, location, severity, and confidence. Severity estimates potential impact, while confidence describes how reliably the plugin recognized the pattern. Neither value measures the likelihood of exploitation in your environment. A medium finding on an unauthenticated endpoint may deserve attention before a high finding in an isolated maintenance script.
Choose an explicit machine-readable format when another system consumes the result:
bandit -r src -f json -o bandit-report.json
bandit -r src -f sarif -o bandit-report.sarif
Treat these reports as internal artifacts. They can expose paths, source snippets, and implementation details. Apply access controls and a sensible retention period instead of publishing them from an unrestricted CI job.
Keep configuration in pyproject.toml
Installing the toml extra allows Bandit to read project settings from pyproject.toml. A versioned configuration keeps local and CI behavior aligned:
[tool.bandit]
exclude_dirs = ["tests/fixtures", ".venv"]
skips = ["B101"]
Every exclusion needs a reviewable reason. Skipping B101, which covers assertions, is defensible only when the team has confirmed that the analyzed code never relies on assertions for security validation and the scope is appropriate. Excluding an entire test tree can also hide helper programs that run in pipelines. Prefer a narrow fixture exclusion over a broad directory pattern.
The -t option selects tests and -s skips them. They are convenient during an investigation, but durable policy belongs in the repository. A coverage change then becomes visible in code review instead of living in a developer's shell history.
Triage and fix common findings
Triage should determine whether an attacker-controlled path exists and then remove its cause. Several groups recur in Python projects:
- commands and processes: pass an argument list to
subprocess.run, leaveshell=False, and use an allowlist when input changes the operation; - temporary files: use APIs that create unpredictable names with suitable permissions instead of constructing a predictable path;
- cryptography and hashes: do not use MD5 or SHA-1 for passwords or security decisions; select a maintained library and a purpose-appropriate algorithm;
- deserialization: treat
pickledata as executable and never load it from an untrusted source; - TLS: do not disable certificate validation to make an environment work; repair its trust chain;
- apparent secrets: remove credentials from source and rotate them if a real value entered version control.
Consider this unsafe command:
import subprocess
def check_host(host: str) -> None:
subprocess.run(f"ping -c 1 {host}", shell=True, check=True)
A regular expression might reject some malicious strings, but the design still leaves the shell interpreting data. Separating arguments removes that interpretation:
import ipaddress
import subprocess
def check_host(host: str) -> None:
address = str(ipaddress.ip_address(host))
subprocess.run(["ping", "-c", "1", address], check=True)
The revised function also states its policy clearly: it accepts an IP address, not an arbitrary command fragment. Timeouts, minimal privileges, and network controls may still be required in the deployed environment.
Suppress false positives precisely
Before suppressing a result, reproduce the path, identify who controls the value, and document why an existing control is sufficient. If an exception is justified, place # nosec on the relevant line and name the test, such as # nosec B603. A nearby explanation gives future reviewers evidence. A generic # nosec may silently suppress another plugin added later.
Review suppressions periodically. Internal functions become public, validation disappears during refactoring, and assumptions expire. Searching for nosec in pull requests makes growth visible, although it does not replace contextual review.
A baseline can record existing findings so that introducing Bandit does not block all development. Version the baseline, restrict new findings, assign owners, and schedule reduction work. It must not become a permanent permission slip for old risk. When code around a baseline item changes, reassess that item.
Run Bandit as a reliable CI gate
A minimal job installs a controlled version and scans only application source:
python -m pip install "bandit[toml]==1.8.6"
bandit -c pyproject.toml -r src
Pinning makes results reproducible. Planned upgrades still matter because releases add checks and fix detection behavior. Upgrade in a dedicated pull request, read the release changes, and triage new findings. Keeping an obsolete version simply to retain a green build weakens the control.
Make tool failure distinct from a clean result. A failed installation, invalid configuration, or wrong source path should fail visibly. Retain reports only behind access controls and keep enough job output for maintainers to locate the fault.
Bandit is one layer. Linters and tests catch general defects, dependency auditing finds known package vulnerabilities, secret scanning detects credentials, and dynamic tests exercise the running application. Threat modeling and human review connect those signals to the assets and trust boundaries that matter.
Track the outcome of triage rather than treating the raw finding count as a performance target. Useful records include the rule, affected component, decision, remediation owner, and review date. A falling count can reflect real risk reduction, but it can also reflect broader exclusions. Review both configuration changes and resolved findings. For recurring mistakes, add a secure helper and a focused test so developers have an easy safe path. The strongest improvement is often making the insecure construction unnecessary, not repeatedly teaching reviewers to recognize it.
Run Bandit before merge and in CI. A baseline can help introduce it to an older system, provided new findings fail and existing debt has ownership. Avoid broad suppression comments; record the test identifier and why the line is safe.
Bandit does not audit dependency versions. Pair it with pip-audit, authorization tests, secure configuration, and architectural review.
The official Bandit documentation and its configuration reference, accessed July 22, 2026, cover plugins, severity, confidence, output formats, and baselines. Its value comes from consistent triage and fixing causes, not artificially reaching zero findings.