Python Supply-Chain & Application Security
Parent: Python Patterns and Best Practices · researched 2026-06-01T05:05:23.552Z· 14 sources · 7 concepts · skill python-supply-chain-security
Python application security splits into two layers that share one toolchain:
Overview
- Python application security splits into two layers that share one toolchain: [source]
- Application security (SAST) - find vulnerabilities in your own code (bandit). [source]
- Supply-chain security - defend the dependencies and the path your artifacts travel: know what you ship (SBOM), know if it is vulnerable (pip-audit), prove where it came from (sigstore/PEP 740 attestations), and guarantee you install exactly what you locked (hash pinning). [source]
- The canonical layered ("defense in depth") posture for a 2026 Python project: pin + hash dependencies → audit them in CI (pip-audit) → SAST-scan your code (bandit) → generate an SBOM → publish with Trusted Publishing + attestations. Each layer closes a gap the others cannot; none is sufficient alone (hash pinning, for example, will faithfully pin a package that was already malicious on day one). [source]
- The four foundational PyPA/PyCQA tools - pip-audit, bandit, pip hash mode, and the PEP 740 attestation chain - are free, open source, and require no account or API key for the scanning paths. [source]
1. Dependency auditing — pip-audit
- What it is: the official PyPA tool that audits Python environments, requirements files, and dependency trees for packages with known vulnerabilities (SCA - software composition analysis). Maintained by the Python Packaging Authority. [source]
- Vulnerability sources: queries the PyPA Advisory Database and the OSV database. Select via --vulnerability-service {osv,pypi} (osv is default); --osv-url points at a custom OSV mirror. [source]
- Input modes: audit the live environment (pip-audit), a requirements file (pip-audit -r requirements.txt), or a PEP 751 lockfile (pylock.toml, supported in recent releases). For fully pinned input, skip resolution with --no-deps (pinned, no hashes) or --require-hashes (pinned + hashed). [source]
- Auto-fix: --fix upgrades vulnerable pins in place to the first non-vulnerable version; --dry-run previews. [source]
- Output / SBOM: -f {columns,json,cyclonedx-json,cyclonedx-xml,markdown} - it can itself emit a CycloneDX SBOM with vulnerabilities linked to affected components via the affects field. [source]
2. SBOM generation (Software Bill of Materials)
- Why: a machine-readable inventory of every component (incl. transitive) so downstream consumers and scanners can answer "am I affected by CVE-X?" Increasingly required by regulation (US EO 14028, EU Cyber Resilience Act). [source]
- Two formats: [source]
- CycloneDX - security-first; built for vulnerability identification and outdated-dependency analysis. Dominant in the Python ecosystem (of the ~1.6% of PyPI packages shipping an SBOM, effectively all are CycloneDX). [source]
- SPDX - license-compliance-first; richer license fields. ISO/IEC 5962 standard. [source]
- Tools: [source]
- cyclonedx-py (the cyclonedx-bom distribution) - the most accurate Python-native generator; reads environments, requirements.txt, poetry.lock, Pipfile.lock, and pip lockfiles with proper hash support. Subcommands: cyclonedx-py environment, ... requirements, ... poetry. [source]
- uv export --format cyclonedx - straight from uv.lock (see references/uv-python-toolchain.md). [source]
- Syft (Anchore) - ecosystem-agnostic; SBOMs from filesystems/container images; emits both CycloneDX and SPDX. Use for the container layer. [source]
- lib4sbom - parse/convert SBOMs between SPDX and CycloneDX. [source]
- PEP 770 (SBOMs inside wheels): standardizes shipping SBOMs in a wheel's .dist-info/sboms/ directory, so consumers get the SBOM automatically on pip install. Solves the "phantom dependency" problem (bundled non-Python libs invisible to Python-level tools). [source]
3. Static application security testing — bandit
- What it is: the PyCQA SAST linter for Python. Builds an AST per file and runs security plugins against it. Catches insecure patterns before runtime. [source]
- B-codes (rule families): B1xx general (B101 assert, B102 exec, B105/B106/B107 hardcoded passwords, B108 temp-file); B3xx blacklisted calls/imports (B301 pickle, B303/B304 weak MD5/SHA1 / insecure ciphers, B307 eval, B311 non-crypto random); B5xx crypto/cert (B501 verify=False); B6xx injection (B602 subprocess with shell=True, B608 SQL string-build); plus newer AI/ML checks (B614 unsafe torch.load, B615 insecure Hugging Face download). [source]
- Severity × confidence: every finding has a severity (LOW/MEDIUM/HIGH) and a confidence (how sure bandit is it is real). Filter both: --severity-level medium --confidence-level medium is the standard noise cut. [source]
- Config: [tool.bandit] in pyproject.toml or a .bandit INI / bandit.yaml - set exclude_dirs, skips (e.g. B101), tests (allowlist), per-plugin options. Inline suppression: # nosec B602 on the offending line (scope the code - bare # nosec is an anti-pattern). [source]
- Baseline workflow: bandit -r src/ -f json -o baseline.json, then bandit -r src/ -b baseline.json so CI only flags newly introduced issues - the practical way to adopt bandit on a legacy codebase without a wall of red. [source]
4. Provenance — sigstore, PEP 740 attestations & Trusted Publishing
- The problem PGP couldn't solve: PyPI deprecated/removed PGP signatures - almost nobody verified them and key management was broken. PEP 740 replaces them with identity-based signing. [source]
- Trusted Publishing (OIDC): instead of a long-lived API token, a CI workflow (GitHub Actions, GitLab CI, etc.) presents a short-lived OIDC identity to PyPI and receives a short-lived upload token. No secret to leak/rotate. This is the prerequisite layer. [source]
- PEP 740 digital attestations: cryptographically signed, publicly verifiable statements about a package (notably build provenance). Built on Sigstore with short-lived signing keys bound to the OIDC identity (keyless signing → Rekor transparency log), and the payload follows the in-toto Attestation Framework. Because there is no private key sitting around, key loss/theft is largely designed out. [source]
- How to get it: if you already publish via Trusted Publishing with pypa/gh-action-pypi-publish v1.11.0+, build provenance attestations are generated and uploaded automatically - usually zero code change. PyPI exposes attestations + Trusted-Publishing metadata as provenance objects through the HTML and JSON Simple APIs. [source]
- Verification: the pypi-attestations CLI / library verifies a downloaded file's attestation against the expected identity. Track ecosystem adoption at the "Are we PEP 740 yet?" dashboard. [source]
5. Hash-pinned dependencies (reproducible, tamper-evident installs)
- What it does: records a cryptographic digest (--hash=sha256:…) for every artifact. On install, pip recomputes and compares; a mismatch aborts the install. [source]
- What it protects against: tampering in transit, in a cache, or on a compromised mirror; a PyPI or TLS-chain compromise; a package whose content changes without a version bump. It is the integrity backstop. [source]
- What it does NOT protect against: a package that is malicious from the first install (you just pin the malicious hash), and it says nothing about whether a dependency is vulnerable (that's pip-audit's job). [source]
- Generating hashes: [source]
- pip-tools: pip-compile --generate-hashes requirements.in → fully pinned requirements.txt with --hash lines. [source]
- uv: uv lock (hashes in uv.lock) or uv pip compile --generate-hashes / uv export --format requirements-txt (see references/uv-python-toolchain.md). [source]
- Pipenv records hashes in Pipfile.lock natively. [source]
- Enforcing: pip install --require-hashes -r requirements.txt. --require-hashes is auto-enabled if any line has a hash; it then demands every requirement be pinned (==) and hashed, including transitive deps - which is why a hash-generating compiler is mandatory. [source]
Methodology — layered project posture
- Lock + hash every dependency (uv lock or pip-compile --generate-hashes); install with --require-hashes (uv sync enforces the lock). [source]
- Audit in CI - pip-audit -r requirements.txt (or against the lockfile); fail the build on findings; use --fix --dry-run to triage upgrades. Do not auto-update to latest blindly. [source]
- SAST in CI - bandit -r src/ -c pyproject.toml; run HIGH-severity only as a blocking gate, full set as non-blocking/local; adopt via a baseline. [source]
- Generate an SBOM as a build artifact (cyclonedx-py for the app, syft for the image); attach to the release; PEP 770 to embed in wheels you publish. [source]
- Publish with provenance - Trusted Publishing (OIDC, no token) + automatic PEP 740 attestations via gh-action-pypi-publish. [source]
- Harden the pipeline itself - pin third-party Actions to a full commit SHA, run zizmor on workflows, track posture with OpenSSF Scorecard. The supply chain includes your CI, not just your deps. [source]
Anti-Patterns
- Trusting hash pinning to vet packages. Hashes guarantee integrity, not safety. Pair with pip-audit (known CVEs) and review for first-time deps. [source]
- Blanket # nosec with no rule code - silently suppresses all future findings on that line. Always # nosec Bxxx. [source]
- Running bandit at default severity in CI - B101 assert noise drowns real findings; teams disable the whole tool. Filter to medium/high and use a baseline. [source]
- Long-lived PyPI API tokens in CI secrets. Migrate to Trusted Publishing; a leaked token (cf. the 2025 GhostAction theft of 3,300+ secrets) is a full publish compromise. [source]
- Unpinned third-party GitHub Actions (@v4/@main). A tag can be force-moved to malicious code. Pin to a full commit SHA; verify with zizmor. [source]
- Partial hashing. --require-hashes requires every (incl. transitive) requirement pinned + hashed; a half-hashed file fails. Always regenerate via a compiler. [source]
- Generating an SBOM once and never again. An SBOM is only useful if regenerated on every release and stored as an artifact you can query when a new CVE drops. [source]
- Auditing only direct dependencies. Most CVEs and most supply-chain attacks ride in transitive deps; audit the full resolved tree/lockfile. [source]
Troubleshooting
- pip-audit exits non-zero but you must ship now: triage with --fix --dry-run; if a finding is a known false positive / unfixable, --ignore-vuln <GHSA/PYSEC id> (document why). [source]
- pip install --require-hashes fails "hashes are required for all packages": a transitive dep is unpinned/unhashed - regenerate with pip-compile --generate-hashes or uv export. [source]
- Hash mismatch on install: the artifact differs from the locked hash - could be a mirror/cache problem or tampering. Do not bypass; re-resolve from PyPI and compare. [source]
- bandit flags B608 SQL or B602 subprocess you know is safe: restructure to remove the pattern (parameterized query, shell=False + list args) rather than suppress - the rule is usually right. [source]
- Attestations not appearing on PyPI: confirm permissions: id-token: write, gh-action-pypi-publish ≥ 1.11.0, and that the repo is registered as a Trusted Publisher (not token auth). [source]
- SBOM missing bundled native libs ("phantom dependencies"): Python-level generators can't see vendored C libs; use Syft on the built artifact/image, and adopt PEP 770 for wheels you publish. [source]
2025-2026 threat landscape (why this matters)
- Typosquatting & dependency confusion remain the top vectors: malicious packages named like requests/tensorflow (500+ typosquats in waves), and internal-name confusion pulling a public package over a private one. [source]
- 2025 PyPI phishing - [email protected] (note the j) proxy credential harvester targeting maintainers. [source]
- GhostAction (Sept 2025) - injected workflows across 570+ repos, exfiltrating 3,300+ secrets incl. PyPI/npm/AWS tokens - the canonical case for Trusted Publishing over tokens and for pinning/auditing CI. [source]
- Shai-Hulud worm (Nov 2025) - cross-ecosystem (npm-origin) worm that also hit PyPI via monorepos sharing credentials. [source]
- PyPI processed 2,000+ malware reports in 2025, 66% within 4 hours - fast, but reactive; your pinning + audit + provenance layers are the proactive defense. [source]
References (sources)
- pip-audit - https://github.com/pypa/pip-audit · https://pypi.org/project/pip-audit/ [source]
- PyPA Advisory Database - https://github.com/pypa/advisory-database · OSV - https://osv.dev [source]
- bandit - https://bandit.readthedocs.io · https://pypi.org/project/bandit/ (PyCQA) [source]
- pip repeatable installs / --require-hashes - https://pip.pypa.io/en/stable/topics/repeatable-installs/ [source]
- pip-tools --generate-hashes - https://github.com/jazzband/pip-tools [source]
- CycloneDX Python (cyclonedx-py) - https://github.com/CycloneDX/cyclonedx-python · https://cyclonedx-bom-tool.readthedocs.io [source]
- Syft - https://github.com/anchore/syft · lib4sbom - https://pypi.org/project/lib4sbom/ [source]
- PEP 740 (digital attestations) - https://peps.python.org/pep-0740/ · PEP 770 (SBOMs in packages) [source]
- PyPI attestations docs - https://docs.pypi.org/attestations/ · blog.pypi.org/posts/2024-11-14-pypi-now-supports-digital-attestations/ [source]
- Trail of Bits "Attestations: a new generation of signatures on PyPI" - https://blog.trailofbits.com/2024/11/14/attestations-a-new-generation-of-signatures-on-pypi/ [source]
- "Are we PEP 740 yet?" - https://trailofbits.github.io/are-we-pep740-yet/ · pypi-attestations - https://pypi.org/project/pypi-attestations/ [source]
- OpenSSF Scorecard - https://scorecard.dev · https://github.com/ossf/scorecard · ossf/malicious-packages [source]
- zizmor (GitHub Actions SAST) - https://docs.zizmor.sh [source]
- bernat.tech "Defense in Depth: A Practical Guide to Python Supply Chain Security" [source]
- sbomify Python SBOM guide - https://sbomify.com/guides/python/ [source]
Children
- Dependency auditing (pip-audit — PyPA, OSV + PyPA Advisory DB, -r/PEP 751 lockfile input, --fix, cyclonedx output) (frontier)
- SBOM generation (CycloneDX vs SPDX, cyclonedx-py, syft, lib4sbom, PEP 770 SBOMs-in-wheels) (frontier)
- Static application security testing (bandit — PyCQA AST plugins, B-codes, severity×confidence, [tool.bandit]/# nosec, baseline workflow) (frontier)
- Provenance — sigstore/PEP 740 digital attestations + Trusted Publishing OIDC (keyless signing, gh-action-pypi-publish ≥1.11.0, pypi-attestations verify) (frontier)
- Hash-pinned reproducible installs (pip --require-hashes, pip-compile --generate-hashes, uv lock) (frontier)
- 2025-2026 threat landscape (typosquatting, dependency confusion, PyPI phishing, GhostAction, Shai-Hulud) (frontier)
- CI/pipeline hardening (OpenSSF Scorecard, zizmor, SHA-pinned Actions) (frontier)
Frontier under this node: 2025-2026 threat landscape (typosquatting, dependency confusion, PyPI phishing, GhostAction, Shai-Hulud), CI/pipeline hardening (OpenSSF Scorecard, zizmor, SHA-pinned Actions), Dependency auditing (pip-audit — PyPA, OSV + PyPA Advisory DB, -r/PEP 751 lockfile input, --fix, cyclonedx output), Hash-pinned reproducible installs (pip --require-hashes, pip-compile --generate-hashes, uv lock), Provenance — sigstore/PEP 740 digital attestations + Trusted Publishing OIDC (keyless signing, gh-action-pypi-publish ≥1.11.0, pypi-attestations verify), SBOM generation (CycloneDX vs SPDX, cyclonedx-py, syft, lib4sbom, PEP 770 SBOMs-in-wheels), Static application security testing (bandit — PyCQA AST plugins, B-codes, severity×confidence, [tool.bandit]/# nosec, baseline workflow)