@@ -1343,6 +1343,135 @@ fn vendored_live_tree_out_of_sync_warns_but_attests() {
13431343 ) ;
13441344}
13451345
1346+ /// REGRESSION (#325): the lock rewires the hoisted `lodash@4.17.21` to the
1347+ /// vendored tarball, but a parent package also BUNDLES `lodash@4.17.21`
1348+ /// (`inBundle: true`). npm unpacks that copy from the parent's tarball, so
1349+ /// no rewire reaches it and the build ships it unpatched. `vex` must not
1350+ /// attest the purl, from the lock basis (no `node_modules`) or after an
1351+ /// install whose hoisted copy is patched and bundled copy is not. The
1352+ /// same lock without the bundled copy is the control and still attests.
1353+ #[ test]
1354+ fn vendored_npm_patch_with_an_unpatched_bundled_copy_is_not_attested ( ) {
1355+ let purl = "pkg:npm/lodash@4.17.21" ;
1356+ let uuid = "0a0a0a0a-1111-4111-8111-0a0a0a0a0a0a" ;
1357+ let patched = b"patched npm bytes\n " ;
1358+ let after_hash = compute_git_sha256_from_bytes ( patched) ;
1359+ for ( label, bundled, installed) in [
1360+ ( "control, lock basis" , false , false ) ,
1361+ ( "bundled, lock basis" , true , false ) ,
1362+ ( "bundled, installed" , true , true ) ,
1363+ ] {
1364+ let tmp = tempfile:: tempdir ( ) . expect ( "create tempdir" ) ;
1365+ let cwd = tmp. path ( ) ;
1366+ let rel = format ! ( ".socket/vendor/npm/{uuid}/lodash-4.17.21.tgz" ) ;
1367+ let sha256 = sha256_hex ( & write_member_tgz (
1368+ & cwd. join ( & rel) ,
1369+ "package/index.js" ,
1370+ patched,
1371+ ) ) ;
1372+ let record = make_record (
1373+ uuid,
1374+ "package/index.js" ,
1375+ & after_hash,
1376+ "GHSA-bndl-aaaa" ,
1377+ & [ "CVE-2026-325" ] ,
1378+ ) ;
1379+ let wiring = write_matrix_wiring ( cwd, "npm" , uuid, & rel) ;
1380+ if bundled {
1381+ let lock_path = cwd. join ( "package-lock.json" ) ;
1382+ let mut lock: Value =
1383+ serde_json:: from_str ( & std:: fs:: read_to_string ( & lock_path) . unwrap ( ) ) . unwrap ( ) ;
1384+ let packages = lock[ "packages" ] . as_object_mut ( ) . unwrap ( ) ;
1385+ packages. insert (
1386+ "node_modules/bund" . to_string ( ) ,
1387+ serde_json:: json!( { "version" : "1.0.0" , "resolved" : "file:bund-1.0.0.tgz" } ) ,
1388+ ) ;
1389+ packages. insert (
1390+ "node_modules/bund/node_modules/lodash" . to_string ( ) ,
1391+ serde_json:: json!( {
1392+ "version" : "4.17.21" ,
1393+ "resolved" : "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" ,
1394+ "integrity" : "sha512-T1JJR0lOQUw=" ,
1395+ "inBundle" : true
1396+ } ) ,
1397+ ) ;
1398+ std:: fs:: write ( & lock_path, lock. to_string ( ) ) . unwrap ( ) ;
1399+ }
1400+ let mut state = VendorState :: new ( ) ;
1401+ state. entries . insert (
1402+ purl. to_string ( ) ,
1403+ detached_matrix_entry ( "npm" , purl, uuid, & rel, sha256, record, wiring) ,
1404+ ) ;
1405+ let dir = cwd. join ( ".socket/vendor" ) ;
1406+ std:: fs:: create_dir_all ( & dir) . unwrap ( ) ;
1407+ std:: fs:: write (
1408+ dir. join ( "state.json" ) ,
1409+ serde_json:: to_string_pretty ( & state) . unwrap ( ) ,
1410+ )
1411+ . unwrap ( ) ;
1412+ if installed {
1413+ for ( pkg_dir, bytes) in [
1414+ ( "node_modules/lodash" , & patched[ ..] ) ,
1415+ (
1416+ "node_modules/bund/node_modules/lodash" ,
1417+ b"original unpatched bytes\n " ,
1418+ ) ,
1419+ ] {
1420+ let nm = cwd. join ( pkg_dir) ;
1421+ std:: fs:: create_dir_all ( & nm) . unwrap ( ) ;
1422+ std:: fs:: write (
1423+ nm. join ( "package.json" ) ,
1424+ r#"{"name":"lodash","version":"4.17.21"}"# ,
1425+ )
1426+ . unwrap ( ) ;
1427+ std:: fs:: write ( nm. join ( "index.js" ) , bytes) . unwrap ( ) ;
1428+ }
1429+ }
1430+
1431+ let vex_path = cwd. join ( "out.vex.json" ) ;
1432+ let out = cli ( )
1433+ . args ( [
1434+ "vex" ,
1435+ "--cwd" ,
1436+ cwd. to_str ( ) . unwrap ( ) ,
1437+ "--json" ,
1438+ "--output" ,
1439+ vex_path. to_str ( ) . unwrap ( ) ,
1440+ "--product" ,
1441+ "pkg:npm/app@1.0.0" ,
1442+ ] )
1443+ . output ( )
1444+ . expect ( "invoke vex" ) ;
1445+ let env: Value = serde_json:: from_slice ( & out. stdout ) . unwrap_or_else ( |e| {
1446+ panic ! (
1447+ "{label}: envelope JSON on stdout ({e}): {}" ,
1448+ String :: from_utf8_lossy( & out. stdout)
1449+ )
1450+ } ) ;
1451+ if !bundled {
1452+ assert ! ( out. status. success( ) , "{label}: {env}" ) ;
1453+ let doc: Value =
1454+ serde_json:: from_str ( & std:: fs:: read_to_string ( & vex_path) . unwrap ( ) ) . unwrap ( ) ;
1455+ assert_eq ! (
1456+ doc[ "statements" ] . as_array( ) . unwrap( ) . len( ) ,
1457+ 1 ,
1458+ "{label}: {doc}"
1459+ ) ;
1460+ continue ;
1461+ }
1462+ assert_eq ! ( out. status. code( ) , Some ( 1 ) , "{label}: {env}" ) ;
1463+ assert ! (
1464+ !vex_path. exists( ) ,
1465+ "{label}: no VEX document may attest the purl: {env}"
1466+ ) ;
1467+ assert ! (
1468+ env. to_string( )
1469+ . contains( "node_modules/bund/node_modules/lodash" ) ,
1470+ "{label}: the envelope names the bundled copy: {env}"
1471+ ) ;
1472+ }
1473+ }
1474+
13461475// ──────────────────────────────────────────────────────────────────────
13471476// 8. an applied, byte-verified agent-mode patch attests whether or not its
13481477// ecosystem has an install hook (there is no setup-state filter).
0 commit comments