Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c7d668e
feat(cli): add `scan --redirect` hosted-vendored-patch mode
mikolalysenko Jul 1, 2026
8fcd7b3
feat(vex): attest redirected patches with a (redirected) provenance m…
mikolalysenko Jul 1, 2026
7b23bc8
Merge remote-tracking branch 'origin/main' into feat/scan-redirect-mode
mikolalysenko Jul 1, 2026
b47b9d4
style: cargo fmt
mikolalysenko Jul 1, 2026
554b486
test(vex): real-install npm redirect capstone + docker gem/composer V…
mikolalysenko Jul 1, 2026
baedf28
fix(redirect,vex): fail-closed attestation, idempotent rewriters, sur…
mikolalysenko Jul 1, 2026
be23ae2
fix(ci): check the redirect golden fixtures out byte-exact on Windows
mikolalysenko Jul 1, 2026
e93f21c
chore: exclude test fixtures from Socket dependency scanning
mikolalysenko Jul 1, 2026
0c925d1
feat(cli): three-mode selector, maven hosted rewriter, nuget config f…
mikolalysenko Jul 2, 2026
a8dfb7e
feat(vendor): NuGet and Maven vendored backends with fragment-level r…
mikolalysenko Jul 2, 2026
1cfd02c
test: three-mode × ecosystem behavioral matrix
mikolalysenko Jul 2, 2026
a85f454
docs: three-mode README, CHANGELOG, CLI_CONTRACT + support matrix
mikolalysenko Jul 2, 2026
8399416
test(vendor): jsr is the unsupported-ecosystem exemplar now that nuge…
mikolalysenko Jul 2, 2026
56f3550
fix(cli): mode-conflict errors match the clap contract phrasing
mikolalysenko Jul 2, 2026
d9aa32f
docs: spell out Maven's checksum-failure fallback for hosted mode
mikolalysenko Jul 2, 2026
7ee80d7
refactor(core): factor bun text-lock grammar + share uri encode; gene…
mikolalysenko Jul 2, 2026
1db0d6d
feat(patches): Rush monorepo support — vendored refusal, scan invento…
mikolalysenko Jul 2, 2026
2716592
feat(core): yarn-berry + bun hosted registry-redirect rewriters + sha…
mikolalysenko Jul 2, 2026
20534a0
feat(cli): berry/bun redirect plumbing + bun.lockb auto-migration
mikolalysenko Jul 2, 2026
8154b18
feat(cli): scan --redirect discovers Rush pnpm locks + stale repo-sta…
mikolalysenko Jul 2, 2026
d6f736b
test(redirect,repair): pnpm hosted-lock legs + flavor repair matrix
mikolalysenko Jul 2, 2026
29bbb80
test(apply): agent-mode legs for bun, yarn-berry node-modules, rush farm
mikolalysenko Jul 2, 2026
58decbc
feat(redirect): fail-closed maven suffixing + trusted checksums
mikolalysenko Jul 2, 2026
bd317b8
test(redirect): real-install berry + bun hosted capstones
mikolalysenko Jul 2, 2026
dea45e6
test(docker): yarn berry 4.x agent + vendored e2e legs
mikolalysenko Jul 2, 2026
9063242
test(redirect): Rush hosted-mode redirect capstone (sim + gated real …
mikolalysenko Jul 2, 2026
bd76bcb
test(redirect): clippy cleanups in berry + rush redirect legs
mikolalysenko Jul 2, 2026
bf74543
docs: berry/bun + Rush hosted support, maven fail-closed, de-document…
mikolalysenko Jul 2, 2026
8f591de
style(core): satisfy clippy cloned_ref_to_slice_refs in rewriter tests
mikolalysenko Jul 2, 2026
14088c9
test(cli): fix windows + release CI failures in in-process suites
mikolalysenko Jul 2, 2026
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
Prev Previous commit
Next Next commit
style: cargo fmt
Pure rustfmt reflows (rewraps, trailing commas, brace elision) — no
logic changes. Normalizes the #116 merge and this branch's test files
to plain `cargo fmt` output.

Assisted-by: Claude Code:claude-fable-5
  • Loading branch information
mikolalysenko committed Jul 1, 2026
commit b47b9d4a7eae638068dda9e76a043afdf4e4f36a
3 changes: 1 addition & 2 deletions crates/socket-patch-cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ use clap::Args;

use socket_patch_core::api::client::ApiClientEnvOverrides;
use socket_patch_core::constants::{
DEFAULT_PATCH_API_PROXY_URL, DEFAULT_PATCH_MANIFEST_PATH,
DEFAULT_SOCKET_API_URL,
DEFAULT_PATCH_API_PROXY_URL, DEFAULT_PATCH_MANIFEST_PATH, DEFAULT_SOCKET_API_URL,
};
use socket_patch_core::crawlers::Ecosystem;
use socket_patch_core::patch::vendor::VendorSource;
Expand Down
8 changes: 3 additions & 5 deletions crates/socket-patch-cli/tests/cli_global_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,9 @@ fn global_flag_cases() -> Vec<(&'static str, Option<&'static str>, fn(&GlobalArg
("--vendor-url", Some("https://vendor.example.com"), |c| {
assert_eq!(c.vendor_url.as_deref(), Some("https://vendor.example.com"))
}),
(
"--patch-server-url",
Some("http://localhost:4026"),
|c| assert_eq!(c.patch_server_url.as_deref(), Some("http://localhost:4026")),
),
("--patch-server-url", Some("http://localhost:4026"), |c| {
assert_eq!(c.patch_server_url.as_deref(), Some("http://localhost:4026"))
}),
("--offline", None, |c| assert!(c.offline)),
("--global", None, |c| assert!(c.global)),
("--global-prefix", Some("/opt/global"), |c| {
Expand Down
11 changes: 9 additions & 2 deletions crates/socket-patch-cli/tests/e2e_vendor_cargo_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,8 +322,15 @@ fn cargo_vendor_fresh_checkout_locked_offline_build_and_revert() {
let vex_doc: serde_json::Value =
serde_json::from_slice(&std::fs::read(&vex_path).unwrap()).unwrap();
let vex_stmts = vex_doc["statements"].as_array().unwrap();
assert_eq!(vex_stmts.len(), 1, "vendored cargo patch must be attested: {vex_doc}");
assert_eq!(vex_stmts[0]["vulnerability"]["name"], "GHSA-vend-cargo-real");
assert_eq!(
vex_stmts.len(),
1,
"vendored cargo patch must be attested: {vex_doc}"
);
assert_eq!(
vex_stmts[0]["vulnerability"]["name"],
"GHSA-vend-cargo-real"
);
assert_eq!(vex_stmts[0]["status"], "not_affected");
assert_eq!(vex_stmts[0]["products"][0]["subcomponents"][0]["@id"], purl);
assert!(
Expand Down
16 changes: 13 additions & 3 deletions crates/socket-patch-cli/tests/e2e_vendor_golang_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,9 +332,19 @@ fn go_vendor_fresh_checkout_offline_build_and_revert() {
let vex_doc: serde_json::Value =
serde_json::from_slice(&std::fs::read(&vex_path).unwrap()).unwrap();
let vex_stmts = vex_doc["statements"].as_array().unwrap();
assert_eq!(vex_stmts.len(), 1, "vendored go patch must be attested: {vex_doc}");
assert_eq!(vex_stmts[0]["vulnerability"]["name"], "GHSA-vend-golang-real");
assert_eq!(vex_stmts[0]["products"][0]["subcomponents"][0]["@id"], UPURL);
assert_eq!(
vex_stmts.len(),
1,
"vendored go patch must be attested: {vex_doc}"
);
assert_eq!(
vex_stmts[0]["vulnerability"]["name"],
"GHSA-vend-golang-real"
);
assert_eq!(
vex_stmts[0]["products"][0]["subcomponents"][0]["@id"],
UPURL
);
assert!(
vex_stmts[0]["impact_statement"]
.as_str()
Expand Down
19 changes: 16 additions & 3 deletions crates/socket-patch-cli/tests/e2e_vendor_npm_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -448,9 +448,18 @@ fn npm_vendor_vex_attests_against_vendored_tarball() {
// Vendor (offline: blob staged locally).
let (code, stdout, stderr) = run_socket(
&proj,
&["vendor", "--json", "--offline", "--cwd", proj.to_str().unwrap()],
&[
"vendor",
"--json",
"--offline",
"--cwd",
proj.to_str().unwrap(),
],
);
assert_eq!(
code, 0,
"vendor failed.\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert_eq!(code, 0, "vendor failed.\nstdout:\n{stdout}\nstderr:\n{stderr}");

// VEX against the vendored tarball (default verify mode).
let vex_path = proj.join("out.vex.json");
Expand All @@ -471,7 +480,11 @@ fn npm_vendor_vex_attests_against_vendored_tarball() {
let doc: serde_json::Value =
serde_json::from_slice(&std::fs::read(&vex_path).unwrap()).unwrap();
let stmts = doc["statements"].as_array().unwrap();
assert_eq!(stmts.len(), 1, "the vendored npm patch must be attested: {doc}");
assert_eq!(
stmts.len(),
1,
"the vendored npm patch must be attested: {doc}"
);
assert_eq!(stmts[0]["vulnerability"]["name"], GHSA);
assert_eq!(stmts[0]["status"], "not_affected");
assert_eq!(stmts[0]["products"][0]["subcomponents"][0]["@id"], purl);
Expand Down
11 changes: 9 additions & 2 deletions crates/socket-patch-cli/tests/e2e_vendor_pnpm_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,11 +290,18 @@ fn run_pnpm_capstone(pm: &str) {
"pkg:npm/app@1.0.0",
],
);
assert_eq!(code, 0, "vex failed ({pm}).\nstdout:\n{stdout}\nstderr:\n{stderr}");
assert_eq!(
code, 0,
"vex failed ({pm}).\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let vex_doc: serde_json::Value =
serde_json::from_slice(&std::fs::read(&vex_path).unwrap()).unwrap();
let vex_stmts = vex_doc["statements"].as_array().unwrap();
assert_eq!(vex_stmts.len(), 1, "vendored patch must be attested: {vex_doc}");
assert_eq!(
vex_stmts.len(),
1,
"vendored patch must be attested: {vex_doc}"
);
assert_eq!(vex_stmts[0]["vulnerability"]["name"], "GHSA-vend-pnpm-real");
assert_eq!(vex_stmts[0]["products"][0]["subcomponents"][0]["@id"], purl);
assert!(
Expand Down
6 changes: 5 additions & 1 deletion crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,11 @@ fn uv_vendor_fresh_checkout_frozen_offline_and_revert() {
let vex_doc: serde_json::Value =
serde_json::from_slice(&std::fs::read(&vex_path).unwrap()).unwrap();
let vex_stmts = vex_doc["statements"].as_array().unwrap();
assert_eq!(vex_stmts.len(), 1, "vendored pypi patch must be attested: {vex_doc}");
assert_eq!(
vex_stmts.len(),
1,
"vendored pypi patch must be attested: {vex_doc}"
);
assert_eq!(vex_stmts[0]["vulnerability"]["name"], "GHSA-vend-pypi-real");
assert_eq!(vex_stmts[0]["products"][0]["subcomponents"][0]["@id"], PURL);
assert!(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,11 @@ fn yarn_classic_vendor_fresh_checkout_frozen_offline_install_and_revert() {
let vex_doc: serde_json::Value =
serde_json::from_slice(&std::fs::read(&vex_path).unwrap()).unwrap();
let vex_stmts = vex_doc["statements"].as_array().unwrap();
assert_eq!(vex_stmts.len(), 1, "vendored patch must be attested: {vex_doc}");
assert_eq!(
vex_stmts.len(),
1,
"vendored patch must be attested: {vex_doc}"
);
assert_eq!(vex_stmts[0]["vulnerability"]["name"], "GHSA-vend-yarn-real");
assert_eq!(vex_stmts[0]["products"][0]["subcomponents"][0]["@id"], purl);
assert!(
Expand Down
37 changes: 29 additions & 8 deletions crates/socket-patch-cli/tests/e2e_vex_redirect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,11 @@ fn redirected_purl_attested_against_installed_tree() {
let patched = b"redirected patched index\n";
let after = compute_git_sha256_from_bytes(patched);
let purl = scaffold_npm(cwd, "left-pad", "1.3.0", patched);
write_redirect_state(cwd, &purl, make_record(UUID, &after, "GHSA-rdir-1111", &["CVE-2024-1"]));
write_redirect_state(
cwd,
&purl,
make_record(UUID, &after, "GHSA-rdir-1111", &["CVE-2024-1"]),
);
assert!(
!cwd.join(".socket/manifest.json").exists(),
"fixture sanity: a redirect project has no manifest"
Expand All @@ -146,7 +150,11 @@ fn redirected_purl_attested_against_installed_tree() {

let doc: Value = serde_json::from_slice(&out.stdout).expect("VEX JSON on stdout");
let stmts = doc["statements"].as_array().unwrap();
assert_eq!(stmts.len(), 1, "the redirected patch must be attested: {doc}");
assert_eq!(
stmts.len(),
1,
"the redirected patch must be attested: {doc}"
);
assert_eq!(stmts[0]["vulnerability"]["name"], "GHSA-rdir-1111");
assert_eq!(stmts[0]["status"], "not_affected");
assert_eq!(stmts[0]["products"][0]["subcomponents"][0]["@id"], purl);
Expand All @@ -170,7 +178,11 @@ fn redirected_purl_bypasses_property7_filter() {
let patched = b"redirected patched index\n";
let after = compute_git_sha256_from_bytes(patched);
let purl = scaffold_npm(cwd, "left-pad", "1.3.0", patched);
write_redirect_state(cwd, &purl, make_record(UUID, &after, "GHSA-rdir-keep", &["CVE-2024-2"]));
write_redirect_state(
cwd,
&purl,
make_record(UUID, &after, "GHSA-rdir-keep", &["CVE-2024-2"]),
);

// Control: a plain manifest npm patch that VERIFIES against node_modules
// but is neither redirected nor set up / manual — property 7 must drop it,
Expand Down Expand Up @@ -243,7 +255,11 @@ fn tampered_installed_file_omits_redirected_patch() {
// The installed file does NOT hash to the record's afterHash.
let after = compute_git_sha256_from_bytes(b"what the patch should contain\n");
let purl = scaffold_npm(cwd, "left-pad", "1.3.0", b"tampered installed bytes\n");
write_redirect_state(cwd, &purl, make_record(UUID, &after, "GHSA-rdir-bad", &["CVE-2024-4"]));
write_redirect_state(
cwd,
&purl,
make_record(UUID, &after, "GHSA-rdir-bad", &["CVE-2024-4"]),
);

let vex_path = cwd.join("out.vex.json");
let out = cli()
Expand Down Expand Up @@ -324,7 +340,11 @@ fn redirected_no_verify_attests_without_installed_tree() {

let doc: Value = serde_json::from_slice(&out.stdout).expect("VEX JSON on stdout");
let stmts = doc["statements"].as_array().unwrap();
assert_eq!(stmts.len(), 1, "the redirected patch must be attested: {doc}");
assert_eq!(
stmts.len(),
1,
"the redirected patch must be attested: {doc}"
);
assert_eq!(stmts[0]["vulnerability"]["name"], "GHSA-rdir-nv");
assert_eq!(
stmts[0]["impact_statement"].as_str().unwrap(),
Expand Down Expand Up @@ -362,9 +382,10 @@ fn no_verify_attests_redirected_patches_across_ecosystems() {

let mut state = RedirectState::new();
for (purl, ghsa) in cases {
state
.records
.insert(purl.to_string(), make_record(UUID, &"b".repeat(64), ghsa, &["CVE-2024-1"]));
state.records.insert(
purl.to_string(),
make_record(UUID, &"b".repeat(64), ghsa, &["CVE-2024-1"]),
);
}
let dir = cwd.join(".socket/vendor");
std::fs::create_dir_all(&dir).unwrap();
Expand Down
51 changes: 32 additions & 19 deletions crates/socket-patch-core/src/api/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,10 @@ impl ApiClient {
}

// ── Step 1: resolve the grant URL + integrity ──────────────────────
let result = match self.request_vendor_package(uuid, free_only, vendor_url).await {
let result = match self
.request_vendor_package(uuid, free_only, vendor_url)
.await
{
Ok(r) => r,
Err(e) => return VendorServiceOutcome::Failed(e),
};
Expand All @@ -743,9 +746,7 @@ impl ApiClient {
"Forbidden: not entitled to this patch (paid tier or no org access).".into(),
))
}
other => {
return VendorServiceOutcome::Unavailable(format!("unknown status `{other}`"))
}
other => return VendorServiceOutcome::Unavailable(format!("unknown status `{other}`")),
}

// Select the native tarball artifact and its sha512 (the universal
Expand Down Expand Up @@ -786,8 +787,7 @@ impl ApiClient {
if a.kind == "tarball" {
continue;
}
let (Some(url), Some(sha512)) =
(a.url.as_deref(), a.integrity.sha512.as_deref())
let (Some(url), Some(sha512)) = (a.url.as_deref(), a.integrity.sha512.as_deref())
else {
continue;
};
Expand Down Expand Up @@ -876,9 +876,10 @@ impl ApiClient {
let resp = resp.map_err(|e| ApiError::Network(format!("Network error: {e}")))?;
let status = resp.status();
if status == StatusCode::OK {
let parsed = resp.json::<PackageVendorResponse>().await.map_err(|e| {
ApiError::Parse(format!("Failed to parse package response: {e}"))
})?;
let parsed = resp
.json::<PackageVendorResponse>()
.await
.map_err(|e| ApiError::Parse(format!("Failed to parse package response: {e}")))?;
return parsed.results.get(uuid).cloned().ok_or_else(|| {
ApiError::Other(format!("package response missing a result for {uuid}"))
});
Expand Down Expand Up @@ -949,9 +950,7 @@ impl ApiClient {
pub async fn download_artifact(&self, url: &str) -> Result<Vec<u8>, ApiError> {
match self.download_vendor_archive(url).await {
ServeDownload::Ok(bytes) => Ok(bytes),
ServeDownload::NotFound => {
Err(ApiError::Other(format!("artifact not found: {url}")))
}
ServeDownload::NotFound => Err(ApiError::Other(format!("artifact not found: {url}"))),
ServeDownload::Pending => {
Err(ApiError::Other(format!("artifact still building: {url}")))
}
Expand Down Expand Up @@ -2546,7 +2545,10 @@ mod vendor_package_tests {
Mock::given(method("POST"))
.and(path("/v0/orgs/acme/patches/package"))
.and(body_partial_json(json!({ "uuids": [UUID] })))
.respond_with(ResponseTemplate::new(200).set_body_json(granted_body(&serve_url, "sha512-ABC123==")))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(granted_body(&serve_url, "sha512-ABC123==")),
)
.expect(1)
.mount(&server)
.await;
Expand Down Expand Up @@ -2579,8 +2581,12 @@ mod vendor_package_tests {
Mock::given(method("POST"))
.and(path("/patch/package"))
.and(NoAuthorizationHeader)
.and(body_partial_json(json!({ "uuids": [UUID], "freeOnly": true })))
.respond_with(ResponseTemplate::new(200).set_body_json(granted_body(&serve_url, "sha512-ZZ==")))
.and(body_partial_json(
json!({ "uuids": [UUID], "freeOnly": true }),
))
.respond_with(
ResponseTemplate::new(200).set_body_json(granted_body(&serve_url, "sha512-ZZ==")),
)
.expect(1)
.mount(&server)
.await;
Expand All @@ -2604,7 +2610,9 @@ mod vendor_package_tests {
let serve_url = format!("{}{SERVE_PATH}", server.uri());
Mock::given(method("POST"))
.and(path("/v0/orgs/acme/patches/package"))
.respond_with(ResponseTemplate::new(200).set_body_json(granted_body(&serve_url, "BAREB64==")))
.respond_with(
ResponseTemplate::new(200).set_body_json(granted_body(&serve_url, "BAREB64==")),
)
.mount(&server)
.await;
Mock::given(method("GET"))
Expand All @@ -2630,7 +2638,9 @@ mod vendor_package_tests {
let baked = format!("https://patch.socket.dev{SERVE_PATH}");
Mock::given(method("POST"))
.and(path("/v0/orgs/acme/patches/package"))
.respond_with(ResponseTemplate::new(200).set_body_json(granted_body(&baked, "sha512-AA==")))
.respond_with(
ResponseTemplate::new(200).set_body_json(granted_body(&baked, "sha512-AA==")),
)
.mount(&server)
.await;
Mock::given(method("GET"))
Expand Down Expand Up @@ -2670,7 +2680,9 @@ mod vendor_package_tests {
let serve_url = format!("{}{SERVE_PATH}", server.uri());
Mock::given(method("POST"))
.and(path("/v0/orgs/acme/patches/package"))
.respond_with(ResponseTemplate::new(200).set_body_json(granted_body(&serve_url, "sha512-AA==")))
.respond_with(
ResponseTemplate::new(200).set_body_json(granted_body(&serve_url, "sha512-AA==")),
)
.mount(&server)
.await;
Mock::given(method("GET"))
Expand Down Expand Up @@ -2721,7 +2733,8 @@ mod vendor_package_tests {
Mock::given(method("POST"))
.and(path("/v0/orgs/acme/patches/package"))
.respond_with(
ResponseTemplate::new(200).set_body_json(granted_body(&serve_url, "sha512-AA==")),
ResponseTemplate::new(200)
.set_body_json(granted_body(&serve_url, "sha512-AA==")),
)
.mount(&server)
.await;
Expand Down
Loading