Skip to content

Commit 331c478

Browse files
committed
Refuse agent patches through first-party links
Agent-mode apply and rollback wrote through any node_modules entry, including a link to an npm/yarn/pnpm/bun workspace member, a file: or link: directory dependency, or an npm link target. When that local package shared a patched package's name and version, the default mismatch policy replaced the user's own source with upstream bytes, and rollback then wrote the upstream original over it. A node_modules entry whose real path is outside every node_modules tree is now refused (dry run included), the same way a link into a store shared with other projects already is. Store links (.pnpm, .store, .vlt, .bun) and real directories are patched as before. Fixes #626 Assisted-by: Claude Code:claude-opus-5-5
1 parent b0497f6 commit 331c478

134 files changed

Lines changed: 2522 additions & 847 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎crates/socket-patch-cli/src/commands/apply.rs‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ use clap::Args;
22
use socket_patch_core::api::blob_fetcher::get_missing_blobs;
33
use socket_patch_core::api::client::{get_api_client_with_overrides, ApiClient};
44
use socket_patch_core::crawlers::ruby_crawler::config_path_ignored_warning;
5-
use socket_patch_core::crawlers::{
6-
detect_npm_pkg_manager, Ecosystem, NpmPkgManager, RubyCrawler,
7-
};
5+
use socket_patch_core::crawlers::{detect_npm_pkg_manager, Ecosystem, NpmPkgManager, RubyCrawler};
86
use socket_patch_core::manifest::operations::read_manifest;
97
use socket_patch_core::manifest::schema::{PatchFileInfo, PatchManifest, PatchRecord};
108
use socket_patch_core::patch::apply::{

‎crates/socket-patch-cli/src/commands/list.rs‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,10 @@ pub async fn run(args: ListArgs) -> i32 {
431431
detail: detail.clone(),
432432
});
433433
} else if !args.common.silent {
434-
eprintln!("Warning: {}", crate::commands::rollback::capitalize_first(detail));
434+
eprintln!(
435+
"Warning: {}",
436+
crate::commands::rollback::capitalize_first(detail)
437+
);
435438
}
436439
}
437440
let vendor_state = crate::commands::vendor_state_lenient(&loaded.vendor, args.common.silent);
@@ -773,12 +776,18 @@ mod tests {
773776
let listings = HostedListing::from_pins(
774777
&[
775778
pin("pkg:npm/minimist@1.2.2", &record.uuid),
776-
pin("pkg:npm/other@1.0.0", "33333333-3333-4333-8333-333333333333"),
779+
pin(
780+
"pkg:npm/other@1.0.0",
781+
"33333333-3333-4333-8333-333333333333",
782+
),
777783
],
778784
Some(&legacy),
779785
);
780786
assert_eq!(listings[0].record, record);
781-
assert_eq!(listings[1].record.uuid, "33333333-3333-4333-8333-333333333333");
787+
assert_eq!(
788+
listings[1].record.uuid,
789+
"33333333-3333-4333-8333-333333333333"
790+
);
782791
assert!(listings[1].record.vulnerabilities.is_empty());
783792
assert_eq!(listings[1].lockfiles, vec!["yarn.lock".to_string()]);
784793
}

‎crates/socket-patch-cli/src/commands/mod.rs‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
pub mod apply;
22
pub(crate) mod bun_preflight;
3-
pub(crate) mod context;
43
pub(crate) mod composer_hints;
4+
pub(crate) mod context;
55
pub(crate) mod fetch_stage;
66
pub mod get;
77
pub mod hosted_bundle;
88
pub mod list;
99
pub(crate) mod lock_cli;
1010
pub mod remove;
1111
pub mod repair;
12-
pub(crate) mod vendored_backend;
1312
pub mod rollback;
1413
pub mod scan;
1514
pub mod update;
1615
pub mod vendor;
16+
pub(crate) mod vendored_backend;
1717
pub mod vex;
1818
pub(crate) mod vex_consumed;
1919
pub(crate) mod vex_sources;
@@ -141,9 +141,11 @@ pub(crate) async fn hosted_state_from_lockfiles(
141141
common: &crate::args::GlobalArgs,
142142
root: &Path,
143143
) -> socket_patch_core::patch::redirect::RedirectState {
144-
hosted_state_from_pins(&socket_patch_core::patch::redirect::upstream::HostedPin::all(
145-
&discover_wiring(common, root).await,
146-
))
144+
hosted_state_from_pins(
145+
&socket_patch_core::patch::redirect::upstream::HostedPin::all(
146+
&discover_wiring(common, root).await,
147+
),
148+
)
147149
}
148150

149151
/// [`hosted_state_from_lockfiles`] over already-discovered pins. A purl
@@ -153,18 +155,17 @@ pub(crate) fn hosted_state_from_pins(
153155
) -> socket_patch_core::patch::redirect::RedirectState {
154156
let mut state = socket_patch_core::patch::redirect::RedirectState::new();
155157
for pin in pins {
156-
state
157-
.records
158-
.entry(pin.purl.clone())
159-
.or_insert_with(|| socket_patch_core::manifest::schema::PatchRecord {
158+
state.records.entry(pin.purl.clone()).or_insert_with(|| {
159+
socket_patch_core::manifest::schema::PatchRecord {
160160
uuid: pin.uuid.clone(),
161161
exported_at: String::new(),
162162
files: Default::default(),
163163
vulnerabilities: Default::default(),
164164
description: String::new(),
165165
license: String::new(),
166166
tier: String::new(),
167-
});
167+
}
168+
});
168169
}
169170
state
170171
}
@@ -191,4 +192,3 @@ pub(crate) fn vendor_state_lenient(
191192
}
192193
}
193194
}
194-

‎crates/socket-patch-cli/src/commands/remove.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ use super::rollback::{
1717
pin_before_hash_blobs, rollback_patches_inner, run_hosted_leg, sweep_failure,
1818
sweep_unused_artifacts, HostedLegOutcome, InnerSelection,
1919
};
20-
use crate::commands::vendored_backend::{RevertedEntry, VendorRevertStep, VendoredBackend};
2120
use crate::args::{apply_env_toggles, GlobalArgs};
2221
use crate::commands::lock_cli::acquire_or_emit;
22+
use crate::commands::vendored_backend::{RevertedEntry, VendorRevertStep, VendoredBackend};
2323
use crate::json_envelope::{Command, Envelope, EnvelopeError, PatchAction, PatchEvent, Status};
2424
use crate::ui::plural;
2525

