Vulnerability scan misses advisories affecting versions allowed by the declared constraint #39

Closed
opened 2026-09-13 20:28:45 +00:00 by agent-pi · 2 comments
Collaborator

Problem

zaojun's vulnerability data comes from the package-level PyPI JSON endpoint (/pypi/{package}/json). On that endpoint the vulnerabilities array lists only advisories affecting the latest published release. Advisories that affect older versions — versions still permitted by the project's own declared constraint — are invisible to zaojun.

So when the latest release is already fixed but the constraint still admits vulnerable versions, zaojun reports nothing: no advisory, no exit code.

Example

longwei 2.1.2 declared httpx2[http2]~=2.3. Six advisories affect httpx2 2.3.0:

  • PYSEC-2026-3846, GHSA-8xx6-hgc6-gc2m (fixed in 2.12.0)
  • PYSEC-2026-3848, PYSEC-2026-3849, GHSA-h4x7-gw46-3wm6, GHSA-pf96-p4fj-6566 (fixed in 2.11.0)

Running zaojun 1.7.5 against longwei 2.1.2's pyproject.toml shows ✅ httpx2~=2.3 is up to date with no advisory output (exit 0 with the project's library = true config — but the gap is independent of library mode: a --no-library run reports the compatible update and still no advisories).

Live evidence (2026-09-13):

  • GET https://pypi.org/pypi/httpx2/json → latest 2.12.0, vulnerabilities: []
  • GET https://pypi.org/pypi/httpx2/2.3.0/json → the six advisories above

The cached entry mirrors the empty list, so cache behaviour is faithful — the blind spot is in the data source, not the cache.

Impact

A constraint that admits known-vulnerable versions passes silently. In the longwei case the vulnerable-range signal came only from an external scanner (pysentry), which motivated the 2.1.3 floor bump to ~=2.12; zaojun's own scan gave no hint. Anyone auditing "does my declared range include vulnerable versions?" gets a false all-clear.

## Problem zaojun's vulnerability data comes from the package-level PyPI JSON endpoint (`/pypi/{package}/json`). On that endpoint the `vulnerabilities` array lists only advisories affecting the **latest published release**. Advisories that affect older versions — versions still permitted by the project's own declared constraint — are invisible to zaojun. So when the latest release is already fixed but the constraint still admits vulnerable versions, zaojun reports nothing: no advisory, no exit code. ## Example longwei 2.1.2 declared `httpx2[http2]~=2.3`. Six advisories affect httpx2 2.3.0: - PYSEC-2026-3846, GHSA-8xx6-hgc6-gc2m (fixed in 2.12.0) - PYSEC-2026-3848, PYSEC-2026-3849, GHSA-h4x7-gw46-3wm6, GHSA-pf96-p4fj-6566 (fixed in 2.11.0) Running zaojun 1.7.5 against longwei 2.1.2's `pyproject.toml` shows `✅ httpx2~=2.3 is up to date` with no advisory output (exit 0 with the project's `library = true` config — but the gap is independent of library mode: a `--no-library` run reports the compatible update and still no advisories). Live evidence (2026-09-13): - `GET https://pypi.org/pypi/httpx2/json` → latest 2.12.0, `vulnerabilities: []` - `GET https://pypi.org/pypi/httpx2/2.3.0/json` → the six advisories above The cached entry mirrors the empty list, so cache behaviour is faithful — the blind spot is in the data source, not the cache. ## Impact A constraint that admits known-vulnerable versions passes silently. In the longwei case the vulnerable-range signal came only from an external scanner (pysentry), which motivated the 2.1.3 floor bump to `~=2.12`; zaojun's own scan gave no hint. Anyone auditing "does my declared range include vulnerable versions?" gets a false all-clear.
Author
Collaborator

Plan of attack:

  • Data: when a dependency spec has a determinable lower bound (>=, ==, ~=), fetch the version-level PyPI JSON for that lower-bound version ({index}/{package}/{version}/json) and read its vulnerabilities. Cached under a distinct key (name@version) with the same 24 h TTL; indexes that don't serve per-version vulnerabilities degrade silently.
  • Classification: new result fields — range_vulnerabilities (non-withdrawn advisories affecting the lower-bound version) plus a suggested floor (max over advisories of their minimum fixed_in). needs_update=True, exit code 1. Independent of --library and --compat-ok: security is not update convenience.
  • Suppression: existing vuln-ignore rules apply to range advisories unchanged.
  • Output: text mode renders a distinct block (floor 2.3.0 admits N advisories; raising the floor to X clears them); JSON gains range_vulnerabilities. README, docs/usage/*, and Release-Notes.md updated (user-facing feature).
  • Tests first (pytest): fixture per-version payloads — flag case, clean case, no-lower-bound skip, index without vulnerabilities field, cache round-trip, ignore rules.
  • Known limitation (documented, possible follow-up): advisories introduced after the floor (affect only versions above the lower bound) are not caught by a lower-bound-only fetch; a full-range OSV query would be needed for those.

Landing together with #37 and the Release-Notes reset recipe split in one branch/PR.

Plan of attack: - **Data**: when a dependency spec has a determinable lower bound (`>=`, `==`, `~=`), fetch the *version-level* PyPI JSON for that lower-bound version (`{index}/{package}/{version}/json`) and read its `vulnerabilities`. Cached under a distinct key (`name@version`) with the same 24 h TTL; indexes that don't serve per-version `vulnerabilities` degrade silently. - **Classification**: new result fields — `range_vulnerabilities` (non-withdrawn advisories affecting the lower-bound version) plus a suggested floor (max over advisories of their minimum `fixed_in`). `needs_update=True`, exit code 1. Independent of `--library` and `--compat-ok`: security is not update convenience. - **Suppression**: existing `vuln-ignore` rules apply to range advisories unchanged. - **Output**: text mode renders a distinct block (floor `2.3.0` admits N advisories; raising the floor to `X` clears them); JSON gains `range_vulnerabilities`. README, `docs/usage/*`, and `Release-Notes.md` updated (user-facing feature). - **Tests first** (pytest): fixture per-version payloads — flag case, clean case, no-lower-bound skip, index without vulnerabilities field, cache round-trip, ignore rules. - **Known limitation** (documented, possible follow-up): advisories introduced *after* the floor (affect only versions above the lower bound) are not caught by a lower-bound-only fetch; a full-range OSV query would be needed for those. Landing together with #37 and the Release-Notes reset recipe split in one branch/PR.
Author
Collaborator

Closed by PR #41 (merged): range-advisory scanning implemented, dependency refresh + security re-verification done, Release-Notes reset recipe split out.

Closed by PR #41 (merged): range-advisory scanning implemented, dependency refresh + security re-verification done, Release-Notes reset recipe split out.
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
marvin8/zaojun#39
No description provided.