Python Dependency Scanner
Unlike the npm scanner, this one needs no network. It reads requirements*.txt and pyproject.toml, pulls a package name and a version out of each line with one regex, and compares the version to a built-in table of 13 packages with a three-part integer compare. A line with no version at all is reported as unpinned.
One line, one verdict
For every dependency line the scanner parses a name, an optional operator and an optional version. If there is no version it reports "unpinned" and stops. Otherwise it looks the normalised name up in the table and tests the version against each range for that package. The actor is the scanner reading a text file; nothing is installed and nothing is contacted.
1Five lines of one requirements.txt. Each is parsed by ^([a-zA-Z0-9_-]+)\s*(?:([=<>!~]+)\s*([.\w]+))?: a name, then an optional operator and version. Names are lower-cased with - and _ removed before the table lookup.
2Django==3.2.0 sits below two ranges in the table, <4.2.8 (HIGH) and <3.2.23 (CRITICAL). Each matching range is its own finding, so one line gives two, at confidence 0.85, with the file and line number.
3PyJWT==2.3.0 is below 2.4.0, the algorithm-confusion fix. CRITICAL, one finding.
4A name with no version is LOW, "Unpinned Python dependency", confidence 0.60. Any release could be installed, including a vulnerable one. The table is not consulted for this line.
5flask>=2.3.2 parses to version 2.3.2, which is not below the fix version, so nothing is reported. The operator is read but never used in the decision.
6Blank lines, comments and any line starting with - (-r, -e, --index-url) are skipped before parsing.
The version compare
Every range in the table has the form <X.Y.Z, and is_version_vulnerable supports nothing else. It splits both versions on dots, keeps three parts, pads with zero and compares the integer lists.
1The version in the file and the range threshold are each cut at the dots to three parts. A short version gets zeros appended, then Python compares the two lists element by element.
2django==4.2 has only two parts, so it becomes [4, 2, 0] against the threshold [4, 2, 8] of the range <4.2.8. The first two match and 0 is below 8, so it is vulnerable. Missing parts count as zero.
310.0.0rc1 splits into 10, 0, 0rc1 and int("0rc1") raises. The scanner catches that and answers "not vulnerable". Pre-release and any non-numeric suffix are silently safe. Four-part versions are cut to their first three.
4The operator is not part of the compare. flask>=2.0.0 is treated as the single version 2.0.0, flagged against <2.3.2, and titled flask==2.0.0, although pip would install the newest release that satisfies >=.
5django<4.2.8 reads as the version 4.2.8. A version equal to the threshold is not below it, so nothing is reported, even though the written range admits vulnerable 4.2.7.
Which files are parsed
The scanner walks every file in the repository index and decides by file name alone. Two different parsers handle the two formats, and the second is deliberately simple.
1Every file in the repository index is tested by name. Nothing is read from a lock file or an environment.
2Requirements files are any file named requirements.txt or starting with requirements and ending in .txt, in any directory. requirements-dev.txt counts.
3Any file whose path ends in pyproject.toml is parsed by a separate, line-based reader.
4Not read at all: files in a requirements/ directory under other names such as base.txt, setup.py, Pipfile, and lock files. Dependencies declared only there are never compared.
5The pyproject reader turns a block on at dependencies = [ or [project.dependencies], reads lines that start with a double quote, and turns it off at the next line starting with [. It never reports unpinned; only a pinned version that matches the table counts.
Why it matters
Python has the same dependency problem as npm. PyJWT before 2.4.0 (CVE-2022-29217) allows an algorithm-confusion attack that forges tokens; PyYAML before 6.0.1 allows arbitrary code execution through yaml.load(); unpatched Django carries denial-of-service CVEs (CVE-2023-46695, CVE-2023-41164). Unpatched dependencies are the most common attack vector across all languages.
Log4Shell was Java-specific, but the same class of flaw, remote code execution through deserialization, exists in Python packages such as PyYAML and Pickle.
Vulnerable vs fixed code
# requirements.txt
Django==3.2.0 # two CVE ranges match
PyJWT==2.3.0 # CVE-2022-29217, token forgery
PyYAML==5.4 # code execution via yaml.load()
requests # unpinned: any version installs# Check for vulnerabilities
pip audit
# Upgrade to safe versions
pip install --upgrade django flask pyjwt
# Pin all dependencies
pip freeze > requirements.txt
# requirements.txt after the fix
Django==4.2.8
PyJWT==2.4.0
PyYAML==6.0.1
requests==2.31.0
# Use Dependabot or Renovate for automated updatesThe table and its limits
Thirteen packages, each with a single range of the form <X.Y.Z, except Django which has two. Package names are matched after lower-casing and removing - and _.
| Package | Vulnerable range | Severity | Note |
|---|---|---|---|
| django | <4.2.8 | High | CVE-2023-46695, DoS via file uploads |
| django | <3.2.23 | Critical | CVE-2023-41164, DoS via URI validation |
| flask | <2.3.2 | High | CVE-2023-30861, cookie handling |
| fastapi | <0.109.1 | Medium | CVE-2024-24762, DoS via multipart parsing |
| requests | <2.31.0 | Medium | CVE-2023-32681, proxy credential leak |
| cryptography | <41.0.6 | High | CVE-2023-49083, NULL pointer dereference |
| pyjwt | <2.4.0 | Critical | CVE-2022-29217, algorithm confusion |
| sqlalchemy | <1.4.49 | Medium | SQL injection via textual SQL expressions |
| jinja2 | <3.1.3 | Medium | CVE-2024-22195, XSS via xmlattr |
| werkzeug | <3.0.1 | High | CVE-2023-46136, DoS via multipart data |
| pillow | <10.0.1 | High | CVE-2023-44271, DoS via crafted images |
| urllib3 | <2.0.7 | Medium | CVE-2023-45803, cookie leak on redirect |
| paramiko | <3.4.0 | Critical | CVE-2023-48795, Terrapin SSH truncation |
| pyyaml | <6.0.1 | Critical | arbitrary code execution via yaml.load() |
The table is frozen in the source and was not updated against newer advisories; a package outside it is never reported, only its missing pin. Ranges with >=, ~= or != are compared as plain versions. The scanner's own docstring lists requirements/*.txt, but the name test accepts only files whose name starts with requirements. Findings carry the file path and line number; the unpinned finding is LOW at 0.60, so the severity range is Critical to Low. Some lines that are not plain pins also reach the unpinned branch: requests[security]==2.0 (extras) and git+https://… are reported as unpinned requests and git. For broader Python coverage see OSV dependencies, and for npm see npm dependencies.