‎crates/socket-patch-cli/src/commands/rollback.rs‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ use socket_patch_core::manifest::operations::{
1010
};
1111
use socket_patch_core::manifest::schema::{PatchFileInfo, PatchManifest, PatchRecord};
1212
use socket_patch_core::patch::apply::select_installed_variants;
13+
use socket_patch_core::patch::redirect::upstream::HostedPin;
1314
use socket_patch_core::patch::rollback::{
1415
cannot_rollback_error, rollback_package_patch, verify_file_rollback, RollbackResult,
1516
VerifyRollbackResult, VerifyRollbackStatus,
1617
};
1718
use socket_patch_core::telemetry::{track_patch_rollback_failed, track_patch_rolled_back};
1819
use socket_patch_core::utils::purl::{patch_matches, strip_purl_qualifiers};
19-
use socket_patch_core::patch::redirect::upstream::HostedPin;
2020
use socket_patch_core::vendor::{purl_keys_cover, RevertOpts, VendorState};
2121
use std::collections::{HashMap, HashSet};
2222
use std::path::{Path, PathBuf};
@@ -1026,7 +1026,8 @@ pub(crate) async fn run_hosted_leg(common: &GlobalArgs, pins: &[HostedPin]) -> H
10261026
.iter()
10271027
.map(|(code, detail)| (code.to_string(), detail.clone())),
10281028
);
1029-
out.edited_files.extend(outcome.reverted_files.iter().cloned());
1029+
out.edited_files
1030+
.extend(outcome.reverted_files.iter().cloned());
10301031
let unwound: Vec<_> = vlt_targets
10311032
.into_iter()
10321033
.filter(|t| out.reverted.iter().any(|p| p == &t.purl))
@@ -1170,7 +1171,11 @@ pub async fn run(args: RollbackArgs) -> i32 {
11701171
} else if !args.common.silent {
11711172
println!(
11721173
"{} the pre-v5 hosted ledger {}: no lockfile pins a hosted patch.",
1173-
if args.common.dry_run { "Would remove" } else { "Removed" },
1174+
if args.common.dry_run {
1175+
"Would remove"
1176+
} else {
1177+
"Removed"
1178+
},
11741179
socket_patch_core::patch::redirect::REDIRECT_STATE_REL
11751180
);
11761181
}

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

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -168,29 +168,32 @@ pub(crate) async fn vendored_ledger_supplement(
168168
}
169169
// `(ledger key, base purl, entry)`; the artifact fallback has no
170170
// entries to probe, so it never reports unwired keys.
171-
let candidates: Vec<(String, String, Option<&socket_patch_core::vendor::VendorEntry>)> =
172-
match state {
173-
Ok(state) => state
174-
.entries
175-
.iter()
176-
.map(|(key, entry)| {
177-
(
178-
key.clone(),
179-
strip_purl_qualifiers(&entry.base_purl).to_string(),
180-
Some(entry),
181-
)
182-
})
183-
.collect(),
184-
// Corrupt/unreadable ledger (a MISSING file is Ok(empty) above):
185-
// recover the vendored set from the committed artifacts, or
186-
// `scan --prune` (whose ledger exemption also degrades to empty)
187-
// would delete still-vendored packages' manifest entries and blobs.
188-
Err(_) => vendored_purls_from_artifacts(common)
189-
.await
190-
.into_iter()
191-
.map(|base| (base.clone(), base, None))
192-
.collect(),
193-
};
171+
let candidates: Vec<(
172+
String,
173+
String,
174+
Option<&socket_patch_core::vendor::VendorEntry>,
175+
)> = match state {
176+
Ok(state) => state
177+
.entries
178+
.iter()
179+
.map(|(key, entry)| {
180+
(
181+
key.clone(),
182+
strip_purl_qualifiers(&entry.base_purl).to_string(),
183+
Some(entry),
184+
)
185+
})
186+
.collect(),
187+
// Corrupt/unreadable ledger (a MISSING file is Ok(empty) above):
188+
// recover the vendored set from the committed artifacts, or
189+
// `scan --prune` (whose ledger exemption also degrades to empty)
190+
// would delete still-vendored packages' manifest entries and blobs.
191+
Err(_) => vendored_purls_from_artifacts(common)
192+
.await
193+
.into_iter()
194+
.map(|base| (base.clone(), base, None))
195+
.collect(),
196+
};
194197
// Composer by release identity: a ledger `@3.0.2.0` is the crawled
195198
// `@3.0.2`, not a second package to supplement.
196199
let key = |p: &str| composer_purl_identity(p).unwrap_or_else(|| normalize_purl(p).into_owned());
@@ -1038,7 +1041,9 @@ mod tests {
10381041
..GlobalArgs::default()
10391042
};
10401043
let state = socket_patch_core::vendor::load_state(root).await;
1041-
vendored_ledger_supplement(&args, crawled, &state).await.packages
1044+
vendored_ledger_supplement(&args, crawled, &state)
1045+
.await
1046+
.packages
10421047
}
10431048

10441049
/// A ledger entry vendored as `@3.0.2.0` is the crawled composer
@@ -1073,7 +1078,9 @@ mod tests {
10731078
out.iter().map(|p| &p.purl).collect::<Vec<_>>()
10741079
);
10751080

1076-
let out = vendored_ledger_supplement(&args, &[], &Ok(state)).await.packages;
1081+
let out = vendored_ledger_supplement(&args, &[], &Ok(state))
1082+
.await
1083+
.packages;
10771084
assert_eq!(
10781085
out.iter().map(|p| p.purl.as_str()).collect::<Vec<_>>(),
10791086
vec!["pkg:composer/psr/log@3.0.2.0"]
@@ -1176,7 +1183,10 @@ mod tests {
11761183
let state = npm_ledger_with_lock(tmp.path(), lock.as_deref()).await;
11771184
let out = vendored_ledger_supplement(&args, &[], &state).await;
11781185
assert_eq!(
1179-
out.packages.iter().map(|p| p.purl.as_str()).collect::<Vec<_>>(),
1186+
out.packages
1187+
.iter()
1188+
.map(|p| p.purl.as_str())
1189+
.collect::<Vec<_>>(),
11801190
vec!["pkg:npm/left-pad@1.3.0"],
11811191
"lock={lock:?}"
11821192
);

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

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -932,7 +932,8 @@ pub(crate) async fn run_redirect_selected(
932932
socket_patch_core::utils::fs::read_regular_to_string_sync(path).ok()
933933
})
934934
};
935-
let rewrite_options = || RewriteOptions {
935+
let rewrite_options = || {
936+
RewriteOptions {
936937
dry_run: common.dry_run,
937938
targets_pipenv_lock,
938939
pipenv_major,
@@ -944,6 +945,7 @@ pub(crate) async fn run_redirect_selected(
944945
npm_allow_remote_config: !common.no_npm_allow_remote_config,
945946
npm_outer: &npm_outer,
946947
blocking: true,
948+
}
947949
};
948950
// The rollout gate plans again without its deferred rows: keep what
949951
// the second pass needs.
@@ -2304,13 +2306,19 @@ fn join_names(names: &[String], max: usize) -> String {
23042306
/// artifacts, then verify with `vex`. After a vendored→hosted takeover
23052307
/// (`vendored_removed`) the commit also has to carry the deleted vendored
23062308
/// ledger entries and artifacts.
2307-
fn format_next_steps(files: &[String], edits: &[socket_patch_core::patch::redirect::FileEdit], vendored_removed: bool) -> Vec<String> {
2309+
fn format_next_steps(
2310+
files: &[String],
2311+
edits: &[socket_patch_core::patch::redirect::FileEdit],
2312+
vendored_removed: bool,
2313+
) -> Vec<String> {
23082314
if files.is_empty() && !vendored_removed {
23092315
return Vec::new();
23102316
}
23112317
let mut commit: Vec<String> = Vec::new();
23122318
if vendored_removed {
2313-
commit.push(".socket/vendor/ (the removed vendored ledger entries and artifacts)".to_string());
2319+
commit.push(
2320+
".socket/vendor/ (the removed vendored ledger entries and artifacts)".to_string(),
2321+
);
23142322
}
23152323
commit.extend(files.iter().cloned());
23162324
let npm = files
@@ -4391,19 +4399,43 @@ mod tests {
43914399
use super::npm_allow_remote_one_line;
43924400
let hosts = ["patch.socket.dev"];
43934401
let cases = [
4394-
(npm_allow_remote_configured_detail(&hosts, true, false), "Note: set"),
4395-
(npm_allow_remote_configured_detail(&hosts, false, false), "Note: set"),
4396-
(npm_allow_remote_configured_detail(&hosts, true, true), "Note: would set"),
4397-
(npm_allow_remote_already_detail(&hosts), "Note: .npmrc already"),
4398-
(npm_allow_remote_user_set_detail(&hosts, "none"), "Warning: npm >=12"),
4399-
(npm_allow_remote_env_set_detail(&hosts, "npm_config_allow_remote", "none"), "Warning: npm >=12"),
4402+
(
4403+
npm_allow_remote_configured_detail(&hosts, true, false),
4404+
"Note: set",
4405+
),
4406+
(
4407+
npm_allow_remote_configured_detail(&hosts, false, false),
4408+
"Note: set",
4409+
),
4410+
(
4411+
npm_allow_remote_configured_detail(&hosts, true, true),
4412+
"Note: would set",
4413+
),
4414+
(
4415+
npm_allow_remote_already_detail(&hosts),
4416+
"Note: .npmrc already",
4417+
),
4418+
(
4419+
npm_allow_remote_user_set_detail(&hosts, "none"),
4420+
"Warning: npm >=12",
4421+
),
4422+
(
4423+
npm_allow_remote_env_set_detail(&hosts, "npm_config_allow_remote", "none"),
4424+
"Warning: npm >=12",
4425+
),
44004426
(npm_allow_remote_manual_detail(&hosts), "Warning: npm >=12"),
4401-
(npm_allow_remote_unreadable_detail(&hosts, "is a symlink"), "Warning: npm >=12"),
4427+
(
4428+
npm_allow_remote_unreadable_detail(&hosts, "is a symlink"),
4429+
"Warning: npm >=12",
4430+
),
44024431
];
44034432
for (detail, start) in cases {
44044433
let line = npm_allow_remote_one_line(&detail);
44054434
assert!(line.starts_with(start), "{line}");
4406-
assert!(!line.contains('\n') && line.ends_with("(details: --verbose)."), "{line}");
4435+
assert!(
4436+
!line.contains('\n') && line.ends_with("(details: --verbose)."),
4437+
"{line}"
4438+
);
44074439
}
44084440
}
44094441
}

0 commit comments

Comments
 (0)