From 1f1e482ac7c600c10695e59e0c9071aea62615f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 08:33:01 +0000 Subject: [PATCH 1/5] Start fix for #325 Assisted-by: Claude Code:claude-opus-5-5 From 9f8515f0d57db0061b0d7ad01fa9b52877eae230 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 08:37:14 +0000 Subject: [PATCH 2/5] Stop in-run VEX attesting npm bundled copies When a package-lock.json also lists a bundled copy of the patched package (inBundle, or the legacy v1 "bundled" flag), hosted mode redirects the regular entry but can't reach the bundled one: npm unpacks it from the parent's tarball. The run already warned that copy stays unpatched, yet `scan --vex` still attested the patch as not_affected. The npm rewriter now records the patch as having a skipped bundled copy, the same way the Bun rewriter does, so the in-run VEX verifies it instead of assuming it applied, and leaves it out. Fixes #325 Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/in_process_redirect.rs | 113 ++++++++++++++++++ .../src/patch/redirect/mod.rs | 18 ++- 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/crates/socket-patch-cli/tests/in_process_redirect.rs b/crates/socket-patch-cli/tests/in_process_redirect.rs index 7ae650809..a62b84986 100644 --- a/crates/socket-patch-cli/tests/in_process_redirect.rs +++ b/crates/socket-patch-cli/tests/in_process_redirect.rs @@ -1179,6 +1179,119 @@ async fn scan_redirect_bun_bundled_copy_is_not_attested_in_run() { ); } +/// REGRESSION (#325): npm's half of #469. The hoisted entry is redirected, +/// but a parent also bundles the same `name@version` (`inBundle: true` in +/// `packages`, or the legacy `bundled: true` spelling in a v1 +/// `dependencies` tree). npm unpacks that copy from the parent's tarball, +/// so it stays unpatched; the run warns +/// `redirect_npm_bundled_instance_skipped`, and its in-run `--vex` must +/// not attest the purl either. +#[tokio::test] +#[serial] +async fn scan_redirect_npm_bundled_copy_is_not_attested_in_run() { + let packages_lock = format!( + r#"{{ + "name": "consumer", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": {{ + "": {{ "name": "consumer", "version": "0.0.0", "dependencies": {{ "{NAME}": "{VERSION}", "parent": "2.0.0" }} }}, + "node_modules/{NAME}": {{ + "version": "{VERSION}", + "resolved": "https://registry.npmjs.org/{NAME}/-/{NAME}-{VERSION}.tgz", + "integrity": "sha512-UPSTREAMupstream==" + }}, + "node_modules/parent": {{ + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parent/-/parent-2.0.0.tgz", + "integrity": "sha512-PARENT==" + }}, + "node_modules/parent/node_modules/{NAME}": {{ + "version": "{VERSION}", + "inBundle": true, + "integrity": "sha512-UPSTREAMupstream==" + }} + }} +}} +"# + ); + let legacy_lock = format!( + r#"{{ + "name": "consumer", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": {{ + "{NAME}": {{ + "version": "{VERSION}", + "resolved": "https://registry.npmjs.org/{NAME}/-/{NAME}-{VERSION}.tgz", + "integrity": "sha512-UPSTREAMupstream==" + }}, + "parent": {{ + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parent/-/parent-2.0.0.tgz", + "integrity": "sha512-PARENT==", + "dependencies": {{ + "{NAME}": {{ + "version": "{VERSION}", + "bundled": true + }} + }} + }} + }} +}} +"# + ); + for (shape, lock) in [("inBundle", packages_lock), ("legacy bundled", legacy_lock)] { + let server = MockServer::start().await; + mock_discovery(&server).await; + mock_reference(&server).await; + mock_view(&server).await; + + let tmp = tempfile::tempdir().unwrap(); + write_project(tmp.path()); + std::fs::write(tmp.path().join("package-lock.json"), &lock).unwrap(); + let copy = tmp + .path() + .join("node_modules/parent/node_modules") + .join(NAME); + std::fs::create_dir_all(©).unwrap(); + std::fs::write( + copy.join("package.json"), + format!(r#"{{ "name": "{NAME}", "version": "{VERSION}" }}"#), + ) + .unwrap(); + std::fs::write( + tmp.path().join("node_modules/parent/package.json"), + format!( + r#"{{ "name": "parent", "version": "2.0.0", "bundleDependencies": ["{NAME}"] }}"# + ), + ) + .unwrap(); + + let out = tmp.path().join("out.vex.json"); + let mut args = redirect_args(tmp.path(), server.uri()); + args.vex.vex = Some(out.clone()); + args.vex.vex_product = Some("pkg:npm/consumer@0.0.0".into()); + let _ = run(args).await; + + let rewritten = std::fs::read_to_string(tmp.path().join("package-lock.json")).unwrap(); + assert!( + rewritten.contains(HOSTED_URL), + "{shape}: the regular entry is redirected:\n{rewritten}" + ); + let attested = std::fs::read_to_string(&out) + .ok() + .and_then(|text| serde_json::from_str::(&text).ok()) + .is_some_and(|doc| doc.to_string().contains(PURL)); + assert!( + !attested, + "{shape}: in-run VEX must not attest a purl whose bundled copy stays unpatched" + ); + } +} + /// The bun 1.4 leg: `"lockfileVersion": 2` is the SAME emitted grammar as 1 /// (bun 1.4 bumped the integer to gate stricter parse checks — oven-sh/bun /// PR #31539 — same-fixture locks are byte-identical except the integer), so diff --git a/crates/socket-patch-core/src/patch/redirect/mod.rs b/crates/socket-patch-core/src/patch/redirect/mod.rs index c5b565053..e8e337e05 100644 --- a/crates/socket-patch-core/src/patch/redirect/mod.rs +++ b/crates/socket-patch-core/src/patch/redirect/mod.rs @@ -966,9 +966,12 @@ fn rewrite_one_npm_lock( // here would put the hosted URL in the lockfile (confirming // and VEX-attesting the patch) while the unpatched bundled // bytes keep installing. Mirrors the vendored backend's - // `vendor_bundled_instance_skipped` refusal. + // `vendor_bundled_instance_skipped` refusal. The uuid is + // recorded so the in-run `--vex` verifies instead of + // assuming the patch applied (#325, as Bun's #469). if entry.get("inBundle").and_then(Value::as_bool) == Some(true) { matched_any = true; + result.bundled_skipped_uuids.insert(dep.patch_uuid.clone()); result.warnings.push(RewriteWarning { code: "redirect_npm_bundled_instance_skipped".into(), detail: format!( @@ -1113,6 +1116,7 @@ fn rewrite_npm_v2_deps( // fail-open as the `packages` guard above. if entry.get("bundled").and_then(Value::as_bool) == Some(true) { *matched_any = true; + result.bundled_skipped_uuids.insert(dep.patch_uuid.clone()); result.warnings.push(RewriteWarning { code: "redirect_npm_bundled_instance_skipped".into(), detail: format!( @@ -13643,6 +13647,10 @@ mod tests { "a bundled skip is a MATCH — not-found must stay quiet: {:?}", r.warnings ); + assert!( + r.bundled_skipped_uuids.contains(&overrides[0].patch_uuid), + "#325: the skipped bundled copy must keep the patch out of the in-run VEX" + ); } /// #326: npm installs a git, remote-tarball or `file:` dependency from @@ -14007,6 +14015,10 @@ mod tests { "partial coverage must be surfaced: {:?}", r.warnings ); + assert!( + r.bundled_skipped_uuids.contains(&overrides[0].patch_uuid), + "#325: a redirected sibling must not let the in-run VEX attest the patch" + ); } /// The v1/v2 legacy `dependencies` tree spells the bundled flag @@ -14054,6 +14066,10 @@ mod tests { "legacy bundled skip must warn: {:?}", r.warnings ); + assert!( + r.bundled_skipped_uuids.contains(&overrides[0].patch_uuid), + "#325: the legacy bundled skip must be recorded like `inBundle`" + ); } /// An alias install (`npm i my-alias@npm:left-pad@1.3.0`) keys the lock From 89eeda845a03eabd3d04e0f4830287c020069051 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 08:45:29 +0000 Subject: [PATCH 3/5] Add changelog entry for #325 Assisted-by: Claude Code:claude-opus-5-5 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3572a298c..60f6308dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,6 +102,12 @@ limits, and required install commands. ### Fixed +- `scan --vex` in hosted mode no longer attests an npm patch as + `not_affected` when `package-lock.json` also lists a bundled copy of the + same `name@version` (`inBundle`, or `bundled` in a v1 lock). npm unpacks + that copy from its parent's tarball, so it stays unpatched; the run + already warned `redirect_npm_bundled_instance_skipped` and now leaves the + patch out of its attestation, like a standalone `vex` run (#325). - Global mode (`-g`) finds npm, yarn, pnpm, bun, RubyGems and Composer on Windows, where they install as `.cmd` / `.bat` shims, instead of reporting an empty scan. The yarn and npm-family global lookups no longer run from the From 4b18db6dd25277794732ad5fa0064a2b1752aad6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 09:11:49 +0000 Subject: [PATCH 4/5] Re-bless npm lock rewrite golden for #325 The seeded npm lock sweep generates bundled entries, and its recorded rewrite result now carries the patch uuids those entries skipped. Input digests are unchanged; only the cases with a bundled match moved. Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/equivalence/npm_lock_rewrite.golden | 126 +++++++++--------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/crates/socket-patch-core/tests/equivalence/npm_lock_rewrite.golden b/crates/socket-patch-core/tests/equivalence/npm_lock_rewrite.golden index c173088c8..d3824900a 100644 --- a/crates/socket-patch-core/tests/equivalence/npm_lock_rewrite.golden +++ b/crates/socket-patch-core/tests/equivalence/npm_lock_rewrite.golden @@ -1,7 +1,7 @@ # One seeded package-lock.json / npm-shrinkwrap.json + overrides. # 0-3 38b943cc2e19137b 6afbf79b2ea51d7f -4-7 39d1bec8b19fe2b3 0234d597163850f1 +4-7 39d1bec8b19fe2b3 3f9fc948794bdbeb 8-11 ad36d989d79734db 0671dbb2288c2520 12-15 481675194b39d860 60ec31624712a77a 16-19 7e15f8f39a3af575 fbb3a1c64927f7dc @@ -14,7 +14,7 @@ 44-47 6131178fcff5f168 fe7bcd7668916be4 48-51 7f23e01097535c51 7f6fc1a7d4b046f9 52-55 0d5e691dad879076 7604ade4a0a89219 -56-59 6aa8ab1476372939 3504a591b574d127 +56-59 6aa8ab1476372939 42897d4994dc18aa 60-63 98d75ab23a2e9c01 dbc423fe3aa2031c 64-67 d839bc25655d17bd c58dce409d967d71 68-71 40f1c301fa3d1824 918eb331f480af44 @@ -24,10 +24,10 @@ 84-87 23d6dfca3dc60d0c 078eeb46fd92ff43 88-91 174afe518c036993 58d76ad81a0736f1 92-95 55c49569b1ff28ea 2c66448481270ec3 -96-99 e9b8cddfc47c2410 08948aed5135ec1f +96-99 e9b8cddfc47c2410 e45224d4571298a6 100-103 21dbb73e40e5a210 af5e7560a9d0c0bd 104-107 0c170349f3052ef9 27e2b78479f64dee -108-111 b9c3e6c7cdfad95f 98f730a5706b25d6 +108-111 b9c3e6c7cdfad95f c25f4a60c3ff3f39 112-115 b7f86471de74f76c 392efc30f7f532cb 116-119 5b317616e7ec450d 892261da3954f566 120-123 1af385991cee8693 985f1874988de52b @@ -37,126 +37,126 @@ 136-139 98773f82425a6fd9 9011cd7a515c0826 140-143 7635eee1f74b943c 9da248d486b3c382 144-147 43ccf3156a238f1d 55da03ffc362d938 -148-151 bd11851bf97a2c30 31b5b52dbaf2afed +148-151 bd11851bf97a2c30 c8f6b758d93c1d43 152-155 219d9afb7f6a45b2 7f198d2d08e2d656 156-159 7c1e1e997c16b02e ca68697c227ebea9 160-163 143279d02db7a456 11e2adc2d8e75624 164-167 d4adfb48c56be5b6 a3fa28797bc91236 -168-171 c063c9818a9362b1 bf8c8db42eeccc8c -172-175 1ee9e3f5a3530fc8 98aa42cd99b2ef29 -176-179 2567d140ce3ceda0 a76633f81f07d4a7 -180-183 8c63f24d3ac472f6 cd0f2d2f7aa925a8 +168-171 c063c9818a9362b1 c196ce989d45b565 +172-175 1ee9e3f5a3530fc8 4b53862c8df8b4bd +176-179 2567d140ce3ceda0 4a9307775128fdc4 +180-183 8c63f24d3ac472f6 31765822966ccbe0 184-187 768a53cbd61c832c d64742504fdaaba0 188-191 1b79c77ae63569a4 fc9fd305e1ddc085 192-195 a652530b7d9610fc 7ef3c98c60d82ab5 196-199 e81b9c1564f40cb9 1834aaaa0f9cf4e5 -200-203 ac3292d6054c9914 a5c6cacbd076196e +200-203 ac3292d6054c9914 45c7b2ecb0f2264f 204-207 f6a5d0e294b53a1f 3fa6da640079201f -208-211 faac70857d5d0378 ce52ae0e9fe5c201 -212-215 10ca242fff41fc4a 44c191684952c35d +208-211 faac70857d5d0378 e06942c9f1a2e3de +212-215 10ca242fff41fc4a 1cb4fecfb0fa6a07 216-219 1c86705c9f655e90 7335c0978cc1c66e -220-223 657b5d828c9a98e7 a8c2c5ed16f51ab2 -224-227 ea4b041b46eee835 19df59fcb5719e5f +220-223 657b5d828c9a98e7 3fcfb270016947a7 +224-227 ea4b041b46eee835 74833e20387de400 228-231 8fdaf1c02bb638d5 10a44be416359fe9 -232-235 f073c1581fe82aca 13685e45ce0530dd +232-235 f073c1581fe82aca 0a02658f5e9682c4 236-239 e1f60ac97e7ef626 2f9548820333e8ca -240-243 737595eab5c4dc3f 2c42475072ac9025 -244-247 00b82a270764acad 5bca5722c86de90b -248-251 b7e7e3ff5921f56b 2dfb1d83eb83cbe4 +240-243 737595eab5c4dc3f 3794f136ad388ef3 +244-247 00b82a270764acad b3edfbe71a44cc50 +248-251 b7e7e3ff5921f56b 12344d9b6f3853f9 252-255 0e058ae2b4f4eef1 f0f9bbd3e645f0a1 -256-259 30c973883ccb389b 1507fd6e0786016b -260-263 3f7575a697a45103 087b2271977ba307 -264-267 f98b36dd3f4431f7 7c0aeabf6e27638f +256-259 30c973883ccb389b 62fb96faf12cb9c2 +260-263 3f7575a697a45103 4f170dce96fa9d41 +264-267 f98b36dd3f4431f7 66fda399b9e04c42 268-271 2242e200be734689 961c2cbf0c72226b 272-275 9fbf78e7d41a7e58 543f347b65a23c70 276-279 ea0a7560bb20466c 0c715c2207044d29 280-283 3ab69851cae70bcd 8e480b61d81592af 284-287 1fdb596cc87f031b 64578f1e84310366 -288-291 ed84081f4124d522 68797a18e99c2dbd +288-291 ed84081f4124d522 68035e745e6933e7 292-295 18bc713c335e243c 9c8a170ff899c58b 296-299 658ce14ea92fffd5 536e77ae46114631 300-303 9a8fde3916e27342 bab49d863a06a18e 304-307 ac9ed568f15ba5d7 ee0cc2e069d5e0f9 308-311 8ddc66826aca8737 28296ac8c39c5dcf -312-315 b5092b2eb6d28e04 98807cd38d0e4fb4 +312-315 b5092b2eb6d28e04 399dc984fbbaab55 316-319 27d1154fe85c9091 281b9314ec579043 320-323 8f7ded70633dd0a3 2ab2a92cb15c09cc 324-327 8c22483048c74dca 2e1a0cad7e7f49b8 328-331 3b6a2eae6af90be3 45de24588519ef65 332-335 291ee053b410dc25 4821eb617b8d86ec 336-339 c00ae81f0a2be968 9f069a599d03be8b -340-343 1b3d34745caf497c 562af394d0cb24db -344-347 8e350ee856023a1f 7f3b5b09c26ad562 +340-343 1b3d34745caf497c 5b7d8b63e80b5270 +344-347 8e350ee856023a1f 3a8e7193929a7048 348-351 7b0511648fa4447c e0f1d4a90bf10bbc -352-355 460a50b29b48043d 471c904c5bd15578 +352-355 460a50b29b48043d ee516e652b88c45a 356-359 a304ae8e7e03c44c d9b4d4de533a4b35 -360-363 7055899176855866 21c59a685c9b48f8 -364-367 f8819f4cbe245541 f9f82c123e2a73d7 +360-363 7055899176855866 8584d97a6081443c +364-367 f8819f4cbe245541 5b51feb62d76b033 368-371 3d30266cadaa3712 44f173d7028a72e7 372-375 a65f748f7e3f6933 e612c7529ecd9fc1 -376-379 ee1ad63fa3935a76 27eb4c24c582e5be +376-379 ee1ad63fa3935a76 2d0b8d59a4cc6d7c 380-383 0c76dfd9b908af15 c62f2970357958d4 384-387 7d113d1c4bcf7ede 694120e2b01d24db 388-391 392bfdf6873fb038 f681d116f11dc011 392-395 8ab3f5ef6c2260dc fdc1bd49d88b0f00 396-399 e70ee73cf9f93cdd 1fe4da6c539ccf86 400-403 dda6b03292721d0b 9795245a300e80da -404-407 cd2441a35b1c5740 6fd4d2a386eed373 +404-407 cd2441a35b1c5740 0ea79c1f4eeded7d 408-411 91ac4721e77b4500 0bc11d72a0297bb9 -412-415 28af4189511719f9 a2330b0fc992c811 -416-419 d9e2eae424bebb3e 97225086b3cde613 -420-423 0f86f1432c081b6c fadc9e8e38e60240 +412-415 28af4189511719f9 3d29075156f4c9fa +416-419 d9e2eae424bebb3e 0931fe460360ff98 +420-423 0f86f1432c081b6c c8cd109152c9000d 424-427 6533c923cb60e539 88649c02c40ace42 -428-431 80c11eb13664c5fb 3875f53d37e204d1 +428-431 80c11eb13664c5fb 47730993f3a4b3f1 432-435 d33b5fff79332f39 47fe549195bb9a15 436-439 c70bbaca1ed730dc d8aaef4d259a74ad 440-443 b1152d879b9de674 a4f72811e3bffae7 444-447 a8f916b2e93c9dbe 66e21676c46442a5 448-451 6203dc0bec8c785e 8a2b3312f0c89fa8 -452-455 9521dde1a284ecb3 fe060608d6730ef9 +452-455 9521dde1a284ecb3 222989e9591280a3 456-459 4f253fce1a2e0160 b2def197c1a7f2e3 -460-463 cb3feed1731c395e b45744c5bfb34150 +460-463 cb3feed1731c395e 08bb0f0668ac08c2 464-467 1b0fa950663ac590 e1259bda18595a9a -468-471 9b8b2340d8531f3e 0a64e4abb6603264 +468-471 9b8b2340d8531f3e 5cc104c917c7cfa8 472-475 736df961ac44dd1f 6e0b231581610bca 476-479 71839c074d059821 e32e1ee9c4690986 480-483 2dfe4501253d999f 3f2903a8a1f3d955 -484-487 6d4cfdfeda102637 a3c8744a57be0dda +484-487 6d4cfdfeda102637 f243f89ebb53ccb8 488-491 8e04bbe242721495 4827195052919674 492-495 4b804da1bed766bc bdeb1a87ef9305c2 496-499 e4358ad81f66c66c 1bd273b112273309 -500-503 d497faab2e6374d1 25ff9866dabb19b5 +500-503 d497faab2e6374d1 a6b7648938f9efcc 504-507 bd134b4a4fd9c1af ec0b3fba5c88c493 -508-511 3a1c53db7ba9c199 37616449222b5305 -512-515 9ac76bdafc788702 cc92331d1da45af9 +508-511 3a1c53db7ba9c199 fa3b06c10bf6d6b0 +512-515 9ac76bdafc788702 1b12e2367ef70fd8 516-519 cc5cb99242e72eea 2412c7294ae0e7fb 520-523 df8749c4f1d4c88d 2151d47d975cb37f -524-527 df1f4eb0a17e6840 7c39d5aa3d8b642a -528-531 5bd935a1966450a1 b061be4d09b62e08 -532-535 d3c766b67ad93619 3ca34eed97e981b6 -536-539 0abea7838ee8c651 d9bc957c8cae0752 +524-527 df1f4eb0a17e6840 a8c5a985520801ad +528-531 5bd935a1966450a1 5405033fc13d5f62 +532-535 d3c766b67ad93619 0eb262d5010e1fca +536-539 0abea7838ee8c651 dafd54e8801c3793 540-543 5a34e67ca32acd76 441ba16c8b63b7b3 544-547 8139c395ec116abf 1e3325d4109dd7b7 548-551 7937635862cd0cc7 cdda5e17b4f48a72 552-555 f18bccaae83b8a26 d0df0c9009bc78a6 -556-559 0d7281f2dc6cab7c 882e42c94ab9c1ab +556-559 0d7281f2dc6cab7c f88a0cefea8fd9d1 560-563 67b3e9f59200ece4 adb2ddac66bceede -564-567 eb7c20329e96b501 69123af19dd03c33 +564-567 eb7c20329e96b501 a3ca918e8d58299e 568-571 d293cc2b575a278d b4e3b6ca4d70ca48 -572-575 c17b85264698e947 c31226a5993d36d0 +572-575 c17b85264698e947 edcae00b1fdd32ad 576-579 86db374e9417a100 b2589b8c5862526d -580-583 ce0fc0bd2408d817 ae1c81bc0e42a310 -584-587 436c95276aa8074e 365bd19a4a6b4001 +580-583 ce0fc0bd2408d817 03db723dd12019c4 +584-587 436c95276aa8074e df1bbf8d7663a3c7 588-591 c09216f32360676b 66b0ec275885c59c 592-595 2c4e9ab4e7011bcd b690d76fc146a3cb -596-599 cc8ac6e16d3c70a2 ec2982d7ac00c04d +596-599 cc8ac6e16d3c70a2 a769d6b0522f7d3f 600-603 5e61ab66a7b47ef0 0a0039c9514fa44a -604-607 890a631fda14796b d0d34470f95f0f00 -608-611 36c6f3f1734989cb c9c44c8231adc276 +604-607 890a631fda14796b bdab2de6ec82c8da +608-611 36c6f3f1734989cb 8a6246b8c05f144f 612-615 918d05dba93bf7af ae6b338ed65863e2 616-619 cc1fc9fe234e6237 e04ec4070749160b 620-623 2b1c61c2f9ffecf7 8fbb465964e46b0e -624-627 9e25c994d6e9f497 3e9ad345fcf9a2c4 +624-627 9e25c994d6e9f497 2402434dc456687e 628-631 9569acd0022c72e9 99eeb42b41eac287 632-635 02a74614410dc8de 82d095eaed3f21e1 636-639 0064b192beb2f16d fc9da6ebac16e671 @@ -165,36 +165,36 @@ 648-651 8ddac4722587be4f ace2b06a8b8c8c2f 652-655 647ba94842de4a99 8fea29aad4870b17 656-659 22c5789b07c59689 0aaa723ae5b9df03 -660-663 f981a3e36eabb727 2320f8c204b31436 +660-663 f981a3e36eabb727 9f448a9a173571e3 664-667 6378568f624c46d3 cb9bf66dc3cda08a 668-671 53b118efd7818da9 4564ab4a7f0d559d 672-675 a3219e99fc477e55 c6df6f0d87b9b9c3 676-679 2dbb5f9d13c34873 7580908dfdcfea1d 680-683 f59699ffa5de8930 804313d49536b1eb 684-687 59cb168e9f9a08b3 398c4bfcc9646dad -688-691 6dfa1486645043d6 e539a4a91043a734 -692-695 bf91bdd85d72f097 cebeb8199696570a +688-691 6dfa1486645043d6 d81171f958bed162 +692-695 bf91bdd85d72f097 a2515c0d5601d71b 696-699 30cef91da832598a 806297d879f04028 -700-703 9914c1d4b503c2fa a3ad262fe1bbe8b1 +700-703 9914c1d4b503c2fa b2d53e04d21b7bc6 704-707 db31d2e2f05ae765 1ab6149d78be4168 708-711 73b257c77137ec8d f77474faf5d33ba7 712-715 2723b37a94e6f61d cac8d4e20c22b5a2 716-719 87b719e81d116915 8bfa142895937a5b 720-723 39aded84cb8ff42d 653e264f24b22aed -724-727 a3a2175101809603 d75e9978ae05cfff +724-727 a3a2175101809603 227854fbbd1f0c23 728-731 60daab1a32e79814 a026a111e5942609 -732-735 8f36fa43d3e14039 1101d2c2bdbc07cb -736-739 0055deef8e34fb7e b43ec4abbe4b9637 +732-735 8f36fa43d3e14039 6fe42e6f7385c02f +736-739 0055deef8e34fb7e 8ae62be3951c4a1f 740-743 219c65c23e60569e 0ac08b9102fe82c4 744-747 13ff170d3d8cf8cf 34c8f3d49bf9d824 748-751 2b646f2ebcca65e2 e1e2ef0f8acc106b 752-755 9171f34232dc9d5d 1f3743ef48f2b715 756-759 6e922d3893444fd8 5e1314867d30fa3d 760-763 d11f1fcb3df5ee76 33ce5e620fa20e08 -764-767 b8a0235197ffeb8f edae9a91a5a0c070 +764-767 b8a0235197ffeb8f 48ae6872a82dae8e 768-771 84d8643c18a5e371 df626fca46bed1f9 772-775 dd10317147218f0a 224f4602080a058c -776-779 19a4e66646f42e84 15185a1ff7aa56d6 +776-779 19a4e66646f42e84 84ab921e068c65f9 780-783 0bb4a00c871b5a42 e910828f4f26627c 784-787 c0bb93b2e4e6cf4e aebb40e04549459e 788-791 850c2fdb73cd8f87 4bed71a422cb5f7a From afd320eb9819d9f30758f307ef6e641370d4c98f Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Sat, 3 Oct 2026 06:40:40 -0400 Subject: [PATCH 5/5] fix(npm): ignore stale bundle flags in legacy mirrors --- CHANGELOG.md | 4 +- .../tests/in_process_redirect.rs | 59 +++++++++++++++++ .../src/patch/redirect/mod.rs | 63 ++++++++++++++++++- .../tests/equivalence/npm_lock_rewrite.golden | 20 +++--- 4 files changed, 134 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60f6308dc..0802c37f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -107,7 +107,9 @@ limits, and required install commands. same `name@version` (`inBundle`, or `bundled` in a v1 lock). npm unpacks that copy from its parent's tarball, so it stays unpatched; the run already warned `redirect_npm_bundled_instance_skipped` and now leaves the - patch out of its attestation, like a standalone `vex` run (#325). + patch out of its attestation, like a standalone `vex` run (#325). When a + `packages` map exists, stale bundled flags in the legacy `dependencies` + mirror do not suppress an attestation for the actual install tree. - Global mode (`-g`) finds npm, yarn, pnpm, bun, RubyGems and Composer on Windows, where they install as `.cmd` / `.bat` shims, instead of reporting an empty scan. The yarn and npm-family global lookups no longer run from the diff --git a/crates/socket-patch-cli/tests/in_process_redirect.rs b/crates/socket-patch-cli/tests/in_process_redirect.rs index a62b84986..66ba2ac4b 100644 --- a/crates/socket-patch-cli/tests/in_process_redirect.rs +++ b/crates/socket-patch-cli/tests/in_process_redirect.rs @@ -1292,6 +1292,65 @@ async fn scan_redirect_npm_bundled_copy_is_not_attested_in_run() { } } +/// npm 7+ installs from the v2 `packages` map. An ignored legacy bundled +/// flag must not block the hosted in-run attestation before that install. +#[tokio::test] +#[serial] +async fn scan_redirect_npm_stale_legacy_bundle_mirror_still_attests() { + for lockfile in ["package-lock.json", "npm-shrinkwrap.json"] { + let server = MockServer::start().await; + mock_discovery(&server).await; + mock_reference(&server).await; + mock_view(&server).await; + let tmp = tempfile::tempdir().unwrap(); + write_project(tmp.path()); + std::fs::write( + tmp.path().join("node_modules").join(NAME).join("index.js"), + b"upstream bytes before the next install\n", + ) + .unwrap(); + let original = tmp.path().join("package-lock.json"); + let mut lock: serde_json::Value = + serde_json::from_slice(&std::fs::read(&original).unwrap()).unwrap(); + lock["lockfileVersion"] = serde_json::json!(2); + lock["dependencies"] = serde_json::json!({ + NAME: { + "version": VERSION, + "resolved": format!("https://registry.npmjs.org/{NAME}/-/{NAME}-{VERSION}.tgz"), + "integrity": "sha512-UPSTREAMupstream==", + "bundled": true + } + }); + std::fs::write(&original, serde_json::to_vec_pretty(&lock).unwrap()).unwrap(); + if lockfile != "package-lock.json" { + std::fs::rename(&original, tmp.path().join(lockfile)).unwrap(); + } + let out = tmp.path().join("out.vex.json"); + let mut args = redirect_args(tmp.path(), server.uri()); + args.vex.vex = Some(out.clone()); + args.vex.vex_product = Some("pkg:npm/consumer@0.0.0".into()); + let exit = run(args).await; + assert_eq!( + exit, 0, + "{lockfile}: npm consumes the normal packages entry" + ); + let document: serde_json::Value = + serde_json::from_slice(&std::fs::read(&out).unwrap()).unwrap(); + assert!( + document.to_string().contains(PURL), + "{lockfile}: {document:#}" + ); + assert_eq!(document["statements"][0]["status"], "not_affected"); + let rewritten: serde_json::Value = + serde_json::from_slice(&std::fs::read(tmp.path().join(lockfile)).unwrap()).unwrap(); + assert_eq!( + rewritten["packages"][format!("node_modules/{NAME}")]["resolved"], + HOSTED_URL + ); + assert_eq!(rewritten["dependencies"][NAME]["bundled"], true); + } +} + /// The bun 1.4 leg: `"lockfileVersion": 2` is the SAME emitted grammar as 1 /// (bun 1.4 bumped the integer to gate stricter parse checks — oven-sh/bun /// PR #31539 — same-fixture locks are byte-identical except the integer), so diff --git a/crates/socket-patch-core/src/patch/redirect/mod.rs b/crates/socket-patch-core/src/patch/redirect/mod.rs index e8e337e05..523acfff0 100644 --- a/crates/socket-patch-core/src/patch/redirect/mod.rs +++ b/crates/socket-patch-core/src/patch/redirect/mod.rs @@ -932,6 +932,10 @@ fn rewrite_one_npm_lock( // Entries npm installs from a git / url / `file:` spec: see // `vendor::npm_origin` (#326). let non_registry = npm_non_registry_entries(&lock, manifest_overrides); + // npm 7+ reads `packages` when it exists; the legacy `dependencies` + // mirror must not suppress an attestation for that install tree. + // Match the shared npm lock inventory's object-valued-map precedence. + let legacy_is_install_tree = lock.get("packages").and_then(Value::as_object).is_none(); let mut changed = false; for dep in npm { let fname = full_name(dep); @@ -1022,6 +1026,7 @@ fn rewrite_one_npm_lock( dep, &sha512, lockfile, + legacy_is_install_tree, result, &mut matched_any, ) || changed; @@ -1103,6 +1108,7 @@ fn rewrite_npm_v2_deps( dep: &DepOverride, sha512: &str, lockfile: &str, + legacy_is_install_tree: bool, result: &mut RewriteResult, matched_any: &mut bool, ) -> bool { @@ -1116,7 +1122,9 @@ fn rewrite_npm_v2_deps( // fail-open as the `packages` guard above. if entry.get("bundled").and_then(Value::as_bool) == Some(true) { *matched_any = true; - result.bundled_skipped_uuids.insert(dep.patch_uuid.clone()); + if legacy_is_install_tree { + result.bundled_skipped_uuids.insert(dep.patch_uuid.clone()); + } result.warnings.push(RewriteWarning { code: "redirect_npm_bundled_instance_skipped".into(), detail: format!( @@ -1150,6 +1158,7 @@ fn rewrite_npm_v2_deps( dep, sha512, lockfile, + legacy_is_install_tree, result, matched_any, ) || changed; @@ -14072,6 +14081,58 @@ mod tests { ); } + /// A stale v2 legacy mirror is not the install tree. Its bundled flag + /// must not suppress in-run VEX for a normal `packages` entry; genuine + /// bundled entries in `packages` still suppress the same patch. + #[test] + fn npm_stale_legacy_bundled_mirror_does_not_contest_packages() { + for nested in [false, true] { + for actual_bundle in [false, true] { + let bundled = json!({"version": "1.3.0", "bundled": true}); + let legacy = if nested { + json!({"parent": {"version": "2.0.0", "dependencies": {"left-pad": bundled}}}) + } else { + json!({"left-pad": bundled}) + }; + let mut lock = json!({ + "lockfileVersion": 2, + "packages": { + "": {"name": "app", "version": "1.0.0"}, + "node_modules/left-pad": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", + "integrity": "sha512-UPSTREAM==" + } + }, + "dependencies": legacy + }); + if actual_bundle { + lock["packages"]["node_modules/parent/node_modules/left-pad"] = + json!({"version": "1.3.0", "inBundle": true}); + } + let files = BTreeMap::from([("package-lock.json".into(), lock.to_string())]); + let overrides = vec![npm_override( + "left-pad", + "1.3.0", + "http://patch.test/lp.tgz", + "sha512-PATCHED==", + )]; + let r = rewrite_registry_redirect(&files, &overrides); + assert_eq!( + r.bundled_skipped_uuids.contains(&overrides[0].patch_uuid), + actual_bundle, + "nested mirror={nested}, actual bundled install={actual_bundle}" + ); + let out: Value = serde_json::from_str(&r.files["package-lock.json"]).unwrap(); + assert_eq!( + out["packages"]["node_modules/left-pad"]["resolved"], + "http://patch.test/lp.tgz" + ); + assert_eq!(out["dependencies"], lock["dependencies"]); + } + } + } + /// An alias install (`npm i my-alias@npm:left-pad@1.3.0`) keys the lock /// entry by the ALIAS with the real package in `name`. Discovery is /// alias-aware (the crawler reads the installed package.json name), so diff --git a/crates/socket-patch-core/tests/equivalence/npm_lock_rewrite.golden b/crates/socket-patch-core/tests/equivalence/npm_lock_rewrite.golden index d3824900a..c0da6d065 100644 --- a/crates/socket-patch-core/tests/equivalence/npm_lock_rewrite.golden +++ b/crates/socket-patch-core/tests/equivalence/npm_lock_rewrite.golden @@ -27,7 +27,7 @@ 96-99 e9b8cddfc47c2410 e45224d4571298a6 100-103 21dbb73e40e5a210 af5e7560a9d0c0bd 104-107 0c170349f3052ef9 27e2b78479f64dee -108-111 b9c3e6c7cdfad95f c25f4a60c3ff3f39 +108-111 b9c3e6c7cdfad95f f0148e213da61805 112-115 b7f86471de74f76c 392efc30f7f532cb 116-119 5b317616e7ec450d 892261da3954f566 120-123 1af385991cee8693 985f1874988de52b @@ -45,7 +45,7 @@ 168-171 c063c9818a9362b1 c196ce989d45b565 172-175 1ee9e3f5a3530fc8 4b53862c8df8b4bd 176-179 2567d140ce3ceda0 4a9307775128fdc4 -180-183 8c63f24d3ac472f6 31765822966ccbe0 +180-183 8c63f24d3ac472f6 cd0f2d2f7aa925a8 184-187 768a53cbd61c832c d64742504fdaaba0 188-191 1b79c77ae63569a4 fc9fd305e1ddc085 192-195 a652530b7d9610fc 7ef3c98c60d82ab5 @@ -72,7 +72,7 @@ 276-279 ea0a7560bb20466c 0c715c2207044d29 280-283 3ab69851cae70bcd 8e480b61d81592af 284-287 1fdb596cc87f031b 64578f1e84310366 -288-291 ed84081f4124d522 68035e745e6933e7 +288-291 ed84081f4124d522 68797a18e99c2dbd 292-295 18bc713c335e243c 9c8a170ff899c58b 296-299 658ce14ea92fffd5 536e77ae46114631 300-303 9a8fde3916e27342 bab49d863a06a18e @@ -85,12 +85,12 @@ 328-331 3b6a2eae6af90be3 45de24588519ef65 332-335 291ee053b410dc25 4821eb617b8d86ec 336-339 c00ae81f0a2be968 9f069a599d03be8b -340-343 1b3d34745caf497c 5b7d8b63e80b5270 -344-347 8e350ee856023a1f 3a8e7193929a7048 +340-343 1b3d34745caf497c 562af394d0cb24db +344-347 8e350ee856023a1f 7f3b5b09c26ad562 348-351 7b0511648fa4447c e0f1d4a90bf10bbc 352-355 460a50b29b48043d ee516e652b88c45a 356-359 a304ae8e7e03c44c d9b4d4de533a4b35 -360-363 7055899176855866 8584d97a6081443c +360-363 7055899176855866 21c59a685c9b48f8 364-367 f8819f4cbe245541 5b51feb62d76b033 368-371 3d30266cadaa3712 44f173d7028a72e7 372-375 a65f748f7e3f6933 e612c7529ecd9fc1 @@ -101,13 +101,13 @@ 392-395 8ab3f5ef6c2260dc fdc1bd49d88b0f00 396-399 e70ee73cf9f93cdd 1fe4da6c539ccf86 400-403 dda6b03292721d0b 9795245a300e80da -404-407 cd2441a35b1c5740 0ea79c1f4eeded7d +404-407 cd2441a35b1c5740 6fd4d2a386eed373 408-411 91ac4721e77b4500 0bc11d72a0297bb9 412-415 28af4189511719f9 3d29075156f4c9fa -416-419 d9e2eae424bebb3e 0931fe460360ff98 +416-419 d9e2eae424bebb3e 97225086b3cde613 420-423 0f86f1432c081b6c c8cd109152c9000d 424-427 6533c923cb60e539 88649c02c40ace42 -428-431 80c11eb13664c5fb 47730993f3a4b3f1 +428-431 80c11eb13664c5fb 3875f53d37e204d1 432-435 d33b5fff79332f39 47fe549195bb9a15 436-439 c70bbaca1ed730dc d8aaef4d259a74ad 440-443 b1152d879b9de674 a4f72811e3bffae7 @@ -125,7 +125,7 @@ 488-491 8e04bbe242721495 4827195052919674 492-495 4b804da1bed766bc bdeb1a87ef9305c2 496-499 e4358ad81f66c66c 1bd273b112273309 -500-503 d497faab2e6374d1 a6b7648938f9efcc +500-503 d497faab2e6374d1 25ff9866dabb19b5 504-507 bd134b4a4fd9c1af ec0b3fba5c88c493 508-511 3a1c53db7ba9c199 fa3b06c10bf6d6b0 512-515 9ac76bdafc788702 1b12e2367ef70fd8