Skip to content

research: evaluate uv.lock (Python) support - separate product or in-scope? #672

Open

Description

@sonukapoor

Research whether uv.lock support belongs in CVE Lite CLI or warrants a separate product.

Background

uv is rapidly becoming the default Python package manager for modern Python projects. uv.lock is its lockfile format. A DINUM (French government) user running CVE Lite CLI across government digital service repositories requested Python lockfile scanning.

Questions to answer

  • Does adding Python/uv.lock support fit the CVE Lite CLI product identity (currently JS/TS focused) or does it dilute it?
  • Would a separate cve-lite-cli-python (or similar) be a better home?
  • What would the OSV query surface look like for Python packages vs npm packages?
  • Are there existing tools in the Python ecosystem this would compete with or complement (pip-audit, Safety)?
  • What is the effort to parse uv.lock vs the JS/TS lockfile formats already supported?

Motivation

Requested by a DINUM (French government) user. No implementation commitment yet - this issue is to scope the decision first.


If you would like to see Python/uv.lock support, please give this issue a 👍 - it helps us prioritize.

Activity

  1. changed the title [-]feat: support uv.lock (Python/uv package manager)[/-] [+]research: evaluate uv.lock (Python) support - separate product or in-scope?[/+] on Jun 15, 2026
  2. kaaloo commented on Jun 15, 2026

    @kaaloo

    Thanks for posting @sonukapoor . A unified CLI would be great as I'm frequently in the situation where we are dealing with polyglot Node / Python monorepos. Hope a polyglot CVE Lite CLI makes sense to others!

  3. pfarikrispy commented on Jun 22, 2026

    @pfarikrispy
    Contributor

    Just my 2 cents...

    Much like checkov supports scanning for several frameworks, perhaps you can "wrap" the scanner type behind a switch:

    • --scanner=python
    • --scanner=ts

    without a switch present, probe for the resp. lock files of each language.

  4. sonukapoor commented on Jun 23, 2026

    @sonukapoor
    CollaboratorAuthor

    Thanks for the suggestion - a --scanner flag is a natural shape for multi-language support if we go that route.

    This issue is currently in research/scoping mode: we're figuring out whether Python/uv.lock fits CVE Lite CLI's JS/TS identity or is better as a separate tool. If we do add Python support, a flag like --scanner=ts / --scanner=python (or auto-detect without a flag, as you suggest) would be worth prototyping. Appreciate the input.

  5. alex-powell commented on Sep 20, 2026

    @alex-powell
    Contributor

    Detect is an addon. --fix is a second product.

    The useful split is detect vs --fix. Those are not the same amount of work.

    Detect

    Detect is: read a lockfile, get {name, version}, ask OSV if that version is known-bad.

    The scanner already does this.

    • PackageRef already has an ecosystem field
    • OSV already understands PyPI the same way it understands npm
    • Parsers already plug in through LockfileHandler in src/parsers/

    A uv.lock / pylock.toml parser is another input file for the same machine. That is an addon. Same shape as adding bun.lock.

    --fix is not that

    Default cve-lite does not rewrite the tree. It prints copy-paste commands.

    --fix only auto-applies direct deps that have a validated safe version, via npm install / pnpm add / yarn add, then rescans. Transitive findings stay “bump this parent yourself.”

    That apply path is the expensive, JS-shaped part of the product.

    Doing the same for Python means a new brain, not a new parser:

    • versions are PEP 440, not npm semver
    • you edit pyproject.toml + extras / groups / markers, not package.json
    • the commands are uv add / uv lock / uv sync
    • parent-aware upgrades do not map cleanly onto a universal uv.lock
    • override hygiene, cooldown, and the npm registry bulk API all need Python equivalents

    Wrong fix commands are worse than no fix commands. So Python --fix is a second product in the same binary. Detect-only is not.

    Product identity

    Keep the tool JS/TS-first.

    • Auto-detect uv.lock / pylock.toml when present
    • Label it experimental
    • Do not advertise Python --fix
    • A --scanner=python flag is fine as an override
    • Do not use that flag as an excuse to rebuild remediations

    A separate cve-lite-python package only makes sense if you commit to that second engine.

    Do not scrape uv.lock as if it were stable

    Astral treats uv.lock as uv-private and points other tools at uv workspace metadata (JSON).

    PEP 751 pylock.toml is the public format, but it is a resolution output, not a full uv workspace graph.

    Prefer metadata / pylock.toml. If you parse raw uv.lock, you own every format bump.

    OSV

    Same batch API, "ecosystem": "PyPI".

    • IDs will be PYSEC-… / GHSA-…
    • Offline mode needs the PyPI slice of OSV, not just npm
    • “In the lockfile” is not “installed on this runner” because of environment markers

    Say which marker set you used, or Linux CI will page on Windows-only wheels.

    Other tools

    This gap got smaller after the issue was opened:

    • uv audit (June 2026) already does native OSV on a uv project
    • osv-scanner already reads uv.lock and pylock.toml
    • pip-audit is a good CI gate but does not read uv.lock (export first)
    • Dependabot already alerts on uv.lock

    So the remaining gap is not “can anyone see PyPI CVEs.”

    It is one local CLI over a Node + Python monorepo with one JSON / SARIF / SBOM contract. That is the DINUM request. Full Python remediations are a different bet.

    Suggestion

    1. Keep it in this repo.
    2. Ship detect-only.
    3. Order:
      • pylock.toml and/or uv workspace metadata
      • OSV PyPI on the existing client
      • same outputs / --fail-on
      • raw uv.lock only if you must, and pin the revision you accept
    4. --fix, parent upgrades, override rules: out of scope until detect-only is used for real.
    5. Docs: uv-only repo → uv audit. CVE Lite → polyglot gate, same output schema.
    6. Success for DINUM: CI fails on known PyPI vulns from the committed lockfile. If they later want validated uv add commands, that is the other product.

    Happy to sketch a LockfileHandler + fixtures if this scoped approach is ok.

  6. sonukapoor commented on Sep 20, 2026

    @sonukapoor
    CollaboratorAuthor

    This is the most useful thing anyone has put on this issue, so let me engage with it properly rather than just thanking you.

    You are right about the plumbing, and I checked rather than took your word for it. PackageRef.ecosystem really is a free string, osv-advisory-source.ts:22 really does pass it straight through, and I confirmed live that a PyPI querybatch returns PYSEC and GHSA ids. LockfileHandler is a genuine compiler-checked table and a new entry touches about five places. The npm-registry supplemental pass is already correctly ecosystem-gated at scanner.ts:258.

    The uv audit catch is the most valuable line in your comment and it changes the competitive picture more than the parser question does.

    Where I think the argument breaks is "same shape as adding bun.lock". Bun is still the npm ecosystem, so every downstream assumption survives. PyPI breaks four of them, and the sharpest one lands in detect, not in --fix:

    looksLikeVersion (utils/version.ts:1) requires a strict X.Y.Z, and findFirstFixedVersion (scanner.ts:1043) skips any fixed value that fails it. So:

    numpy@1.21.0, GHSA-fpfv-jqm9-f5jm, declares fixed: "1.22". Two components. We would report "no fix available" for a vulnerability that numpy 1.22 fixes.

    That is your own rule, wrong output is worse than no output, violated in the half you called cheap. It is not an edge case either: four of the thirty most-downloaded PyPI packages currently ship a version that check rejects (python-dateutil@2.9.0.post0, idna@3.20, packaging@26.3, pytz@2026.3.post1).

    Three more, all verified against the code:

    purl.ts:9 hardcodes pkg:npm/, and it feeds both SPDX and CycloneDX. Python packages would be emitted as pkg:npm/requests@2.19.1. On a project that talks about the Cyber Resilience Act, that is a wrong compliance artifact rather than a cosmetic bug.

    pylock.toml, the input you recommend, is flat, and readDirectDependencyNames only reads package.json. So classifyRelationship falls back to path length, every finding comes out "unknown", and fix-commands.ts:237-245 routes every "unknown" finding into skipped. Detect-only via pylock.toml gives a findings list where every row has no command and a confusing reason.

    And findRootLockfile picks exactly one lockfile per root, so a polyglot repo root scans one and lists the other as skipped. Your headline use case needs the two ecosystems in separate subfolders.

    So the seam is not detect versus fix. It is "does this ecosystem's version algebra match semver." Detect here is four steps, not one: OSV query, version reasoning, relationship classification, output. Only the first is ecosystem-neutral today.

    And the strategic question your comment does not ask. osv-scanner already reads uv.lock and pylock.toml and emits SARIF, so "one CLI over a polyglot repo with one SARIF contract" is already available. Our differentiator is remediation quality. Detect-only walks into the segment where we are weakest against an incumbent, and drops the axis where we win. What does it win us that we are not already losing?

    Two things came out of reading this that are worth fixing regardless of whether Python ever happens, both latent on main today, and I have filed them: #1192 for the unparseable-fix-version case above, and #1191 for deriveAdvisoryPackageRows storing GIT-range commit hashes as versions, where compareVersions("2.32.4", "74ea7cf...") returns -1 so a patched version can be flagged vulnerable. Your comment found both. Thank you for that.

    I am not taking the LockfileHandler sketch, and I want to be straight about why rather than leave it hanging: the handler is the easy five percent, and merging it would imply a scope decision this issue exists to make. If anything here is worth prototyping it is a PEP 440 version module behind the existing compareVersions and looksLikeVersion seam, which is the actual gate and is testable on its own without committing the project to a second ecosystem.

    Leaving this open as research. You have moved it forward considerably.

  7. alex-powell commented on Oct 24, 2026

    @alex-powell
    Contributor

    I've had a while to think about this, use the tool on prod and gain context.

    Real-world use case

    I run a production site with an Astro frontend (package-lock.json) and a Python backend and crawler (pinned requirements.txt, no uv.lock or pylock.toml). The frontend gates CI with cve-lite-cli (fail-on: high, SARIF, a committed baseline). The backend gates with pip-audit. Both run on every PR and weekly.

    What that taught me:

    • Every npm high since the first scan came in transitively (through astro or tailwind). The parent-aware commands (npm update astro, npm update source-map-js) were the right fix every time. That is the feature I'd miss most.
    • pip-audit finds things, but gives no parent path and no baseline, so the gate can't ratchet on new findings only.
    • A mixed repo needs two tools, two CI jobs and two reports. One baseline and one report across npm and Python would remove most of that duplication.
    • Lockfile-only Python support would not cover this repo. A plain pinned requirements.txt matters.

    So to the "what does it win us?" question: for polyglot repos, a single gate with ratcheting and fix guidance across both ecosystems. Detect-only parity with osv-scanner is not the win.

    --fix should be the goal, not detect-only

    I've changed my mind since my last comment. --fix is the best feature of this tool, and Python support should aim for it. The question is order. A safe Python fix needs foundations first:

    1. Versions: a PEP 440 module behind the existing compareVersions / looksLikeVersion seam (epochs, .post/.dev, two-part versions like numpy's 1.22). This is the prototype you invited, and it's needed to pick a correct fix version at all.
    2. purl: build purls per ecosystem (pkg:pypi/ with PEP 503 normalised names) instead of hardcoding pkg:npm/.
    3. Multi-lockfile roots: let one root hold both package-lock.json and a Python manifest.
    4. Direct vs transitive: read pyproject.toml dependencies, so findings aren't all classified unknown and skipped.
    5. Inputs: pinned requirements.txt, pylock.toml / uv export, and later uv.lock (which is uv-private and moves).
    6. Offline DB: sync the PyPI slice of OSV.
    7. --fix for uv: uv lock --upgrade-package / uv add pkg>=X, with parent upgrades where the graph allows.
    8. --fix for pinned requirements.txt: edit the pin, and use a constraint for transitive deps with no parent upgrade.
    9. Docs: mark Python experimental until fix support lands.

    Steps 1 to 4 are worth having even if Python stalls, because they make the core ecosystem-neutral. Reachability for Python (import names vs distribution names, e.g. PIL vs pillow) I'd leave out of scope.

    Happy to start with the PEP 440 module PR if that works for you.

  8. sonukapoor commented on Oct 24, 2026

    @sonukapoor
    CollaboratorAuthor

    Yes. Let us do this, experimental, and judge it from the outcome rather than from more analysis on this issue. Enough people have asked for Python now, and you are the first to offer a plan I agree with.

    A correction to what I argued in September, because it was my strongest objection and it no longer holds. I said an advisory declaring its fix as 1.22 would be read as no fix at all, and I used numpy to make the point. That got filed and fixed days later, so advisory boundaries written with a missing component are normalised before they are compared. Of the four packages I named then, idna@3.20 and packaging@26.3 parse fine today. The two that still fail are python-dateutil@2.9.0.post0 and pytz@2026.3.post1, and the forms that still fail are post and dev releases, epochs, and a release candidate written without a separator.

    So your step 1 is still the gate, just a narrower one than I described, and what is left is PEP 440 and nothing else. That makes it a better first piece of work than it was a month ago.

    Your own repo reframes this issue more than the rest of the thread does. It was filed about uv.lock, and you are describing pinned requirement files with no lockfile at all. That is a different input and probably the more common one. If we only ever supported the format in the title we would miss the case you have just described.

    What experimental has to mean here

    The word does not exist anywhere in the tool today, so I would rather define it now than discover we disagree about it over a PR. Three rules, and underneath they are all one sentence: an experimental ecosystem must never be able to report clean when it means it could not look.

    1. Python findings print, but they do not touch the exit code yet. You already know why, because you found the npm side of this yourself: pnpm 8 lockfiles parsed to zero packages, so those projects scanned clean under --fail-on. The general case is still open as #1344, where a scan that finds nothing to parse exits 0 even under the gate. A new ecosystem will meet formats we do not handle, and until that is fixed, wiring Python into the exit code means shipping a silent false negative on purpose.

    2. Python stays out of SPDX and CycloneDX until identifiers are built per ecosystem. Right now that builder hardcodes npm and does not accept an ecosystem at all, so a Python package would be published as an npm package in both formats. A wrong identifier inside a compliance artefact is worse than not emitting one, which is your step 2 and is why I want it early.

    3. Offline plus Python refuses rather than reporting clean. The local advisory database is ecosystem-aware in its structure, which is the good news, but the sync only downloads the npm slice of OSV.

    How it switches on: an explicit flag to begin with, not auto-detection. @pfarikrispy suggested a scanner switch on this issue back in June and that is the right shape for now, because auto-detection would change output for every existing adopter who happens to have a Python file in their tree. Auto-detect is where this should end up, once it has earned it.

    Graduation

    That is the outcome I want to judge from, so here are the conditions rather than a vague "later": #1344 fixed, identifiers built per ecosystem, and the parsers exercised against real trees rather than fixtures. At that point Python joins the gate and the label comes off. Your repo is the obvious first real tree, and I would rather have your numbers than my predictions.

    Scope

    Yes to your steps 1 and 2 now, as two separate PRs rather than one. Both are narrow, both have an exact success criterion, and both are worth having even if Python stalls, which is your argument and it is the right one.

    Steps 3 and 4 I want to look at after those land, because reading declared dependencies changes how every finding gets classified, not just Python ones.

    The fix side, your steps 7 and 8, I am not committing to yet, and I would rather tell you that now than let you build toward a maybe. That is where a wrong recommendation does real damage, and it is the part of the tool I keep in house. If the first half goes well, that conversation gets much easier.

    One process note, so nothing goes wrong by accident: this stays an open issue rather than becoming an assignment, because first working PR wins here and I am not going to break that rule for anyone. But say here when you start on the version work and I will watch for collisions.

    Two of your other PRs are approved and in the queue. I merge them one at a time rather than in a batch, so they will land shortly.

  9. alex-powell commented on Oct 24, 2026

    @alex-powell
    Contributor

    Cheers, working on steps 1&2 now. I'll try to make it as a requirement from day 1, rather than some messy bolt-on

  10. sonukapoor commented on Nov 6, 2026

    @sonukapoor
    CollaboratorAuthor

    Steps 1 and 2 are in. #1397 reads and orders PEP 440 versions, #1398 builds package URLs from the ecosystem a package actually belongs to, and #1434 added the manual check against packaging. Thanks @alex-powell, building them as requirements rather than a bolt-on made both easy to review.

    So steps 3 and 4 are open now: reading a Python project, starting with pinned requirements.txt since that's the real use case here, behind an explicit flag. The experimental rules from my earlier comment still hold for that work: findings print but don't touch the exit code, Python stays out of SPDX and CycloneDX, and offline mode refuses rather than reporting clean.

    Two things sit next to it. #1344 (a scan that finds nothing to parse exits 0 under --fail-on) is open for anyone and is the main thing between experimental and graduating. #1516, checking a PyPI upgrade target against the package's other advisories, is the fix side, and I'm keeping that one in-house.

  11. bhushan4work commented on Nov 6, 2026

    @bhushan4work
    Contributor

    I'd like to pick the remaining parts , will raise a pr as intended

  12. bhushan4work commented on Nov 6, 2026

    @bhushan4work
    Contributor

    @sonukapoor fix for step 3,4 is open at #1546 . besides this, on issue #1344 , I could still see an existing pr #1353 still in progress but if u need help with it do let me know so I could raise a pr

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions