Skip to content

fix(pnpr): screen registry responses with osv#12566

Merged
zkochan merged 8 commits into
mainfrom
feat/12561
Jun 22, 2026
Merged

fix(pnpr): screen registry responses with osv#12566
zkochan merged 8 commits into
mainfrom
feat/12561

Conversation

@zkochan

@zkochan zkochan commented Jun 22, 2026

Copy link
Copy Markdown
Member

Summary

Closes #12561.

This applies pnpr's local OSV npm database to the registry serving surface, not just the resolver endpoints. Registry-enabled servers now load the OSV index when osv.enabled is set, packument-style responses omit vulnerable versions, and tarball requests for vulnerable versions are refused before reading from cache or proxying upstream.

Squash Commit Body

Load the local OSV index for the pnpr registry surface when osv is enabled, not just for the resolver surface.

Registry responses now apply the index at serve time. Full and abbreviated packuments, version manifests, and dist-tag responses omit vulnerable versions without mutating cached upstream bytes. Direct version-manifest requests for filtered versions return 404, and dist-tags pointing at filtered versions are removed from served responses.

Tarball requests parse the version from the validated npm tarball filename and return 403 for vulnerable versions after the normal access policy check, before reading from cache or fetching upstream. This keeps private package metadata behind the existing auth boundary.

The tests cover proxied and cached packuments, direct version manifests, dist-tags, tarballs before upstream fetch, tarballs from cache, registry-only OSV loading, and private-package access ordering.

Closes https://github.com/pnpm/pnpm/issues/12561.

Checklist

  • The change is limited to pnpr/; pnpr has no TypeScript CLI or pacquet/ counterpart.
  • Added or updated tests.

Written by an agent (Codex, gpt-5).

Summary by CodeRabbit

  • New Features
    • OSV filtering now applies to packuments, version manifests, and dist-tags, removing vulnerable versions and related entries.
    • Vulnerable version manifests return 404 Not Found; vulnerable tarballs return 403 Forbidden, while preserving existing access-control behavior.
    • The server now loads and uses the local OSV index when either the resolver or registry surface is enabled.
  • Bug Fixes
    • More accurate OSV vulnerability detection for package/version lookups, with improved error classification.
  • Tests
    • Added end-to-end coverage for OSV filtering, caching behavior, and canonical tarball path handling.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

OSV vulnerability screening is extended from the resolver surface to all registry-serving endpoints (packument, tarball, dist-tags, version manifest). A new OsvVulnerability error variant is added with 403 Forbidden status, the OsvIndex gains an is_vulnerable query method, two helper functions implement packument filtering and tarball blocking, and the OSV index is now loaded whenever either the resolver or registry surface is enabled. The validate_tarball_name method is removed and its call sites replaced with parse_tarball_name.

Changes

OSV screening extended to registry serving path

Layer / File(s) Summary
OsvVulnerability error variant and HTTP/log mapping
pnpr/crates/pnpr/src/error.rs
Adds RegistryError::OsvVulnerability { package, version, advisories } with a display message, maps it to 403 Forbidden in status_code(), and maps it to "osv_vulnerability" in log_kind().
OsvIndex::is_vulnerable method and advisories helper
pnpr/crates/pnpr/src/resolver/osv.rs, pnpr/crates/pnpr/src/resolver/osv/tests.rs
Adds is_vulnerable public method to OsvIndex for checking whether a package/version is vulnerable, refactors vulnerability_ids to use a shared advisory lookup via a new private advisories helper, and adds test coverage for is_vulnerable outcomes.
Remove validate_tarball_name; update call sites and tests
pnpr/crates/pnpr/src/package_name.rs, pnpr/crates/pnpr/src/package_name/tests.rs
Removes the public validate_tarball_name method from PackageName and updates all tests to use parse_tarball_name directly.
filter_osv_vulnerable_versions and ensure_osv_allowed helpers
pnpr/crates/pnpr/src/server.rs
Introduces the two core OSV helper functions: one mutates packument JSON to strip vulnerable versions, dist-tags, and time entries; the other rejects a request when any OSV advisory ID matches the requested package/version. Adds HashSet import required by filtering logic.
OSV index loading condition and endpoint wiring
pnpr/crates/pnpr/src/config.rs, pnpr/crates/pnpr/src/server.rs
Expands load_active_osv_index to load when either resolver.enabled or registry.enabled is set. Wires filter_osv_vulnerable_versions into packument_response, serve_version_manifest, and get_dist_tags. Replaces validate_tarball_name with parse_tarball_name + ensure_osv_allowed in serve_tarball. Updates config and server doc comments.
OSV integration tests
pnpr/crates/pnpr/tests/server.rs
Adds body_json, osv_database, and enable_osv test helpers plus six integration tests covering: packument filtering from proxy and cache, packument identity mismatch handling, tarball 403 before upstream fetch, access-gate precedence over OSV screening, tarball 403 from cached content, and scoped tarball filename canonicalization.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant serve_packument
  participant filter_osv_vulnerable_versions
  participant serve_tarball
  participant ensure_osv_allowed
  participant Upstream

  rect rgba(100, 149, 237, 0.5)
    note over Client,filter_osv_vulnerable_versions: Packument path
    Client->>serve_packument: GET /:package
    serve_packument->>Upstream: fetch packument
    Upstream-->>serve_packument: full packument JSON
    serve_packument->>filter_osv_vulnerable_versions: packument + osv_index
    filter_osv_vulnerable_versions-->>serve_packument: vulnerable versions/dist-tags/time removed
    serve_packument-->>Client: filtered packument (200)
  end

  rect rgba(220, 100, 100, 0.5)
    note over Client,ensure_osv_allowed: Tarball path
    Client->>serve_tarball: GET /:package/-/:filename
    serve_tarball->>ensure_osv_allowed: name + parsed version
    ensure_osv_allowed-->>serve_tarball: OsvVulnerability error
    serve_tarball-->>Client: 403 Forbidden (no upstream hit)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Assessment against linked issues

