From fdc9a1c3f9c84fb0854e151121b2f2848d94b10b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 04:23:12 +0000 Subject: [PATCH 1/7] Start fix for #1549 Assisted-by: Claude Code:claude-opus-5-5 From 3763ea3138dd62ef4588aa0ec45e0afd0be6c3ff Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 04:41:17 +0000 Subject: [PATCH 2/7] Contest wiring with non-Socket Pipenv/pip refs A Pipfile.lock `file` url or `path` wheel, or a requirements line like `six @ https://...whl`, installs the upstream release. Discovery took such a reference for no evidence at all, so when another lock wired the same release to a Socket patch, `vendor --check` stayed green and `vex` attested not_affected while `pipenv sync` / `pip install -r` shipped the unpatched package. These references now count as resolving the package elsewhere, exactly like a registry `==` pin: the other lock's wiring is withheld with patched_ref_unattributable naming both files. The release comes from an exact `version` pin or the wheel / sdist filename. A reference with no readable version (a source directory, a VCS checkout) may install any version, so it contests every version of that package. Fixes #1549 Assisted-by: Claude Code:claude-opus-5-5 --- .../socket-patch-core/src/vex/discover/mod.rs | 71 ++++- .../src/vex/discover/pypi_other.rs | 272 +++++++++++++++++- 2 files changed, 326 insertions(+), 17 deletions(-) diff --git a/crates/socket-patch-core/src/vex/discover/mod.rs b/crates/socket-patch-core/src/vex/discover/mod.rs index ec1c7b265..68982a618 100644 --- a/crates/socket-patch-core/src/vex/discover/mod.rs +++ b/crates/socket-patch-core/src/vex/discover/mod.rs @@ -453,12 +453,36 @@ impl UnlockedPin { /// [`Discovery::resolved_elsewhere`]). #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct ResolvedElsewhere { - /// Canonical base purl ([`canonical_base_purl`]). + /// Canonical base purl ([`canonical_base_purl`]). Versionless + /// (`pkg:pypi/six`) for an entry whose version can't be read: it may + /// resolve any version of that package + /// ([`Discovery::resolved_elsewhere_any_version`]). pub purl: String, /// Root-relative lock file. pub file: PathBuf, } +impl ResolvedElsewhere { + /// Whether this entry resolves the canonical base `purl`: the same + /// `name@version`, or any version of `name` for a versionless entry. + fn covers(&self, purl: &str) -> bool { + self.purl == purl || purl_without_version(purl) == Some(self.purl.as_str()) + } + + /// Whether the entry names no version, so may resolve any. + fn any_version(&self) -> bool { + purl_without_version(&self.purl).is_none() + } +} + +/// `pkg:/` of a versioned `pkg:/@`; +/// `None` for a purl with no version (an npm scope's `@` is part of the +/// name). +fn purl_without_version(purl: &str) -> Option<&str> { + let (head, _) = purl.rsplit_once('@')?; + (head.contains('/') && !head.ends_with('/')).then_some(head) +} + /// A lock entry that installs its OWN copy of a package — a `file:` /// directory or tarball, a user url, git, a registry block no Socket rewrite /// reached — beside a Socket wiring of the same `name@version` in the SAME @@ -901,6 +925,24 @@ impl Discovery { }); } + /// Record that lock `file` resolves the package `name_purl` + /// (`pkg:/`, no version; `None` is ignored) from a + /// non-Socket source whose version can't be read: a source directory, a + /// VCS checkout, an artifact named for no release. It may install any + /// version, so it is [`Discovery::resolved_elsewhere`] evidence against + /// every version of that package (#1549). + pub(crate) fn resolved_elsewhere_any_version(&mut self, file: &str, name_purl: Option) { + let Some(name_purl) = name_purl else { return }; + let purl = canonical_base_purl(&name_purl); + if purl_without_version(&purl).is_some() { + return; + } + self.elsewhere.push(ResolvedElsewhere { + purl, + file: PathBuf::from(file), + }); + } + /// Record that lock `file`'s entry `key` installs its own copy of `purl` /// (`None` is ignored), `how` saying from where. The cross-lock contest /// only weighs OTHER locks (a lock that wires a package is never its @@ -1051,14 +1093,14 @@ impl Discovery { let tree = self.tree_of(&r.source_file); self.elsewhere.iter().find(|e| { let other = self.tree_of(&e.file); - e.purl == r.purl + e.covers(&r.purl) && other != tree && !wiring.contains(&(e.purl.clone(), other.to_path_buf())) }) }) .flatten(); match other { - Some(e) => contested.push((r, e.file.clone())), + Some(e) => contested.push((r, e.clone())), None => self.refs.push(r), } } @@ -1068,21 +1110,25 @@ impl Discovery { uuid: r.uuid.clone(), mode: r.mode, file: r.source_file.clone(), - other: other.clone(), + other: other.file.clone(), }); let file = r.source_file.to_string_lossy().into_owned(); + let resolves = if other.any_version() { + "resolves the package, at a version it doesn't name, from elsewhere" + } else { + "resolves the same version from elsewhere" + }; self.diag( DIAG_REF_UNATTRIBUTABLE, &file, format!( - "{file}: {} is wired to Socket patch {}, but {} resolves the same version \ - from elsewhere (not a Socket patch) — which lock the build installs from \ - depends on the package manager that runs, so the patch is not attested; \ - rewire both locks (re-run `socket-patch scan` / `vendor`) or delete the \ - stale one", + "{file}: {} is wired to Socket patch {}, but {} {resolves} (not a Socket \ + patch) — which lock the build installs from depends on the package \ + manager that runs, so the patch is not attested; rewire both locks \ + (re-run `socket-patch scan` / `vendor`) or delete the stale one", r.purl, r.uuid, - other.display() + other.file.display() ), ); self.withhold_rewirable(r); @@ -1190,7 +1236,10 @@ impl Discovery { let key = canonical_base_purl(purl); self.refs.iter().any(|r| PurlKey::same(&r.purl, &key)) || self.contested.iter().any(|c| PurlKey::same(&c.purl, &key)) - || self.elsewhere.iter().any(|e| PurlKey::same(&e.purl, &key)) + || self + .elsewhere + .iter() + .any(|e| PurlKey::same(&e.purl, &key) || e.covers(&key)) } fn recognize(&mut self, uuid: &str, mode: WiringMode, file: &str) { diff --git a/crates/socket-patch-core/src/vex/discover/pypi_other.rs b/crates/socket-patch-core/src/vex/discover/pypi_other.rs index e9dee4f2f..007f81f89 100644 --- a/crates/socket-patch-core/src/vex/discover/pypi_other.rs +++ b/crates/socket-patch-core/src/vex/discover/pypi_other.rs @@ -29,8 +29,14 @@ //! //! Anything else Socket-SHAPED (a `.socket/vendor/` path that escapes the //! root or is not a pypi wheel, a patch-server url with no patch uuid) is a -//! [`DIAG_REF_INVALID`]; registry pins and users' own urls/paths are -//! skipped silently. +//! [`DIAG_REF_INVALID`]. Registry pins and, in `Pipfile.lock` and the +//! requirements files, users' own urls / paths / VCS checkouts wire +//! nothing: they are evidence that the lock installs the package from +//! elsewhere ([`Discovery::resolved_elsewhere`]), which contests another +//! lock's wiring of the same release (#1549). Such a reference's version +//! is an exact `version` pin beside it or its artifact's own wheel / sdist +//! filename; one with neither may install any version +//! ([`Discovery::resolved_elsewhere_any_version`]). //! //! ## Pipfile.lock //! @@ -168,9 +174,12 @@ async fn extract_pipfile_lock(ctx: &DiscoverCtx<'_>, out: &mut Discovery) { let references = pkg.references(); let location = match references.as_slice() { [] => { - // A registry pin (`version: "==X"`, no VCS / editable - // source): evidence against another lock's wiring. - if let (false, Some(version)) = (pkg.is_vcs(), pkg.exact_pin()) { + // A registry pin (`version: "==X"`): evidence against + // another lock's wiring. A VCS / editable source names no + // release, so it may be any version (#1549). + if pkg.is_vcs() { + out.resolved_elsewhere_any_version(PIPFILE_LOCK, pypi_name_purl(name)); + } else if let Some(version) = pkg.exact_pin() { out.resolved_elsewhere(PIPFILE_LOCK, pypi_purl(name, version)); } continue; @@ -208,6 +217,7 @@ async fn extract_pipfile_lock(ctx: &DiscoverCtx<'_>, out: &mut Discovery) { pinned_version: pkg.exact_pin(), tag: None, root_uri: false, + foreign_is_evidence: true, }, location, integrity_of(location, hashes), @@ -270,6 +280,7 @@ fn requirements_line(ctx: &DiscoverCtx<'_>, file: &str, line: &str, out: &mut Di pinned_version: None, tag, root_uri: false, + foreign_is_evidence: true, }, location, integrity_of(location, hash_options(code)), @@ -384,6 +395,7 @@ fn hatch_spec( pinned_version: None, tag: None, root_uri: expanded, + foreign_is_evidence: false, }, location, integrity_of(location, Vec::new()), @@ -408,6 +420,12 @@ struct Entry<'a> { /// URI, so a `#sha256=…` after the path is a fragment, not part of the /// file name. root_uri: bool, + /// A non-Socket reference here installs the package from elsewhere, + /// evidence against another lock's wiring ([`foreign_reference`]): + /// `Pipfile.lock` and requirements files, which install exactly what + /// they reference. Hatch's dependency tables are declarations a + /// resolver reads, not an install source of their own. + foreign_is_evidence: bool, } impl Entry<'_> { @@ -469,7 +487,12 @@ fn emit( ) { let uri = entry.root_uri || url_form(location); let result = match classify(ctx, location, uri) { - Ok(None) => return, + Ok(None) => { + if entry.foreign_is_evidence { + foreign_reference(entry, location, out); + } + return; + } Ok(Some(Wired::Hosted(uuid))) => hosted_ref(entry, location, uuid, integrity), Ok(Some(Wired::Vendored(vref))) => vendored_ref(entry, &vref, integrity), Err(detail) => Err(detail), @@ -484,6 +507,49 @@ fn emit( } } +/// Record a user's own (non-Socket) reference `location` as evidence that +/// `entry`'s file installs its package from elsewhere (#1549): pip and +/// Pipenv install that artifact, unpatched, whichever lock wires the same +/// release to a Socket patch. The release is an exact `version` pin beside +/// the reference, else the artifact's wheel / sdist filename (when it names +/// the declared package). With neither, it may be any version. A bare +/// requirements line names its package only through its filename, so one +/// whose filename isn't an artifact's (a source directory) is left out. +fn foreign_reference(entry: &Entry<'_>, location: &str, out: &mut Discovery) { + let leaf = artifact_leaf(location).and_then(|leaf| vendored_leaf_purl("pypi", &leaf)); + let leaf = leaf + .as_deref() + .and_then(|purl| purl.strip_prefix("pkg:pypi/")) + .and_then(|nv| nv.rsplit_once('@')); + let Some(name) = entry.name.or(leaf.map(|(name, _)| name)) else { + return; + }; + let version = entry.pinned_version.filter(|v| !v.is_empty()).or_else(|| { + leaf.filter(|(leaf_name, _)| *leaf_name == canonicalize_pypi_name(name)) + .map(|(_, version)| version) + }); + match version { + Some(version) => out.resolved_elsewhere(entry.file, pypi_purl(name, version)), + None => out.resolved_elsewhere_any_version(entry.file, pypi_name_purl(name)), + } +} + +/// The percent-decoded last path segment of a url or path, its `#` +/// fragment and `?` query cut: the artifact's filename. +fn artifact_leaf(location: &str) -> Option { + let path = location.split(['#', '?']).next()?; + let leaf = path.rsplit(['/', '\\']).next()?; + (!leaf.is_empty()).then(|| crate::utils::purl::percent_decode_purl_component(leaf).into_owned()) +} + +/// `pkg:pypi/` (PEP 503-canonical, no version) for +/// [`Discovery::resolved_elsewhere_any_version`]; `None` for a name a purl +/// can't safely carry. +fn pypi_name_purl(name: &str) -> Option { + let purl = pypi_purl(name, "0")?; + Some(purl.strip_suffix("@0")?.to_string()) +} + fn hosted_ref( entry: &Entry<'_>, url: &str, @@ -1527,4 +1593,198 @@ mod tests { .all(|r| r.purl == "pkg:pypi/urllib3@1.26.18" && r.uuid == UUID_A)); assert!(out.diagnostics.is_empty(), "{:?}", out.diagnostics); } + + /// REGRESSION (#1549): a Pipfile.lock `file` url or `path` wheel, or a + /// requirements `name @ ` line, that installs the same release + /// from a non-Socket artifact contests another lock's wiring exactly + /// like a registry `==` pin does: Pipenv / pip install the upstream + /// artifact, so the ref is withheld (`patched_ref_unattributable`, + /// naming both files) instead of attested. The version comes from an + /// exact `version` pin or the artifact's own wheel / sdist filename. + #[tokio::test] + async fn issue_1549_non_socket_direct_references_contest_wiring() { + const WHL: &str = "six-1.16.0-py2.py3-none-any.whl"; + let upstream = format!("https://files.pythonhosted.org/packages/d9/5a/{WHL}"); + let vendored = vendored_wheel(UUID_A, WHL); + let vendored_requirements = + format!("./{vendored} --hash=sha256:{SHA} # socket-patch vendor: six==1.16.0\n"); + let vendored_pipfile = pipfile_lock(serde_json::json!({ "default": { + "six": { "file": format!("./{vendored}"), "hashes": [format!("sha256:{SHA}")] }, + }})); + let pipfile = |entry: serde_json::Value| { + pipfile_lock(serde_json::json!({ "default": { "six": entry } })) + }; + type Case = (&'static str, (&'static str, String), (&'static str, String)); + let cases: Vec = vec![ + ( + "Pipfile.lock file url", + ("requirements.txt", vendored_requirements.clone()), + ( + "Pipfile.lock", + pipfile(serde_json::json!({ + "file": upstream, "hashes": [format!("sha256:{SHA_B}")], + })), + ), + ), + ( + "Pipfile.lock path wheel", + ("requirements.txt", vendored_requirements.clone()), + ( + "Pipfile.lock", + pipfile(serde_json::json!({ + "path": format!("./wheels/{WHL}"), "hashes": [format!("sha256:{SHA_B}")], + })), + ), + ), + ( + "Pipfile.lock file:// url", + ("requirements.txt", vendored_requirements.clone()), + ( + "Pipfile.lock", + pipfile(serde_json::json!({ "file": format!("file:///srv/wheels/{WHL}") })), + ), + ), + ( + "Pipfile.lock path with an exact version", + ("requirements.txt", vendored_requirements.clone()), + ( + "Pipfile.lock", + pipfile(serde_json::json!({ "path": "./src/six", "version": "==1.16.0" })), + ), + ), + ( + "requirements name @ url (mirror)", + ("Pipfile.lock", vendored_pipfile.clone()), + ( + "requirements.txt", + format!("six @ {upstream} --hash=sha256:{SHA_B}\n"), + ), + ), + ( + "requirements bare url line", + ("Pipfile.lock", vendored_pipfile.clone()), + ("requirements.txt", format!("{upstream}#sha256={SHA_B}\n")), + ), + ( + "requirements bare path line", + ("Pipfile.lock", vendored_pipfile.clone()), + ("requirements.txt", format!("./wheels/{WHL}\n")), + ), + ]; + for (name, wired, other) in cases { + let p = Project::new(); + p.write(wired.0, &wired.1).write(other.0, &other.1); + let out = p.discover().await; + assert!(out.refs.is_empty(), "{name}: {:#?}", out.refs); + assert!( + out.diagnostics + .iter() + .any(|d| d.code == DIAG_REF_UNATTRIBUTABLE + && d.file == std::path::Path::new(wired.0) + && d.detail.contains(UUID_A) + && d.detail.contains(other.0)), + "{name}: {:#?}", + out.diagnostics + ); + let c = out + .vendored_contest("pkg:pypi/six@1.16.0", UUID_A) + .expect(name); + assert_eq!(c.other, std::path::Path::new(other.0), "{name}"); + + // Without the contesting file the same wiring is a ref. + let alone = Project::new(); + alone.write(wired.0, &wired.1); + assert_eq!(alone.discover().await.refs.len(), 1, "{name} alone"); + } + } + + /// REGRESSION (#1549): a non-Socket direct reference whose version + /// can't be read (a source directory, a VCS url, an artifact named + /// for no release) may install any version of its package, so it + /// contests another lock's wiring of that package at every version; + /// one of ANOTHER package, or of another release, contests nothing. + #[tokio::test] + async fn issue_1549_unversioned_direct_references_contest_every_version() { + const WHL: &str = "six-1.16.0-py2.py3-none-any.whl"; + let vendored = vendored_wheel(UUID_A, WHL); + let vendored_requirements = + format!("./{vendored} --hash=sha256:{SHA} # socket-patch vendor: six==1.16.0\n"); + let pipfile = |entry: serde_json::Value| { + pipfile_lock(serde_json::json!({ "default": { "six": entry } })) + }; + for (name, other) in [ + ( + "path dir", + pipfile(serde_json::json!({ "path": "./src/six" })), + ), + ( + "file sdist url with no version", + pipfile(serde_json::json!({ "file": "https://example.com/dl/six.zip" })), + ), + ( + "vcs entry", + pipfile(serde_json::json!({ + "git": "https://github.com/benjaminp/six.git", "ref": "abc123", + })), + ), + ] { + let p = Project::new(); + p.write("requirements.txt", &vendored_requirements) + .write("Pipfile.lock", &other); + let out = p.discover().await; + assert!(out.refs.is_empty(), "{name}: {:#?}", out.refs); + assert!( + out.vendored_contest("pkg:pypi/six@1.16.0", UUID_A) + .is_some(), + "{name}: {:#?}", + out.diagnostics + ); + } + for (name, line) in [ + ( + "vcs requirement", + "six @ git+https://github.com/benjaminp/six.git@abc123\n", + ), + ("directory requirement", "six @ file:///srv/src/six\n"), + ] { + let p = Project::new(); + p.write( + "Pipfile.lock", + pipfile(serde_json::json!({ + "file": format!("./{vendored}"), "hashes": [format!("sha256:{SHA}")], + })), + ) + .write("requirements.txt", line); + let out = p.discover().await; + assert!(out.refs.is_empty(), "{name}: {:#?}", out.refs); + assert!( + out.vendored_contest("pkg:pypi/six@1.16.0", UUID_A) + .is_some(), + "{name}" + ); + } + + // Another package, or another release of this one, leaves the + // wiring alone. + for (name, other) in [ + ( + "other package dir", + pipfile_lock(serde_json::json!({ "default": { + "idna": { "path": "./src/idna" }, + }})), + ), + ( + "other release wheel", + pipfile(serde_json::json!({ + "file": "https://files.pythonhosted.org/packages/six-1.15.0-py2.py3-none-any.whl", + })), + ), + ] { + let p = Project::new(); + p.write("requirements.txt", &vendored_requirements) + .write("Pipfile.lock", &other); + let out = p.discover().await; + assert_eq!(out.refs.len(), 1, "{name}: {:#?}", out.diagnostics); + } + } } From 39605f681b70b0e219b97c4b25b013f05fc968af Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 04:51:49 +0000 Subject: [PATCH 3/7] Test vex contest by Pipenv/pip direct refs End-to-end cells for #1549: the real CLI wires a Pipfile.lock or a requirements.txt, then a second install source pins the same release to a non-Socket url or wheel path. `vex` must withhold the statement and explain the contest, as it already does for a registry pin. Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/e2e_vex_lockfile/pip.rs | 37 ++++++++++++++++ .../tests/e2e_vex_lockfile/pipenv.rs | 21 +++++++++ .../tests/vex_pipenv_pip_common/mod.rs | 44 +++++++++++++++++++ 3 files changed, 102 insertions(+) diff --git a/crates/socket-patch-cli/tests/e2e_vex_lockfile/pip.rs b/crates/socket-patch-cli/tests/e2e_vex_lockfile/pip.rs index 9552d95fb..c3ee23161 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_lockfile/pip.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_lockfile/pip.rs @@ -341,3 +341,40 @@ fn vendored_extras_pin_is_refused_and_nothing_is_attested() { assert_ne!(out.code, Some(0), "{out}"); assert_no_statement(&out, "vendored extras"); } + +/// #1549: a Pipfile.lock whose entry installs the same release from a +/// non-Socket `file` url or `path` wheel contests the requirements wiring, +/// as a registry `version: "==1.2.3"` entry there already does. +#[test] +fn pipfile_lock_direct_reference_contests_the_requirements_wiring() { + let lock = |entry: serde_json::Value| { + serde_json::json!({ + "_meta": { "pipfile-spec": 6, "hash": { "sha256": "x" }, "requires": {}, "sources": [] }, + "default": { NAME: entry }, + "develop": {}, + }) + .to_string() + }; + let hashes = serde_json::json!([format!("sha256:{}", "c".repeat(64))]); + contested_by_a_non_socket_direct_reference( + &flavors()[..1], + "Pipfile.lock", + &[ + ( + "file url", + lock(serde_json::json!({ "file": upstream_wheel_url(), "hashes": hashes })), + ), + ( + "path wheel", + lock(serde_json::json!({ + "path": format!("./wheels/{NAME}-{VERSION}-py3-none-any.whl"), + "hashes": hashes, + })), + ), + ( + "registry pin (control)", + lock(serde_json::json!({ "version": format!("=={VERSION}"), "hashes": hashes })), + ), + ], + ); +} diff --git a/crates/socket-patch-cli/tests/e2e_vex_lockfile/pipenv.rs b/crates/socket-patch-cli/tests/e2e_vex_lockfile/pipenv.rs index 002db0a5d..f922cfb4e 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_lockfile/pipenv.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_lockfile/pipenv.rs @@ -346,3 +346,24 @@ fn vendored_legacy_installer_is_refused_and_nothing_is_attested() { assert_ne!(out.code, Some(0), "{out}"); assert_no_statement(&out, "legacy vendored"); } + +/// #1549 (mirror case): a requirements.txt line that installs the same +/// release from a non-Socket url or wheel path contests the Pipfile.lock +/// wiring, as a `vexdemo==1.2.3` registry pin there already does. +#[test] +fn requirements_direct_reference_contests_the_lock_wiring() { + let url = upstream_wheel_url(); + contested_by_a_non_socket_direct_reference( + &flavors()[..1], + "requirements.txt", + &[ + ("name @ url", format!("{NAME} @ {url}\n")), + ("bare url", format!("{url}\n")), + ( + "bare wheel path", + format!("./wheels/{NAME}-{VERSION}-py3-none-any.whl\n"), + ), + ("registry pin (control)", format!("{NAME}=={VERSION}\n")), + ], + ); +} diff --git a/crates/socket-patch-cli/tests/vex_pipenv_pip_common/mod.rs b/crates/socket-patch-cli/tests/vex_pipenv_pip_common/mod.rs index 60ddd3d25..799780f5d 100644 --- a/crates/socket-patch-cli/tests/vex_pipenv_pip_common/mod.rs +++ b/crates/socket-patch-cli/tests/vex_pipenv_pip_common/mod.rs @@ -1349,3 +1349,47 @@ pub fn embedded_apply_and_vendor_vex_attest_a_manifest_less_checkout(flavors: &[ } } } + +/// #1549: beside the wired lock, ANOTHER install source (`other`, written +/// to `other_rel`) resolves the same release from a non-Socket artifact (a +/// Pipfile.lock `file` url / `path` wheel, a requirements `name @ url` +/// line). Pipenv / pip install that upstream artifact, so the wiring is +/// contested: never attested, `--no-verify` included, and the contest is +/// explained. Without `other` the same checkout attests. +pub fn contested_by_a_non_socket_direct_reference( + flavors: &[Flavor], + other_rel: &str, + others: &[(&str, String)], +) { + for (flavor, mode) in cells(flavors) { + let wired = wired(&flavor, mode); + for (label, other) in others { + let what = format!("{} + {other_rel} {label}", wired.what()); + let (_tmp, cwd) = fresh(); + wired.restore(&cwd, NOTHING); + let api = api_with(mode.uuid(), PURL); + assert_ok_attested(&vex(&cwd, &vex_run(Some(&api))), mode, &what); + put(&cwd, other_rel, other.as_bytes()); + for no_verify in [false, true] { + let run = crate::vex_e2e_common::VexRun { + no_verify, + ..vex_run(Some(&api)) + }; + let out = vex(&cwd, &run); + let what = format!("{what} no_verify={no_verify}"); + assert_ne!(out.code, Some(0), "{what}: {out}"); + assert_no_statement(&out, &what); + let text = out.stdout.clone() + &out.stderr; + assert!( + text.contains("patched_ref_unattributable") && text.contains(other_rel), + "{what}: the contested wiring must be explained: {out}" + ); + } + } + } +} + +/// The upstream wheel of the fictitious package on a non-Socket host. +pub fn upstream_wheel_url() -> String { + format!("https://files.pythonhosted.org/packages/ab/cd/{NAME}-{VERSION}-py3-none-any.whl") +} From 37e7c125488203984c9da6d07d2b213a467d9c5c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 05:19:57 +0000 Subject: [PATCH 4/7] Contest wiring with editable and egg-named refs Review follow-ups for #1549. pip installs `-e git+...#egg=six` and a bare `git+...#egg=six` line from source, so both now count as resolving six elsewhere at any version. A Pipfile.lock wheel's own filename now decides its release ahead of a `version` pin beside it, and a wildcard pin like `==1.16.*` is never taken for a release. The "which packages does any lock resolve" query keeps its old meaning. Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/vex_pipenv_pip_common/mod.rs | 3 +- .../socket-patch-core/src/vex/discover/mod.rs | 7 +- .../src/vex/discover/pypi_other.rs | 88 ++++++++++++++++--- 3 files changed, 79 insertions(+), 19 deletions(-) diff --git a/crates/socket-patch-cli/tests/vex_pipenv_pip_common/mod.rs b/crates/socket-patch-cli/tests/vex_pipenv_pip_common/mod.rs index 799780f5d..28875fe4e 100644 --- a/crates/socket-patch-cli/tests/vex_pipenv_pip_common/mod.rs +++ b/crates/socket-patch-cli/tests/vex_pipenv_pip_common/mod.rs @@ -1381,7 +1381,8 @@ pub fn contested_by_a_non_socket_direct_reference( assert_no_statement(&out, &what); let text = out.stdout.clone() + &out.stderr; assert!( - text.contains("patched_ref_unattributable") && text.contains(other_rel), + text.contains("patched_ref_unattributable") + && text.contains(&format!("but {other_rel} resolves the same version")), "{what}: the contested wiring must be explained: {out}" ); } diff --git a/crates/socket-patch-core/src/vex/discover/mod.rs b/crates/socket-patch-core/src/vex/discover/mod.rs index 68982a618..fe2c998c9 100644 --- a/crates/socket-patch-core/src/vex/discover/mod.rs +++ b/crates/socket-patch-core/src/vex/discover/mod.rs @@ -1095,7 +1095,7 @@ impl Discovery { let other = self.tree_of(&e.file); e.covers(&r.purl) && other != tree - && !wiring.contains(&(e.purl.clone(), other.to_path_buf())) + && !wiring.contains(&(r.purl.clone(), other.to_path_buf())) }) }) .flatten(); @@ -1236,10 +1236,7 @@ impl Discovery { let key = canonical_base_purl(purl); self.refs.iter().any(|r| PurlKey::same(&r.purl, &key)) || self.contested.iter().any(|c| PurlKey::same(&c.purl, &key)) - || self - .elsewhere - .iter() - .any(|e| PurlKey::same(&e.purl, &key) || e.covers(&key)) + || self.elsewhere.iter().any(|e| PurlKey::same(&e.purl, &key)) } fn recognize(&mut self, uuid: &str, mode: WiringMode, file: &str) { diff --git a/crates/socket-patch-core/src/vex/discover/pypi_other.rs b/crates/socket-patch-core/src/vex/discover/pypi_other.rs index 007f81f89..a9fc944d2 100644 --- a/crates/socket-patch-core/src/vex/discover/pypi_other.rs +++ b/crates/socket-patch-core/src/vex/discover/pypi_other.rs @@ -261,7 +261,19 @@ async fn extract_requirements(ctx: &DiscoverCtx<'_>, out: &mut Discovery) { fn requirements_line(ctx: &DiscoverCtx<'_>, file: &str, line: &str, out: &mut Discovery) { let (code, comment) = split_comment(line); let code = code.trim(); - // Blank lines, comments, and option lines (`-r`, `--index-url`, `-e`). + if let Some(location) = editable_location(code) { + // pip installs an editable source checkout of whatever package it + // holds, never a Socket artifact: evidence for the package its + // `#egg=` names, at any version (#1549). + if !mentions_vendor_dir(location) { + if let Some(name) = egg_name(location) { + out.resolved_elsewhere_any_version(file, pypi_name_purl(name)); + } + } + return; + } + // Blank lines, comments, and the other option lines (`-r`, + // `--index-url`). if code.is_empty() || code.starts_with('-') { return; } @@ -288,6 +300,30 @@ fn requirements_line(ctx: &DiscoverCtx<'_>, file: &str, line: &str, out: &mut Di ); } +/// The location of an editable requirement (`-e `, `--editable `, +/// `--editable=`). +fn editable_location(code: &str) -> Option<&str> { + let rest = code + .strip_prefix("--editable") + .or_else(|| code.strip_prefix("-e"))?; + let rest = match rest.strip_prefix('=') { + Some(rest) => rest, + None if rest.starts_with(char::is_whitespace) => rest, + None => return None, + }; + rest.split_whitespace().next() +} + +/// The package a VCS / url requirement names in its `#egg=` fragment. +fn egg_name(location: &str) -> Option<&str> { + let (_, fragment) = location.split_once('#')?; + let egg = fragment.split('&').find_map(|p| p.strip_prefix("egg="))?; + let end = egg + .find(|c: char| !(c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'))) + .unwrap_or(egg.len()); + Some(&egg[..end]).filter(|name| !name.is_empty()) +} + /// The `pypi_purl` of an exact `name[extras]==X` registry requirement /// ([`exact_pin`]). fn registry_pin(code: &str) -> Option { @@ -510,24 +546,32 @@ fn emit( /// Record a user's own (non-Socket) reference `location` as evidence that /// `entry`'s file installs its package from elsewhere (#1549): pip and /// Pipenv install that artifact, unpatched, whichever lock wires the same -/// release to a Socket patch. The release is an exact `version` pin beside -/// the reference, else the artifact's wheel / sdist filename (when it names -/// the declared package). With neither, it may be any version. A bare -/// requirements line names its package only through its filename, so one -/// whose filename isn't an artifact's (a source directory) is left out. +/// release to a Socket patch. The release is the artifact's wheel / sdist +/// filename (when it names the declared package), else an exact `version` +/// pin beside the reference. With neither, it may be any version. A bare +/// requirements line names its package only through its filename or an +/// `#egg=` fragment, so one with neither (a source directory) is left out. fn foreign_reference(entry: &Entry<'_>, location: &str, out: &mut Discovery) { let leaf = artifact_leaf(location).and_then(|leaf| vendored_leaf_purl("pypi", &leaf)); let leaf = leaf .as_deref() .and_then(|purl| purl.strip_prefix("pkg:pypi/")) .and_then(|nv| nv.rsplit_once('@')); - let Some(name) = entry.name.or(leaf.map(|(name, _)| name)) else { + let Some(name) = entry + .name + .or(leaf.map(|(name, _)| name)) + .or_else(|| egg_name(location)) + else { return; }; - let version = entry.pinned_version.filter(|v| !v.is_empty()).or_else(|| { - leaf.filter(|(leaf_name, _)| *leaf_name == canonicalize_pypi_name(name)) - .map(|(_, version)| version) - }); + let version = leaf + .filter(|(leaf_name, _)| *leaf_name == canonicalize_pypi_name(name)) + .map(|(_, version)| version) + .or_else(|| { + entry + .pinned_version + .filter(|v| !v.is_empty() && !v.contains('*')) + }); match version { Some(version) => out.resolved_elsewhere(entry.file, pypi_purl(name, version)), None => out.resolved_elsewhere_any_version(entry.file, pypi_name_purl(name)), @@ -546,8 +590,8 @@ fn artifact_leaf(location: &str) -> Option { /// [`Discovery::resolved_elsewhere_any_version`]; `None` for a name a purl /// can't safely carry. fn pypi_name_purl(name: &str) -> Option { - let purl = pypi_purl(name, "0")?; - Some(purl.strip_suffix("@0")?.to_string()) + let name = canonicalize_pypi_name(name); + crate::patch::path_safety::is_safe_single_segment(&name).then(|| format!("pkg:pypi/{name}")) } fn hosted_ref( @@ -1644,6 +1688,14 @@ mod tests { pipfile(serde_json::json!({ "file": format!("file:///srv/wheels/{WHL}") })), ), ), + ( + "Pipfile.lock wheel beside a wildcard version", + ("requirements.txt", vendored_requirements.clone()), + ( + "Pipfile.lock", + pipfile(serde_json::json!({ "file": upstream, "version": "==1.16.*" })), + ), + ), ( "Pipfile.lock path with an exact version", ("requirements.txt", vendored_requirements.clone()), @@ -1746,6 +1798,16 @@ mod tests { "six @ git+https://github.com/benjaminp/six.git@abc123\n", ), ("directory requirement", "six @ file:///srv/src/six\n"), + ( + "bare vcs line named by #egg", + "git+https://github.com/benjaminp/six.git@1.16.0#egg=six\n", + ), + ( + "editable vcs", + "-e git+https://github.com/benjaminp/six.git#egg=six\n", + ), + ("editable dir", "--editable=./src/six#egg=Six\n"), + ("editable dir, long flag", "--editable ./src/six#egg=six\n"), ] { let p = Project::new(); p.write( From e97bcebe999e41a2731344aace12fbb542acd6d0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 05:41:16 +0000 Subject: [PATCH 5/7] Ignore user refs in an abandoned Pipfile.lock Pipenv installs from Pipfile.lock only when its Pipfile sits beside it. A user's own file/path/VCS entry in a leftover lock with no Pipfile installs nothing, so it no longer contests a requirements.txt wiring, and a hosted scan still redirects requirements.txt there (#333). Refs #1549 Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/vex_pipenv_pip_common/mod.rs | 7 +++- .../src/vex/discover/pypi_other.rs | 37 ++++++++++++++++--- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/crates/socket-patch-cli/tests/vex_pipenv_pip_common/mod.rs b/crates/socket-patch-cli/tests/vex_pipenv_pip_common/mod.rs index 28875fe4e..524c44296 100644 --- a/crates/socket-patch-cli/tests/vex_pipenv_pip_common/mod.rs +++ b/crates/socket-patch-cli/tests/vex_pipenv_pip_common/mod.rs @@ -1353,7 +1353,8 @@ pub fn embedded_apply_and_vendor_vex_attest_a_manifest_less_checkout(flavors: &[ /// #1549: beside the wired lock, ANOTHER install source (`other`, written /// to `other_rel`) resolves the same release from a non-Socket artifact (a /// Pipfile.lock `file` url / `path` wheel, a requirements `name @ url` -/// line). Pipenv / pip install that upstream artifact, so the wiring is +/// line; a contesting Pipfile.lock gets its Pipfile). Pipenv / pip install +/// that upstream artifact, so the wiring is /// contested: never attested, `--no-verify` included, and the contest is /// explained. Without `other` the same checkout attests. pub fn contested_by_a_non_socket_direct_reference( @@ -1370,6 +1371,10 @@ pub fn contested_by_a_non_socket_direct_reference( let api = api_with(mode.uuid(), PURL); assert_ok_attested(&vex(&cwd, &vex_run(Some(&api))), mode, &what); put(&cwd, other_rel, other.as_bytes()); + if other_rel == "Pipfile.lock" { + // Pipenv installs from the lock only with its Pipfile. + put(&cwd, "Pipfile", PIPFILE.as_bytes()); + } for no_verify in [false, true] { let run = crate::vex_e2e_common::VexRun { no_verify, diff --git a/crates/socket-patch-core/src/vex/discover/pypi_other.rs b/crates/socket-patch-core/src/vex/discover/pypi_other.rs index a9fc944d2..4632194e8 100644 --- a/crates/socket-patch-core/src/vex/discover/pypi_other.rs +++ b/crates/socket-patch-core/src/vex/discover/pypi_other.rs @@ -124,6 +124,7 @@ use crate::vendor::common::DeclTable; use crate::vendor::lock_inventory::pypi::{hosted_artifact_url, parse_pipfile_lock}; use crate::vendor::lock_inventory::{pipfile_lock_entries, LockIntegrity}; +const PIPFILE: &str = "Pipfile"; const PIPFILE_LOCK: &str = "Pipfile.lock"; const ROOT_REQUIREMENTS: &str = "requirements.txt"; const PYPROJECT: &str = "pyproject.toml"; @@ -169,6 +170,11 @@ async fn extract_pipfile_lock(ctx: &DiscoverCtx<'_>, out: &mut Discovery) { ); return; }; + // Pipenv installs from the lock only with its Pipfile beside it. A + // user's own reference in an abandoned lock (no Pipfile) installs + // nothing, so it contests no other lock's wiring (#333, #1549); the + // hosted planner leaves such an entry alone. + let live = ctx.exists(PIPFILE).await; for pkg in entries { let (category, name) = (pkg.category, pkg.name); let references = pkg.references(); @@ -178,7 +184,9 @@ async fn extract_pipfile_lock(ctx: &DiscoverCtx<'_>, out: &mut Discovery) { // another lock's wiring. A VCS / editable source names no // release, so it may be any version (#1549). if pkg.is_vcs() { - out.resolved_elsewhere_any_version(PIPFILE_LOCK, pypi_name_purl(name)); + if live { + out.resolved_elsewhere_any_version(PIPFILE_LOCK, pypi_name_purl(name)); + } } else if let Some(version) = pkg.exact_pin() { out.resolved_elsewhere(PIPFILE_LOCK, pypi_purl(name, version)); } @@ -217,7 +225,7 @@ async fn extract_pipfile_lock(ctx: &DiscoverCtx<'_>, out: &mut Discovery) { pinned_version: pkg.exact_pin(), tag: None, root_uri: false, - foreign_is_evidence: true, + foreign_is_evidence: live, }, location, integrity_of(location, hashes), @@ -1725,7 +1733,9 @@ mod tests { ]; for (name, wired, other) in cases { let p = Project::new(); - p.write(wired.0, &wired.1).write(other.0, &other.1); + p.write(wired.0, &wired.1) + .write(other.0, &other.1) + .write("Pipfile", "[packages]\nsix = \"*\"\n"); let out = p.discover().await; assert!(out.refs.is_empty(), "{name}: {:#?}", out.refs); assert!( @@ -1782,7 +1792,8 @@ mod tests { ] { let p = Project::new(); p.write("requirements.txt", &vendored_requirements) - .write("Pipfile.lock", &other); + .write("Pipfile.lock", &other) + .write("Pipfile", "[packages]\nsix = \"*\"\n"); let out = p.discover().await; assert!(out.refs.is_empty(), "{name}: {:#?}", out.refs); assert!( @@ -1844,9 +1855,25 @@ mod tests { ] { let p = Project::new(); p.write("requirements.txt", &vendored_requirements) - .write("Pipfile.lock", &other); + .write("Pipfile.lock", &other) + .write("Pipfile", "[packages]\nsix = \"*\"\n"); let out = p.discover().await; assert_eq!(out.refs.len(), 1, "{name}: {:#?}", out.diagnostics); } + + // #333: with no Pipfile beside it, Pipenv installs nothing from the + // lock, so a user's own reference there contests nothing. + for other in [ + pipfile(serde_json::json!({ + "file": "https://files.pythonhosted.org/packages/six-1.16.0-py2.py3-none-any.whl", + })), + pipfile(serde_json::json!({ "git": "https://github.com/benjaminp/six.git" })), + ] { + let p = Project::new(); + p.write("requirements.txt", &vendored_requirements) + .write("Pipfile.lock", &other); + let out = p.discover().await; + assert_eq!(out.refs.len(), 1, "abandoned: {:#?}", out.diagnostics); + } } } From 543e124a4cadb747162b3bb82b87980dec58b263 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 05:57:25 +0000 Subject: [PATCH 6/7] Read legacy name-version #egg= fragments `#egg=six-1.16.0` names the package six, not "six-1.16.0". The egg name now stops at the first `-`, so a legacy editable or VCS line still contests a Socket wiring of the real package (#1549, Bugbot). Assisted-by: Claude Code:claude-opus-5-5 --- .../socket-patch-core/src/vex/discover/pypi_other.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/socket-patch-core/src/vex/discover/pypi_other.rs b/crates/socket-patch-core/src/vex/discover/pypi_other.rs index 4632194e8..9232dc6d4 100644 --- a/crates/socket-patch-core/src/vex/discover/pypi_other.rs +++ b/crates/socket-patch-core/src/vex/discover/pypi_other.rs @@ -323,12 +323,19 @@ fn editable_location(code: &str) -> Option<&str> { } /// The package a VCS / url requirement names in its `#egg=` fragment. +/// A legacy `#egg=-` fragment names the package before its +/// first `-`, as setuptools and older pip read it. fn egg_name(location: &str) -> Option<&str> { let (_, fragment) = location.split_once('#')?; let egg = fragment.split('&').find_map(|p| p.strip_prefix("egg="))?; let end = egg .find(|c: char| !(c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'))) .unwrap_or(egg.len()); + let egg = &egg[..end]; + let end = egg + .match_indices('-') + .find(|(i, _)| egg[i + 1..].starts_with(|c: char| c.is_ascii_digit())) + .map_or(egg.len(), |(i, _)| i); Some(&egg[..end]).filter(|name| !name.is_empty()) } @@ -1819,6 +1826,10 @@ mod tests { ), ("editable dir", "--editable=./src/six#egg=Six\n"), ("editable dir, long flag", "--editable ./src/six#egg=six\n"), + ( + "legacy name-version egg", + "-e git+https://github.com/benjaminp/six.git#egg=six-1.16.0\n", + ), ] { let p = Project::new(); p.write( From 07503e78d2c3451f0adfb5a11f1f6c0d5cd5dc69 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 08:26:45 +0000 Subject: [PATCH 7/7] Read -e editable values with no space pip accepts `-e./src/six#egg=six`, the value attached to the short option. Discovery skipped that spelling, so such a line didn't contest a Socket wiring of six (#1549, final-review note). Assisted-by: Claude Code:claude-opus-5-5 --- .../src/vex/discover/pypi_other.rs | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/crates/socket-patch-core/src/vex/discover/pypi_other.rs b/crates/socket-patch-core/src/vex/discover/pypi_other.rs index 9232dc6d4..c097ad022 100644 --- a/crates/socket-patch-core/src/vex/discover/pypi_other.rs +++ b/crates/socket-patch-core/src/vex/discover/pypi_other.rs @@ -308,16 +308,19 @@ fn requirements_line(ctx: &DiscoverCtx<'_>, file: &str, line: &str, out: &mut Di ); } -/// The location of an editable requirement (`-e `, `--editable `, -/// `--editable=`). +/// The location of an editable requirement (`-e `, `-e`, +/// `--editable `, `--editable=`), as pip's option parser reads it. fn editable_location(code: &str) -> Option<&str> { - let rest = code - .strip_prefix("--editable") - .or_else(|| code.strip_prefix("-e"))?; - let rest = match rest.strip_prefix('=') { - Some(rest) => rest, - None if rest.starts_with(char::is_whitespace) => rest, - None => return None, + let rest = if let Some(rest) = code.strip_prefix("--editable") { + match rest.strip_prefix('=') { + Some(rest) => rest, + None if rest.starts_with(char::is_whitespace) => rest, + None => return None, + } + } else { + // A short option takes its value attached or after a space. + let rest = code.strip_prefix("-e")?; + rest.strip_prefix('=').unwrap_or(rest) }; rest.split_whitespace().next() } @@ -1826,6 +1829,7 @@ mod tests { ), ("editable dir", "--editable=./src/six#egg=Six\n"), ("editable dir, long flag", "--editable ./src/six#egg=six\n"), + ("editable dir, attached short flag", "-e./src/six#egg=six\n"), ( "legacy name-version egg", "-e git+https://github.com/benjaminp/six.git#egg=six-1.16.0\n",