Skip to content

fix(supply-chain): treat only == / <= as version pins in requirements.txt (#294) - #302

Open
Mark2Mac wants to merge 2 commits into
NVIDIA:mainfrom
Mark2Mac:fix/requirements-specifier-treated-as-pin
Open

fix(supply-chain): treat only == / <= as version pins in requirements.txt (#294)#302
Mark2Mac wants to merge 2 commits into
NVIDIA:mainfrom
Mark2Mac:fix/requirements-specifier-treated-as-pin

Conversation

@Mark2Mac

Copy link
Copy Markdown
Contributor

What

_extract_packages_from_requirements recorded the captured version for any operator, so a floor like pillow>=10.0.0 was stored as the exact release 10.0.0. The OSV/CVE lookup then attributed that release's vulnerabilities to a dependency that pins nothing — a false CRITICAL supply-chain finding.

Only == and <= bind a dependency to a concrete, CVE-checkable release. >=, >, !=, ~= are floors/ranges. The fix applies the same guard already used by _extract_packages_from_setup_py (m.group(2) in ("==", "<=")), so the two extractors agree.

Reproduction (before)

$ printf 'pillow>=10.0.0\n' > requirements.txt && printf -- '---\nname: r\ndescription: r\n---\n' > SKILL.md
$ skillspector scan . --no-llm --format json -o out.json
$ jq -r '.issues[]|select(.category=="Supply Chain")|"[\(.severity)] \(.finding)"' out.json
[LOW] pillow>=10.0.0
[CRITICAL] pillow==10.0.0   # <- the floor, rewritten as a pin, with 10.0.0's CVEs

Test

Added test_extract_packages_requirements_specifier_is_not_a_pin: asserts ==/<= keep the version while >=, ~=, != and bare names yield None. Verified red before the fix (pillow came back '10.0.0'), green after. Full existing suite for the analyzer stays green (320 passed).

Fixes #294

@rng1995 rng1995 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Automated SkillSpector Review]

Requesting changes because the new guard still treats non-exact constraints as concrete installed versions. <=8.1.0 admits every earlier release, and ==1.* is also a wildcard range, so either can still be sent to the vulnerability lookup as a version the dependency may not install. Please retain only truly exact pins, or model ranges explicitly, and add regressions for both cases.

# version makes a floor like "pillow>=10.0.0" report as "pillow==10.0.0" and
# attributes that release's CVEs to an unpinned dependency. Mirrors the guard
# already used by _extract_packages_from_setup_py.
version = m.group(3) if m.group(2) in ("==", "<=") else None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

<= is still a range, not a concrete release: pkg<=8.1.0 can install any earlier version, so keeping 8.1.0 here continues the same false CVE attribution. The regex also accepts wildcard equality such as pkg==1.*, which is not exact either. Please retain only a non-wildcard exact equality (or model ranges explicitly), and update both extractors consistently.

@Mark2Mac

Copy link
Copy Markdown
Contributor Author

Gentle ping — happy to rebase or adjust if the approach needs changing.

Re-checked against v2.5.0: the behaviour is still there. _extract_packages_from_requirements keeps m.group(3) for any operator (static_patterns_supply_chain.py:430), while _extract_packages_from_pyproject right below it already guards with m.group(2) in ("==", "<=") (:498) — this PR just makes the two consistent.

Field data since opening it: scanning 65 skill/plugin units, every SC4 finding in the corpus came from a range being read as a pin (or from the version-less variant I just filed as #318). No manifest actually pinned a vulnerable release.

Mark2Mac added 2 commits July 30, 2026 23:11
….txt

`_extract_packages_from_requirements` kept the captured version for any operator,
so a floor like `pillow>=10.0.0` was recorded as the exact release `10.0.0` and
the OSV/CVE lookup attributed that version's vulnerabilities to an unpinned
dependency — a false CRITICAL on a requirements file that pins nothing.

Only `==` and `<=` bound the dependency to a concrete, CVE-checkable release;
`>=`, `>`, `!=`, `~=` are floors/ranges. This mirrors the guard already present
in `_extract_packages_from_setup_py` (`m.group(2) in ("==", "<=")`), so the two
extractors now agree.

Added a regression test asserting `>=`, `~=`, `!=` and bare names yield
version=None while `==` / `<=` keep the version.

Fixes NVIDIA#294

Signed-off-by: Mark2Mac <Mark2Mac@users.noreply.github.com>
Addresses the review on NVIDIA#302: the previous guard still admitted non-exact
constraints. `<=8.1.0` matches every earlier release and `==1.*` is a wildcard,
so both were handed to the vulnerability lookup as a version the dependency may
never install.

A vulnerability lookup answers "is THIS release affected?", which is only
meaningful when the manifest admits exactly one release. That predicate is now
explicit and shared instead of being re-derived at each call site:

- `_pinned_version` (PEP 440): only `==` with a fully concrete version. Floors,
  caps, exclusions, compatible releases and wildcard equality yield None.
- `_pinned_npm_version` (semver): only a bare `x.y.z`. npm defaults to caret
  ranges, so `"^1.8.3"` was being stripped into the concrete release `1.8.3`.

Applied to all three extractors — requirements.txt, pyproject.toml and
package.json — because the objection in the review holds verbatim for the two
that were not touched by the original patch.

Note for the maintainer: dropping these specifiers moves more dependencies to
version=None, which NVIDIA#318 shows is currently reported as CRITICAL carrying the
package's worst-ever advisory. The two fixes are complementary; happy to send
the severity side as a separate PR.

Regressions cover both cases named in the review (`<=` and `==1.*`) plus the
npm caret/tilde/wildcard/range forms.

Signed-off-by: Mark2Mac <Mark2Mac@users.noreply.github.com>
@Mark2Mac
Mark2Mac force-pushed the fix/requirements-specifier-treated-as-pin branch from f97651a to 15788af Compare July 30, 2026 21:17
@Mark2Mac

Copy link
Copy Markdown
Contributor Author

Rebased onto main (past #305) and reworked per the review — thank you, the objection was correct and it goes further than the original patch did.

What changed. The predicate is now explicit and shared instead of being re-derived at each call site:

  • _pinned_version (PEP 440): only == with a fully concrete version. <=8.1.0, <8.1.0, >=, >, !=, ~= and ==1.* all yield None.
  • _pinned_npm_version (semver): only a bare x.y.z. ^1.8.3, ~4.18.0, 1.x, * and >=1.2.3 <2.0.0 yield None.

Applied to all three extractors — requirements.txt, pyproject.toml, package.json — because the objection holds verbatim for the two the original patch did not touch. Regressions cover both cases you named plus the npm forms. Full unit suite green.

One thing worth flagging before this lands, because I measured it. On its own, this PR does not remove a single false positive — it only changes their label. With the patch applied:

# package.json:  "shell-quote": "^1.8.3"
CRITICAL SC4 shell-quote        # was: CRITICAL SC4 shell-quote==1.8.3

The fake pin is gone, the CRITICAL is not. Dropping the specifier moves the dependency to version=None, and _sc4_from_osv then queries OSV by name alone and takes the package's worst-ever advisory as the finding's severity. That is #318, and the two fixes are complementary halves of the same defect: this PR stops inventing a version, #318 stops claiming a match that was never made.

I have opened that as a separate PR. Measured together on the same inputs:

manifest today #302 alone #318 alone both
setuptools>=61 CRITICAL CRITICAL LOW LOW
"shell-quote": "^1.8.3" CRITICAL ==1.8.3 CRITICAL CRITICAL ==1.8.3 LOW
jinja2==2.4.1 (really vulnerable) HIGH HIGH HIGH HIGH

The last row is the one I care about most: real version matches are untouched.

Happy to squash, split, or reorder these two in whatever way is easiest to review.

@Mark2Mac

Copy link
Copy Markdown
Contributor Author

Cross-PR note, from merging all my open PRs together locally and running them on real corpora.

1. #323 touches the same function. _extract_packages_from_package_json is rewritten there to parse JSON instead of scanning lines, and it conflicts with this PR's _pinned_npm_version. The resolution is trivial — the JSON parser calls the pin predicate from here — and with both applied the full suite is green (1585 passed, 14 skipped, 6 xfailed). Whichever lands second just needs that one-line reconciliation; I'm happy to rebase this PR onto the other, in either order.

2. This PR and #319 interact, in both directions. Worth knowing before either lands. A real example from the corpus:

# requirements.txt
anthropic>=0.40.0
today this PR alone with #319
resolved version 0.40.0 (invented) none none
OSV query anthropic==0.40.0 → clean name-only → advisories name-only → advisories
finding none SC4 with the worst-ever severity SC4 LOW "unverifiable"

So the false pin was accidentally producing silence here, and the pair turns it into an honest low-severity note. Measured across four real units: 776 findings → 768, with one unit gaining exactly this LOW entry and another losing eight (that one is #322, unrelated).

@rng1995 rng1995 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Automated SkillSpector Review]

Re-review: approved. The prior blocker is resolved. _pinned_version() now accepts only concrete non-wildcard == pins, rejecting caps, floors, exclusions, compatible ranges, and wildcard equality; npm resolution likewise accepts only a bare exact semver. Requirements, pyproject, and package.json paths use the shared predicates, with regressions for the specific <= and ==1.* cases plus npm ranges. The focused suite passes (244 tests).

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.

[BUG] requirements.txt: any version specifier is treated as an exact pin (pillow>=10.0.0 reported as pillow==10.0.0 with its CVEs)

2 participants