Skip to content

feat(versions): compare each ecosystem with its own version scheme (PEP 440 for PyPI) - #1397

Open
alex-powell wants to merge 1 commit into
OWASP:mainfrom
alex-powell:feat/pep440-version-scheme
Open

alex-powell wants to merge 1 commit into
OWASP:mainfrom
alex-powell:feat/pep440-version-scheme

Conversation

@alex-powell

@alex-powell alex-powell commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1401
Refs #672

What changed and why

PyPI versions now order with PEP 440, through the helpers that already live in src/utils/version.ts. compareVersions, looksLikeVersion, isPreReleaseVersion, and normalizeAdvisoryVersion keep their names. A caller that has an ecosystem passes it. PyPI and pypi select PEP 440. A missing ecosystem, and every other ecosystem, stays on semver.

npm callers are unchanged. sort and filter still receive these functions directly. The extra argument ignores the numeric index those callbacks pass, so npm ordering stays the same.

The scanner and the offline database pass the ecosystem they already store. A PyPI advisory whose only fix is 2026.3.post1 reports that fix. An unreadable PyPI boundary throws, so a miss stays visible.

This change is the comparison step. Lockfile parsing, package URLs, exit codes, SBOMs, and the offline database shape are later steps.

Done when

Every form in the table parses, and the two orderings come out right, with tests for each. tests/utils/pep440-helpers.test.ts calls the helpers with ecosystem PyPI.

Form Test
2.9.0.post0 parses 2.9.0.post0 and sorts it after 2.9.0
2026.3.post1 parses 2026.3.post1 and sorts it after 2026.3
1!2.0.0 parses the epoch form 1!2.0.0
4.0.0.dev1 parses 4.0.0.dev1 and sorts it before 4.0.0
1.0.0rc1 parses 1.0.0rc1
1.0.0a1 parses 1.0.0a1
2.0.0b2 parses 2.0.0b2
1.0.0.post1.dev2 parses 1.0.0.post1.dev2
1.2.3+local after 1.2.3 sorts 1.2.3+local after 1.2.3
1.0.0rc1 < 1.0.0 orders 1.0.0rc1 before 1.0.0
1!0.1.0 > 2.0.0 orders 1!0.1.0 after 2.0.0

tests/scanner/pep440-fix-version.test.ts, "reports 2026.3.post1 when that is the only fix", calls findFirstFixedVersion for pytz at 2026.3 and expects 2026.3.post1.

The full suite passes with no existing test changed. git diff upstream/main -- tests/ lists only new files.

 tests/advisory/pep440-local-db.test.ts          |  99 +++++++
 tests/scanner/pep440-fix-version.test.ts        |  94 ++++++
 tests/scripts/check-pep440-requirements.test.ts | 114 +++++++
 tests/utils/pep440-helpers.test.ts              |  67 +++++
 tests/utils/pep440.test.ts                      | 376 ++++++++++++++++++++++++
 5 files changed, 750 insertions(+)

npm test on this commit passed 183 suites and 2513 tests. That run is the npm parity check. tests/helpers.test.ts, tests/utils/version-extensions.test.ts, tests/output/parent-update-display.test.ts, and tests/remediation/fix-commands-parent-update-merge.test.ts match upstream c5a5180.

No new runtime dependency. git diff upstream/main -- package.json is empty.

Checked against PyPI and OSV

Node v22.23.4, from a checkout of this branch.

npm ci
npx tsx scripts/check-pep440.ts --requirements path/to/requirements.txt
python3 -m pip install 'packaging==26.3'
npx tsx scripts/check-pep440.ts --sample

--requirements prints JSON for each exact pin. Comments, blank lines, extras, and environment markers are ignored. Each pin is queried on OSV and judged with PEP 440.

--sample compares PyPI release lists and OSV ranges for python-dateutil, pytz, numpy, django, requests, urllib3, jinja2, pillow, cryptography, setuptools, idna, and packaging with packaging 26.3. 2.9.0, 2.9.0.post0, 2026.3, 2026.3.post1, 1.0rc1, 1!2.0, 1.22, and 1.22.0 are checked against the same oracle and are left out of the seen and parsed totals.

Re-run on 52d1792.

2.9.0 -> 2.9.0, 2.9.0.post0 -> 2.9.0.post0, compare=-1
2026.3 -> 2026.3, 2026.3.post1 -> 2026.3.post1, compare=-1
versions seen: 2074
versions parsed: 2029
versions rejected: 45
parse disagreements: 0
ordering mismatches: 0
range verdicts: 306617
range mismatches: 0
advisories checked: 671

