diff --git a/CHANGELOG.md b/CHANGELOG.md index e0fba4525..915e52c40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - `phylum project list --no-group` flag to only show personal projects - Organization support for `phylum group` subcommands +### Removed + +- `phylum batch` subcommand + ## 6.6.6 - 2024-07-12 ### Added diff --git a/cli/src/app.rs b/cli/src/app.rs index 99aacb8d0..17db5db0a 100644 --- a/cli/src/app.rs +++ b/cli/src/app.rs @@ -427,43 +427,6 @@ pub fn add_subcommands(command: Command) -> Command { .help("Disable generation of lockfiles from manifests"), ]), ) - .subcommand( - Command::new("batch") - .hide(true) - .about("Submits a batch of requests to the processing system") - .args(&[ - Arg::new("file") - .short('f') - .long("file") - .value_name("FILE") - .help( - "File (or piped stdin) containing the list of packages (format \ - `:`)", - ) - .value_hint(ValueHint::FilePath), - Arg::new("type") - .short('t') - .long("type") - .value_name("TYPE") - .help("Package ecosystem type") - .value_parser([ - "npm", "rubygems", "pypi", "maven", "nuget", "golang", "cargo", - ]) - .required(true), - Arg::new("label").short('l').long("label").help("Label to use for analysis"), - Arg::new("project") - .short('p') - .long("project") - .value_name("PROJECT_NAME") - .help("Project to use for analysis"), - Arg::new("group") - .short('g') - .long("group") - .value_name("GROUP_NAME") - .help("Group to use for analysis") - .requires("project"), - ]), - ) .subcommand(Command::new("version").about("Display application version")) .subcommand( Command::new("group") diff --git a/cli/src/bin/phylum.rs b/cli/src/bin/phylum.rs index 4926473bd..56f56bb3e 100644 --- a/cli/src/bin/phylum.rs +++ b/cli/src/bin/phylum.rs @@ -140,7 +140,7 @@ async fn handle_commands() -> CommandResult { "package" => packages::handle_get_package(&Spinner::wrap(api).await?, sub_matches).await, "history" => jobs::handle_history(&Spinner::wrap(api).await?, sub_matches).await, "group" => group::handle_group(&Spinner::wrap(api).await?, sub_matches, config).await, - "analyze" | "batch" => jobs::handle_submission(&Spinner::wrap(api).await?, &matches).await, + "analyze" => jobs::handle_analyze(&Spinner::wrap(api).await?, sub_matches).await, "init" => init::handle_init(&Spinner::wrap(api).await?, sub_matches).await, "status" => status::handle_status(sub_matches).await, "org" => org::handle_org(&Spinner::wrap(api).await?, sub_matches, config).await, diff --git a/cli/src/commands/jobs.rs b/cli/src/commands/jobs.rs index a10cc6bec..8d3216459 100644 --- a/cli/src/commands/jobs.rs +++ b/cli/src/commands/jobs.rs @@ -1,5 +1,7 @@ +use std::fs; +#[cfg(feature = "vulnreach")] +use std::io; use std::str::FromStr; -use std::{fs, io}; use anyhow::{anyhow, Context, Result}; use console::style; @@ -7,7 +9,7 @@ use log::debug; use phylum_lockfile::ParseError; use phylum_project::DepfileConfig; use phylum_types::types::common::{JobId, ProjectId}; -use phylum_types::types::package::{PackageDescriptor, PackageType}; +use phylum_types::types::package::PackageDescriptor; use reqwest::StatusCode; #[cfg(feature = "vulnreach")] use vulnreach_types::{Job, JobPackage}; @@ -85,133 +87,73 @@ pub async fn handle_history(api: &PhylumApi, matches: &clap::ArgMatches) -> Comm Ok(ExitCode::Ok) } -/// Handles submission of packages to the system for analysis and -/// displays summary information about the submitted package(s) -pub async fn handle_submission(api: &PhylumApi, matches: &clap::ArgMatches) -> CommandResult { - let mut ignored_packages: Vec = vec![]; - let mut packages = vec![]; - let mut synch = false; // get status after submission - let mut pretty_print = false; - let jobs_project; - let label; - - if let Some(matches) = matches.subcommand_matches("analyze") { - let sandbox_generation = !matches.get_flag("skip-sandbox"); - let generate_lockfiles = !matches.get_flag("no-generation"); - label = matches.get_one::("label"); - pretty_print = !matches.get_flag("json"); - synch = true; - - jobs_project = JobsProject::new(api, matches).await?; - - // Get .phylum_project path - let current_project = phylum_project::get_current_project(); - let project_root = current_project.as_ref().map(|p| p.root()); - - for depfile in jobs_project.depfiles { - let parse_result = parse::parse_depfile( - &depfile.path, - project_root, - Some(&depfile.depfile_type), - sandbox_generation, - generate_lockfiles, - ); - - // Map dedicated exit codes for failures due to disabled generation or - // unknown dependency file format. - let parsed_depfile = match parse_result { - Ok(parsed_depfile) => parsed_depfile, - Err(err @ ParseError::ManifestWithoutGeneration(_)) => { - print_user_failure!("Could not parse manifest: {}", err); - return Ok(ExitCode::ManifestWithoutGeneration); - }, - Err(err @ ParseError::UnknownManifestFormat(_)) => { - print_user_failure!("Could not parse manifest: {}", err); - return Ok(ExitCode::UnknownManifestFormat); - }, - Err(ParseError::Other(err)) => { - return Err(err).with_context(|| { - format!( - "Could not parse dependency file {:?} as {:?} type", - depfile.path.display(), - depfile.depfile_type - ) - }); - }, - }; - - if pretty_print { - print_user_success!( - "Successfully parsed dependency file {:?} as type {:?}", - parsed_depfile.path, - parsed_depfile.format.name() - ); - } - - let mut analysis_packages = - AnalysisPackageDescriptor::descriptors_from_lockfile(parsed_depfile); - packages.append(&mut analysis_packages); - } - - if let Some(base) = matches.get_one::("base") { - let base_text = fs::read_to_string(base)?; - ignored_packages = serde_json::from_str(&base_text)?; - } - } else if let Some(matches) = matches.subcommand_matches("batch") { - jobs_project = JobsProject::new(api, matches).await?; - - let mut eof = false; - let mut line = String::new(); - let mut reader: Box = if let Some(file) = matches.get_one::("file") - { - // read entries from the file - Box::new(io::BufReader::new(std::fs::File::open(file).unwrap())) - } else { - // read from stdin - log::info!("Waiting on stdin..."); - Box::new(io::BufReader::new(io::stdin())) - }; +/// Handle `phylum analyze` subcommand. +pub async fn handle_analyze(api: &PhylumApi, matches: &clap::ArgMatches) -> CommandResult { + let sandbox_generation = !matches.get_flag("skip-sandbox"); + let generate_lockfiles = !matches.get_flag("no-generation"); + let label = matches.get_one::("label"); + let pretty_print = !matches.get_flag("json"); - let request_type = { - let package_type = matches.get_one::("type").unwrap(); - PackageType::from_str(package_type) - .map_err(|_| anyhow!("invalid package type: {}", package_type))? + let jobs_project = JobsProject::new(api, matches).await?; + + // Get .phylum_project path. + let current_project = phylum_project::get_current_project(); + let project_root = current_project.as_ref().map(|p| p.root()); + + let mut packages = Vec::new(); + for depfile in jobs_project.depfiles { + let parse_result = parse::parse_depfile( + &depfile.path, + project_root, + Some(&depfile.depfile_type), + sandbox_generation, + generate_lockfiles, + ); + + // Map dedicated exit codes for failures due to disabled generation or + // unknown dependency file format. + let parsed_depfile = match parse_result { + Ok(parsed_depfile) => parsed_depfile, + Err(err @ ParseError::ManifestWithoutGeneration(_)) => { + print_user_failure!("Could not parse manifest: {}", err); + return Ok(ExitCode::ManifestWithoutGeneration); + }, + Err(err @ ParseError::UnknownManifestFormat(_)) => { + print_user_failure!("Could not parse manifest: {}", err); + return Ok(ExitCode::UnknownManifestFormat); + }, + Err(ParseError::Other(err)) => { + return Err(err).with_context(|| { + format!( + "Could not parse dependency file {:?} as {:?} type", + depfile.path.display(), + depfile.depfile_type + ) + }); + }, }; - label = matches.get_one::("label"); - - while !eof { - match reader.read_line(&mut line) { - Ok(0) => eof = true, - Ok(_) => { - line.pop(); - let mut pkg_info = line.split(':').collect::>(); - if pkg_info.len() < 2 { - debug!("Invalid package input: `{}`", line); - continue; - } - let pkg_version = pkg_info.pop().unwrap(); - let pkg_name = pkg_info.join(":"); - - packages.push(AnalysisPackageDescriptor::PackageDescriptor( - PackageDescriptor { - name: pkg_name.to_owned(), - version: pkg_version.to_owned(), - package_type: request_type.to_owned(), - } - .into(), - )); - line.clear(); - }, - Err(err) => { - return Err(anyhow!(err)); - }, - } + if pretty_print { + print_user_success!( + "Successfully parsed dependency file {:?} as type {:?}", + parsed_depfile.path, + parsed_depfile.format.name() + ); } - } else { - unreachable!(); + + let mut analysis_packages = + AnalysisPackageDescriptor::descriptors_from_lockfile(parsed_depfile); + packages.append(&mut analysis_packages); } + let ignored_packages: Vec = match matches.get_one::("base") { + Some(base) => { + let base_text = fs::read_to_string(base)?; + serde_json::from_str(&base_text)? + }, + None => Vec::new(), + }; + // Avoid request error without dependencies. if packages.is_empty() { print_user_warning!("No packages found in dependency file"); @@ -231,31 +173,25 @@ pub async fn handle_submission(api: &PhylumApi, matches: &clap::ArgMatches) -> C if pretty_print { print_user_success!("Job ID: {}", job_id); - } - if synch { - if pretty_print { - #[cfg(feature = "vulnreach")] - let packages: Vec<_> = packages - .into_iter() - .filter_map(|pkg| match pkg { - AnalysisPackageDescriptor::PackageDescriptor(package) => { - Some(package.package_descriptor) - }, - AnalysisPackageDescriptor::Purl(_) => None, - }) - .collect(); - #[cfg(feature = "vulnreach")] - if let Err(err) = vulnreach(api, matches, packages, job_id.to_string()).await { - print_user_failure!("Reachability analysis failed: {err:?}"); - } + #[cfg(feature = "vulnreach")] + let packages: Vec<_> = packages + .into_iter() + .filter_map(|pkg| match pkg { + AnalysisPackageDescriptor::PackageDescriptor(package) => { + Some(package.package_descriptor) + }, + AnalysisPackageDescriptor::Purl(_) => None, + }) + .collect(); + #[cfg(feature = "vulnreach")] + if let Err(err) = vulnreach(api, matches, packages, job_id.to_string()).await { + print_user_failure!("Reachability analysis failed: {err:?}"); } - - debug!("Requesting status..."); - print_job_status(api, &job_id, ignored_packages, pretty_print).await - } else { - Ok(ExitCode::Ok) } + + debug!("Requesting status..."); + print_job_status(api, &job_id, ignored_packages, pretty_print).await } /// Perform vulnerability reachability analysis. @@ -303,7 +239,7 @@ async fn vulnreach( Ok(()) } -/// Project information for analyze/batch. +/// Project information for analyze. struct JobsProject { project_id: ProjectId, group: Option, diff --git a/lockfile/src/parsers/gem.rs b/lockfile/src/parsers/gem.rs index e8abf6e9d..c755131e4 100644 --- a/lockfile/src/parsers/gem.rs +++ b/lockfile/src/parsers/gem.rs @@ -28,7 +28,7 @@ struct Section<'a> { impl<'a> Section<'a> { /// Parse lockfile into dependency sections. - fn from_lockfile(mut input: &'a str) -> IResult<&str, Vec> { + fn from_lockfile(mut input: &'a str) -> IResult<&'a str, Vec> { let mut sections = Vec::new(); while !input.is_empty() { diff --git a/lockfile/src/parsers/pypi.rs b/lockfile/src/parsers/pypi.rs index 950bb4796..c2620d535 100644 --- a/lockfile/src/parsers/pypi.rs +++ b/lockfile/src/parsers/pypi.rs @@ -242,9 +242,9 @@ fn package_hash(input: &str) -> IResult<&str, &str> { /// A combinator that takes a parser `inner` and produces a parser that also /// consumes both leading and trailing whitespace, returning the output of /// `inner`. -fn ws<'a, F>(inner: F) -> impl FnMut(&'a str) -> IResult<&str, &str> +fn ws<'a, F>(inner: F) -> impl FnMut(&'a str) -> IResult<&'a str, &'a str> where - F: Fn(&'a str) -> IResult<&str, &str>, + F: Fn(&'a str) -> IResult<&'a str, &'a str>, { delimited(nl_space0, inner, nl_space0) } diff --git a/lockfile/src/parsers/spdx.rs b/lockfile/src/parsers/spdx.rs index 7a74109e6..41490c6f9 100644 --- a/lockfile/src/parsers/spdx.rs +++ b/lockfile/src/parsers/spdx.rs @@ -180,9 +180,9 @@ fn parse_external_refs(input: &str) -> IResult<&str, ExternalRefs> { /// A combinator that takes a parser `inner` and produces a parser that also /// consumes both leading and trailing whitespace, returning the output of /// `inner`. -fn ws<'a, F>(inner: F) -> impl FnMut(&'a str) -> IResult<&str, &str> +fn ws<'a, F>(inner: F) -> impl FnMut(&'a str) -> IResult<&'a str, &'a str> where - F: Fn(&'a str) -> IResult<&str, &str>, + F: Fn(&'a str) -> IResult<&'a str, &'a str>, { delimited(multispace0, inner, multispace0) }