Bandit is the de facto standard for Python security linting. Maintained under the PyCQA umbrella, it scans Python code for common security issues like hardcoded passwords, insecure use of cryptographic functions, and potential injection vulnerabilities.
The tool is easy to install (pip install bandit) and easy to run (bandit -r your_project/). Getting useful results out of it requires more thought.
What Bandit Actually Checks
Bandit operates by parsing Python source into an AST (Abstract Syntax Tree) and running a set of plugins against each node. Each plugin looks for a specific security anti-pattern.
The checks fall into several categories:
Dangerous function calls. Bandit flags calls to functions known to be dangerous: eval(), exec(), pickle.loads(), yaml.load() without Loader=SafeLoader, subprocess.call() with shell=True, and others. These are legitimate security concerns -- each represents a potential injection or deserialization vector.
Hardcoded secrets. Bandit looks for string literals assigned to variables with names like password, secret, token, or key. It also checks for hardcoded connection strings and API keys.
Cryptographic issues. Bandit flags use of deprecated hash algorithms (MD5, SHA1 for security purposes), weak cipher modes, and insufficient key sizes. It also detects insecure random number generation (using random instead of secrets for security-sensitive operations).
Network security. Bandit detects binding to all interfaces (0.0.0.0), disabled TLS certificate verification, and use of insecure protocols like FTP and Telnet.
Injection patterns. Bandit flags SQL string formatting, command injection through os.system() and subprocess with shell=True, and template injection patterns.
The False Positive Problem
Run Bandit on any non-trivial Python project and you will get a wall of findings. Many of them are false positives or low-value true positives that do not represent real risk in context.
The B101 check flags every use of assert. Bandit warns about this because assert statements are removed when Python runs with optimization (-O flag), so they should not be used for security checks. True in principle, but in practice, most assert statements are in test code or development-time invariant checks. Flagging every one is not useful.
The B303 check flags every use of hashlib.md5() and hashlib.sha1(). These algorithms are cryptographically broken for collision resistance, but they are perfectly fine for non-security purposes like cache keys, checksums, and content addressing. Bandit cannot distinguish between security-relevant and non-security uses.
The B608 check flags SQL statements that use string formatting. This is a legitimate concern, but Bandit flags it even when the formatted values come from trusted sources like configuration constants or enum values, not user input.
Effective Configuration
The key to getting value from Bandit is aggressive configuration. Start by identifying which checks are useful for your codebase and suppress the rest.
Create a .bandit configuration file:
[bandit]
skips = B101,B601
confidence = HIGH
exclude = tests,test,fixtures
Skip B101 (assert_used) globally. If you need to ensure asserts are not used in production code, enforce that through a simpler grep-based check or a custom linter rule that only applies to non-test files.
Use confidence filtering. Bandit assigns confidence levels (HIGH, MEDIUM, LOW) to each finding. Starting with only HIGH confidence findings dramatically reduces noise while keeping the most reliable detections.
Exclude test directories. Test code legitimately uses patterns that Bandit flags -- hardcoded test credentials, eval for test fixtures, assert statements everywhere. Scanning test code creates enormous noise with minimal security value.
Use severity filtering. In CI/CD, consider only failing the build on HIGH severity findings. Report MEDIUM and LOW findings for developer awareness without blocking merges.
Integrating With CI/CD
Bandit works well as a CI/CD gate, but the configuration matters.
- name: Run Bandit
run: |
bandit -r src/ -c .bandit -f json -o bandit-results.json
bandit -r src/ -c .bandit --severity-level high --confidence-level high
Generate a JSON report for tracking and dashboards, but use a separate invocation with strict filters for the pass/fail decision. This gives you visibility into all findings while only blocking on the ones that matter.
Baseline your existing findings. If you are adding Bandit to an existing codebase, generate a baseline file and only flag new findings. This prevents a wall of existing issues from blocking adoption.
bandit -r src/ -f json -o baseline.json
bandit -r src/ -b baseline.json
Bandit vs. Other Python Security Tools
Semgrep offers more expressive rules than Bandit and can track data flow to some degree. For teams willing to invest in custom rules, Semgrep provides better precision. But Bandit has the advantage of zero configuration for basic use.
Pylint has some security-adjacent checks (like flagging exec and eval) but is primarily a code quality tool. It is not a substitute for Bandit.
Safety / pip-audit check your dependencies for known vulnerabilities. These complement Bandit, which only analyzes your own code. You should run both.
Custom Plugins
Bandit supports custom plugins for organization-specific checks. If your codebase has patterns that are dangerous in your context -- like direct database connections instead of using the approved connection pool, or custom serialization instead of the approved framework -- you can write a Bandit plugin to detect them.
Custom plugins examine AST nodes and return Issue objects when they find problems. The API is straightforward, and the existing plugins serve as good examples.
How Safeguard.sh Helps
Safeguard.sh complements Bandit by covering the dependency side of Python security. While Bandit scans your source code for insecure patterns, Safeguard.sh monitors the packages those patterns rely on. We detect vulnerable dependencies, track malicious package publications, and generate SBOMs that give you a complete picture of your Python security posture -- both the code you write and the code you import.