Skip to content

Commit 6e18576

Browse files
committed
expand ecosystem support
1 parent b32cbba commit 6e18576

File tree

12 files changed

+423
-28
lines changed

12 files changed

+423
-28
lines changed

.github/workflows/ci.yml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ jobs:
3535
restore-keys: ${{ matrix.os }}-cargo-
3636

3737
- name: Run clippy
38-
run: cargo clippy --workspace -- -D warnings
38+
run: cargo clippy --workspace --all-features -- -D warnings
3939

4040
- name: Run tests
41-
run: cargo test --workspace
41+
run: cargo test --workspace --all-features
4242

4343
dispatch-tests:
4444
runs-on: ubuntu-latest
@@ -72,6 +72,18 @@ jobs:
7272
suite: e2e_npm
7373
- os: ubuntu-latest
7474
suite: e2e_pypi
75+
- os: ubuntu-latest
76+
suite: e2e_cargo
77+
- os: ubuntu-latest
78+
suite: e2e_golang
79+
- os: ubuntu-latest
80+
suite: e2e_maven
81+
- os: ubuntu-latest
82+
suite: e2e_gem
83+
- os: ubuntu-latest
84+
suite: e2e_composer
85+
- os: ubuntu-latest
86+
suite: e2e_nuget
7587
- os: macos-latest
7688
suite: e2e_npm
7789
- os: macos-latest
@@ -109,4 +121,4 @@ jobs:
109121
python-version: "3.12"
110122

111123
- name: Run e2e tests
112-
run: cargo test -p socket-patch-cli --test ${{ matrix.suite }} -- --ignored
124+
run: cargo test -p socket-patch-cli --all-features --test ${{ matrix.suite }} -- --ignored

README.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Socket Patch CLI
22

3-
Apply security patches to npm, Python, and Rust dependencies without waiting for upstream fixes.
3+
Apply security patches to npm and Python dependencies without waiting for upstream fixes.
44

55
## Installation
66

