Gating pip-audit in Python CI Pipelines
pip-audit matches your installed or declared Python packages against the PyPI Advisory Database and OSV, and returns a non-zero exit code when any of them carries a known vulnerability. That exit code is what makes it a CI gate — but only if it runs against a fully resolved dependency set rather than the loose top-level pins most requirements.txt files contain. This guide walks through resolving dependencies to a lockfile with pip-tools, Poetry, or uv, running pip-audit against that lockfile, ignoring specific unfixable advisory IDs with a justification, and emitting SARIF so findings land in code scanning. It implements, for the Python toolchain, the enforcement pattern from the parent Dependency Scanning & CI Security Gates guide within Supply Chain & Dependency Security. For the Node.js counterpart, see the sibling failing npm audit builds on high severity walkthrough.
Prerequisites
- Python 3.9+ and one resolver:
pip-tools, Poetry, oruv pip-auditavailable in CI (pip install pip-auditoruv tool install pip-audit)- A CI runner (examples use GitHub Actions) able to fail a required check
- Repository code scanning enabled if you want SARIF findings in the security tab
Expected Outcomes
- Dependencies resolved to a hashed, pinned lockfile that the audit and the build share
pip-auditfailing CI on any advisory in the resolved set- Specific unfixable advisories ignored by exact PYSEC/GHSA ID, each with a recorded justification
- SARIF output uploaded so findings appear in code scanning and gate pull requests
Step 1: Pin and Resolve Dependencies to a Lockfile
An audit is only as trustworthy as the version set it reads. Loose specifiers like django>=4.2 resolve differently over time, so audit and deploy the exact same fully pinned, hashed lockfile. Pick the resolver your project already uses.
# pip-tools: compile a hashed lockfile from requirements.in
pip-compile --generate-hashes --output-file=requirements.lock requirements.in
# Poetry: export the locked graph to a requirements file for auditing
poetry export --without-hashes=false --format requirements.txt --output requirements.lock
# uv: compile a locked, hashed requirements file
uv pip compile requirements.in --generate-hashes --output-file requirements.lock
Commit requirements.lock. Every transitive dependency now appears with an exact version, which is what allows pip-audit to map advisories precisely and reproducibly across every CI run.
Step 2: Run pip-audit Against the Resolved Requirements
Point pip-audit at the lockfile with --requirement rather than letting it inspect the ambient environment. Requirement scanning is deterministic; environment scanning depends on whatever the runner happened to install.
# Audit the resolved lockfile (deterministic, reproducible)
pip-audit --requirement requirements.lock
# Ignore specific unfixable advisories by exact ID, and emit machine-readable output
pip-audit \
--requirement requirements.lock \
--ignore-vuln PYSEC-2024-48 \
--ignore-vuln GHSA-3ww4-gg4f-jr7f \
--format json \
--output pip-audit.json
--ignore-vuln removes only the named advisories from the failure decision; any other vulnerability still produces a non-zero exit. Keep the ignored IDs in a versioned policy file so each suppression carries an owner, a reason, and a review date rather than living as an opaque command-line flag:
# .security/pip-audit-ignore.yml
# Consumed by scripts/build-pip-audit-args.py to render --ignore-vuln flags.
ignored:
- id: PYSEC-2024-48
package: py
reason: "ReDoS in py.path; only reached by pytest at build time, not shipped."
owner: platform-security
review_by: "2026-08-15"
- id: GHSA-3ww4-gg4f-jr7f
package: certifi
reason: "Bundled CA removal; we pin our own trust store. No fixed release for our pin."
owner: platform-security
review_by: "2026-07-30"
The end-to-end flow from lockfile to gate decision looks like this:
Step 3: Wire CI and Upload SARIF to Code Scanning
Run the resolver, run pip-audit in SARIF mode, upload the report, then let a dedicated gate step fail the job on any non-ignored vulnerability. Splitting the upload from the gate ensures findings reach the security tab even when the build is destined to fail.
# .github/workflows/pip-audit-gate.yml
name: pip-audit gate
on: [pull_request]
permissions:
contents: read
security-events: write
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install tooling
run: pip install pip-audit pip-tools
- name: Resolve lockfile
run: pip-compile --generate-hashes -o requirements.lock requirements.in
- name: pip-audit (SARIF for code scanning)
run: |
pip-audit \
--requirement requirements.lock \
--ignore-vuln PYSEC-2024-48 \
--format sarif \
--output pip-audit.sarif
continue-on-error: true
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: pip-audit.sarif
- name: Enforce gate (fail on remaining vulnerabilities)
run: pip-audit --requirement requirements.lock --ignore-vuln PYSEC-2024-48
The first pip-audit run produces SARIF and is marked continue-on-error so the upload always executes; the final run has no error tolerance and is the authoritative pass/fail decision.
Step 4: Verify Vulnerabilities Fail the Build
Prove each branch of the gate by exercising it locally and reading the exit code that CI depends on.
# Clean lockfile -> exit 0 (build passes)
pip-audit --requirement requirements.lock
echo "exit: $?" # expect: exit: 0
# Introduce a known-vulnerable release to prove the gate fires
echo "jinja2==2.11.2" >> requirements.lock
pip-audit --requirement requirements.lock
echo "exit: $?" # expect: exit: 1 (build blocked)
# Ignore that specific advisory by ID -> exit 0 again
pip-audit --requirement requirements.lock --ignore-vuln GHSA-h5c8-rqwp-cp95
echo "exit: $?" # expect: exit: 0 when the ID matches the finding
The exit status is the contract: 0 lets the CI step succeed, non-zero fails it. Remove the deliberately vulnerable pin after verifying, and confirm the SARIF file lists the advisory ID you expect with jq '.runs[0].results[].ruleId' pip-audit.sarif.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| pip-audit finds nothing but you know a dependency is vulnerable | Auditing loose top-level pins, not the resolved graph | Resolve to a lockfile with pip-compile/poetry export/uv pip compile and audit that file with --requirement |
| Different results locally vs CI | Environment scan reads whatever is installed on each host | Always pass --requirement requirements.lock; never audit the ambient environment in CI |
--ignore-vuln has no effect |
ID format or value mismatch (PYSEC vs GHSA vs aliased ID) | Copy the exact ruleId from the JSON or SARIF output; pip-audit matches the advisory alias precisely |
| Advisory is transitive-only, no direct pin to bump | The vulnerable package is pulled in by another dependency | Add a constraint pin forcing the transitive package to a patched version, then re-resolve the lockfile |
| SARIF upload succeeds but no alerts appear | security-events: write permission missing or code scanning disabled |
Grant the permission in the workflow and enable code scanning for the repository |
Common Implementation Mistakes
Frequently Asked Questions
Should pip-audit scan the environment or a requirements file?
Scan a fully resolved requirements or lockfile with --requirement rather than the ambient environment. Requirement scanning is reproducible and covers the exact versions the build ships, whereas environment scanning depends on whatever happens to be installed on the runner and can silently miss or add packages between runs. Resolve first with pip-compile, poetry export, or uv pip compile, then audit that committed file.
How do I ignore a single unfixable vulnerability in pip-audit?
Pass --ignore-vuln with the exact PYSEC or GHSA advisory ID printed in the report. pip-audit removes only that advisory from the failure decision and still fails on every other finding. Track each ignored ID in a versioned policy file with a package name, a justification, and a review date so the suppression is auditable and does not quietly become permanent when a fixed release later appears upstream.
Can pip-audit output feed GitHub code scanning?
Yes. Run pip-audit with --format sarif and write the result to a file, then upload it with the CodeQL upload-sarif action. Findings then appear in the repository security tab with full advisory details, and pull requests can be gated on new alerts in addition to the non-zero exit code from the audit step itself. Grant the workflow security-events: write or the upload silently fails.
Related
- Dependency Scanning & CI Security Gates — the parent guide on scanner selection, thresholds, and exception policy
- Failing npm audit Builds on High-Severity Advisories — the equivalent gate for Node.js projects
- Injection Attack Prevention — securing the code you write, complementing the third-party code these gates cover