diff --git a/crates/socket-patch-cli/tests/vendor_jvm_cli.rs b/crates/socket-patch-cli/tests/vendor_jvm_cli.rs
index 18b749a51..205f129aa 100644
--- a/crates/socket-patch-cli/tests/vendor_jvm_cli.rs
+++ b/crates/socket-patch-cli/tests/vendor_jvm_cli.rs
@@ -1237,6 +1237,62 @@ fn maven_reactor_uncertain_property_version_withholds_vex() {
}
}
+/// #584: a module added after vendoring that declares the base literally
+/// builds the unpatched jar. `vendor --check` flags the drift and `vex`
+/// withholds attestation until a re-vendor rewrites the module.
+#[test]
+fn maven_reactor_module_added_at_base_after_vendoring_withholds_vex() {
+ let tmp = tempfile::tempdir().unwrap();
+ let root = tmp.path();
+ fixture(root, Shape::Reactor, &[("foo", FOO_UUID)]);
+ let pristine = std::fs::read_to_string(root.join("proj/a/pom.xml")).unwrap();
+ ok(root, &["vendor"]);
+ let vex = root.join("vex.json");
+ let vex_args = [
+ "vex",
+ "-O",
+ vex.to_str().unwrap(),
+ "--product",
+ "pkg:generic/x@1",
+ ];
+ ok(root, &vex_args);
+ std::fs::remove_file(&vex).unwrap();
+
+ let agg = std::fs::read_to_string(root.join("proj/pom.xml")).unwrap();
+ let agg = agg.replace(
+ "a",
+ "a\n b",
+ );
+ write(root, "proj/pom.xml", agg.as_bytes());
+ std::fs::create_dir_all(root.join("proj/b")).unwrap();
+ let b = pristine.replace("a", "b");
+ assert!(b.contains("1.0"), "{b}");
+ write(root, "proj/b/pom.xml", b.as_bytes());
+
+ let (code, env) = socket(root, &["vendor", "--check"]);
+ assert_ne!(code, Some(0), "{env}");
+ let (_, env) = socket(root, &vex_args);
+ assert_eq!(
+ events(&env),
+ [(
+ purl("foo"),
+ "skipped".to_string(),
+ "vendor_unwired".to_string()
+ )],
+ "{env}"
+ );
+ assert!(!vex.exists(), "{env}");
+
+ // Re-vendoring wires `b` too, and the reactor attests again.
+ ok(root, &["vendor"]);
+ assert!(!std::fs::read_to_string(root.join("proj/b/pom.xml"))
+ .unwrap()
+ .contains("1.0"));
+ ok(root, &vex_args);
+ let doc: serde_json::Value = serde_json::from_slice(&std::fs::read(&vex).unwrap()).unwrap();
+ assert_eq!(doc["statements"][0]["status"], "not_affected", "{doc}");
+}
+
/// #429 repair: a deleted owned `.gitattributes` (and nothing else) is
/// rewritten by `repair` (no download), so the next autocrlf clone keeps the
/// script and index LF.
diff --git a/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs b/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs
index 38a1fa623..d041bbaf4 100644
--- a/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs
+++ b/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs
@@ -479,9 +479,11 @@ pub fn wired_checked(
}
/// [`wired_checked`], and no declaration of `c`'s g:a is left at a version
-/// that may still read the base: a `${...}` the checkout does not define
-/// or that an inheriting pom or a profile moves (#513, #459). Such a
-/// module builds the unpatched jar, so `vex` must not attest.
+/// that may still read the base: the base itself, as a module or
+/// dependency added since vendoring declares it (#584), or a `${...}` the
+/// checkout does not define or that an inheriting pom or a profile moves
+/// (#513, #459). Such a module builds the unpatched jar, so `vex` must
+/// not attest.
pub fn attestable_checked(
read: ReadFn<'_>,
list: ListFn<'_>,
@@ -507,20 +509,21 @@ pub fn attestable_checked(
extra_artifacts: &[],
patched_members: &[],
};
+ let mut edits = BTreeMap::new();
let mut uncertain = BTreeSet::new();
for rel in &reactor.scope {
reactor.rewrite_declarations(
rel,
&patch,
&sv,
- &mut BTreeMap::new(),
+ &mut edits,
&mut Vec::new(),
&mut BTreeSet::new(),
&mut BTreeSet::new(),
&mut uncertain,
);
}
- Ok(uncertain.is_empty())
+ Ok(edits.is_empty() && uncertain.is_empty())
}
/// `