@@ -63,6 +63,12 @@ pip install socket-patch
6363
cargo install socket-patch-cli
6464
```
6565

66+
By default this builds with npm and PyPI support. For additional ecosystems:
67+
68+
```bash
69+
cargo install socket-patch-cli --features cargo,golang,maven,gem,composer,nuget
70+
```
71+
6672
## Quick Start
6773

6874
You can pass a patch UUID directly to `socket-patch` as a shortcut:
@@ -143,7 +149,7 @@ socket-patch scan [options]
143149
|------|-------------|
144150
| `--org <slug>` | Organization slug |
145151
| `--json` | Output results as JSON |
146-
| `--ecosystems <list>` | Restrict to specific ecosystems (comma-separated: `npm,pypi,cargo`) |
152+
| `--ecosystems <list>` | Restrict to specific ecosystems (comma-separated, e.g. `npm,pypi`) |
147153
| `-g, --global` | Scan globally installed packages |
148154
| `--global-prefix <path>` | Custom path to global `node_modules` |
149155
| `--batch-size <n>` | Packages per API request (default: `100`) |
@@ -425,7 +431,7 @@ When stdin is not a TTY (e.g., in CI pipelines), interactive prompts auto-procee
425431

426432
## Manifest Format
427433

428-
Downloaded patches (for npm, Python, and Rust packages) are stored in `.socket/manifest.json`:
434+
Downloaded patches are stored in `.socket/manifest.json`:
429435

430436
```json
431437
{
@@ -462,5 +468,6 @@ Patched file contents are in `.socket/blob/` (named by git SHA256 hash).
462468
| Platform | Architecture |
463469
|----------|-------------|
464470
| macOS | ARM64 (Apple Silicon), x86_64 (Intel) |
465-
| Linux | x86_64, ARM64 |
466-
| Windows | x86_64 |
471+
| Linux | x86_64, ARM64, ARMv7, i686 |
472+
| Windows | x86_64, ARM64, i686 |
473+
| Android | ARM64 |
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
#![cfg(feature = "composer")]
2+
//! End-to-end tests for the Composer/PHP package patching lifecycle.
3+
//!
4+
//! These tests exercise crawling against a temporary directory with a fake
5+
//! Composer vendor layout. They do **not** require network access or a real
6+
//! PHP/Composer installation.
7+
//!
8+
//! # Running
9+
//! ```sh
10+
//! cargo test -p socket-patch-cli --features composer --test e2e_composer
11+
//! ```
12+
13+
use std::path::PathBuf;
14+
use std::process::{Command, Output};
15+
16+
// ---------------------------------------------------------------------------
17+
// Helpers
18+
// ---------------------------------------------------------------------------
19+
20+
fn binary() -> PathBuf {
21+
env!("CARGO_BIN_EXE_socket-patch").into()
22+
}
23+
24+
fn run(args: &[&str], cwd: &std::path::Path) -> Output {
25+
Command::new(binary())
26+
.args(args)
27+
.current_dir(cwd)
28+
.output()
29+
.expect("Failed to run socket-patch binary")
30+
}
31+
32+
// ---------------------------------------------------------------------------
33+
// Tests
34+
// ---------------------------------------------------------------------------
35+
36+
/// Verify that `socket-patch scan` discovers packages via Composer 2 installed.json.
37+
#[test]
38+
fn scan_discovers_composer2_packages() {
39+
let dir = tempfile::tempdir().unwrap();
40+
let project_dir = dir.path().join("project");
41+
std::fs::create_dir_all(&project_dir).unwrap();
42+
43+
// Create composer.json so local mode activates
44+
std::fs::write(
45+
project_dir.join("composer.json"),
46+
r#"{"require": {"monolog/monolog": "^3.0"}}"#,
47+
)
48+
.unwrap();
49+
50+
// Set up vendor directory with installed.json (Composer 2 format)
51+
let vendor_dir = project_dir.join("vendor");
52+
let composer_dir = vendor_dir.join("composer");
53+
std::fs::create_dir_all(&composer_dir).unwrap();
54+
55+
// Create Composer 2 installed.json with packages array
56+
std::fs::write(
57+
composer_dir.join("installed.json"),
58+
r#"{"packages": [
59+
{"name": "monolog/monolog", "version": "3.5.0"},
60+
{"name": "symfony/console", "version": "6.4.1"}
61+
]}"#,
62+
)
63+
.unwrap();
64+
65+
// Create the actual vendor directories for the packages
66+
std::fs::create_dir_all(vendor_dir.join("monolog").join("monolog")).unwrap();
67+
std::fs::create_dir_all(vendor_dir.join("symfony").join("console")).unwrap();
68+
69+
let output = run(
70+
&["scan", "--cwd", project_dir.to_str().unwrap()],
71+
&project_dir,
72+
);
73+
let stderr = String::from_utf8_lossy(&output.stderr);
74+
let stdout = String::from_utf8_lossy(&output.stdout);
75+
let combined = format!("{stdout}{stderr}");
76+
77+
assert!(
78+
combined.contains("Found") || combined.contains("packages"),
79+
"Expected scan to discover Composer packages, got:\n{combined}"
80+
);
81+
}
82+
83+
/// Verify that `socket-patch scan` discovers packages via Composer 1 installed.json (flat array).
84+
#[test]
85+
fn scan_discovers_composer1_packages() {
86+
let dir = tempfile::tempdir().unwrap();
87+
let project_dir = dir.path().join("project");
88+
std::fs::create_dir_all(&project_dir).unwrap();
89+
90+
// Create composer.lock so local mode activates
91+
std::fs::write(
92+
project_dir.join("composer.lock"),
93+
r#"{"packages": []}"#,
94+
)
95+
.unwrap();
96+
97+
// Set up vendor directory with Composer 1 installed.json (flat array)
98+
let vendor_dir = project_dir.join("vendor");
99+
let composer_dir = vendor_dir.join("composer");
100+
std::fs::create_dir_all(&composer_dir).unwrap();
101+
102+
std::fs::write(
103+
composer_dir.join("installed.json"),
104+
r#"[
105+
{"name": "guzzlehttp/guzzle", "version": "7.8.1"}
106+
]"#,
107+
)
108+
.unwrap();
109+
110+
// Create the actual vendor directory for the package
111+
std::fs::create_dir_all(vendor_dir.join("guzzlehttp").join("guzzle")).unwrap();
112+
113+
let output = run(
114+
&["scan", "--json", "--cwd", project_dir.to_str().unwrap()],
115+
&project_dir,
116+
);
117+
let stdout = String::from_utf8_lossy(&output.stdout);
118+
let stderr = String::from_utf8_lossy(&output.stderr);
119+
let combined = format!("{stdout}{stderr}");
120+
121+
assert!(
122+
combined.contains("scannedPackages") || combined.contains("Found"),
123+
"Expected scan output, got:\n{combined}"
124+
);
125+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#![cfg(feature = "gem")]
2+
//! End-to-end tests for the RubyGems package patching lifecycle.
3+
//!
4+
//! These tests exercise crawling against a temporary directory with fake
5+
//! gem layouts. They do **not** require network access or a real Ruby
6+
//! installation.
7+
//!
8+
//! # Running
9+
//! ```sh
10+
//! cargo test -p socket-patch-cli --features gem --test e2e_gem
11+
//! ```
12+
13+
use std::path::PathBuf;
14+
use std::process::{Command, Output};
15+
16+
// ---------------------------------------------------------------------------
17+
// Helpers
18+
// ---------------------------------------------------------------------------
19+
20+
fn binary() -> PathBuf {
21+
env!("CARGO_BIN_EXE_socket-patch").into()
22+
}
23+
24+
fn run(args: &[&str], cwd: &std::path::Path) -> Output {
25+
Command::new(binary())
26+
.args(args)
27+
.current_dir(cwd)
28+
.output()
29+
.expect("Failed to run socket-patch binary")
30+
}
31+
32+
// ---------------------------------------------------------------------------
33+
// Tests
34+
// ---------------------------------------------------------------------------
35+
36+
/// Verify that `socket-patch scan` discovers gems in a vendor/bundle layout.
37+
#[test]
38+
fn scan_discovers_vendored_gems() {
39+
let dir = tempfile::tempdir().unwrap();
40+
let project_dir = dir.path().join("project");
41+
std::fs::create_dir_all(&project_dir).unwrap();
42+
43+
// Create Gemfile so local mode activates
44+
std::fs::write(project_dir.join("Gemfile"), "source 'https://rubygems.org'\n").unwrap();
45+
46+
// Set up vendor/bundle/ruby/<version>/gems/ layout
47+
let gems_dir = project_dir
48+
.join("vendor")
49+
.join("bundle")
50+
.join("ruby")
51+
.join("3.2.0")
52+
.join("gems");
53+
54+
// Create rails-7.1.0 with lib/ marker
55+
let rails_dir = gems_dir.join("rails-7.1.0");
56+
std::fs::create_dir_all(rails_dir.join("lib")).unwrap();
57+
58+
// Create nokogiri-1.15.4 with lib/ marker
59+
let nokogiri_dir = gems_dir.join("nokogiri-1.15.4");
60+
std::fs::create_dir_all(nokogiri_dir.join("lib")).unwrap();
61+
62+
let output = run(
63+
&["scan", "--cwd", project_dir.to_str().unwrap()],
64+
&project_dir,
65+
);
66+
let stderr = String::from_utf8_lossy(&output.stderr);
67+
let stdout = String::from_utf8_lossy(&output.stdout);
68+
let combined = format!("{stdout}{stderr}");
69+
70+
assert!(
71+
combined.contains("Found") || combined.contains("packages"),
72+
"Expected scan to discover vendored gems, got:\n{combined}"
73+
);
74+
}
75+
76+
/// Verify that `socket-patch scan` discovers gems with gemspec markers.
77+
#[test]
78+
fn scan_discovers_gems_with_gemspec() {
79+
let dir = tempfile::tempdir().unwrap();
80+
let project_dir = dir.path().join("project");
81+
std::fs::create_dir_all(&project_dir).unwrap();
82+
83+
// Create Gemfile.lock so local mode activates
84+
std::fs::write(project_dir.join("Gemfile.lock"), "GEM\n specs:\n").unwrap();
85+
86+
// Set up vendor/bundle/ruby/<version>/gems/ layout
87+
let gems_dir = project_dir
88+
.join("vendor")
89+
.join("bundle")
90+
.join("ruby")
91+
.join("3.1.0")
92+
.join("gems");
93+
94+
// Create net-http-0.4.1 with .gemspec marker (no lib/)
95+
let net_http_dir = gems_dir.join("net-http-0.4.1");
96+
std::fs::create_dir_all(&net_http_dir).unwrap();
97+
std::fs::write(net_http_dir.join("net-http.gemspec"), "# gemspec\n").unwrap();
98+
99+
let output = run(
100+
&["scan", "--json", "--cwd", project_dir.to_str().unwrap()],
101+
&project_dir,
102+
);
103+
let stdout = String::from_utf8_lossy(&output.stdout);
104+
let stderr = String::from_utf8_lossy(&output.stderr);
105+
let combined = format!("{stdout}{stderr}");
106+
107+
assert!(
108+
combined.contains("scannedPackages") || combined.contains("Found"),
109+
"Expected scan output, got:\n{combined}"
110+
);
111+
}

0 commit comments

Comments
 (0)