Skip to content

How to Use pip-audit to Scan Python Packages for Vulnerabilities?

Problem

I ran pip install flask on a fresh project. Three weeks later, I discovered Flask 0.5 had two known CVEs that had been patched years ago. My application had been vulnerable the entire time.

Here’s what pip-audit showed me:

pip-audit output showing vulnerabilities
$ pip-audit
Found 2 known vulnerabilities in 1 package
Name Version ID Fix Versions
---- ------- -------------- ------------
Flask 0.5 PYSEC-2019-179 1.0
Flask 0.5 PYSEC-2018-66 0.12.3

Two vulnerabilities. One package. And I had no idea until I accidentally ran a security scan.

Why did this happen?

I had been installing Python packages the same way for years:

typical pip install workflow
pip install flask
pip install requests
pip install pandas

I never checked if these packages had known security issues. I assumed PyPI was safe. I assumed popular packages were maintained. I assumed someone else was watching for vulnerabilities.

None of those assumptions were true.

The Python ecosystem has experienced multiple supply chain attacks:

  • Malicious packages uploaded with typosquatted names
  • Compromised maintainer accounts
  • Dependencies with known CVEs that remain unpatched

The telnyx incident was a wake-up call. A legitimate package was compromised, and developers who had installed it got malware. pip-audit wouldn’t catch novel supply chain attacks, but it would catch the thousands of known CVEs sitting in requirements files everywhere.

What is pip-audit?

pip-audit is a command-line tool that checks your Python dependencies against the Python Packaging Advisory Database. It identifies packages with known vulnerabilities and tells you which versions fix them.

The key insight: pip-audit checks known CVEs. It cannot catch zero-day exploits or novel supply chain attacks. But it catches the documented, fixable vulnerabilities that most developers ignore.

How to use pip-audit

Basic installation

Install pip-audit
python -m pip install pip-audit

Why use python -m pip instead of pip? It ensures pip-audit installs in the same Python environment you’re auditing.

Scan your current environment

Scan installed packages
pip-audit

This audits everything installed in your current Python environment. pip-audit resolves transitive dependencies automatically, so it catches vulnerabilities in packages you didn’t directly install.

Scan a requirements file

Scan requirements.txt
pip-audit -r requirements.txt

This is the safer approach. You audit before installing, not after. You catch vulnerabilities before they reach your environment.

Output to different formats

Different output formats
pip-audit -r requirements.txt --format json > audit.json
pip-audit -r requirements.txt --format markdown > AUDIT.md

JSON is useful for CI pipelines. Markdown is useful for documentation.

What happens when vulnerabilities are found

When pip-audit finds issues, you get a report like this:

Vulnerability report example
Name Version ID Fix Versions Description
---- ------- -------------- ------------ -----------
jinja2 2.4 PYSEC-2021-142 2.11.3 Jinja2 is affected by...
flask 0.5 PYSEC-2019-179 1.0 Flask before 1.0...
requests 2.0 PYSEC-2023-123 2.31.0 requests is affected by...

The important columns:

  • ID: The vulnerability identifier (links to details in the advisory database)
  • Fix Versions: The versions that patch this vulnerability

To fix, upgrade to a safe version:

Fix vulnerable packages
pip install flask==1.0
pip install jinja2==2.11.3
pip install requests==2.31.0

Then run pip-audit again to confirm:

Verify fix
$ pip-audit
No known vulnerabilities found

Integrating pip-audit into CI/CD

One-time scanning is worthless. New CVEs emerge constantly. The advisory database updates daily. You need continuous scanning.

GitHub Actions integration

GitHub Actions workflow
name: Security Audit
on: [push, pull_request]
jobs:
pip-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Run pip-audit
uses: pypa/[email protected]
with:
inputs: requirements.txt

This runs pip-audit on every push and pull request. If vulnerabilities are found, the CI fails. Vulnerable code cannot merge.

Pre-commit hook

.pre-commit-config.yaml
repos:
- repo: https://github.com/pypa/pip-audit
rev: v2.9.0
hooks:
- id: pip-audit
args: ["-r", "requirements.txt"]

Install the hook:

Install pre-commit hooks
pip install pre-commit
pre-commit install

Now pip-audit runs before every commit. You catch vulnerabilities locally before they reach the repository.

Scheduled cron jobs for continuous monitoring

CVE databases lag supply chain attacks by 24-72 hours. A vulnerability might be published after your last deployment. Scheduled scans catch these.

Cron job for daily scanning
# Edit crontab
crontab -e
# Add daily scan at 2 AM
0 2 * * * cd /path/to/project && pip-audit -r requirements.txt >> /var/log/pip-audit.log 2>&1

Or with email notification:

Cron with email
0 2 * * * cd /path/to/project && pip-audit -r requirements.txt | mail -s "pip-audit report" [email protected]

Why pip-audit alone is not enough

pip-audit checks known CVEs. It cannot catch:

  • Novel supply chain attacks (package compromised but CVE not yet published)
  • Malicious code in legitimate packages (no CVE exists yet)
  • Typosquatted packages (looks like a real package but isn’t)

A Reddit discussion highlighted this:

“pip-audit plus locking everything. The telnyx thing is a reminder that CVE scanning cannot catch novel supply chain attacks”

The solution: pip-audit + dependency locking + hashed requirements.

Lock dependencies with hashes

requirements.txt with hashes
Flask==2.3.0 \
--hash=sha256:123abc... \
--hash=sha256:456def...
requests==2.31.0 \
--hash=sha256:789ghi...

Hashed requirements ensure you install exactly what you tested. Even if PyPI is compromised, the hash won’t match.

Generate hashes with pip-tools:

Generate hashed requirements
pip install pip-tools
pip-compile requirements.in --generate-hashes

Common mistakes

Mistake 1: One-time scanning only

MISTAKE: Only running pip-audit once
# Initial setup
pip-audit
# Then never run it again...

This misses CVEs published after your initial scan. The advisory database updates constantly.

Fix: Schedule daily or weekly scans. Integrate into CI.

Mistake 2: Ignoring transitive dependencies

Why transitive dependencies matter
Your requirements.txt:
flask==2.0
Flask depends on:
Jinja2
Werkzeug
itsdangerous
If Jinja2 has a CVE, Flask is vulnerable too.

pip-audit resolves transitive dependencies by default. Don’t override this with --no-deps unless you’re scanning hashed requirements.

Mistake 3: Not pinning versions

MISTAKE: Unpinned dependencies
# requirements.txt
flask # Bad: installs latest, changes over time
requests # Bad

Unpinned versions mean:

  • Different developers get different versions
  • CI might install a different version than production
  • A “safe” version today might be vulnerable tomorrow

Fix: Pin to exact versions:

requirements.txt with pinned versions
flask==2.3.0
requests==2.31.0
jinja2==3.1.2

Mistake 4: Blindly ignoring vulnerabilities

MISTAKE: Suppressing warnings without investigation
pip-audit --ignore-vuln PYSEC-2019-179 --ignore-vuln PYSEC-2018-66

Sometimes you cannot upgrade immediately. But ignoring without investigation is dangerous.

Fix: Document each ignored vulnerability:

vulnerability-ignore-log.txt
PYSEC-2019-179 (Flask):
- Reason: Cannot upgrade to 1.0 due to breaking API changes
- Impact assessment: Our app does not use affected feature
- Remediation timeline: Upgrade scheduled for Q2 2026
- Approved by: security-team

Mistake 5: Skipping CI integration

MISTAKE: Manual scans only
# Developer remembers to run pip-audit sometimes
pip-audit
# But usually forgets...

Manual processes fail under deadline pressure. Developers skip security checks when rushing.

Fix: CI integration enforces scanning on every PR. Vulnerable code cannot merge.

Architecture for defense in depth

pip-audit is one layer. A complete security strategy needs multiple layers:

Defense in depth architecture
Layer 1: pip-audit (known CVEs)
├── CI integration (every PR)
├── Scheduled scans (daily/weekly)
└── Pre-commit hooks (local check)
Layer 2: Dependency locking
├── Pin exact versions
├── Hash requirements
└── No unpinned dependencies
Layer 3: Supply chain verification
├── Verify package signatures
├── Check maintainer reputation
├── Monitor dependency changes
Layer 4: Runtime protection
├── Container scanning
├── Network segmentation
├── Audit logging

pip-audit handles Layer 1. You need the other layers for complete protection.

Alternatives to pip-audit

uv audit (faster, Rust-based)

uv audit
pip install uv
uv audit

uv is significantly faster than pip. If you have large dependency trees, uv audit completes in seconds while pip-audit takes minutes.

Safety (commercial + free tier)

safety check
pip install safety
safety check

Safety uses a proprietary vulnerability database. The free tier is limited. Commercial plans include more CVEs and continuous monitoring.

Quick reference

pip-audit quick reference
# Install
python -m pip install pip-audit
# Basic scan
pip-audit # Scan current environment
pip-audit -r requirements.txt # Scan requirements file
# Output formats
pip-audit --format json # JSON output
pip-audit --format markdown # Markdown output
# Fix vulnerabilities
pip install package==safe-version # Upgrade to fix version
# Ignore specific vulnerability (use sparingly)
pip-audit --ignore-vuln PYSEC-XXXX
# Multiple requirements files
pip-audit -r requirements.txt -r requirements-dev.txt
# Scan hashed requirements (no dependency resolution)
pip-audit -r requirements.txt --no-deps

Summary

pip-audit is essential for Python security. It scans dependencies against the Python Packaging Advisory Database and reports known CVEs.

But pip-audit alone is insufficient. Use it with:

  • CI integration (every push, every PR)
  • Scheduled scans (catch CVEs published after deployment)
  • Dependency locking (pin versions, add hashes)
  • Documentation for ignored vulnerabilities

The telnyx incident proved that CVE scanning cannot catch novel attacks. But it catches thousands of known, fixable vulnerabilities that developers otherwise ignore.

Key takeaway

pip-audit automates vulnerability scanning. But automation without enforcement is theater. Integrate pip-audit into CI so vulnerable code cannot merge. Schedule regular scans so CVEs published after deployment are caught. Lock dependencies so you install exactly what you tested.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments