The go-to resource for upgrading Python, Django, Flask, and your dependencies.

Securing Your Python Supply Chain: Scanning poetry.lock for Compromised Maintainers


PyPI has suspended over 200 accounts between 2023-2025 due to takeovers that enable supply chain attacks. We can write a CLI script that parses poetry.lock, queries the PyPI API for maintainers, and cross-references them against known compromised accounts. Note that this approach has limitations, such as relying on manual curation of bad actors and PyPI rate limits.

Why You Might Want to Scan for Compromised Maintainers\n\nBefore we look at the script itself, let’s step back and consider why maintainer compromises matter for Python projects using Poetry.\n\nMaintainer account takeovers represent a serious supply chain risk. When someone gains access to a maintainer’s PyPI account, they can upload malicious versions of legitimate packages. PyPI suspended over 200 such accounts from 2023 to 2025, often linked to data exfiltration or cryptocurrency mining payloads.\n\nA typical poetry.lock file lists hundreds of transitive dependencies. If even one comes from a compromised maintainer, your project could introduce malware that vulnerability scanners miss—particularly code uploaded before any CVE exists.\n\nTools like poetry audit and pip-audit excel at known vulnerabilities, but they generally don’t check maintainer history. Similarly, safety focuses on published advisories and malware signatures, leaving a gap for proactive maintainer verification.\n\n| Attack Vector | Gap in Standard Tools | What Maintainer Scanning Addresses |\n|---------------|-----------------------|-----------------------------------|\n| Account Takeover (Token Theft) | No maintainer data in lockfiles | Queries PyPI API for maintainer names |\n| Typosquatting (pre-compromise) | Vulnerability DB lags | Cross-reference with curated bad lists |\n| Backdoors in Trusted Packages | Partial malware detection | Flags suspicious maintainer histories |

