Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion crates/socket-patch-core/src/formats/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,19 @@ pub use registry::registry;
mod architecture_tests {
use std::path::Path;

const IMPURE: [&str; 9] = [
const IMPURE: [&str; 12] = [
"tokio::fs",
"std::fs",
"read_regular_",
"File::open",
"OpenOptions",
// The host policy (`crate::origin`). Its pure path grammar
// (`origin::vendor_ref`) is allowed: it applies no host policy.
"hosted_patch_uuid",
"hosted_patch_url_uuids",
"hosted_uuid(",
"OriginPolicy",
"SOCKET_PATCH_SERVER_HOST",
"async fn",
".await",
];
Expand Down
1 change: 1 addition & 0 deletions crates/socket-patch-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod hash;
pub mod hosted;
pub mod ledgers;
pub mod manifest;
pub mod origin;
pub mod patch;
pub mod policy;
pub mod rollout;
Expand Down
876 changes: 876 additions & 0 deletions crates/socket-patch-core/src/origin.rs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion crates/socket-patch-core/src/patch/redirect/generation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ pub(crate) fn hosted_pin_name(uuid: &str) -> String {
/// `socket-patch-vendor-<canonical-uuid>` (maven's vendored repository id).
/// No trimming: the rewriter must never treat a user's padded pin as its
/// own, while lockfile discovery trims at its call site
/// (`vex::discover::socket_patch_name_uuid`).
/// (`origin::socket_patch_name_uuid`).
pub(crate) fn pin_name_uuid(name: &str, vendored: bool) -> Option<&str> {
let prefix = if vendored {
VENDOR_PIN_NAME_PREFIX
Expand Down
214 changes: 0 additions & 214 deletions crates/socket-patch-core/src/patch/redirect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7606,97 +7606,6 @@ pub fn grant_token_path_segment(url: &str, patch_uuid: &str) -> Option<String> {
(!token.is_empty()).then(|| token.to_string())
}

/// Public host of Socket's patch server: the origin every production hosted
/// artifact / registry URL is served from (`https://patch.socket.dev/patch/…`,
/// `…/patch-registry/…`), and the root of the Go module namespace
/// [`crate::vendor::go_mod_edit::HOSTED_GO_MODULE_PREFIX`].
pub const SOCKET_PATCH_SERVER_HOST: &str = "patch.socket.dev";

/// The Socket patch uuid a lockfile-recorded HOSTED reference names, or
/// `None` when `url` is not a Socket-hosted patch URL — the inverse of the
/// rewriters, used by `vex`'s manifest-less lockfile discovery.
///
/// Recognition is deliberately strict, because the answer decides whether a
/// committed (tamper-able) lockfile line becomes an attestation input:
///
/// * the ORIGIN must be Socket's patch server (`https://` +
/// [`SOCKET_PATCH_SERVER_HOST`]) or one of `extra_origins` — the
/// operator's `--patch-server-url` deployment, compared on scheme + host +
/// port. A uuid inside any other host's URL is a user's own dependency
/// source, never a patch reference;
/// * no userinfo — a Socket-written URL never carries credentials;
/// * the uuid is the LAST path segment passing the canonical-uuid grammar:
/// hosted URLs carry the grant token in the level before the uuid
/// (`…/patch/npm/<name>/<ver>/<token>/<uuid>/<leaf>`,
/// `…/patch-registry/<eco>/<token>/<uuid>/…`), and grant tokens may
/// themselves be uuid-shaped, so "the first uuid" would elect the token.
///
/// Every spelling the lock formats record the same URL in is accepted: a
/// `#fragment` (yarn classic `#<sha1>`, pip `#sha256=`) and a `?query` are
/// ignored; `\/`-escaped slashes (older composer locks) are unescaped; a
/// wholly percent-encoded URL (yarn berry's `__archiveUrl=` binding) is
/// decoded; a cargo source-kind prefix (`sparse+`, `registry+`) is dropped.
/// Path segments are percent-decoded AFTER splitting, so an encoded `/`
/// can never manufacture a segment.
pub fn hosted_patch_uuid(url: &str, extra_origins: &[String]) -> Option<String> {
hosted_patch_url_uuids(url, extra_origins)?.pop()
}

/// EVERY canonical-uuid path segment of a Socket-HOSTED url, in path order
/// (the grant token first when it is uuid-shaped, the patch uuid last), or
/// `None` when `url` is not on an accepted origin — [`hosted_patch_uuid`]'s
/// exact acceptance rules, without electing one segment. `vex` discovery
/// uses it to RECOGNIZE every Socket identity a lockfile mentions, including
/// the malformed or rejected shapes whose "last uuid" is not a patch.
pub fn hosted_patch_url_uuids(url: &str, extra_origins: &[String]) -> Option<Vec<String>> {
use crate::patch::path_safety::is_canonical_uuid;
use crate::utils::purl::percent_decode_purl_component;

let unescaped = url.trim().replace("\\/", "/");
let lower = unescaped.to_ascii_lowercase();
let decoded = if lower.starts_with("https%3a%2f%2f") || lower.starts_with("http%3a%2f%2f") {
percent_decode_purl_component(&unescaped).into_owned()
} else {
unescaped
};
// `sparse+https://…` / `registry+https://…` (Cargo.lock `source`).
let (scheme, _) = decoded.split_once("://")?;
let text = match scheme.rsplit_once('+') {
Some((kind, _)) if !kind.is_empty() && kind.bytes().all(|b| b.is_ascii_alphabetic()) => {
&decoded[kind.len() + 1..]
}
_ => decoded.as_str(),
};
let parsed = reqwest::Url::parse(text).ok()?;
if !matches!(parsed.scheme(), "https" | "http")
|| !parsed.username().is_empty()
|| parsed.password().is_some()
{
return None;
}
let socket_host = parsed.scheme() == "https"
&& parsed.host_str() == Some(SOCKET_PATCH_SERVER_HOST)
&& parsed.port_or_known_default() == Some(443);
let configured = extra_origins.iter().any(|origin| {
reqwest::Url::parse(origin.trim()).is_ok_and(|o| {
o.scheme() == parsed.scheme()
&& o.host_str().is_some()
&& o.host_str() == parsed.host_str()
&& o.port_or_known_default() == parsed.port_or_known_default()
})
});
if !socket_host && !configured {
return None;
}
Some(
parsed
.path_segments()?
.map(|segment| percent_decode_purl_component(segment).into_owned())
.filter(|segment| is_canonical_uuid(segment))
.collect(),
)
}

/// A dep's Socket index URL as a regex source with the per-request rotating
/// segments (grant token, patch uuid) wildcarded — an exact-URL pattern
/// misses the URL a previous run wrote under an older grant. The grant token
Expand Down Expand Up @@ -29658,129 +29567,6 @@ mod hatch_tests {
}
}

#[cfg(test)]
mod hosted_patch_uuid_tests {
//! `hosted_patch_uuid` is the trust gate between a committed lockfile
//! line and a VEX attestation input: pin the accepted spellings AND the
//! rejections (foreign hosts, credentials, non-canonical tokens).
use super::*;

const UUID: &str = "7c8d9e0f-1a2b-4a1b-8c2d-3e4f5a6b7c8d";
/// A uuid-SHAPED grant token: the fixtures use them, and production
/// tokens are not guaranteed otherwise — the LAST uuid segment must win.
const TOKEN: &str = "11111111-2222-4333-8444-555555555555";

fn none() -> Vec<String> {
Vec::new()
}

#[test]
fn artifact_and_registry_shapes_yield_the_patch_uuid_not_the_token() {
for url in [
format!("https://patch.socket.dev/patch/npm/left-pad/1.3.0/{TOKEN}/{UUID}/left-pad-1.3.0.tgz"),
format!("https://patch.socket.dev/patch/npm/{TOKEN}/{UUID}/left-pad-1.3.0.tgz"),
format!("https://patch.socket.dev/patch-registry/gem/{TOKEN}/{UUID}/"),
format!("https://patch.socket.dev/patch-registry/gem/{TOKEN}/{UUID}/gems/rack-2.2.3.gem"),
format!("https://patch.socket.dev/patch-registry/maven/{TOKEN}/{UUID}/maven2"),
format!("https://patch.socket.dev/patch-registry/nuget/{TOKEN}/{UUID}/index.json"),
format!("sparse+https://patch.socket.dev/patch-registry/cargo/{TOKEN}/{UUID}/index/"),
format!("registry+https://patch.socket.dev/patch-registry/cargo/{TOKEN}/{UUID}/index/"),
] {
assert_eq!(
hosted_patch_uuid(&url, &none()).as_deref(),
Some(UUID),
"{url}"
);
}
}

#[test]
fn lock_format_spellings_are_normalized() {
let url = format!("https://patch.socket.dev/patch/npm/{TOKEN}/{UUID}/left-pad-1.3.0.tgz");
// yarn classic `#<sha1>`, pip/hatch `#sha256=`, a stray query.
for spelled in [
format!("{url}#0123456789abcdef0123456789abcdef01234567"),
format!("{url}#sha256=abc"),
format!("{url}?x=1"),
// composer's `\/`-escaped slashes.
url.replace('/', "\\/"),
// yarn berry's percent-encoded `__archiveUrl=` binding value.
url.replace(':', "%3A").replace('/', "%2F"),
format!(" {url} "),
] {
assert_eq!(
hosted_patch_uuid(&spelled, &none()).as_deref(),
Some(UUID),
"{spelled}"
);
}
}

#[test]
fn foreign_hosts_credentials_and_plain_http_are_refused() {
for url in [
format!("https://registry.npmjs.org/{TOKEN}/{UUID}/x.tgz"),
format!("https://patch.socket.dev.evil.example/{TOKEN}/{UUID}/x.tgz"),
format!("https://evil.example/patch.socket.dev/{TOKEN}/{UUID}/x.tgz"),
format!("http://patch.socket.dev/patch/npm/{TOKEN}/{UUID}/x.tgz"),
format!("https://patch.socket.dev:8443/patch/npm/{TOKEN}/{UUID}/x.tgz"),
format!("https://user:pw@patch.socket.dev/patch/npm/{TOKEN}/{UUID}/x.tgz"),
format!("git+ssh://patch.socket.dev/{UUID}"),
format!("file:.socket/vendor/npm/{UUID}/x.tgz"),
format!("patch.socket.dev/gopatch/{UUID}"),
] {
assert_eq!(hosted_patch_uuid(&url, &none()), None, "{url}");
}
}

#[test]
fn non_canonical_segments_are_not_patch_uuids() {
for url in [
// Placeholder tokens (fixtures use `uuid`, `tok`, `some-uuid`).
"https://patch.socket.dev/patch/npm/tok/uuid/x.tgz".to_string(),
// Uppercase is not the canonical grammar.
format!(
"https://patch.socket.dev/patch/npm/tok/{}/x.tgz",
UUID.to_ascii_uppercase()
),
// A uuid only in the query / fragment is not a path level.
format!("https://patch.socket.dev/patch/npm/x.tgz?u={UUID}"),
format!("https://patch.socket.dev/patch/npm/x.tgz#{UUID}"),
// An encoded `/` cannot split a segment into a uuid.
format!("https://patch.socket.dev/patch/npm/tok%2F{UUID}/x.tgz"),
] {
assert_eq!(hosted_patch_uuid(&url, &none()), None, "{url}");
}
}

#[test]
fn configured_patch_server_origin_is_accepted_exactly() {
let origins = vec!["http://127.0.0.1:4545/some/base".to_string()];
let url = format!("http://127.0.0.1:4545/patch/npm/{TOKEN}/{UUID}/x.tgz");
assert_eq!(hosted_patch_uuid(&url, &origins).as_deref(), Some(UUID));
// Same host, other port / scheme: a different origin.
for other in [
format!("http://127.0.0.1:4546/patch/npm/{TOKEN}/{UUID}/x.tgz"),
format!("https://127.0.0.1:4545/patch/npm/{TOKEN}/{UUID}/x.tgz"),
] {
assert_eq!(hosted_patch_uuid(&other, &origins), None, "{other}");
}
// The default host stays accepted alongside the override, and a
// malformed override is ignored rather than widening the allowlist.
let default = format!("https://patch.socket.dev/patch/npm/{TOKEN}/{UUID}/x.tgz");
assert_eq!(hosted_patch_uuid(&default, &origins).as_deref(), Some(UUID));
assert_eq!(hosted_patch_uuid(&url, &["not a url".to_string()]), None);
}

/// The Go hosted namespace lives on the same host the URL allowlist
/// pins — the two spellings of "Socket's patch server" cannot drift.
#[test]
fn go_module_namespace_is_on_the_patch_server_host() {
assert!(crate::vendor::go_mod_edit::HOSTED_GO_MODULE_PREFIX
.starts_with(&format!("{SOCKET_PATCH_SERVER_HOST}/")));
}
}

/// #742 / #650: a superseding patch uuid for a package an earlier hosted
/// scan already wired re-pins socket-patch's own source, end to end through
/// the redirect planner, instead of warning "revert it" and leaving the old
Expand Down
9 changes: 6 additions & 3 deletions crates/socket-patch-core/src/patch/redirect/upstream/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,8 +476,11 @@ pub fn foreign_dep_origins<'a>(
dep_origins(deps)
.into_iter()
.filter(|origin| {
let known = std::iter::once(format!("https://{}", super::SOCKET_PATCH_SERVER_HOST))
.chain(configured.iter().cloned());
let known = std::iter::once(format!(
"https://{}",
crate::origin::SOCKET_PATCH_SERVER_HOST
))
.chain(configured.iter().cloned());
!known
.into_iter()
.any(|k| dep_origins_of_url(&k).as_deref() == Some(origin.as_str()))
Expand Down Expand Up @@ -768,7 +771,7 @@ impl Ctx<'_> {
/// The hosted patch uuid `url` names, under the same host allowlist
/// discovery applies.
pub(crate) fn hosted_uuid(&self, url: &str) -> Option<String> {
super::hosted_patch_uuid(url, self.origins)
crate::origin::hosted_patch_uuid(url, self.origins)
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/socket-patch-core/src/patch/redirect/vlt_heal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ use std::path::{Path, PathBuf};

use serde_json::{Map, Value};

use super::hosted_patch_uuid;
use crate::constants::npm_family::{VLT_HIDDEN_LOCK_REL, VLT_STORE_DIR};
use crate::manifest::schema::PatchRecord;
use crate::origin::hosted_patch_uuid;
use crate::patch::apply::{is_safe_relative_subpath, normalize_file_path};
use crate::patch::file_hash::compute_file_git_sha256;
use crate::patch::package::read_archive_bytes_to_map_strict;
Expand Down
5 changes: 4 additions & 1 deletion crates/socket-patch-core/src/vendor/lock_inventory/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,14 +402,17 @@ mod architecture_tests {
const PURE_READER: &str = "// ── pure reader ──";
/// Anything that reads content, does I/O, or applies the hosted-origin
/// policy.
const IMPURE: [&str; 9] = [
const IMPURE: [&str; 12] = [
"tokio::fs",
"std::fs",
"read_regular_",
"File::open",
"OpenOptions",
"hosted_patch_uuid",
"hosted_patch_url_uuids",
"hosted_uuid(",
"OriginPolicy",
"crate::origin::",
"async fn",
".await",
];
Expand Down
4 changes: 2 additions & 2 deletions crates/socket-patch-core/src/vendor/lock_inventory/pypi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,12 @@ pub(crate) fn hosted_artifact_url(url: &str) -> Result<HostedArtifactUrl, String
/// Pipenv rotation (`redirect::pipenv`) and the vendored Pipenv guard
/// (`vendor::pypi_pipenv`) share. Ours means both: served from an accepted
/// origin (patch.socket.dev or one of `origins`, with no userinfo —
/// [`crate::patch::redirect::hosted_patch_url_uuids`]'s origin policy), and
/// [`crate::origin::hosted_patch_url_uuids`]'s origin policy), and
/// a [`hosted_artifact_url`] whose `…/patch/pypi/<name>/<version>/<grant>/
/// <uuid>/<artifact>` tail is matched from the END, so a path-prefixed
/// `--patch-server-url` deployment and a hosted sdist are recognized too.
pub(crate) fn hosted_pypi_reference(url: &str, origins: &[String]) -> Option<HostedArtifactUrl> {
crate::patch::redirect::hosted_patch_url_uuids(url, origins)?;
crate::origin::hosted_patch_url_uuids(url, origins)?;
hosted_artifact_url(url).ok().filter(|coords| {
coords
.uuid_level
Expand Down
2 changes: 1 addition & 1 deletion crates/socket-patch-core/src/vendor/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ fn split_nuget_leaf(stem: &str) -> Option<(&str, &str)> {
///
/// The leaf must be CLEAN (no lock-format suffix such as yarn's `#<sha1>` or
/// berry's `::locator=…`) and pypi dist names come back un-canonicalized;
/// `vex::discover::vendored_leaf_purl` strips the former and canonicalizes
/// `origin::vendored_leaf_purl` strips the former and canonicalizes
/// the latter for lockfile-discovered references.
pub(crate) fn leaf_to_purl(eco: &str, leaf: &str) -> Option<String> {
match eco {
Expand Down
Loading
Loading