Skip to content
Merged
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
4 changes: 4 additions & 0 deletions crates/socket-patch-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ uuid = { workspace = true }
regex = { workspace = true }
tempfile = { workspace = true }

[features]
default = []
cargo = ["socket-patch-core/cargo"]

[dev-dependencies]
sha2 = { workspace = true }
hex = { workspace = true }
97 changes: 20 additions & 77 deletions crates/socket-patch-cli/src/commands/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ use socket_patch_core::api::blob_fetcher::{
};
use socket_patch_core::api::client::get_api_client_from_env;
use socket_patch_core::constants::DEFAULT_PATCH_MANIFEST_PATH;
use socket_patch_core::crawlers::{CrawlerOptions, NpmCrawler, PythonCrawler};
use socket_patch_core::crawlers::{CrawlerOptions, Ecosystem};
use socket_patch_core::manifest::operations::read_manifest;
use socket_patch_core::patch::apply::{apply_package_patch, verify_file_patch, ApplyResult};
use socket_patch_core::utils::cleanup_blobs::{cleanup_unused_blobs, format_cleanup_result};
use socket_patch_core::utils::purl::{is_npm_purl, is_pypi_purl, strip_purl_qualifiers};
use socket_patch_core::utils::purl::strip_purl_qualifiers;
use socket_patch_core::utils::telemetry::{track_patch_applied, track_patch_apply_failed};
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};

use crate::ecosystem_dispatch::{find_packages_for_purls, partition_purls};