Maintaining the Known Compromised List\n\nThe effectiveness of this script depends on keeping the KNOWN_COMPROMISED dictionary up to date. We can curate it from PyPI suspension announcements, Hacker News discussions, and security reports.\n\nFor illustration, here’s an example structure:\n\npython\nKNOWN_COMPROMISED = {\n # Illustrative examples; replace with real data from:\n # - PyPI security blog: https://pypi.org/security/\n # - PyPA advisory DB: https://github.com/pypa/advisory-database\n # - Malware reports: https://pyup.io/security/\n # "example-bad": "suspended 2024 for malware upload",\n}\n\n\nYou might host this as a JSON file on GitHub and fetch it automatically, updating quarterly or after major incidents.

The Scanning Script\n\nNow let’s examine the script. It performs three main tasks:\n\n1. Parses the poetry.lock TOML file to extract package names and versions.\n2. For each package, queries the PyPI JSON API to retrieve maintainer information.\n3. Checks if any maintainer matches our known compromised list, reporting risks.\n\nHere’s the complete script (save as maintainer-scan.py and make executable with chmod +x):\n\n```python

#!/usr/bin/env python3 import toml import requests import sys from pathlib import Path from typing import Dict, Any

KNOWN_COMPROMISED = {\n # Populate with real compromised maintainers from:\n # PyPI security announcements: https://pypi.org/security/\n # PyPA advisory database: https://github.com/pypa/advisory-database\n # Third-party malware trackers like pyup.io/security\n # Example format (research actual cases):\n # “known-bad-maintainer”: “suspended 2024 for malware upload”,\n}

def load_poetry_lock(lock_path: Path) -> list[Dict[str, Any]]:\n """Parse poetry.lock TOML and return list of packages."""\n with lock_path.open(“r”) as f:\n data = toml.load(f)\n return data.get(“package”, [])

def get_maintainers(pkg_name: str) -> list[Dict[str, str]]:\n """Fetch maintainers from PyPI JSON API. Returns empty list on failure."""\n try:\n resp = requests.get(\n f”https://pypi.org/pypi/{pkg_name}/json”,\n timeout=10, # Increased for reliability\n headers={“User-Agent”: “Maintainer-Scanner/1.0”}, # Polite API usage\n )\n resp.raise_for_status()\n info = resp.json()[“info”]\n return info.get(“maintainers”, [])\n except (requests.RequestException, KeyError, ValueError) as e:\n # Silent fail for non-critical packages; log in production\n print(f”Warning: Could not fetch maintainers for {pkg_name}: {e}”, file=sys.stderr)\n return []

def scan_maintainers(packages: list[Dict[str, Any]]) -> list[str]: risky = [] for pkg in packages: name = pkg[“name”] version = pkg[“version”] maintainers = get_maintainers(name) for m in maintainers: mname = m.get(“name”, "").lower() if mname in [k.lower() for k in KNOWN_COMPROMISED]: risky.append(f”RISKY: {name}=={version} by {m[‘name’]} ({KNOWN_COMPROMISED.get(mname.title(), ‘unknown’)})”) return risky

if name == “main”: lock_path = Path(“poetry.lock”)\n if not lock_path.exists():\n print(“Error: poetry.lock not found. Please run from a Poetry project root.”, file=sys.stderr)\n sys.exit(1)\n packages = load_poetry_lock(lock_path) risks = scan_maintainers(packages) if risks: print(“\n”.join(risks)) sys.exit(1) print(“No known compromised maintainers found.”)


### Install & Run
```bash
pip install toml requests
python maintainer-scan.py

PyPI enforces rate limits (typically around 10 requests per second per IP). The script uses timeouts and polite headers, but for lockfiles with hundreds of packages, you may hit limits—consider adding time.sleep(0.1) between requests or running in batches.\n\n## Limitations and Complementary Tools\n\nThis script provides targeted maintainer verification, but it has notable limitations:\n\n- Manual curation: Relies on you maintaining the KNOWN_COMPROMISED list; misses new compromises until updated.\n- Name matching: Uses exact username matches (case-insensitive); can’t detect account sales/renames or indirect control.\n- Performance: API calls scale poorly with large lockfiles (100+ unique packages may take minutes).\n- False negatives: Legitimate packages with suspicious history or zero-day takeovers won’t trigger.\n- Not a vuln scanner: Doesn’t detect CVEs, malware signatures, or license issues.\n\nFor comprehensive supply chain security, combine with:\n\n| Tool | Maintainer Check | Known Vulnerabilities | Malware Signatures |\n|------|------------------|-----------------------|-------------------|\n| This Script | Yes | No | No |\n| poetry audit / pip-audit | No | Yes | Partial |\n| safety check --malware | No | Yes | Yes |\n\nEach tool addresses different gaps; running them together in CI provides broader coverage, though with increased runtime.\n\n## CI/CD Integration\n\nBefore integrating, commit a known-good poetry.lock to source control. The scan fails the build (exit code 1) if risks are found.\n\n### GitHub Actions\n\nAdd this step before poetry install to catch issues early:\n\nyaml\n- name: Scan for Compromised Maintainers\n run: |\n pipx install toml requests # Or use poetry/pip for deps\n python maintainer-scan.py\n continue-on-error: false\n\n\nVerify success with ${{ steps.scan.outcome }} in later steps if needed.\n\n### pre-commit\n\nyaml\n- repo: local\n hooks:\n - id: poetry-maintainer-scan\n name: Scan poetry.lock for compromised maintainers\n entry: bash -c 'pipx install toml requests && python maintainer-scan.py'\n language: system\n files: '^poetry\\.lock$'\n pass_filenames: false\n\n\npipx isolates deps; adjust for your environment. Test with pre-commit run --all-files.

Advanced: Auto-Update Blacklist + Malware Check

Fetch live list:

import requests
blacklist_url = "https://raw.githubusercontent.com/user/pypi-compromised/maintainers.json"
KNOWN_COMPROMISED = requests.get(blacklist_url).json()

Combine w/ poetry audit + safety check --malware poetry.lock --format json:

ToolMaintainer CheckVulnMalware
Maintainer Scan ScriptYesNoNo

Mitigation: Lock + Verify

  • poetry lock --no-update
  • Pin transitive: poetry add pkg@1.2.3
  • Migrate to sigstore: pip --require-hashes

This script provides one layer of supply chain verification; consider combining it with other tools for comprehensive security. Feel free to fork and contribute additional known compromised maintainers.


Related: 23. poetry audit vs pip-audit, 24. PCI DSS deps

Sponsored by Durable Programming

Need help maintaining or upgrading your Python application? Durable Programming specializes in keeping Python apps secure, performant, and up-to-date.

Hire Durable Programming