Skip to content

Commit 9003ae8

Browse files
committed
Merge origin/main to resolve conflicts
Union the CHANGELOG Fixed entries from both sides. Co-Authored-By: Claude <noreply@anthropic.com>
2 parents 522febb + d984832 commit 9003ae8

14 files changed

Lines changed: 718 additions & 26 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1062,9 +1062,13 @@ jobs:
10621062
- {os: macos-latest, suite: e2e_vex_build, test_filter: 'pip:: --ignored', pip: '22 26'}
10631063
# Real-Maven hosted + vendored capstones, one leg per Maven line:
10641064
# 3.6 (pre http-blocker), 3.8 (resolver 1.6: no trusted checksums),
1065-
# 3.9 (trusted checksums), 4.0 rc.
1065+
# 3.9 (trusted checksums), 4.0 rc. Hosted also runs both sides of
1066+
# the trusted-checksums floor: 3.9.3 (last release that ignores
1067+
# the .mvn/checksums pin) and 3.9.4 (first that enforces it).
10661068
- {os: ubuntu-latest, suite: e2e_redirect_maven_build, maven: '3.6.3'}
10671069
- {os: ubuntu-latest, suite: e2e_redirect_maven_build, maven: '3.8.9'}
1070+
- {os: ubuntu-latest, suite: e2e_redirect_maven_build, maven: '3.9.3'}
1071+
- {os: ubuntu-latest, suite: e2e_redirect_maven_build, maven: '3.9.4'}
10681072
- {os: ubuntu-latest, suite: e2e_redirect_maven_build, maven: '3.9.16'}
10691073
- {os: ubuntu-latest, suite: e2e_redirect_maven_build, maven: '4.0.0-rc-6'}
10701074
- {os: macos-latest, suite: e2e_redirect_maven_build, maven: '3.9.16'}