Both parsers reject the same 45 pytz legacy tags, 2004d through 2013d. The sampled release lists contain no epoch. Epoch ordering is covered by 1!2.0 in the explicit check and by the unit tests.

Verified against a production requirements.txt

I ran --requirements against the pinned requirements.txt of the production Python backend I mentioned in #672, and compared it with pip-audit 2.10.1 on the same file. That run used Node 20.19.2 at c65cdc5.

this branch pip-audit 2.10.1
pins checked 20 20
skipped 0 0
unparseable 0 n/a
affected pins 1 1

Both flag opentelemetry-api==1.45.0. This branch reports GHSA-xcq7-p3pf-p88h. pip-audit reports CVE-2026-114603, fixed in 1.45.1. OSV lists them as aliases of the same advisory, so the two results agree. The other 19 pins are unaffected in both.

That file has no post, dev, pre-release, epoch, or two-part versions, so this run confirms parity on a real tree. The new forms are covered by the sample run above. The one unusual form is the single-segment date version pdfminer.six==20260107, which parses.

compareVersions, looksLikeVersion, isPreReleaseVersion, and
normalizeAdvisoryVersion keep their names. Passing a PyPI ecosystem
selects PEP 440. Other callers stay on semver, including sort and
filter callbacks that pass an index.

A PyPI advisory whose only fix is 2026.3.post1 is reported.
scripts/check-pep440.ts judges a pinned requirements file against OSV
and compares a PyPI sample with packaging.
@cursor
cursor Bot force-pushed the feat/pep440-version-scheme branch from c65cdc5 to 52d1792 Compare October 25, 2026 03:14
@sonukapoor
sonukapoor requested a balanced review from Copilot October 25, 2026 13:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Order-dependent advisory evaluation can miss vulnerable versions, its reference check repeats the error, and scanner fix selection still discards invalid PyPI fixed boundaries.

3 open issues
What changed in this PR

Adds ecosystem-aware version comparison for experimental Python support, using PEP 440 for PyPI while preserving the default semver behavior.

Changes:

  • Adds PEP 440 parsing, normalization, ordering, and version-scheme selection.
  • Passes ecosystem information through scanner fix selection and offline advisory matching.
  • Adds regression tests and a requirements/PyPI validation script without new runtime dependencies.
File Description
tsconfig.test.json Includes the validation script in test compilation.
tests/​utils/​pep440.test.ts Tests parsing, ordering, schemes, and advisory evaluation.
tests/​utils/​pep440-helpers.test.ts Tests PyPI dispatch through existing version helpers.
tests/​scripts/​check-pep440-requirements.test.ts Tests requirements parsing and advisory reports.
tests/​scanner/​pep440-fix-version.test.ts Tests PyPI fix selection and npm compatibility.
tests/​advisory/​pep440-local-db.test.ts Tests offline PyPI matching and invalid boundaries.
src/​utils/​versions/​scheme.ts Defines the version-scheme contract and parse error.
src/​utils/​versions/​registry.ts Selects the scheme by ecosystem.
src/​utils/​versions/​pep440.ts Implements PEP 440 parsing and comparison.
src/​utils/​versions/​advisory.ts Adds advisory-bound and PyPI event evaluation.
src/​utils/​version.ts Adds ecosystem-aware dispatch to existing helpers.
src/​scanner.ts Uses ecosystem-aware comparison for fix selection.
src/​advisory/​local-db.ts Applies ecosystem-aware offline range matching.
scripts/​check-pep440.ts Adds requirements reports and Python reference comparisons.

Review effort: Balanced

if (event.fixed !== undefined) {
const fixed = boundary(event.fixed);
if (!fixed) unparsed.push({ field: "fixed", value: event.fixed });
else if (pep440Scheme.compare(version, fixed) >= 0) affected = false;
Comment thread scripts/check-pep440.ts
Comment on lines +354 to +355
elif ver >= parsed:
affected = False
Comment thread src/scanner.ts
Comment on lines +1131 to +1132
const fixed = normalizeAdvisoryVersion(event.fixed, ecosystem);
if (!fixed || isPreReleaseVersion(fixed, ecosystem)) continue;

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Forced process termination can truncate piped JSON reports while still returning a successful exit status.

4 open issues

Review effort: Balanced

Comment thread scripts/check-pep440.ts
process.exit(1);
}
const code = requirements !== undefined ? await runRequirements(requirements) : await runSample();
process.exit(code);

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(python): read and order PEP 440 versions

2 participants