Safeguard
AppSec

Python Vulnerability Scanner: How It Works and What to Use

A Python vulnerability scanner checks your code and dependencies for known security flaws. Here is how the different scanner types work and how to combine them in CI.

Safeguard Team
Product
6 min read

A Python vulnerability scanner is a tool that inspects your Python project for known security flaws, and in practice you need more than one type: a dependency scanner for the packages you install, a static analysis scanner for the code you write, and a secrets scanner for credentials you leaked into the repo. No single Python vulnerability scanner covers all three surfaces well, and the most common mistake is running one, seeing a clean report, and assuming the whole project is safe. This guide explains what each scanner type actually finds, names the reliable open source options, and shows how to wire them into a pipeline.

The three things worth scanning

Python security risk lives in three distinct places, and each has its own scanner category.

Your dependencies are the largest surface. A typical requirements.txt or pyproject.toml pulls in dozens of packages, which pull in dozens more transitively. Most of your project's code, by line count, is other people's. A dependency scanner (software composition analysis) matches every installed package version against vulnerability databases like the NVD, the GitHub Advisory Database, and PyPA's own advisory data, and tells you which ones have known CVEs.

Your own code is the second surface. Static analysis (SAST) reads your source without running it and flags dangerous patterns: a subprocess call with shell=True on user input, an eval of untrusted data, a hardcoded weak cipher, or a SQL query built by string formatting. It catches classes of bug that dependency scanning cannot, because these are flaws you wrote.

Secrets are the third. Developers commit API keys, database passwords, and cloud credentials to git more often than anyone likes to admit, and once a secret is in git history it is effectively public. A secrets scanner greps for the tell-tale patterns and high-entropy strings.

Dependency scanning in Python

The workhorse open source options are pip-audit (maintained under the PyPA umbrella) and OSV-Scanner (from Google, using the OSV database). Both take your resolved dependencies and report known vulnerabilities with the affected and fixed versions.

# scan the current environment or a requirements file
pip install pip-audit
pip-audit -r requirements.txt

# OSV-Scanner reads lockfiles directly
osv-scanner --lockfile=poetry.lock

The critical detail is scanning resolved versions, not loose ranges. A requirements.txt line of requests>=2.0 tells a scanner nothing precise; what actually installs might be safe or vulnerable. Pin your dependencies and commit a lockfile (poetry.lock, Pipfile.lock, or a hash-pinned requirements.txt), then scan that. A scanner that reads the exact resolved tree also catches transitive vulnerabilities, the ones several layers deep in packages you never chose directly, which is where a lot of real exposure hides. A hosted SCA scanner does this continuously and keeps alerting when a new advisory is published against a version you already shipped, which a one-time local run cannot.

Static analysis for Python code

For your own source, bandit is the standard Python-specific SAST tool. It knows Python's particular footguns and rates findings by severity and confidence.

pip install bandit
bandit -r ./src -ll   # -ll reports medium severity and above

Bandit will flag patterns like this, which is worth understanding as a defensive example rather than an exploit:

import subprocess

def run(cmd):
    # Bandit flags shell=True with a non-literal argument:
    # command injection if `cmd` is user-controlled
    return subprocess.run(cmd, shell=True)

The safe form passes arguments as a list and avoids the shell entirely:

def run(args):
    return subprocess.run(args, shell=False)  # args is a list, no shell parsing

For broader coverage across languages and richer rules, semgrep runs custom and community rule packs and complements bandit well. Neither replaces the other perfectly; bandit is deep on Python idioms, semgrep is broad and extensible.

Secrets scanning

gitleaks and detect-secrets scan both your working tree and git history for committed credentials.

# scan working tree and full history
gitleaks detect --source . -v

Run this as a pre-commit hook so a secret is caught before it ever lands in a commit, not after. If a secret does reach history, rotate it immediately; removing it from the repo does not un-leak it, because clones and forks already have it.

Putting it together in CI

The three scanners belong in your pipeline, and the design choice that makes or breaks adoption is what fails the build. Fail on newly introduced high and critical findings; do not fail on the entire pre-existing backlog, or developers will disable the checks.

# .github/workflows/security.yml
name: python-security
on: [pull_request]
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install pip-audit bandit
      - name: Dependency scan
        run: pip-audit -r requirements.txt
      - name: Static analysis
        run: bandit -r ./src -ll
      - name: Secret scan
        uses: gitleaks/gitleaks-action@v2

Run dependency scanning on a schedule too, not only on pull requests, because new vulnerabilities are disclosed against code you already shipped. A dependency that was clean when you merged can be vulnerable next week without a single line of your code changing.

Choosing a scanner strategy

For a small project, the open source trio (pip-audit or OSV-Scanner, bandit, and gitleaks) covers all three surfaces at zero cost and is the right starting point. As a project or organization grows, the friction shifts from "can we detect issues" to "can we triage and remediate them across many repos," which is where consolidated platforms earn their place by deduplicating findings, tracking remediation, and prioritizing by real reachability. Whatever you choose, the principle holds: run a scanner for each surface, gate on new findings, and re-scan continuously. Our academy walks through building this pipeline end to end.

FAQ

What is the best free Python vulnerability scanner?

There is no single best; use one per surface. pip-audit or OSV-Scanner for dependencies, bandit for your own code, and gitleaks for committed secrets. Together they cover the three main risk areas at no cost.

What is the difference between pip-audit and bandit?

pip-audit is a dependency scanner (SCA): it checks your installed packages against vulnerability databases for known CVEs. bandit is a static analysis tool (SAST): it reads your own source for insecure coding patterns. They cover different risks and are complementary, not interchangeable.

How often should I run a Python vulnerability scanner?

Run code and secrets scanning on every pull request, and run dependency scanning both on pull requests and on a schedule. New vulnerabilities are disclosed continuously against packages you have already shipped, so a scheduled re-scan catches issues that appear after your code stopped changing.

Can a scanner catch vulnerabilities in transitive dependencies?

Yes, if it reads your resolved dependency tree (a lockfile) rather than loose version ranges. Transitive dependencies, the packages your packages depend on, are a major source of exposure, so scanning the fully resolved tree is essential.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.