‎CHANGELOG.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ limits, and required install commands.
106106
twin or a `BUNDLE_GEMFILE` setting (environment or `.bundle/config`) no longer
107107
leads to an edit of an ignored `Gemfile` that reports success and attests an
108108
unpatched gem; unsupported layouts are refused before any write (#341, #390).
109+
- Hosted Maven warns `redirect_maven_trusted_checksums_unenforced` when
110+
`.mvn/wrapper/maven-wrapper.properties` pins a Maven older than 3.9.4. Maven
111+
3.9.0–3.9.3 never enforce the Trusted Checksums pin that hosted mode writes;
112+
the 4.0.0 notes and docs wrongly said every 3.9 release does. The version
113+
suffix still fails closed. CI runs the real-Maven hosted capstone on 3.9.3 and
114+
3.9.4 (#258).
109115
- Patch application, reversal, and cleanup handle missing files, release variants,
110116
corrupt state, newer ledger formats, and unsafe manifest paths without silently
111117
dropping protection. File ownership restoration failures produce warnings.
@@ -127,6 +133,11 @@ limits, and required install commands.
127133
not build from a previous patch's modified bytes. Verified service artifacts
128134
keep their identity; integrity failures do not fall through to a local rebuild.
129135
Repair rebuilds against recorded pins and reports unavailable inputs.
136+
- VEX no longer attests an npm package that also ships a bundled, unpatched
137+
copy of the same `name@version` (`inBundle: true`, or v1 `bundled: true`).
138+
npm unpacks that copy from the parent's tarball, so no rewire reaches it; the
139+
reference is now reported `patched_ref_unattributable`, naming the bundled
140+
copy, in hosted and vendored mode (#325).
130141
- API throttling uses bounded retries, failed queries appear in JSON diagnostics,
131142
and hosted reference resolution handles batches larger than 500 patches.
132143
- Transient apply locks are removed on normal command exit; no-op scans and full

‎crates/socket-patch-cli/CLI_CONTRACT.md‎

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

‎crates/socket-patch-cli/src/commands/scan/hosted.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3551,6 +3551,7 @@ mod tests {
35513551
"pom.xml",
35523552
".mvn/maven.config",
35533553
".mvn/checksums/checksums.sha256",
3554+
".mvn/wrapper/maven-wrapper.properties",
35543555
"settings.gradle",
35553556
"settings.gradle.kts",
35563557
"build.gradle",

‎crates/socket-patch-cli/tests/e2e_redirect_maven_build.rs‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,19 @@ fn maven_scan_hosted_fresh_checkout_install_and_manifestless_vex() {
310310
return;
311311
};
312312
let pristine_pom = std::fs::read_to_string(proj.join("pom.xml")).unwrap();
313+
// The project pins the Maven under test through the Maven Wrapper, so
314+
// the rewriter's "your Maven ignores the Trusted Checksums pin" warning
315+
// is judged against the same release that step 5b drives (#258).
316+
std::fs::create_dir_all(proj.join(".mvn/wrapper")).unwrap();
317+
std::fs::write(
318+
proj.join(".mvn/wrapper/maven-wrapper.properties"),
319+
format!(
320+
"distributionUrl=https\\://repo.maven.apache.org/maven2/org/apache/maven/\
321+
apache-maven/{v}/apache-maven-{v}-bin.zip\n",
322+
v = mvn.version
323+
),
324+
)
325+
.unwrap();
313326

314327
// 2. Patched jar + served pom + the record (real before/after hashes).
315328
let (_orig, patched) = patched_member(&jar, UUID);
@@ -353,6 +366,17 @@ fn maven_scan_hosted_fresh_checkout_install_and_manifestless_vex() {
353366
assert_eq!(env["redirect"]["mode"], "hosted", "{env}");
354367
assert_eq!(env["redirect"]["redirected"], 1, "{env}");
355368
assert_eq!(env["vex"]["statements"], 1, "{env}");
369+
let unenforced = env["redirect"]["warnings"]
370+
.as_array()
371+
.into_iter()
372+
.flatten()
373+
.any(|w| w["code"] == "redirect_maven_trusted_checksums_unenforced");
374+
assert_eq!(
375+
unenforced,
376+
!mvn.enforces_trusted_checksums(),
377+
"Maven {}: the unenforced-pin warning must match what step 5b proves: {env}",
378+
mvn.version
379+
);
356380
let embedded: serde_json::Value =
357381
serde_json::from_slice(&std::fs::read(proj.join("embedded.vex.json")).unwrap()).unwrap();
358382
assert_attested(&embedded, &purl(), UUID, Marker::Redirected, &vulns());

‎crates/socket-patch-cli/tests/e2e_vex_redirect.rs‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1618,6 +1618,61 @@ fn a_sibling_lock_resolving_the_registry_contests_a_ledger_record() {
16181618
}
16191619
}
16201620

1621+
/// REGRESSION (#325): the hosted rewriter rewires the hoisted
1622+
/// `left-pad@1.3.0` and skips a parent's bundled copy of the same version
1623+
/// (`redirect_npm_bundled_instance_skipped`: npm unpacks it from the
1624+
/// parent's tarball, so it stays unpatched). The ledger record must be dead
1625+
/// (`redirect_unwired`, naming the bundled copy), not attested from the
1626+
/// lock basis. Without the bundled copy the same ledger attests.
1627+
#[test]
1628+
fn hosted_npm_patch_with_an_unpatched_bundled_copy_is_not_attested() {
1629+
let purl = "pkg:npm/left-pad@1.3.0";
1630+
for bundled in [false, true] {
1631+
let tmp = tempfile::tempdir().unwrap();
1632+
let cwd = tmp.path();
1633+
write_hosted_package_lock(cwd, &[("left-pad", "1.3.0", UUID)], true);
1634+
if bundled {
1635+
let lock_path = cwd.join("package-lock.json");
1636+
let mut lock: Value =
1637+
serde_json::from_str(&std::fs::read_to_string(&lock_path).unwrap()).unwrap();
1638+
let packages = lock["packages"].as_object_mut().unwrap();
1639+
packages.insert(
1640+
"node_modules/bund".to_string(),
1641+
serde_json::json!({ "version": "1.0.0", "resolved": "file:bund-1.0.0.tgz" }),
1642+
);
1643+
packages.insert(
1644+
"node_modules/bund/node_modules/left-pad".to_string(),
1645+
serde_json::json!({
1646+
"version": "1.3.0",
1647+
"resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz",
1648+
"inBundle": true
1649+
}),
1650+
);
1651+
std::fs::write(&lock_path, lock.to_string()).unwrap();
1652+
}
1653+
write_redirect_ledger(
1654+
cwd,
1655+
&[(
1656+
purl,
1657+
make_record(UUID, &"b".repeat(64), "GHSA-bndl-host", &["CVE-2026-325"]),
1658+
)],
1659+
&[("package-lock.json", "npm_lock_entry")],
1660+
);
1661+
let (code, env) = vex_json(cwd, &["--offline", "--no-verify"]);
1662+
if !bundled {
1663+
assert_eq!(code, Some(0), "control: {env}");
1664+
continue;
1665+
}
1666+
assert_eq!(code, Some(1), "{env}");
1667+
assert_eq!(skipped_reason(&env, purl), "redirect_unwired", "{env}");
1668+
assert!(
1669+
env.to_string()
1670+
.contains("node_modules/bund/node_modules/left-pad"),
1671+
"the bundled copy is named: {env}"
1672+
);
1673+
}
1674+
}
1675+
16211676
/// REGRESSION: a hosted pin with NO lock at all is the rewriters' ordinary
16221677
/// output, not a stale shape — `rewrite_nuget` edits only nuget.config for a
16231678
/// project without RestorePackagesWithLockFile, `rewrite_cargo` only

‎crates/socket-patch-cli/tests/e2e_vex_vendor.rs‎

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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).

‎crates/socket-patch-cli/tests/maven_build_common/mod.rs‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -179,13 +179,15 @@ impl Mvn {
179179
self.numeric()[0]
180180
}
181181

182-
/// Maven ≥ 3.9 (resolver ≥ 1.9) enforces the trusted-checksums summary
183-
/// file the hosted rewriter commits under `.mvn/checksums/`; older
184-
/// lines ignore the `aether.*` properties and only the transport
185-
/// `checksumPolicy=fail` sidecar check protects the download.
182+
/// Maven ≥ 3.9.4 enforces the trusted-checksums summary file the hosted
183+
/// rewriter commits under `.mvn/checksums/` (#258). 3.9.0 / 3.9.1 leave
184+
/// `${session.rootDirectory}` in `maven.config` uninterpolated, so the
185+
/// file is never found; 3.9.2 / 3.9.3 ignore `checksumAlgorithms=SHA-256`
186+
/// and check SHA-1 only; older lines have no trusted-checksums support.
187+
/// Below 3.9.4 only the transport `checksumPolicy=fail` sidecar check
188+
/// protects the download.
186189
pub fn enforces_trusted_checksums(&self) -> bool {
187-
let v = self.numeric();
188-
v[0] > 3 || (v[0] == 3 && v[1] >= 9)
190+
self.numeric() >= vec![3, 9, 4]
189191
}
190192

191193
/// `mvn -B <args>` in `cwd` against the local repository `m2`, with the

‎crates/socket-patch-core/src/formats/registry.rs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,14 @@ const REGISTRY: &[FormatFile] = &[
140140
// into (read so an existing user config / checksum set is preserved).
141141
row(".mvn/maven.config", "maven", HOSTED),
142142
row(".mvn/checksums/checksums.sha256", "maven", HOSTED),
143+
// Never edited: its `distributionUrl` names the project's Maven, which
144+
// the maven planner checks against the Trusted Checksums floor (3.9.4)
145+
// to warn when the `.mvn/*` pin above would be inert.
146+
row(
147+
".mvn/wrapper/maven-wrapper.properties",
148+
"maven",
149+
HOSTED | PRESENCE_ONLY,
150+
),
143151
// Gradle build scripts are never edited — their presence only feeds the
144152
// maven planner's paste-able `exclusiveContent` snippet warning.
145153
row("settings.gradle", "maven", HOSTED | PRESENCE_ONLY),

0 commit comments

Comments
 (0)