#[derive(Args)]
pub struct ApplyArgs {
/// Working directory
Expand Down Expand Up @@ -173,18 +175,8 @@ async fn apply_patches_inner(

// Partition manifest PURLs by ecosystem
let manifest_purls: Vec<String> = manifest.patches.keys().cloned().collect();
let mut npm_purls: Vec<String> = manifest_purls.iter().filter(|p| is_npm_purl(p)).cloned().collect();
let mut pypi_purls: Vec<String> = manifest_purls.iter().filter(|p| is_pypi_purl(p)).cloned().collect();

// Filter by ecosystem if specified
if let Some(ref ecosystems) = args.ecosystems {
if !ecosystems.iter().any(|e| e == "npm") {
npm_purls.clear();
}
if !ecosystems.iter().any(|e| e == "pypi") {
pypi_purls.clear();
}
}
let partitioned =
partition_purls(&manifest_purls, args.ecosystems.as_deref());

let crawler_options = CrawlerOptions {
cwd: args.cwd.clone(),
Expand All @@ -193,63 +185,12 @@ async fn apply_patches_inner(
batch_size: 100,
};

let mut all_packages: HashMap<String, PathBuf> = HashMap::new();
let all_packages =
find_packages_for_purls(&partitioned, &crawler_options, args.silent).await;

// Find npm packages
if !npm_purls.is_empty() {
let npm_crawler = NpmCrawler;
match npm_crawler.get_node_modules_paths(&crawler_options).await {
Ok(nm_paths) => {
if (args.global || args.global_prefix.is_some()) && !args.silent {
if let Some(first) = nm_paths.first() {
println!("Using global npm packages at: {}", first.display());
}
}
for nm_path in &nm_paths {
if let Ok(packages) = npm_crawler.find_by_purls(nm_path, &npm_purls).await {
for (purl, pkg) in packages {
all_packages.entry(purl).or_insert(pkg.path);
}
}
}
}
Err(e) => {
if !args.silent {
eprintln!("Failed to find npm packages: {e}");
}
}
}
}
let has_any_purls = !partitioned.is_empty();

// Find Python packages
if !pypi_purls.is_empty() {
let python_crawler = PythonCrawler;
let base_pypi_purls: Vec<String> = pypi_purls
.iter()
.map(|p| strip_purl_qualifiers(p).to_string())
.collect::<HashSet<_>>()
.into_iter()
.collect();

match python_crawler.get_site_packages_paths(&crawler_options).await {
Ok(sp_paths) => {
for sp_path in &sp_paths {
if let Ok(packages) = python_crawler.find_by_purls(sp_path, &base_pypi_purls).await {
for (purl, pkg) in packages {
all_packages.entry(purl).or_insert(pkg.path);
}
}
}
}
Err(e) => {
if !args.silent {
eprintln!("Failed to find Python packages: {e}");
}
}
}
}

if all_packages.is_empty() && npm_purls.is_empty() && pypi_purls.is_empty() {
if all_packages.is_empty() && !has_any_purls {
if !args.silent {
if args.global || args.global_prefix.is_some() {
eprintln!("No global packages found");
Expand All @@ -271,20 +212,22 @@ async fn apply_patches_inner(
let mut results: Vec<ApplyResult> = Vec::new();
let mut has_errors = false;

// Group pypi PURLs by base
// Group pypi PURLs by base (for variant matching with qualifiers)
let mut pypi_qualified_groups: HashMap<String, Vec<String>> = HashMap::new();
for purl in &pypi_purls {
let base = strip_purl_qualifiers(purl).to_string();
pypi_qualified_groups
.entry(base)
.or_default()
.push(purl.clone());
if let Some(pypi_purls) = partitioned.get(&Ecosystem::Pypi) {
for purl in pypi_purls {
let base = strip_purl_qualifiers(purl).to_string();
pypi_qualified_groups
.entry(base)
.or_default()
.push(purl.clone());
}
}

let mut applied_base_purls: HashSet<String> = HashSet::new();

for (purl, pkg_path) in &all_packages {
if is_pypi_purl(purl) {
if Ecosystem::from_purl(purl) == Some(Ecosystem::Pypi) {
let base_purl = strip_purl_qualifiers(purl).to_string();
if applied_base_purls.contains(&base_purl) {
continue;
Expand Down
17 changes: 9 additions & 8 deletions crates/socket-patch-cli/src/commands/get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use clap::Args;
use regex::Regex;
use socket_patch_core::api::client::get_api_client_from_env;
use socket_patch_core::api::types::{PatchSearchResult, SearchResponse};
use socket_patch_core::crawlers::{CrawlerOptions, NpmCrawler, PythonCrawler};
use socket_patch_core::crawlers::CrawlerOptions;
use socket_patch_core::manifest::operations::{read_manifest, write_manifest};
use socket_patch_core::manifest::schema::{
PatchFileInfo, PatchManifest, PatchRecord, VulnerabilityInfo,
Expand All @@ -13,6 +13,8 @@ use std::collections::HashMap;
use std::io::{self, Write};
use std::path::PathBuf;

use crate::ecosystem_dispatch::crawl_all_ecosystems;

#[derive(Args)]
pub struct GetArgs {
/// Patch identifier (UUID, CVE ID, GHSA ID, PURL, or package name)
Expand Down Expand Up @@ -236,18 +238,17 @@ pub async fn run(args: GetArgs) -> i32 {
global_prefix: args.global_prefix.clone(),
batch_size: 100,
};
let npm_crawler = NpmCrawler;
let python_crawler = PythonCrawler;
let npm_packages = npm_crawler.crawl_all(&crawler_options).await;
let python_packages = python_crawler.crawl_all(&crawler_options).await;
let mut all_packages = npm_packages;
all_packages.extend(python_packages);
let (all_packages, _) = crawl_all_ecosystems(&crawler_options).await;

if all_packages.is_empty() {
if args.global {
println!("No global packages found.");
} else {
println!("No packages found. Run npm/yarn/pnpm/pip install first.");
#[cfg(feature = "cargo")]
let install_cmds = "npm/yarn/pnpm/pip/cargo";
#[cfg(not(feature = "cargo"))]
let install_cmds = "npm/yarn/pnpm/pip";
println!("No packages found. Run {install_cmds} install first.");
}
return 0;
}
Expand Down
87 changes: 8 additions & 79 deletions crates/socket-patch-cli/src/commands/rollback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ use socket_patch_core::api::blob_fetcher::{
};
use socket_patch_core::api::client::get_api_client_from_env;
use socket_patch_core::constants::DEFAULT_PATCH_MANIFEST_PATH;
use socket_patch_core::crawlers::{CrawlerOptions, NpmCrawler, PythonCrawler};
use socket_patch_core::crawlers::CrawlerOptions;
use socket_patch_core::manifest::operations::read_manifest;
use socket_patch_core::manifest::schema::{PatchManifest, PatchRecord};
use socket_patch_core::patch::rollback::{rollback_package_patch, RollbackResult};
use socket_patch_core::utils::global_packages::get_global_prefix;
use socket_patch_core::utils::purl::{is_pypi_purl, strip_purl_qualifiers};
use socket_patch_core::utils::telemetry::{track_patch_rolled_back, track_patch_rollback_failed};
use std::collections::{HashMap, HashSet};
use std::collections::HashSet;
use std::path::{Path, PathBuf};

use crate::ecosystem_dispatch::{find_packages_for_rollback, partition_purls};

#[derive(Args)]
pub struct RollbackArgs {
/// Package PURL or patch UUID to rollback. Omit to rollback all patches.
Expand Down Expand Up @@ -335,17 +335,8 @@ async fn rollback_patches_inner(

// Partition PURLs by ecosystem
let rollback_purls: Vec<String> = patches_to_rollback.iter().map(|p| p.purl.clone()).collect();
let mut npm_purls: Vec<String> = rollback_purls.iter().filter(|p| !is_pypi_purl(p)).cloned().collect();
let mut pypi_purls: Vec<String> = rollback_purls.iter().filter(|p| is_pypi_purl(p)).cloned().collect();

if let Some(ref ecosystems) = args.ecosystems {
if !ecosystems.iter().any(|e| e == "npm") {
npm_purls.clear();
}
if !ecosystems.iter().any(|e| e == "pypi") {
pypi_purls.clear();
}
}
let partitioned =
partition_purls(&rollback_purls, args.ecosystems.as_deref());

let crawler_options = CrawlerOptions {
cwd: args.cwd.clone(),
Expand All @@ -354,70 +345,8 @@ async fn rollback_patches_inner(
batch_size: 100,
};

let mut all_packages: HashMap<String, PathBuf> = HashMap::new();

// Find npm packages
if !npm_purls.is_empty() {
if args.global || args.global_prefix.is_some() {
match get_global_prefix(args.global_prefix.as_ref().map(|p| p.to_str().unwrap_or(""))) {
Ok(prefix) => {
if !args.silent {
println!("Using global npm packages at: {prefix}");
}
let npm_crawler = NpmCrawler;
if let Ok(packages) = npm_crawler.find_by_purls(Path::new(&prefix), &npm_purls).await {
for (purl, pkg) in packages {
all_packages.entry(purl).or_insert(pkg.path);
}
}
}
Err(e) => {
if !args.silent {
eprintln!("Failed to find global npm packages: {e}");
}
return Ok((false, Vec::new()));
}
}
} else {
let npm_crawler = NpmCrawler;
if let Ok(nm_paths) = npm_crawler.get_node_modules_paths(&crawler_options).await {
for nm_path in &nm_paths {
if let Ok(packages) = npm_crawler.find_by_purls(nm_path, &npm_purls).await {
for (purl, pkg) in packages {
all_packages.entry(purl).or_insert(pkg.path);
}
}
}
}
}
}

// Find Python packages
if !pypi_purls.is_empty() {
let python_crawler = PythonCrawler;
let base_pypi_purls: Vec<String> = pypi_purls
.iter()
.map(|p| strip_purl_qualifiers(p).to_string())
.collect::<HashSet<_>>()
.into_iter()
.collect();

if let Ok(sp_paths) = python_crawler.get_site_packages_paths(&crawler_options).await {
for sp_path in &sp_paths {
if let Ok(packages) = python_crawler.find_by_purls(sp_path, &base_pypi_purls).await {
for (base_purl, pkg) in packages {
for qualified_purl in &pypi_purls {
if strip_purl_qualifiers(qualified_purl) == base_purl
&& !all_packages.contains_key(qualified_purl)
{
all_packages.insert(qualified_purl.clone(), pkg.path.clone());
}
}
}
}
}
}
}
let all_packages =
find_packages_for_rollback(&partitioned, &crawler_options, args.silent).await;

if all_packages.is_empty() {
if !args.silent {
Expand Down
37 changes: 15 additions & 22 deletions crates/socket-patch-cli/src/commands/scan.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use clap::Args;
use socket_patch_core::api::client::get_api_client_from_env;
use socket_patch_core::api::types::BatchPackagePatches;
use socket_patch_core::crawlers::{CrawlerOptions, NpmCrawler, PythonCrawler};
use socket_patch_core::crawlers::{CrawlerOptions, Ecosystem};
use std::collections::HashSet;
use std::path::PathBuf;

use crate::ecosystem_dispatch::crawl_all_ecosystems;

const DEFAULT_BATCH_SIZE: usize = 100;

#[derive(Args)]
Expand Down Expand Up @@ -82,23 +84,10 @@ pub async fn run(args: ScanArgs) -> i32 {
}

// Crawl packages
let npm_crawler = NpmCrawler;
let python_crawler = PythonCrawler;

let npm_packages = npm_crawler.crawl_all(&crawler_options).await;
let python_packages = python_crawler.crawl_all(&crawler_options).await;

let mut all_purls: Vec<String> = Vec::new();
for pkg in &npm_packages {
all_purls.push(pkg.purl.clone());
}
for pkg in &python_packages {
all_purls.push(pkg.purl.clone());
}
let (all_crawled, eco_counts) = crawl_all_ecosystems(&crawler_options).await;

let all_purls: Vec<String> = all_crawled.iter().map(|p| p.purl.clone()).collect();
let package_count = all_purls.len();
let npm_count = npm_packages.len();
let python_count = python_packages.len();

if package_count == 0 {
if !args.json {
Expand All @@ -121,18 +110,22 @@ pub async fn run(args: ScanArgs) -> i32 {
} else if args.global || args.global_prefix.is_some() {
println!("No global packages found.");
} else {
println!("No packages found. Run npm/yarn/pnpm/pip install first.");
#[cfg(feature = "cargo")]
let install_cmds = "npm/yarn/pnpm/pip/cargo";
#[cfg(not(feature = "cargo"))]
let install_cmds = "npm/yarn/pnpm/pip";
println!("No packages found. Run {install_cmds} install first.");
}
return 0;
}

// Build ecosystem summary
let mut eco_parts = Vec::new();
if npm_count > 0 {
eco_parts.push(format!("{npm_count} npm"));
}
if python_count > 0 {
eco_parts.push(format!("{python_count} python"));
for eco in Ecosystem::all() {
let count = eco_counts.get(eco).copied().unwrap_or(0);
if count > 0 {
eco_parts.push(format!("{count} {}", eco.display_name()));
}
}
let eco_summary = if eco_parts.is_empty() {
String::new()
Expand Down
Loading
Loading