Objective Addressed Explanation
Packument serving (proxy and cache): omit known-vulnerable versions from served packuments [#12561]
Tarball serving (proxy and cache): refuse to serve a tarball for a known-vulnerable version [#12561]
Screening applies regardless of whether bytes come from upstream proxy or local cache [#12561]
OSV setting remains cross-cutting, governed by registry/resolver surface enablement [#12561]

Possibly related PRs

  • pnpm/pnpm#12506: Introduces package version rejection/guard plumbing in the resolution path, which this PR complements by extending the same enforcement to the registry serving path.
  • pnpm/pnpm#12563: Changes load_active_osv_index and OSV surface gating in server.rs; this PR's expansion of OSV loading to registry.enabled directly continues that work.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/12561

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

pnpr registry: filter vulnerable versions and tarballs using local OSV index
🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

Description

• Load the local OSV index for registry endpoints when osv.enabled is set.
• Filter packument, manifest, and dist-tag responses to omit vulnerable versions.
• Refuse vulnerable tarball downloads after access checks, before cache/upstream reads.
Diagram

graph TD
  C(["npm client"]) --> R["pnpr registry routes"] --> A{"access allowed?"}
  A -->|"no"| AD["401/403"]
  A -->|"yes"| O{"OSV allows?"}
  O -->|"no"| OD["403 (tarball) / 404 (manifest)"]
  O -->|"yes"| K[("cache storage")] --> U[["uplink registry"]] --> S["served response"]

  R --> I[("OSV index")]

  subgraph Legend
    direction LR
    _cli(["Client"]) ~~~ _svc["Handler/service"] ~~~ _dec{"Decision"} ~~~ _db[("Data store")] ~~~ _ext[["External"]]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Filter at cache-write time (store sanitized packuments)
  • ➕ Less per-request JSON parsing/filtering work after the first fetch
  • ➕ Makes cached bytes match what clients will see
  • ➖ Mutates upstream-cached bytes, complicating revalidation/debugging and cache correctness
  • ➖ Harder to change policy later without rewarming cache
2. Enforce OSV only in resolver endpoints (status quo)
  • ➕ Smaller runtime footprint on the registry surface
  • ➕ Avoids JSON surgery on served packuments
  • ➖ Registry users can still fetch vulnerable versions directly via packuments/tarballs
  • ➖ Does not close the reported gap for registry-serving mode
3. Use live OSV API lookups instead of local DB
  • ➕ Always freshest advisory data without local dumps
  • ➖ Adds external dependency/latency and availability risk
  • ➖ Harder to run air-gapped; higher operational complexity

Recommendation: The PR’s serve-time filtering/denial approach is the best trade-off: it closes the registry gap without mutating cached upstream bytes, keeps behavior consistent across proxied and cached responses, and (critically) preserves the existing private-package access boundary by running auth checks before OSV denial on tarballs. The added tests cover the key invariants (proxy vs cache, manifests, dist-tags, and tarball denial ordering).

Files changed (6) +304 / -34

Enhancement (1) +15 / -0
error.rsAdd explicit OSV vulnerability registry error mapped to 403 +15/-0

Add explicit OSV vulnerability registry error mapped to 403

• Introduces a new 'RegistryError::OsvVulnerability' variant with a user-facing message including advisory IDs. Registers the new error for logging kind and maps it to HTTP 403.

pnpr/crates/pnpr/src/error.rs

Bug fix (1) +81 / -13
server.rsApply OSV screening to registry responses and tarball downloads +81/-13

Apply OSV screening to registry responses and tarball downloads

• Loads the OSV index when either resolver or registry surfaces are enabled. Filters packuments, version-manifest reads, and dist-tag responses to omit vulnerable versions; removes dist-tags/time entries that point to filtered versions. For tarballs, parses the version from the filename, enforces access first, then returns 403 for OSV-blocked versions before reading from cache or fetching upstream.

pnpr/crates/pnpr/src/server.rs

Refactor (1) +0 / -13
package_name.rsUse tarball-name parsing that also returns version; drop validate helper +0/-13

Use tarball-name parsing that also returns version; drop validate helper

• Removes the standalone tarball-name validation helper and relies on 'parse_tarball_name' to both validate and extract the version string. This enables downstream logic (e.g., OSV screening) to act on the requested tarball version.

pnpr/crates/pnpr/src/package_name.rs

Tests (2) +206 / -6
tests.rsUpdate tarball-name tests to use version-extracting parser +5/-5

Update tarball-name tests to use version-extracting parser

• Adjusts tests to call 'parse_tarball_name' instead of the removed 'validate_tarball_name', preserving coverage of accepted and rejected filename forms.

pnpr/crates/pnpr/src/package_name/tests.rs

server.rsAdd integration tests for OSV filtering across proxy/cache and tarballs +201/-1

Add integration tests for OSV filtering across proxy/cache and tarballs

• Adds helpers to construct a minimal local OSV database and enable OSV in test configs. Introduces tests covering filtered packuments (proxy and cache), 404 for direct manifest of filtered versions, dist-tag filtering, tarball denial before upstream fetch, tarball denial from cache, and ensuring auth gating still returns 401 for private packages before OSV details are exposed.

pnpr/crates/pnpr/tests/server.rs

Documentation (1) +2 / -2
config.rsClarify OSV applies to all mounted surfaces, not just resolver +2/-2

Clarify OSV applies to all mounted surfaces, not just resolver

• Updates the OSV config docstring to reflect that the local OSV database can be consulted by any mounted surface (resolver and registry).

pnpr/crates/pnpr/src/config.rs

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 22, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. No OSV-package fast-path 🐞 Bug ➹ Performance
Description
filter_osv_vulnerable_versions() scans every packument version and calls OSV checks even when the
package has no advisories in the local OSV index, adding avoidable O(number_of_versions) CPU on the
hot packument GET path when osv.enabled is set. Because the OSV index only contains entries for
packages with advisories, a single package-level presence check can skip all filtering work for most
packages.
Code

pnpr/crates/pnpr/src/server.rs[R2119-2130]

+    let Some(osv_index) = osv_index else { return };
+    let package_name = name.as_str();
+    let mut blocked_keys = HashSet::new();
+    let mut retained_version_keys = HashSet::new();
+    let has_time = packument.get("time").and_then(Value::as_object).is_some();
+    if let Some(versions) = packument.get_mut("versions").and_then(Value::as_object_mut) {
+        versions.retain(|key, manifest| {
+            let manifest_version = manifest.get("version").and_then(Value::as_str);
+            let key_is_vulnerable = osv_index.is_vulnerable(package_name, key);
+            let manifest_is_vulnerable = manifest_version.is_some_and(|version| {
+                version != key && osv_index.is_vulnerable(package_name, version)
+            });
Evidence
The new packument filtering runs OSV checks per version entry; for packages absent from the OSV
index this work is provably unnecessary because the index map only contains packages that have
advisories (and thus missing means “no advisories”).

pnpr/crates/pnpr/src/server.rs[2114-2147]
pnpr/crates/pnpr/src/resolver/osv.rs[109-115]
pnpr/crates/pnpr/src/resolver/osv.rs[451-474]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When `osv.enabled` is on, `filter_osv_vulnerable_versions()` currently iterates the packument and runs `osv_index.is_vulnerable(...)` per version entry even for packages that have **no** OSV advisories. Since OSV lookups are only meaningful for packages present in the OSV index, this adds avoidable work to a hot registry read path.
## Issue Context
`OsvIndex` only stores packages that appear in the OSV dump (i.e., packages with advisories). Most packages will not be present, so the common case should short-circuit before scanning `versions`, `dist-tags`, and `time`.
## Fix Focus Areas
- pnpr/crates/pnpr/src/resolver/osv.rs[109-137]
- pnpr/crates/pnpr/src/resolver/osv.rs[478-488]
- pnpr/crates/pnpr/src/server.rs[2114-2157]
### Suggested implementation shape
- Add a cheap method on `OsvIndex`, e.g. `pub(crate) fn has_advisories(&self, name: &str) -> bool`, implemented as `self.packages.contains_key(normalized_name(name).as_ref())`.
- In `filter_osv_vulnerable_versions()`, early-return if `!osv_index.has_advisories(package_name)`.
- Optionally apply the same early-return pattern in `filter_osv_vulnerable_dist_tags()` / `is_osv_vulnerable_packument_version()` / `ensure_osv_allowed()` to avoid unnecessary work on those endpoints too.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Redundant OSV checks ✓ Resolved 🐞 Bug ➹ Performance
Description
In filter_osv_vulnerable_versions(), OSV vulnerability is checked for every version key to build
blocked_keys, then checked again for each entry in the packument's time object, redoing the same
advisory scan for all safe versions. On large packuments, this adds avoidable CPU and tail latency
on the hot packument GET path when OSV is enabled.
Code

pnpr/crates/pnpr/src/server.rs[R2143-2146]

+    if let Some(time) = packument.get_mut("time").and_then(Value::as_object_mut) {
+        time.retain(|key, _| {
+            !blocked_keys.contains(key) && !osv_index.is_vulnerable(package_name, key)
+        });
Evidence
The function already calls osv_index.is_vulnerable() while iterating versions to build
blocked_keys, then calls osv_index.is_vulnerable() again inside time.retain(...) for each
time key not in blocked_keys, which includes all safe version keys. This duplicates OSV advisory
scans for the common case where time contains all version keys.

pnpr/crates/pnpr/src/server.rs[2119-2135]
pnpr/crates/pnpr/src/server.rs[2143-2147]
pnpr/crates/pnpr/src/resolver/osv.rs[109-115]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`filter_osv_vulnerable_versions()` performs OSV checks while building `blocked_keys` from the `versions` map, and then performs OSV checks again when filtering the `time` map. For the typical case where `time` keys mirror `versions` keys, this re-runs expensive OSV advisory scans for every non-blocked version.
### Issue Context
This runs on the hot registry packument serving path (`GET /:pkg`). With OSV enabled, packuments with many versions will pay this extra work per request.
### Fix Focus Areas
- pnpr/crates/pnpr/src/server.rs[2119-2147]
### Suggested fix
When `versions` is present, treat `blocked_keys` as the authoritative set of version keys to remove and avoid re-calling `osv_index.is_vulnerable()` for `time` keys.
Example approach:
- Keep `modified` (and `created` if present) unconditionally.
- For other `time` keys, remove if `blocked_keys.contains(key)`; otherwise keep.
If you still want to be defensive for malformed packuments where `time` may contain version keys not in `versions`, you can limit OSV checks to only those `time` keys that are not known special keys and not present in the `versions` object (requires capturing the post-filter `versions` keys or a flag indicating whether `versions` existed).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Extra packument scan passes ✓ Resolved 🐞 Bug ➹ Performance
Description
filter_osv_vulnerable_versions() does a full versions-map iteration to build blocked_keys and then
iterates the same map again via retain(), adding avoidable O(number_of_versions) work per packument
response when OSV is enabled. On large packuments this increases CPU and tail latency on the hot
registry GET path.
Code

pnpr/crates/pnpr/src/server.rs[R2117-2131]

+    let mut blocked_keys = HashSet::new();
+    if let Some(versions) = packument.get_mut("versions").and_then(Value::as_object_mut) {
+        blocked_keys = versions
+            .iter()
+            .filter_map(|(key, manifest)| {
+                let manifest_version = manifest.get("version").and_then(Value::as_str);
+                let key_is_vulnerable = osv_index.is_vulnerable(package_name, key);
+                let manifest_is_vulnerable = manifest_version.is_some_and(|version| {
+                    version != key && osv_index.is_vulnerable(package_name, version)
+                });
+                (key_is_vulnerable || manifest_is_vulnerable).then(|| key.clone())
+            })
+            .collect();
+        versions.retain(|key, _| !blocked_keys.contains(key));
+    }
Evidence
packument_response() calls filter_osv_vulnerable_versions() on every packument response when OSV is
enabled, and the filter implementation iterates versions twice (first with .iter() +
.collect(), then again with versions.retain(...)).

pnpr/crates/pnpr/src/server.rs[2091-2107]
pnpr/crates/pnpr/src/server.rs[2110-2131]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`filter_osv_vulnerable_versions()` currently performs two passes over the `versions` object: one to collect `blocked_keys`, then another via `versions.retain(...)`. This adds avoidable CPU work and hash lookups on every packument response when OSV is enabled.
### Issue Context
Packument serving is a hot read path. The OSV filter is called from `packument_response()` for every packument request (including cached packuments), so the extra traversal scales with packument size.
### Fix Focus Areas
- pnpr/crates/pnpr/src/server.rs[2110-2144]
### Suggested fix approach
- Convert the `versions` filtering to a single `versions.retain(|key, manifest| { ... })` pass.
- Inside the retain closure, compute whether the entry is blocked.
- If blocked, `blocked_keys.insert(key.clone())` and return `false`.
- Otherwise return `true`.
- Keep the subsequent `dist-tags` / `time` filtering using `blocked_keys` (and only fall back to additional OSV lookups if strictly needed).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (4)
4. Manifest endpoint scans all versions ✓ Resolved 🐞 Bug ➹ Performance
Description
Both serve_version_manifest() and get_dist_tags() call filter_osv_vulnerable_versions() on the
full packument before extracting a single manifest or the dist-tags object, forcing an unnecessary
traversal of every entry in versions and triggering OSV lookups on endpoints that don’t need
full-packument filtering. This adds avoidable O(number_of_versions) CPU overhead and latency on hot
registry read paths, especially for large packuments, when only the resolved version (for manifests)
or the versions referenced by dist-tags need screening.
Code

pnpr/crates/pnpr/src/server.rs[R752-756]

+    let mut packument = packument;
+    filter_osv_vulnerable_versions(&mut packument, &name, state.inner.osv_index.as_ref());
let Some(manifest) =
extract_version_manifest(&packument, &name, version_or_tag, &state.inner.config.public_url)
else {
Evidence
In the version-manifest handler, filter_osv_vulnerable_versions() is invoked immediately after
parsing the packument, and that helper explicitly iterates the entire versions map and calls
OsvIndex::is_vulnerable() per version; is_vulnerable itself does SemVer parsing and advisory
scanning, so each /:pkg/:versionOrTag request pays full-packument cost even though it returns only
one manifest. Likewise, the dist-tags handler filters the entire packument before extracting
dist-tags, even though the response only needs the dist-tags object, and the filter’s
implementation again walks all versions entries and performs OSV checks per entry—work that is
unnecessary for this endpoint and directly explains the avoidable O(number_of_versions) overhead.

pnpr/crates/pnpr/src/server.rs[730-758]
pnpr/crates/pnpr/src/server.rs[2110-2130]
pnpr/crates/pnpr/src/resolver/osv.rs[109-115]
pnpr/crates/pnpr/src/server.rs[1656-1675]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Two hot endpoints (`serve_version_manifest()` for `/:pkg/:versionOrTag` and `get_dist_tags()` for `/-/package/:pkg/dist-tags`) currently call `filter_osv_vulnerable_versions()` over the entire packument, which iterates `packument["versions"]` and performs OSV checks per version. This causes avoidable O(number_of_versions) CPU/latency on requests that only need to screen a single resolved version (manifest) or the versions referenced by `dist-tags`.
## Issue Context
- These routes are frequently hit by clients and the cost scales with packument size; large npm packages can have very large `versions` maps.
- Desired behavior to preserve:
- For `serve_version_manifest()`: if the requested version (or the version a dist-tag resolves to) is vulnerable, respond 404; otherwise serve the manifest.
- For `get_dist_tags()`: return `dist-tags`, but avoid scanning all `versions`; only tags pointing to vulnerable versions should be removed (or otherwise handled) based on `osv_index.is_vulnerable(package, version)`.
- Consider consistency for non-string dist-tag values (drop defensively or preserve consistently), but do not iterate `versions` or `time` for the dist-tags endpoint.
## Fix Focus Areas
- pnpr/crates/pnpr/src/server.rs[730-762]
- pnpr/crates/pnpr/src/server.rs[1656-1680]
- pnpr/crates/pnpr/src/server.rs[2110-2143]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Tarball name not canonicalized ✓ Resolved 🐞 Bug ⛨ Security
Description
In serve_tarball(), parse_tarball_name() returns a canonical filename but the code discards it and
continues using the raw filename segment for later cache/upstream lookups, allowing
non-canonical-but-accepted inputs to map to different on-disk keys and cause cache misses or
spurious 404s. Because storage joins filename directly into the package directory, this also
creates alternate directory paths for the same tarball.
Code

pnpr/crates/pnpr/src/server.rs[R775-776]

+    let (_, version) = match name.parse_tarball_name(filename) {
+        Ok(parsed) => parsed,
Evidence
The canonical filename is computed and dropped in the new tarball handler, while the storage layer
uses the provided filename directly as a filesystem path component. Since parse_tarball_name can
canonicalize inputs, ignoring its canonical output creates inconsistent storage/upstream keys.

pnpr/crates/pnpr/src/server.rs[765-792]
pnpr/crates/pnpr/src/package_name.rs[40-70]
pnpr/crates/pnpr/src/storage.rs[585-600]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`serve_tarball()` calls `PackageName::parse_tarball_name(filename)` and receives `(canonical_filename, version)`, but discards `canonical_filename`. The subsequent tarball cache read/write and upstream fetch use the raw `filename` string, which can differ from the canonical form for accepted inputs. This can split cache entries, cause unexpected 404s, and create alternate filesystem paths under the package directory.
### Issue Context
- `parse_tarball_name()` intentionally accepts multiple forms and returns a canonical disk filename.
- The storage layer builds the tarball path by `package_dir(name).join(filename)`.
### Fix Focus Areas
- pnpr/crates/pnpr/src/server.rs[772-822]
- pnpr/crates/pnpr/src/package_name.rs[40-70]
- pnpr/crates/pnpr/src/storage.rs[585-600]
### Concrete fix
- Capture `canonical_filename` from `parse_tarball_name()`.
- Use `canonical_filename` (not the raw `filename`) for:
- `storage.open_tarball(...)`
- `storage.open_cached_tarball_tmp(...)`
- `upstream.fetch_tarball_response(...)`
- Optionally (recommended): reject non-canonical tarball filenames on the GET path by requiring `canonical_filename == filename`, returning `InvalidTarballName` otherwise, to prevent cache-key aliasing via odd encodings.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. blocked_keys mismatches version strings ✓ Resolved 📎 Requirement gap ⛨ Security
Description
filter_osv_vulnerable_versions() can be bypassed by a malformed or malicious upstream packument
because it consults the nested versions[...].version field for OSV checks while removals are keyed
off the versions map keys, and dist-tags/time are filtered using version strings. If
versions keys don’t match the embedded version field, OSV-vulnerable version strings can still
appear in served packument surfaces, weakening the “never resolve to vulnerable versions” policy on
proxy/cache responses (even though tarball screening still uses the filename-derived version).
Code

pnpr/crates/pnpr/src/server.rs[R2118-2137]

+    let blocked_keys: HashSet<String> = versions
+        .iter()
+        .filter_map(|(key, version)| {
+            let version = version.get("version").and_then(Value::as_str).unwrap_or(key);
+            (!osv_index.vulnerability_ids(name.as_str(), version).is_empty()).then(|| key.clone())
+        })
+        .collect();
+    if blocked_keys.is_empty() {
+        return;
+    }
+    versions.retain(|key, _| !blocked_keys.contains(key));
+    if let Some(tags) = packument.get_mut("dist-tags").and_then(Value::as_object_mut) {
+        tags.retain(|_, version| {
+            version.as_str().is_none_or(|version| !blocked_keys.contains(version))
+        });
+    }
+    if let Some(time) = packument.get_mut("time").and_then(Value::as_object_mut) {
+        for key in &blocked_keys {
+            time.remove(key);
+        }
Evidence
PR Compliance IDs 1 and 3 require that registry packument responses omit/handle OSV-vulnerable
versions consistently across all served surfaces. The current implementation processes
attacker-controlled packument JSON loaded from upstream/cache without validating versions key ↔
nested version consistency, computes removals using versions keys (e.g., via a blocked_keys
set), but performs the OSV vulnerability decision using versions[...].version when present and
prunes dist-tags and time using version-string values; therefore, a mismatch can create false
negatives/incorrect pruning such that a vulnerable key/version can remain visible in
dist-tags/time or other responses despite intended filtering.

Apply OSV screening to registry packument serving (proxy and cache)
Ensure OSV screening is cross-cutting across all mounted surfaces (not resolver-only)
pnpr/crates/pnpr/src/server.rs[2118-2137]
pnpr/crates/pnpr/src/server.rs[1931-1996]
pnpr/crates/pnpr/src/server.rs[2109-2139]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`filter_osv_vulnerable_versions()` currently decides OSV vulnerability using the nested `versions[...].version` string when present, but then removes entries keyed by the `versions` map keys and filters `dist-tags`/`time` using version strings. Because packument bytes are attacker-controlled and not validated for `versions` key ↔ nested `version` field consistency, a malformed/malicious upstream packument can lie in the nested `version` field (or otherwise cause mismatches) and bypass filtering so that OSV-vulnerable version strings remain visible in served packument/manifest/dist-tags/time responses, violating the intended “never resolve/serve vulnerable versions” policy on proxy/cache paths.
## Issue Context
- Packument bytes are loaded from upstream/cache and parsed as JSON; inputs must be treated as attacker-controlled.
- The filter currently prefers the nested `version` field over the `versions` object key for OSV screening.
- The implementation computes removals using `versions` keys, but compares `dist-tags` values (version strings) and prunes `time` entries in a way that assumes keys and embedded version strings are consistent.
- Even if tarball screening still uses the filename-derived version, packument surfaces must not leak or enable resolution to OSV-vulnerable versions.
## Fix Focus Areas
- pnpr/crates/pnpr/src/server.rs[2118-2137]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Alloc-heavy OSV checks hot-path ✓ Resolved 🐞 Bug ➹ Performance
Description
The new registry-side filtering calls OsvIndex::vulnerability_ids() once per version entry when
serving packuments/manifests/dist-tags, but vulnerability_ids() allocates (Vec + per-call HashSet)
and clones advisory IDs even when callers only need a boolean. For large packuments (many versions)
this can significantly increase CPU/allocator pressure on a hot request path.
Code

pnpr/crates/pnpr/src/server.rs[R2118-2124]

+    let blocked_keys: HashSet<String> = versions
+        .iter()
+        .filter_map(|(key, version)| {
+            let version = version.get("version").and_then(Value::as_str).unwrap_or(key);
+            (!osv_index.vulnerability_ids(name.as_str(), version).is_empty()).then(|| key.clone())
+        })
+        .collect();
Evidence
The new filter calls vulnerability_ids per version entry; vulnerability_ids returns Vec and
builds a temporary HashSet each call, which is unnecessary overhead when only checking
vulnerability presence.

pnpr/crates/pnpr/src/server.rs[2109-2124]
pnpr/crates/pnpr/src/resolver/osv.rs[109-125]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`filter_osv_vulnerable_versions` invokes `osv_index.vulnerability_ids(...)` for every version in a packument just to test emptiness. `vulnerability_ids` allocates and clones strings, which compounds on large packuments and high QPS.
## Issue Context
- This code runs on registry packument/manifest/dist-tags responses.
- `vulnerability_ids` is still needed for tarball denial responses (to include advisory IDs), but filtering only needs a boolean.
## Fix
- Add a lightweight API on `OsvIndex`, e.g. `is_vulnerable(name, version) -> bool` (or `has_vulnerabilities`) that:
- avoids allocating/cloning advisory IDs
- short-circuits on first match
- avoids per-call `seen` HashSet when only boolean is required
- Use that boolean API from `filter_osv_vulnerable_versions`.
## Fix Focus Areas
- pnpr/crates/pnpr/src/server.rs[2118-2124]
- pnpr/crates/pnpr/src/resolver/osv.rs[109-125]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

8. Duplicate OSV checks ✓ Resolved 🐞 Bug ➹ Performance
Description
filter_osv_vulnerable_versions() calls is_vulnerable() for both the versions map key and the
nested manifest's version field, which is redundant when they match (the common case). For large
packuments, this approximately doubles OSV parsing/advisory scans per version entry.
Code

pnpr/crates/pnpr/src/server.rs[R2118-2125]

+        blocked_keys = versions
+            .iter()
+            .filter_map(|(key, manifest)| {
+                let manifest_version = manifest.get("version").and_then(Value::as_str);
+                (osv_index.is_vulnerable(package_name, key)
+                    || manifest_version
+                        .is_some_and(|version| osv_index.is_vulnerable(package_name, version)))
+                .then(|| key.clone())
Evidence
The new filter calls is_vulnerable() twice per version entry; is_vulnerable() parses the version and
scans advisories, so repeating it for identical strings is wasted work.

pnpr/crates/pnpr/src/server.rs[2109-2142]
pnpr/crates/pnpr/src/resolver/osv.rs[109-115]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`filter_osv_vulnerable_versions()` checks both `key` and `manifest["version"]` against OSV. Most upstream packuments have `manifest.version == key`, so the second lookup typically repeats the same work.
### Issue Context
The second check is useful for guarding against identity mismatches, but it can be made conditional to preserve that protection without double work.
### Fix Focus Areas
- pnpr/crates/pnpr/src/server.rs[2109-2142]
- pnpr/crates/pnpr/src/resolver/osv.rs[109-115]
### Suggested change
Change the per-entry logic to:
- Compute `let key_vuln = osv_index.is_vulnerable(package_name, key);`
- Only if `manifest_version` exists and `manifest_version != key`, compute the second `is_vulnerable` check.
This keeps the mismatch defense while removing redundant OSV scans for well-formed packuments.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
pnpr/crates/pnpr/tests/server.rs (1)

154-159: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Strengthen cached-packument assertions for full OSV parity.

The cached response currently asserts only versions filtering. Add time and dist-tags checks here too, so the cache path is explicitly validated for the same filtered fields as the proxy path.

Suggested assertion additions
     let cached =
         app.clone().oneshot(Request::get("/foo").body(Body::empty()).unwrap()).await.unwrap();
     assert_eq!(cached.status(), StatusCode::OK);
     let cached_body = body_json(cached.into_body()).await;
     assert!(cached_body["versions"].get("1.1.0").is_none());
+    assert!(cached_body["time"].get("1.1.0").is_none());
+    assert!(cached_body["dist-tags"].get("latest").is_none());
+    assert_eq!(cached_body["dist-tags"]["stable"], "1.0.0");

As per coding guidelines, REVIEW_GUIDE.md asks for meaningful cross-surface regression assertions that prove filtering gates aren’t bypassed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pnpr/crates/pnpr/tests/server.rs` around lines 154 - 159, The cached
packument test currently validates only that filtered versions are absent from
the cached_body response but lacks assertions for the time and dist-tags fields,
which should also be filtered consistently with the proxy path. After the
existing assertion checking cached_body["versions"].get("1.1.0").is_none(), add
additional assertions to verify that cached_body["time"] and
cached_body["dist-tags"] are also properly filtered, ensuring the cache path
enforces the same filtering gates as the proxy path for complete OSV parity
validation.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pnpr/crates/pnpr/src/server.rs`:
- Around line 2118-2132: The OSV vulnerability bypass occurs in two places:
first, in the filter_map closure around line 2121, only the embedded version
field is checked against vulnerabilities while the key itself is not validated;
an attacker can keep a vulnerable version visible by setting its embedded
version to a safe value. Second, in the dist-tags retention logic around line
2131, dist-tags pointing to versions that were blocked and removed from the
versions map are not being filtered out. Fix this by modifying the filter_map to
check both the key and the embedded version against
osv_index.vulnerability_ids(), and ensure the dist-tags retain condition also
checks that the version still exists in the versions map after blocking,
removing any tags that point to blocked or missing versions.

---

Nitpick comments:
In `@pnpr/crates/pnpr/tests/server.rs`:
- Around line 154-159: The cached packument test currently validates only that
filtered versions are absent from the cached_body response but lacks assertions
for the time and dist-tags fields, which should also be filtered consistently
with the proxy path. After the existing assertion checking
cached_body["versions"].get("1.1.0").is_none(), add additional assertions to
verify that cached_body["time"] and cached_body["dist-tags"] are also properly
filtered, ensuring the cache path enforces the same filtering gates as the proxy
path for complete OSV parity validation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 82f64d83-c0e6-4239-86e5-571b818accb7

📥 Commits

Reviewing files that changed from the base of the PR and between bed6c59 and 41c88b5.

📒 Files selected for processing (6)
  • pnpr/crates/pnpr/src/config.rs
  • pnpr/crates/pnpr/src/error.rs
  • pnpr/crates/pnpr/src/package_name.rs
  • pnpr/crates/pnpr/src/package_name/tests.rs
  • pnpr/crates/pnpr/src/server.rs
  • pnpr/crates/pnpr/tests/server.rs
💤 Files with no reviewable changes (1)
  • pnpr/crates/pnpr/src/package_name.rs

Comment thread pnpr/crates/pnpr/src/server.rs Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 41c88b5

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 22, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 22, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@zkochan

zkochan commented Jun 22, 2026

Copy link
Copy Markdown
Member Author

Addressed the review feedback in follow-up commits:

  • 252779745f hardens packument filtering by checking both versions map keys and nested manifest version values, scrubbing matching dist-tags/time entries, and using a new allocation-free OsvIndex::is_vulnerable boolean path for filtering.
  • d0af04f8e7 expands the cached-packument test assertions to cover time and dist-tags filtering as well as versions.

Verification run locally:

  • cargo test -p pnpr --test server
  • cargo test -p pnpr osv --lib
  • cargo check --locked -p pnpr --all-targets
  • cargo clippy --locked -p pnpr --all-targets -- --deny warnings
  • git diff --check
  • repository pre-push hook on both pushes

Written by an agent (Codex, gpt-5).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pnpr/crates/pnpr/tests/server.rs`:
- Around line 236-240: The assertions for JSON object keys in this test are
using order-dependent vector comparison (collecting keys as Vec and comparing
equality), which can fail due to map ordering differences. Replace these
assertions by converting the keys to a set and comparing against an expected set
of keys, or use order-independent checks like verifying each expected key exists
and the total length matches. Update all three assertions in the block (for
body["versions"], body["dist-tags"], and body["time"]) to use set comparisons
instead of direct vector equality to make the test order-independent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 563ce02d-8057-4f3b-b6d1-68559a16c4ab

📥 Commits

Reviewing files that changed from the base of the PR and between 41c88b5 and 2527797.

📒 Files selected for processing (4)
  • pnpr/crates/pnpr/src/resolver/osv.rs
  • pnpr/crates/pnpr/src/resolver/osv/tests.rs
  • pnpr/crates/pnpr/src/server.rs
  • pnpr/crates/pnpr/tests/server.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • pnpr/crates/pnpr/src/server.rs

Comment thread pnpr/crates/pnpr/tests/server.rs Outdated
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 22, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

1 similar comment
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 22, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@github-actions github-actions Bot added the reviewed: coderabbit CodeRabbit submitted an approving review label Jun 22, 2026
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit ef0c62f

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit ef0c62f

@zkochan

zkochan commented Jun 22, 2026

Copy link
Copy Markdown
Member Author

Addressed Qodo’s tarball canonicalization finding in 299f935cd8: serve_tarball() now uses the canonical filename returned by parse_tarball_name() for cache reads, upstream fetches, and cache writes, while preserving the raw URL segment only for validation/error context.

Added scoped_tarball_filename_is_canonicalized_before_fetch_and_cache to cover an encoded full scoped package filename and verify it fetches/caches under the canonical tarball name.

Verification run locally:

  • cargo test -p pnpr --test server scoped_tarball_filename_is_canonicalized_before_fetch_and_cache -- --nocapture
  • cargo test -p pnpr --test server
  • cargo check --locked -p pnpr --all-targets
  • cargo clippy --locked -p pnpr --all-targets -- --deny warnings
  • git diff --check
  • repository pre-push hook

Written by an agent (Codex, gpt-5).

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 299f935

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 22, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@zkochan

zkochan commented Jun 22, 2026

Copy link
Copy Markdown
Member Author

Addressed Qodo's duplicate OSV manifest check finding in f1b3bf6837: filter_osv_vulnerable_versions() now always checks the version map key, but only checks the nested manifest version when it exists and differs from the key. That preserves mismatch protection while avoiding the common duplicate is_vulnerable() scan.

Verification run locally:

  • cargo test -p pnpr --test server
  • cargo check --locked -p pnpr --all-targets
  • cargo clippy --locked -p pnpr --all-targets -- --deny warnings
  • git diff --check
  • repository pre-push hook

Written by an agent (Codex, gpt-5).

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit f1b3bf6

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 22, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@zkochan

zkochan commented Jun 22, 2026

Copy link
Copy Markdown
Member Author

Addressed Qodo's manifest/dist-tags hot-path finding in 7aef872527: version-manifest requests now resolve the requested tag/version and run OSV checks only for that resolved version plus a mismatched embedded manifest version, and the dist-tags endpoint now filters only the tag values it returns instead of running the full packument scrub over every published version.

The mismatch regression now also covers direct version-manifest requests and direct dist-tags responses so the faster path keeps the previous OSV bypass protections.

Verification run locally:

  • cargo test -p pnpr --test server osv_filters_packument_identity_mismatches -- --nocapture
  • cargo test -p pnpr --test server
  • cargo check --locked -p pnpr --all-targets
  • cargo clippy --locked -p pnpr --all-targets -- --deny warnings
  • git diff --check
  • repository pre-push hook

Written by an agent (Codex, gpt-5).

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 7aef872

1 similar comment
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 7aef872

@codecov-commenter

codecov-commenter commented Jun 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.03546% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.06%. Comparing base (bed6c59) to head (6b07a50).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
pnpr/crates/pnpr/src/server.rs 95.34% 6 Missing ⚠️
pnpr/crates/pnpr/src/resolver/osv.rs 90.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12566      +/-   ##
==========================================
- Coverage   88.07%   88.06%   -0.02%     
==========================================
  Files         321      323       +2     
  Lines       45078    45368     +290     
==========================================
+ Hits        39704    39952     +248     
- Misses       5374     5416      +42     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD.

Scenario: Isolated linker: fresh restore, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 3.789 ± 0.268 3.545 4.374 1.84 ± 0.16
pacquet@main 3.712 ± 0.153 3.571 3.979 1.80 ± 0.12
pnpr@HEAD 2.146 ± 0.111 2.011 2.340 1.04 ± 0.07
pnpr@main 2.058 ± 0.101 1.927 2.250 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 3.7886291563800008,
      "stddev": 0.26803275808066984,
      "median": 3.64752333918,
      "user": 3.47899776,
      "system": 3.3836806599999996,
      "min": 3.54485305618,
      "max": 4.37372923618,
      "times": [
        4.37372923618,
        3.57613539818,
        3.66112884818,
        3.61105099418,
        3.9397874341800003,
        3.63391783018,
        3.60132158518,
        4.00829597318,
        3.54485305618,
        3.9360712081800004
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.7117354375800007,
      "stddev": 0.15307595567183074,
      "median": 3.63785655718,
      "user": 3.50111756,
      "system": 3.3511476599999996,
      "min": 3.57147937018,
      "max": 3.9789396641800003,
      "times": [
        3.6405359491800002,
        3.57147937018,
        3.68958224018,
        3.6307614531800003,
        3.5966677771800004,
        3.63517716518,
        3.9789396641800003,
        3.58880348418,
        3.82949574618,
        3.9559115261800004
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.14638516708,
      "stddev": 0.11109694095616475,
      "median": 2.1491749346800004,
      "user": 2.5932186600000007,
      "system": 2.9877140599999996,
      "min": 2.01126625718,
      "max": 2.34031723618,
      "times": [
        2.34031723618,
        2.20111137718,
        2.1057322751800003,
        2.2593306871800003,
        2.03009739018,
        2.0205505981800003,
        2.20738954718,
        2.09543870818,
        2.19261759418,
        2.01126625718
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.0582144545800003,
      "stddev": 0.10124445205557782,
      "median": 2.0496057491800004,
      "user": 2.61692926,
      "system": 2.95400286,
      "min": 1.9269663631799998,
      "max": 2.24952621518,
      "times": [
        1.9965485721799998,
        2.06299852418,
        2.09449399818,
        1.94414526318,
        2.04979979918,
        1.9269663631799998,
        2.0149364801800003,
        2.04941169918,
        2.1933176311800002,
        2.24952621518
      ]
    }
  ]
}

Scenario: Isolated linker: fresh restore, hot cache + hot store

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 631.8 ± 15.0 611.3 655.7 1.00
pacquet@main 644.9 ± 17.4 612.7 666.4 1.02 ± 0.04
pnpr@HEAD 722.9 ± 78.8 662.9 942.5 1.14 ± 0.13
pnpr@main 714.4 ± 82.4 652.6 939.1 1.13 ± 0.13
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6317573008,
      "stddev": 0.015004909824401419,
      "median": 0.6321784907,
      "user": 0.36856229999999995,
      "system": 1.32746642,
      "min": 0.6112559797,
      "max": 0.6556978537,
      "times": [
        0.6112559797,
        0.6488127377,
        0.6132716087,
        0.6294683257,
        0.6556978537,
        0.6232600937,
        0.6381127857,
        0.6348886557,
        0.6197326167,
        0.6430723507
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.6448947506,
      "stddev": 0.017374438475559074,
      "median": 0.6489139452,
      "user": 0.3756411,
      "system": 1.33085912,
      "min": 0.6126619307,
      "max": 0.6664037877,
      "times": [
        0.6466351977,
        0.6547282747000001,
        0.6258958987,
        0.6597090307,
        0.6511926927,
        0.6664037877,
        0.6612393807,
        0.6306643067,
        0.6398170057,
        0.6126619307
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.7229202953,
      "stddev": 0.0787537190027842,
      "median": 0.7044403817,
      "user": 0.38594560000000006,
      "system": 1.3705735199999998,
      "min": 0.6628824507,
      "max": 0.9424922147,
      "times": [
        0.7088738347,
        0.6878039137,
        0.6887798877,
        0.6628824507,
        0.7208949817,
        0.7066690687,
        0.7071489527,
        0.7022116947,
        0.7014459537,
        0.9424922147
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.7144119255000001,
      "stddev": 0.08239918421428115,
      "median": 0.6920626747,
      "user": 0.39042620000000006,
      "system": 1.35234162,
      "min": 0.6526185987,
      "max": 0.9391371667,
      "times": [
        0.6688180877,
        0.6652904847,
        0.7121542597,
        0.7034230077,
        0.6959937257,
        0.6859676037,
        0.6526185987,
        0.6881316237,
        0.9391371667,
        0.7325846967
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.161 ± 0.037 4.112 4.242 1.94 ± 0.09
pacquet@main 4.159 ± 0.041 4.104 4.219 1.94 ± 0.09
pnpr@HEAD 2.141 ± 0.101 2.029 2.339 1.00
pnpr@main 2.218 ± 0.128 2.051 2.392 1.04 ± 0.08
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.16106503176,
      "stddev": 0.03717480963377625,
      "median": 4.15563516456,
      "user": 3.6669685199999997,
      "system": 3.3267433999999994,
      "min": 4.11202522456,
      "max": 4.24211138856,
      "times": [
        4.14907781456,
        4.12384395756,
        4.16495949556,
        4.24211138856,
        4.16271662656,
        4.20064982956,
        4.15564883256,
        4.11202522456,
        4.15562149656,
        4.14399565156
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.15851549936,
      "stddev": 0.04105107487145694,
      "median": 4.14910995306,
      "user": 3.6620164200000005,
      "system": 3.337551,
      "min": 4.10403871156,
      "max": 4.21928095756,
      "times": [
        4.16476690956,
        4.10403871156,
        4.1875963045599995,
        4.19266826156,
        4.20719647356,
        4.11505550556,
        4.21928095756,
        4.12906078056,
        4.13203809256,
        4.13345299656
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.14129089896,
      "stddev": 0.10083950249471413,
      "median": 2.09984774456,
      "user": 2.468240919999999,
      "system": 2.8767052,
      "min": 2.02949210556,
      "max": 2.33907625856,
      "times": [
        2.08777797156,
        2.05636808356,
        2.08986072856,
        2.20129514456,
        2.33907625856,
        2.02949210556,
        2.10983476056,
        2.08814785656,
        2.28139584156,
        2.12966023856
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.2175148355600003,
      "stddev": 0.12785991679844677,
      "median": 2.20478807906,
      "user": 2.4271606199999995,
      "system": 2.9025352,
      "min": 2.05115365556,
      "max": 2.39163377156,
      "times": [
        2.21151178156,
        2.19806437656,
        2.30994972256,
        2.09157629656,
        2.39163377156,
        2.05115365556,
        2.36293293756,
        2.05559987656,
        2.33699773756,
        2.16572819956
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, hot cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.308 ± 0.012 1.292 1.326 2.03 ± 0.03
pacquet@main 1.355 ± 0.044 1.322 1.473 2.11 ± 0.07
pnpr@HEAD 0.674 ± 0.085 0.635 0.914 1.05 ± 0.13
pnpr@main 0.643 ± 0.009 0.632 0.662 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.30784410404,
      "stddev": 0.012071869640314443,
      "median": 1.30586282124,
      "user": 1.2890770999999996,
      "system": 1.7111762,
      "min": 1.2922784682400001,
      "max": 1.32619003224,
      "times": [
        1.31552237524,
        1.2922784682400001,
        1.32619003224,
        1.29850580224,
        1.29707580024,
        1.3110682042400001,
        1.31982673924,
        1.32013938924,
        1.30065743824,
        1.29717679124
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.35473015584,
      "stddev": 0.044289711001699816,
      "median": 1.3386479282400001,
      "user": 1.3137156999999997,
      "system": 1.7682381999999996,
      "min": 1.32184445424,
      "max": 1.47258058324,
      "times": [
        1.34641684524,
        1.37732504424,
        1.32184445424,
        1.47258058324,
        1.35437449524,
        1.3373333122400002,
        1.3396237262400001,
        1.33579804724,
        1.33767213024,
        1.32433292024
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.67357434224,
      "stddev": 0.08473474792312322,
      "median": 0.64430041474,
      "user": 0.3377559,
      "system": 1.2844017,
      "min": 0.6345491602400001,
      "max": 0.9136293322400001,
      "times": [
        0.6583716552400001,
        0.64393179824,
        0.6345491602400001,
        0.9136293322400001,
        0.65901258224,
        0.64447224024,
        0.6403944582400001,
        0.64195826824,
        0.64412858924,
        0.6552953382400001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6429654462400002,
      "stddev": 0.00865202688761687,
      "median": 0.6396053702400001,
      "user": 0.3318463,
      "system": 1.2864617,
      "min": 0.6320775592400001,
      "max": 0.6620332962400001,
      "times": [
        0.6620332962400001,
        0.63773151524,
        0.64580103424,
        0.6399665402400001,
        0.6320775592400001,
        0.65048980724,
        0.63680455024,
        0.6471490292400001,
        0.6392442002400001,
        0.6383569302400001
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.966 ± 0.037 2.912 3.047 4.59 ± 0.09
pacquet@main 3.003 ± 0.043 2.968 3.118 4.65 ± 0.10
pnpr@HEAD 0.663 ± 0.016 0.638 0.680 1.03 ± 0.03
pnpr@main 0.646 ± 0.010 0.636 0.670 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.9662301827000004,
      "stddev": 0.03743214109296507,
      "median": 2.9679507013000004,
      "user": 1.71812228,
      "system": 1.94530292,
      "min": 2.9119107838000002,
      "max": 3.0471645498,
      "times": [
        2.9922226378000003,
        2.9666734798000003,
        2.9239126738000003,
        2.9708258818,
        2.9781377148000003,
        2.9119107838000002,
        2.9537581708000005,
        3.0471645498,
        2.9692279228,
        2.9484680118
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.0034692179,
      "stddev": 0.04259503134105539,
      "median": 2.9909114788,
      "user": 1.7196206799999998,
      "system": 2.0053145199999998,
      "min": 2.9684333358000003,
      "max": 3.1175952438000003,
      "times": [
        3.0150527128,
        2.9868168128000003,
        3.0007070748,
        2.9684333358000003,
        2.9827364088,
        2.9950061448,
        2.9742817828000003,
        2.9865102588,
        3.1175952438000003,
        3.0075524038
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.662541383,
      "stddev": 0.0163980017412811,
      "median": 0.6662758833000001,
      "user": 0.33595548,
      "system": 1.2906068199999998,
      "min": 0.6378379608000001,
      "max": 0.6803069558000001,
      "times": [
        0.6491996298000001,
        0.6801263668,
        0.6803069558000001,
        0.6640151928000001,
        0.6685365738000001,
        0.6460170338000001,
        0.6765003728000001,
        0.6378379608000001,
        0.6772242028000001,
        0.6456495408
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6462483329,
      "stddev": 0.009737286646791908,
      "median": 0.6448376463000001,
      "user": 0.32767478000000005,
      "system": 1.2854211199999999,
      "min": 0.6362040578000001,
      "max": 0.6696410678000001,
      "times": [
        0.6414370778,
        0.6393619808000001,
        0.6463377128000001,
        0.6552286038,
        0.6696410678000001,
        0.6447204848000001,
        0.6449548078,
        0.6392331258,
        0.6362040578000001,
        0.6453644098000001
      ]
    }
  ]
}

@github-actions

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12566
Testbedpacquet
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
🚷 view threshold
4,161.07 ms
(-1.33%)Baseline: 4,217.25 ms
5,060.70 ms
(82.22%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
2,966.23 ms
(-1.34%)Baseline: 3,006.54 ms
3,607.85 ms
(82.22%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,307.84 ms
(-1.44%)Baseline: 1,326.93 ms
1,592.32 ms
(82.13%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
3,788.63 ms
(-4.42%)Baseline: 3,963.76 ms
4,756.52 ms
(79.65%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
631.76 ms
(+1.46%)Baseline: 622.68 ms
747.22 ms
(84.55%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12566
Testbedpnpr

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
BenchmarkLatencymilliseconds (ms)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,141.29 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
662.54 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
673.57 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,146.39 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
722.92 ms
🐰 View full continuous benchmarking report in Bencher

@zkochan

zkochan commented Jun 22, 2026

Copy link
Copy Markdown
Member Author

Addressed Qodo's redundant OSV time-check finding in 6b07a50565: filter_osv_vulnerable_versions() now filters versions in a single retain pass, records blocked keys and retained safe version keys during that pass, and lets time reuse those decisions instead of re-running OSV checks for ordinary version timestamps. It still falls back to a direct OSV check for dangling non-special time keys, so malformed packuments with vulnerable version strings outside versions remain filtered.

Verification run locally:

  • cargo test -p pnpr --test server osv_filters_packument_identity_mismatches -- --nocapture
  • cargo test -p pnpr --test server
  • cargo check --locked -p pnpr --all-targets
  • cargo clippy --locked -p pnpr --all-targets -- --deny warnings
  • git diff --check
  • repository pre-push hook

Written by an agent (Codex, gpt-5).

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 6b07a50

1 similar comment
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 6b07a50

@zkochan zkochan merged commit faf31ef into main Jun 22, 2026
31 checks passed
@zkochan zkochan deleted the feat/12561 branch June 22, 2026 08:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

product: pnpr reviewed: coderabbit CodeRabbit submitted an approving review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pnpr: OSV screening is not applied on the registry serving path (proxy and cache)

2 participants