From 1d881575994f51ce72689ea94b9195e489fcdc48 Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Fri, 13 Dec 2024 21:49:14 +0100 Subject: [PATCH 01/11] Fix firewall clap args --- cli/src/app.rs | 86 ++++++++++++++++++++++++-------------------------- 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/cli/src/app.rs b/cli/src/app.rs index d147de160..0189f604a 100644 --- a/cli/src/app.rs +++ b/cli/src/app.rs @@ -610,53 +610,51 @@ pub fn add_subcommands(command: Command) -> Command { .arg_required_else_help(true) .subcommand_required(true) .subcommand( - Command::new("log") - .about("Show firewall activity log") - .args(&[Arg::new("json") + Command::new("log").about("Show firewall activity log").args(&[ + Arg::new("json") .action(ArgAction::SetTrue) .short('j') .long("json") - .help("Produce output in json format (default: false)")]) - .args(&[ - Arg::new("group") - .value_name("GROUP_NAME") - .help("Specify a group to use for analysis") - .required(true), - Arg::new("ecosystem") - .long("ecosystem") - .value_name("ECOSYSTEM") - .help("Only show logs matching this ecosystem") - .value_parser([ - "npm", "rubygems", "pypi", "maven", "nuget", "cargo", - ]), - Arg::new("package") - .long("package") - .value_name("PURL") - .help("Only show logs matching this PURL") - .conflicts_with("ecosystem"), - Arg::new("action") - .long("action") - .value_name("ACTION") - .help("Only show logs matching this log action") - .value_parser([ - "Download", - "AnalysisSuccess", - "AnalysisFailure", - "AnalysisWarning", - ]), - Arg::new("before").long("before").value_name("TIMESTAMP").help( - "Only show logs created before this timestamp (RFC3339 format)", - ), - Arg::new("after").long("after").value_name("TIMESTAMP").help( - "Only show logs created after this timestamp (RFC3339 format)", - ), - Arg::new("limit") - .long("limit") - .value_name("COUNT") - .help("Maximum number of log entries to show") - .default_value("10") - .value_parser(1..=10_000), - ]), + .help("Produce output in json format (default: false)"), + Arg::new("group") + .value_name("GROUP_NAME") + .help("Firewall group to list log activity for") + .required(true), + Arg::new("ecosystem") + .long("ecosystem") + .value_name("ECOSYSTEM") + .help("Only show logs matching this ecosystem") + .value_parser(["npm", "rubygems", "pypi", "maven", "nuget", "cargo"]), + Arg::new("package") + .long("package") + .value_name("PURL") + .help("Only show logs matching this PURL") + .conflicts_with("ecosystem"), + Arg::new("action") + .long("action") + .value_name("ACTION") + .help("Only show logs matching this log action") + .value_parser([ + "Download", + "AnalysisSuccess", + "AnalysisFailure", + "AnalysisWarning", + ]), + Arg::new("before") + .long("before") + .value_name("TIMESTAMP") + .help("Only show logs created before this timestamp (RFC3339 format)"), + Arg::new("after") + .long("after") + .value_name("TIMESTAMP") + .help("Only show logs created after this timestamp (RFC3339 format)"), + Arg::new("limit") + .long("limit") + .value_name("COUNT") + .help("Maximum number of log entries to show") + .default_value("10") + .value_parser(1..=10_000), + ]), ), ); From da76b214fb4b5c75b6fb74c069c79161c23cee4e Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Tue, 17 Dec 2024 23:24:51 +0100 Subject: [PATCH 02/11] Add `phylum exception` subcommand This patch adds a new `phylum exception` subcommand, with the three `add`, `remove`, and `list` commands under it. These new subcommands allow managing package exceptions to remove friction with Phylum's firewall. The add and remove subcommands are targeted at interactive use, pulling logs from the package firewall and existing exceptions to suggest what is most likely to be used for adding/removing an exception. While currently firewall logs are pulled for adding exceptions, the individual analysis results for each version are not listed in the suggestions. The `list` and `remove` subcommand allow managing both package and issue suppressions, however `add` currently only supports adding package exceptions. Closes #1552. --- CHANGELOG.md | 4 + Cargo.lock | 6 +- cli/Cargo.toml | 5 +- cli/src/api/endpoints.rs | 113 +++++++++++ cli/src/api/mod.rs | 91 ++++++++- cli/src/app.rs | 138 ++++++++++++- cli/src/bin/phylum.rs | 7 +- cli/src/commands/exception.rs | 365 ++++++++++++++++++++++++++++++++++ cli/src/commands/firewall.rs | 21 +- cli/src/commands/mod.rs | 1 + cli/src/format.rs | 25 ++- cli/src/types.rs | 71 ++++++- 12 files changed, 824 insertions(+), 23 deletions(-) create mode 100644 cli/src/commands/exception.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 21495f8ed..7a8d45586 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased +### Added + +- `phylum exception` subcommand for managing suppressions + ## 7.2.0 - 2024-12-10 ### Added diff --git a/Cargo.lock b/Cargo.lock index 5b0256763..77e2294c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4506,6 +4506,7 @@ dependencies = [ "git2", "home", "ignore", + "indexmap", "lazy_static", "libc", "log", @@ -4948,13 +4949,14 @@ dependencies = [ [[package]] name = "purl" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c14fe28c8495f7eaf77a6e6106966f95211c0a2404b9da50d248fc32af3a3f14" +checksum = "f112b0e2a9bca03924c39166775b74fec9a831f7d4d8fa539dee0e565f403a0e" dependencies = [ "hex", "percent-encoding", "phf", + "serde", "smartstring", "thiserror 1.0.69", "unicase", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index f994ae056..f31cf1097 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -4,7 +4,7 @@ version = "7.2.0" authors = ["Phylum, Inc. "] license = "GPL-3.0-or-later" edition = "2021" -rust-version = "1.80.0" +rust-version = "1.82.0" autotests = false [[test]] @@ -48,7 +48,7 @@ phylum_lockfile = { path = "../lockfile", features = ["generator"] } phylum_project = { path = "../phylum_project" } phylum_types = { git = "https://github.com/phylum-dev/phylum-types", branch = "development" } prettytable-rs = "0.10.0" -purl = "0.1.1" +purl = { version = "0.1.5", features = ["serde"] } rand = "0.8.4" regex = "1.5.5" reqwest = { version = "0.12.7", features = ["blocking", "json", "rustls-tls", "rustls-tls-native-roots", "rustls-tls-webpki-roots"], default-features = false } @@ -70,6 +70,7 @@ vuln-reach = { git = "https://github.com/phylum-dev/vuln-reach", optional = true vulnreach_types = { path = "../vulnreach_types", optional = true } walkdir = "2.3.2" zip = { version = "2.1.0", default-features = false, features = ["deflate"] } +indexmap = "2.7.0" [target.'cfg(unix)'.dependencies] birdcage = { version = "0.8.1" } diff --git a/cli/src/api/endpoints.rs b/cli/src/api/endpoints.rs index fe0c15da5..5680149ab 100644 --- a/cli/src/api/endpoints.rs +++ b/cli/src/api/endpoints.rs @@ -197,6 +197,119 @@ pub fn firewall_log(api_uri: &str) -> Result { Ok(get_firewall_path(api_uri)?.join("activity")?) } +/// GET /organizations//groups//preferences. +pub fn org_group_preferences( + api_uri: &str, + org_name: &str, + group_name: &str, +) -> Result { + let mut url = get_api_path(api_uri)?; + url.path_segments_mut().unwrap().pop_if_empty().extend([ + "organizations", + org_name, + "groups", + group_name, + "preferences", + ]); + Ok(url) +} + +/// GET /preferences/group/ +pub fn group_preferences(api_uri: &str, group_name: &str) -> Result { + let mut url = get_api_path(api_uri)?; + url.path_segments_mut().unwrap().pop_if_empty().extend(["preferences", "group", group_name]); + Ok(url) +} + +/// GET /preferences/project/ +pub fn project_preferences(api_uri: &str, project_id: &str) -> Result { + let mut url = get_api_path(api_uri)?; + url.path_segments_mut().unwrap().pop_if_empty().extend(["preferences", "project", project_id]); + Ok(url) +} + +/// POST /organizations//groups//suppress. +pub fn org_group_suppress( + api_uri: &str, + org_name: &str, + group_name: &str, +) -> Result { + let mut url = get_api_path(api_uri)?; + url.path_segments_mut().unwrap().pop_if_empty().extend([ + "organizations", + org_name, + "groups", + group_name, + "suppress", + ]); + Ok(url) +} + +/// POST /preferences/group//suppress. +pub fn group_suppress(api_uri: &str, group_name: &str) -> Result { + let mut url = get_api_path(api_uri)?; + url.path_segments_mut().unwrap().pop_if_empty().extend([ + "preferences", + "group", + group_name, + "suppress", + ]); + Ok(url) +} + +/// POST /preferences/project//suppress. +pub fn project_suppress(api_uri: &str, project_id: &str) -> Result { + let mut url = get_api_path(api_uri)?; + url.path_segments_mut().unwrap().pop_if_empty().extend([ + "preferences", + "project", + project_id, + "suppress", + ]); + Ok(url) +} + +/// POST /organizations//groups//unsuppress. +pub fn org_group_unsuppress( + api_uri: &str, + org_name: &str, + group_name: &str, +) -> Result { + let mut url = get_api_path(api_uri)?; + url.path_segments_mut().unwrap().pop_if_empty().extend([ + "organizations", + org_name, + "groups", + group_name, + "unsuppress", + ]); + Ok(url) +} + +/// POST /preferences/group//unsuppress. +pub fn group_unsuppress(api_uri: &str, group_name: &str) -> Result { + let mut url = get_api_path(api_uri)?; + url.path_segments_mut().unwrap().pop_if_empty().extend([ + "preferences", + "group", + group_name, + "unsuppress", + ]); + Ok(url) +} + +/// POST /preferences/project//unsuppress. +pub fn project_unsuppress(api_uri: &str, project_id: &str) -> Result { + let mut url = get_api_path(api_uri)?; + url.path_segments_mut().unwrap().pop_if_empty().extend([ + "preferences", + "project", + project_id, + "unsuppress", + ]); + Ok(url) +} + /// GET /.well-known/openid-configuration pub fn oidc_discovery(api_uri: &str) -> Result { Ok(get_api_path(api_uri)?.join(".well-known/openid-configuration")?) diff --git a/cli/src/api/mod.rs b/cli/src/api/mod.rs index 7c4e5df8c..1a5c3b763 100644 --- a/cli/src/api/mod.rs +++ b/cli/src/api/mod.rs @@ -32,8 +32,8 @@ use crate::types::{ FirewallLogFilter, FirewallLogResponse, FirewallPaginated, GetProjectResponse, HistoryJob, ListUserGroupsResponse, OrgGroupsResponse, OrgMembersResponse, OrgsResponse, PackageSpecifier, PackageSubmitResponse, Paginated, PingResponse, PolicyEvaluationRequest, - PolicyEvaluationResponse, PolicyEvaluationResponseRaw, ProjectListEntry, RevokeTokenRequest, - SubmitPackageRequest, UpdateProjectRequest, UserToken, + PolicyEvaluationResponse, PolicyEvaluationResponseRaw, Preferences, ProjectListEntry, + RevokeTokenRequest, SubmitPackageRequest, Suppression, UpdateProjectRequest, UserToken, }; pub mod endpoints; @@ -596,6 +596,93 @@ impl PhylumApi { Ok(log) } + /// Get group preferences. + pub async fn group_preferences( + &self, + org: Option<&str>, + group: &str, + ) -> Result> { + match org { + Some(org) => { + let url = + endpoints::org_group_preferences(&self.config.connection.uri, org, group)?; + self.get(url).await + }, + None => { + #[derive(Deserialize)] + struct Response<'a> { + preferences: Preferences<'a>, + } + + let url = endpoints::group_preferences(&self.config.connection.uri, group)?; + Ok(self.get::(url).await?.preferences) + }, + } + } + + /// Get project preferences. + pub async fn project_preferences(&self, project_id: &str) -> Result> { + #[derive(Deserialize)] + struct Response<'a> { + preferences: Preferences<'a>, + } + + let url = endpoints::project_preferences(&self.config.connection.uri, project_id)?; + Ok(self.get::(url).await?.preferences) + } + + /// Add group suppression. + pub async fn group_suppress( + &self, + org: Option<&str>, + group: &str, + suppressions: &[Suppression<'_>], + ) -> Result<()> { + let url = match org { + Some(org) => endpoints::org_group_suppress(&self.config.connection.uri, org, group)?, + None => endpoints::group_suppress(&self.config.connection.uri, group)?, + }; + self.send_request_raw(Method::POST, url, Some(suppressions)).await?; + Ok(()) + } + + /// Get project suppression. + pub async fn project_suppress( + &self, + project_id: &str, + suppressions: &[Suppression<'_>], + ) -> Result<()> { + let url = endpoints::project_suppress(&self.config.connection.uri, project_id)?; + self.send_request_raw(Method::POST, url, Some(suppressions)).await?; + Ok(()) + } + + /// Remove group suppression. + pub async fn group_unsuppress( + &self, + org: Option<&str>, + group: &str, + unsuppressions: &[Suppression<'_>], + ) -> Result<()> { + let url = match org { + Some(org) => endpoints::org_group_unsuppress(&self.config.connection.uri, org, group)?, + None => endpoints::group_unsuppress(&self.config.connection.uri, group)?, + }; + self.send_request_raw(Method::POST, url, Some(unsuppressions)).await?; + Ok(()) + } + + /// Remove project suppression. + pub async fn project_unsuppress( + &self, + project_id: &str, + unsuppressions: &[Suppression<'_>], + ) -> Result<()> { + let url = endpoints::project_unsuppress(&self.config.connection.uri, project_id)?; + self.send_request_raw(Method::POST, url, Some(unsuppressions)).await?; + Ok(()) + } + /// Get reachable vulnerabilities. #[cfg(feature = "vulnreach")] pub async fn vulnerabilities(&self, job: Job) -> Result> { diff --git a/cli/src/app.rs b/cli/src/app.rs index 0189f604a..e3a77b41b 100644 --- a/cli/src/app.rs +++ b/cli/src/app.rs @@ -1,5 +1,5 @@ use clap::builder::PossibleValuesParser; -use clap::{Arg, ArgAction, Command, ValueHint}; +use clap::{Arg, ArgAction, ArgGroup, Command, ValueHint}; use git_version::git_version; use lazy_static::lazy_static; @@ -656,6 +656,142 @@ pub fn add_subcommands(command: Command) -> Command { .value_parser(1..=10_000), ]), ), + ) + .subcommand( + Command::new("exception") + .about("Manage analysis exceptions") + .arg_required_else_help(true) + .subcommand_required(true) + .subcommand( + Command::new("list") + .about("List active analysis exceptions") + .group(ArgGroup::new("subject").args(["group", "project"]).required(true)) + .args(&[ + Arg::new("json") + .action(ArgAction::SetTrue) + .short('j') + .long("json") + .help("Produce output in json format (default: false)"), + Arg::new("group") + .short('g') + .long("group") + .value_name("GROUP_NAME") + .help("Group to list exceptions for"), + Arg::new("project") + .short('p') + .long("project") + .value_name("PROJECT_NAME") + .help("Project to list exceptions for"), + ]), + ) + .subcommand( + Command::new("add") + .about("Add a new analysis exception") + .group(ArgGroup::new("subject").args(["group", "project"]).required(true)) + .args(&[ + Arg::new("group") + .short('g') + .long("group") + .value_name("GROUP_NAME") + .help("Group to add exception to"), + Arg::new("project") + .short('p') + .long("project") + .value_name("PROJECT_NAME") + .help("Project to add exceptions to"), + Arg::new("ecosystem") + .short('e') + .long("ecosystem") + .value_name("ECOSYSTEM") + .help("Ecosystem of the package to add an exception for") + .value_parser([ + "npm", "rubygems", "pypi", "maven", "nuget", "golang", "cargo", + ]), + Arg::new("name") + .short('n') + .long("name") + .value_name("PACKAGE_NAME") + .help( + "Name and optional namespace of the package to add an \ + exception for", + ), + Arg::new("version") + .long("version") + .value_name("VERSION") + .help("Version of the package to add an exception for"), + Arg::new("purl") + .long("purl") + .value_name("PURL") + .help("Package in PURL format") + .conflicts_with_all(["ecosystem", "name", "version"]), + Arg::new("reason") + .short('r') + .long("reason") + .value_name("REASON") + .help("Reason for adding this exception"), + Arg::new("no-suggestions") + .short('s') + .long("no-suggestions") + .action(ArgAction::SetTrue) + .help("Do not query package firewall to make suggestions"), + ]), + ) + .subcommand( + Command::new("remove") + .about("Remove an existing analysis exception") + .group(ArgGroup::new("subject").args(["group", "project"]).required(true)) + .group( + ArgGroup::new("package") + .args(["ecosystem", "name", "version", "purl"]) + .conflicts_with("issue"), + ) + .group(ArgGroup::new("issue").args(["id", "tag"])) + .args(&[ + Arg::new("group") + .short('g') + .long("group") + .value_name("GROUP_NAME") + .help("Group to add exception to"), + Arg::new("project") + .short('p') + .long("project") + .value_name("PROJECT_NAME") + .help("Project to add exceptions to"), + Arg::new("ecosystem") + .short('e') + .long("ecosystem") + .value_name("ECOSYSTEM") + .help("Ecosystem of the exception which should be removed") + .value_parser([ + "npm", "rubygems", "pypi", "maven", "nuget", "golang", "cargo", + ]), + Arg::new("name") + .short('n') + .long("name") + .value_name("PACKAGE_NAME") + .help( + "Package name and optional namespace of the exception which \ + should be removed", + ), + Arg::new("version") + .long("version") + .value_name("VERSION") + .help("Package version of the exception which should be removed"), + Arg::new("purl") + .long("purl") + .value_name("PURL") + .help("Package in PURL format") + .conflicts_with_all(["ecosystem", "name", "version"]), + Arg::new("id") + .long("id") + .value_name("ISSUE_ID") + .help("Issue ID of the exception which should be removed"), + Arg::new("tag") + .long("tag") + .value_name("ISSUE_TAG") + .help("Issue tag of the exception which should be removed"), + ]), + ), ); #[cfg(feature = "extensions")] diff --git a/cli/src/bin/phylum.rs b/cli/src/bin/phylum.rs index 7333343ea..0a9edbae4 100644 --- a/cli/src/bin/phylum.rs +++ b/cli/src/bin/phylum.rs @@ -14,8 +14,8 @@ use phylum_cli::commands::sandbox; #[cfg(feature = "selfmanage")] use phylum_cli::commands::uninstall; use phylum_cli::commands::{ - auth, find_dependency_files, firewall, group, init, jobs, org, packages, parse, project, - status, CommandResult, ExitCode, + auth, exception, find_dependency_files, firewall, group, init, jobs, org, packages, parse, + project, status, CommandResult, ExitCode, }; use phylum_cli::config::{self, Config}; use phylum_cli::spinner::Spinner; @@ -148,6 +148,9 @@ async fn handle_commands() -> CommandResult { "firewall" => { firewall::handle_firewall(&Spinner::wrap(api).await?, sub_matches, config).await }, + "exception" => { + exception::handle_exception(&Spinner::wrap(api).await?, sub_matches, config).await + }, #[cfg(feature = "selfmanage")] "uninstall" => uninstall::handle_uninstall(sub_matches), diff --git a/cli/src/commands/exception.rs b/cli/src/commands/exception.rs new file mode 100644 index 000000000..38016db31 --- /dev/null +++ b/cli/src/commands/exception.rs @@ -0,0 +1,365 @@ +//! Subcommand `phylum exception`. + +use std::borrow::Cow; +use std::str::FromStr; + +use clap::ArgMatches; +use console::Term; +use dialoguer::{FuzzySelect, Input}; +use indexmap::IndexSet; +use purl::{PackageType, Purl}; + +use crate::api::PhylumApi; +use crate::commands::{CommandResult, ExitCode}; +use crate::config::Config; +use crate::format::Format; +use crate::print_user_success; +use crate::spinner::Spinner; +use crate::types::{ + FirewallAction, FirewallLogFilter, IgnoredIssue, IgnoredPackage, Preferences, Suppression, +}; + +/// Maximum number of package names or versions proposed for exceptions. +const MAX_SUGGESTIONS: usize = 25; + +/// Handle `phylum exception` subcommand. +pub async fn handle_exception( + api: &PhylumApi, + matches: &ArgMatches, + config: Config, +) -> CommandResult { + match matches.subcommand() { + Some(("list", matches)) => handle_list(api, matches, config).await, + Some(("add", matches)) => handle_add(api, matches, config).await, + Some(("remove", matches)) => handle_remove(api, matches, config).await, + _ => unreachable!("invalid clap configuration"), + } +} + +/// Handle `phylum exception list` subcommand. +pub async fn handle_list(api: &PhylumApi, matches: &ArgMatches, config: Config) -> CommandResult { + let group = matches.get_one::("group"); + let org = config.org(); + + let exceptions = match matches.get_one::("project") { + Some(project_name) => { + let group = group.map(String::as_str); + let project_id = api.get_project_id(project_name, org, group).await?.to_string(); + api.project_preferences(&project_id).await? + }, + None => api.group_preferences(config.org(), group.unwrap()).await?, + }; + + let pretty = !matches.get_flag("json"); + exceptions.write_stdout(pretty); + + Ok(ExitCode::Ok) +} + +/// Handle `phylum exception add` subcommand. +pub async fn handle_add(api: &PhylumApi, matches: &ArgMatches, config: Config) -> CommandResult { + let no_suggestions = matches.get_flag("no-suggestions"); + let ecosystem = matches.get_one::("ecosystem"); + let project = matches.get_one::("project"); + let version = matches.get_one::("version"); + let reason = matches.get_one::("reason"); + let group = matches.get_one::("group"); + let name = matches.get_one::("name"); + let purl = matches.get_one::("purl"); + let org = config.org(); + + // Parse PURL argument or assemble it from its components. + let mut purl = match purl { + Some(purl) => Purl::from_str(purl)?, + None => purl_from_components(api, ecosystem, name, group, org, !no_suggestions).await?, + }; + + // TODO: Show short description of the issues for each version. + // => Create issue. + // => Maybe also provide full list while prompting for reason? + // + // Get suggested versions from Aviary if no argument was supplied. + let version = purl.version().or(version.map(String::as_str)); + let mut suggested_versions = IndexSet::new(); + if let Some(group) = group.filter(|_| !no_suggestions && version.is_none()) { + let spinner = Spinner::new(); + + let filter = FirewallLogFilter { + ecosystem: Some(*purl.package_type()), + action: Some(FirewallAction::AnalysisFailure), + namespace: purl.namespace(), + name: Some(purl.name()), + ..Default::default() + }; + if let Ok(logs) = api.firewall_log(org, group, filter).await { + suggested_versions = logs.data.into_iter().map(|log| log.package.version).collect(); + } + + spinner.stop().await; + } + + // Prompt for version if it wasn't supplied as an argument. + let version = match version { + Some(version) => version.to_string().into(), + None => prompt_version(&suggested_versions)?, + }; + purl = purl.into_builder().with_version(version).build()?; + + // Prompt for exception reason. if it wasn't supplied as an argument. + let reason = match reason { + Some(reason) => reason.into(), + None => prompt_reason()?, + }; + + // Build suppression API object. + let suppressions = vec![Suppression::Package(IgnoredPackage { + purl: Cow::Owned(purl.to_string()), + reason: Cow::Borrowed(&reason), + })]; + + match project { + Some(project_name) => { + let group = group.map(String::as_str); + let project_id = api.get_project_id(project_name, org, group).await?.to_string(); + api.project_suppress(&project_id, &suppressions).await?; + }, + None => api.group_suppress(org, group.unwrap(), &suppressions).await?, + } + + print_user_success!("Successfully added suppression for {}", purl.to_string()); + + Ok(ExitCode::Ok) +} + +/// Handle `phylum exception remove` subcommand. +pub async fn handle_remove(api: &PhylumApi, matches: &ArgMatches, config: Config) -> CommandResult { + let ecosystem = matches.get_one::("ecosystem"); + let project = matches.get_one::("project"); + let version = matches.get_one::("version"); + let group = matches.get_one::("group"); + let name = matches.get_one::("name"); + let purl = matches.get_one::("purl"); + let tag = matches.get_one::("tag"); + let id = matches.get_one::("id"); + let org = config.org(); + + let mut exceptions = match matches.get_one::("project") { + Some(project_name) => { + let group = group.map(String::as_str); + let project_id = api.get_project_id(project_name, org, group).await?.to_string(); + api.project_preferences(&project_id).await? + }, + None => api.group_preferences(config.org(), group.unwrap()).await?, + }; + + // Filter issue suppressions with CLI args. + if tag.is_some() || id.is_some() { + exceptions.ignored_issues.retain(|issue| { + id.is_none_or(|id| id == &issue.id) && tag.is_none_or(|tag| tag == &issue.tag) + }); + } + + // Filter package suppressions with CLI args. + if ecosystem.is_some() || name.is_some() || version.is_some() || purl.is_some() { + let purl = purl.map(|purl| Purl::from_str(purl)); + let (ecosystem, namespace, name, version) = match purl { + Some(Ok(ref purl)) => { + let package_type = Cow::Owned(purl.package_type().to_string()); + (Some(package_type), purl.namespace(), Some(purl.name()), purl.version()) + }, + Some(Err(err)) => return Err(err.into()), + None => ( + ecosystem.map(Cow::Borrowed), + None, + name.map(String::as_str), + version.map(String::as_str), + ), + }; + + exceptions.ignored_packages.retain(|pkg| { + let purl = match Purl::from_str(&pkg.purl) { + Ok(purl) => purl, + Err(_) => return false, + }; + + let package_type = purl.package_type().to_string(); + ecosystem.as_ref().is_none_or(|ecosystem| **ecosystem == package_type) + && namespace.is_none_or(|namespace| Some(namespace) == purl.namespace()) + && name.is_none_or(|name| name == purl.name()) + && version.is_none_or(|version| Some(version) == purl.version()) + }); + } + + let unsuppressions = vec![prompt_removal(&exceptions)?]; + + match project { + Some(project_name) => { + let group = group.map(String::as_str); + let project_id = api.get_project_id(project_name, org, group).await?.to_string(); + api.project_unsuppress(&project_id, &unsuppressions).await?; + }, + None => api.group_unsuppress(org, group.unwrap(), &unsuppressions).await?, + } + + match &unsuppressions[0] { + Suppression::Package(IgnoredPackage { purl, .. }) => { + print_user_success!("Successfully removed suppression for package {purl:?}"); + }, + Suppression::Issue(IgnoredIssue { id, tag, .. }) => { + print_user_success!("Successfully removed suppression for issue {tag:?} [{id}]"); + }, + } + + Ok(ExitCode::Ok) +} + +/// Creat a PURL from its individual components. +async fn purl_from_components( + api: &PhylumApi, + cli_ecosystem: Option<&String>, + cli_name: Option<&String>, + group: Option<&String>, + org: Option<&str>, + suggestions: bool, +) -> anyhow::Result { + // Prompt for ecosystem if it wasn't supplied as an argument. + let ecosystem = match cli_ecosystem { + Some(ecosystem) => ecosystem.into(), + None => Cow::Owned(prompt_ecosystem()?), + }; + let ecosystem = PackageType::from_str(&ecosystem).unwrap(); + + // Get suggested names from Aviary if no argument was supplied. + let mut suggested_names: IndexSet = IndexSet::new(); + if let Some(group) = group.filter(|_| suggestions && cli_name.is_none()) { + let spinner = Spinner::new(); + + let filter = FirewallLogFilter { + ecosystem: Some(ecosystem), + action: Some(FirewallAction::AnalysisFailure), + ..Default::default() + }; + if let Ok(logs) = api.firewall_log(org, group, filter).await { + for log in logs.data { + let purl = Purl::builder(ecosystem, log.package.name) + .with_namespace(log.package.namespace) + .build()?; + suggested_names.insert(purl); + } + } + + spinner.stop().await; + } + + // Prompt for name if it wasn't supplied as an argument. + let purl = match cli_name { + Some(name) => Purl::builder_with_combined_name(ecosystem, name).build()?, + None => prompt_name(ecosystem, &suggested_names)?, + }; + + Ok(purl) +} + +/// Ask for an ecosystem. +fn prompt_ecosystem() -> dialoguer::Result { + let ecosystems = ["cargo", "gem", "golang", "maven", "npm", "nuget", "pypi"]; + + let prompt = "[ENTER] Select and Confirm\nSelect ecosystem"; + let index = FuzzySelect::new().with_prompt(prompt).items(&ecosystems).interact()?; + + println!(); + + Ok(ecosystems[index].to_owned()) +} + +/// Ask for a package name. +fn prompt_name(ecosystem: PackageType, suggestions: &'_ IndexSet) -> anyhow::Result { + // Get space available for suggestions. + let term_size = Term::stdout().size_checked().unwrap_or((u16::MAX, u16::MAX)); + let max_suggestions = (term_size.0 as usize - 3).min(MAX_SUGGESTIONS); + + let mut prompt = "[ENTER] Confirm\nSpecify package name"; + + // Suggest possible names. + if !suggestions.is_empty() { + prompt = "[ENTER] Confirm\nEnter number or specify package name"; + + for (i, suggestion) in suggestions.iter().take(max_suggestions).enumerate().rev() { + println!("({i}) {}", suggestion.combined_name()); + } + println!(); + } + + let input: String = Input::new().with_prompt(prompt).interact_text()?; + + let purl = match usize::from_str(&input) { + Ok(index) if index < suggestions.len() && index < MAX_SUGGESTIONS => { + suggestions[index].clone() + }, + _ => Purl::builder_with_combined_name(ecosystem, &input).build()?, + }; + + println!("Using package {}\n", purl.combined_name()); + + Ok(purl) +} + +/// Ask for a package version. +fn prompt_version(suggestions: &'_ IndexSet) -> dialoguer::Result> { + // Get space available for suggestions. + let term_size = Term::stdout().size_checked().unwrap_or((u16::MAX, u16::MAX)); + let max_suggestions = (term_size.0 as usize - 3).min(MAX_SUGGESTIONS); + + let mut prompt = "[ENTER] Confirm\nSpecify package version"; + + // Suggest possible names. + if !suggestions.is_empty() { + prompt = "[ENTER] Confirm\nEnter number or specify package name"; + + for (i, suggestion) in suggestions.iter().take(max_suggestions).enumerate().rev() { + println!("({i}) {suggestion}"); + } + println!(); + } + + let input: String = Input::new().with_prompt(prompt).interact_text()?; + + let version: Cow<'_, str> = match usize::from_str(&input) { + Ok(index) if index < suggestions.len() && index < MAX_SUGGESTIONS => { + Cow::Borrowed(&suggestions[index]) + }, + _ => Cow::Owned(input), + }; + + println!("Using version {version:?}\n"); + + Ok(version) +} + +/// Ask for suppression reason. +fn prompt_reason() -> dialoguer::Result { + let prompt = "[ENTER] Confirm\nEnter reason for this exception"; + let reason: String = Input::new().with_prompt(prompt).interact_text()?; + println!("Using reason {reason:?}\n"); + Ok(reason) +} + +/// Ask for suppression reason. +fn prompt_removal<'a>(preferences: &'a Preferences<'a>) -> dialoguer::Result> { + let ignored_packages = preferences.ignored_packages.iter().map(|pkg| Cow::Borrowed(&*pkg.purl)); + let ignored_issues = preferences + .ignored_issues + .iter() + .map(|issue| Cow::Owned(format!("[{}] {}", issue.tag, issue.id))); + let exceptions: Vec<_> = ignored_packages.chain(ignored_issues).collect(); + + let prompt = "[ENTER] Select and Confirm\nSelect exception"; + let index = FuzzySelect::new().with_prompt(prompt).items(&exceptions).interact()?; + + println!(); + + match index.checked_sub(preferences.ignored_packages.len()) { + Some(index) => Ok(Suppression::from(&preferences.ignored_issues[index])), + None => Ok(Suppression::from(&preferences.ignored_packages[index])), + } +} diff --git a/cli/src/commands/firewall.rs b/cli/src/commands/firewall.rs index 0e98e528a..105f29c74 100644 --- a/cli/src/commands/firewall.rs +++ b/cli/src/commands/firewall.rs @@ -1,17 +1,16 @@ //! Subcommand `phylum firewall`. -use std::borrow::Cow; use std::str::FromStr; use clap::ArgMatches; -use purl::Purl; +use purl::{PackageType, Purl}; use crate::api::PhylumApi; use crate::commands::{CommandResult, ExitCode}; use crate::config::Config; use crate::format::Format; use crate::print_user_failure; -use crate::types::FirewallLogFilter; +use crate::types::{FirewallAction, FirewallLogFilter}; /// Handle `phylum firewall` subcommand. pub async fn handle_firewall( @@ -33,7 +32,7 @@ pub async fn handle_log(api: &PhylumApi, matches: &ArgMatches, config: Config) - // Get log filter args. let ecosystem = matches.get_one::("ecosystem"); let purl = matches.get_one::("package"); - let action = matches.get_one::("action"); + let action = matches.get_one::("action"); let before = matches.get_one::("before"); let after = matches.get_one::("after"); let limit = matches.get_one::("limit").unwrap(); @@ -42,26 +41,28 @@ pub async fn handle_log(api: &PhylumApi, matches: &ArgMatches, config: Config) - let parsed_purl = purl.map(|purl| Purl::from_str(purl)); let (ecosystem, namespace, name, version) = match &parsed_purl { Some(Ok(purl)) => { - let ecosystem = Cow::Owned(purl.package_type().to_string()); - (Some(ecosystem), purl.namespace(), Some(purl.name()), purl.version()) + (Some(*purl.package_type()), purl.namespace(), Some(purl.name()), purl.version()) }, Some(Err(err)) => { print_user_failure!("Could not parse purl {purl:?}: {err}"); return Ok(ExitCode::Generic); }, - None => (ecosystem.map(Cow::Borrowed), None, None, None), + None => { + let ecosystem = ecosystem.and_then(|ecosystem| PackageType::from_str(ecosystem).ok()); + (ecosystem, None, None, None) + }, }; // Construct the filter. let filter = FirewallLogFilter { - ecosystem: ecosystem.as_ref().map(|e| e.as_str()), + ecosystem, namespace, - name, version, - action: action.map(String::as_str), + name, before: before.map(String::as_str), after: after.map(String::as_str), limit: Some(*limit as i32), + action: action.copied(), }; let response = api.firewall_log(org, group, filter).await?; diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index 99dc186f1..5d35582e7 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -1,6 +1,7 @@ use std::process; pub mod auth; +pub mod exception; #[cfg(feature = "extensions")] pub mod extensions; pub mod find_dependency_files; diff --git a/cli/src/format.rs b/cli/src/format.rs index c732f1174..00b310032 100644 --- a/cli/src/format.rs +++ b/cli/src/format.rs @@ -21,7 +21,7 @@ use crate::print::{self, table_format}; use crate::types::{ FirewallAction, FirewallLogResponse, GetProjectResponse, HistoryJob, Issue, OrgMember, OrgMembersResponse, OrgsResponse, Package, PolicyEvaluationResponse, - PolicyEvaluationResponseRaw, ProjectListEntry, RiskLevel, UserToken, + PolicyEvaluationResponseRaw, Preferences, ProjectListEntry, RiskLevel, UserToken, }; // Maximum length of email column. @@ -441,6 +441,29 @@ impl Format for Vec { } } +impl Format for Preferences<'_> { + fn pretty(&self, writer: &mut W) { + let issue_exceptions = self + .ignored_issues + .iter() + .map(|issue| (format!("[{}] {}", issue.tag, issue.id), issue.reason.to_string())); + let package_exceptions = + self.ignored_packages.iter().map(|pkg| (pkg.purl.to_string(), pkg.reason.to_string())); + let exceptions: Vec<_> = issue_exceptions.chain(package_exceptions).collect(); + + if exceptions.is_empty() { + println!("No exceptions present."); + return; + } + + let table = format_table:: String, _>(&exceptions, &[ + ("Subject", |(subject, _)| subject.into()), + ("Reason", |(_, reason)| reason.to_string()), + ]); + let _ = writeln!(writer, "{table}"); + } +} + #[cfg(feature = "vulnreach")] impl Format for Vulnerability { fn pretty(&self, writer: &mut W) { diff --git a/cli/src/types.rs b/cli/src/types.rs index d3ee7a93a..9d7461b37 100644 --- a/cli/src/types.rs +++ b/cli/src/types.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::fmt::{self, Display, Formatter}; use std::str::FromStr; @@ -549,13 +550,27 @@ pub enum FirewallAction { AnalysisWarning, } +impl FromStr for FirewallAction { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "Download" => Ok(Self::Download), + "AnalysisSuccess" => Ok(Self::AnalysisSuccess), + "AnalysisFailure" => Ok(Self::AnalysisFailure), + "AnalysisWarning" => Ok(Self::AnalysisWarning), + _ => Err(()), + } + } +} + /// Aviary log package. #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, Serialize, Deserialize)] pub struct FirewallPackage { pub ecosystem: String, pub name: String, - pub namespace: String, pub version: String, + pub namespace: String, } impl FirewallPackage { @@ -572,12 +587,62 @@ impl FirewallPackage { /// Aviary log filter query. #[derive(Serialize, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, Default)] pub struct FirewallLogFilter<'a> { - pub ecosystem: Option<&'a str>, + pub ecosystem: Option, pub namespace: Option<&'a str>, pub name: Option<&'a str>, pub version: Option<&'a str>, - pub action: Option<&'a str>, + pub action: Option, pub before: Option<&'a str>, pub after: Option<&'a str>, pub limit: Option, } + +/// Project/Group preferences. +#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, Default)] +pub struct Preferences<'a> { + #[serde(rename = "ignoredIssues", default)] + pub ignored_issues: Vec>, + #[serde(rename = "ignoredPackages", default)] + pub ignored_packages: Vec>, +} + +/// Project/Group preferences ignored issues. +#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, Default)] +pub struct IgnoredIssue<'a> { + pub id: Cow<'a, str>, + pub tag: Cow<'a, str>, + pub reason: Cow<'a, str>, +} + +/// Project/Group preferences ignored packages. +#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, Default)] +pub struct IgnoredPackage<'a> { + pub purl: Cow<'a, str>, + pub reason: Cow<'a, str>, +} + +/// Suppression request variants. +#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)] +pub enum Suppression<'a> { + Package(IgnoredPackage<'a>), + Issue(IgnoredIssue<'a>), +} + +impl<'a> From<&'a IgnoredIssue<'a>> for Suppression<'a> { + fn from(issue: &'a IgnoredIssue<'a>) -> Self { + Self::Issue(IgnoredIssue { + id: Cow::Borrowed(&issue.id), + tag: Cow::Borrowed(&issue.tag), + reason: Cow::Borrowed(&issue.reason), + }) + } +} + +impl<'a> From<&'a IgnoredPackage<'a>> for Suppression<'a> { + fn from(package: &'a IgnoredPackage<'a>) -> Self { + Self::Package(IgnoredPackage { + purl: Cow::Borrowed(&package.purl), + reason: Cow::Borrowed(&package.reason), + }) + } +} From b20070a6e928b3afb94c134b0f5021de8e03aa8e Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Tue, 17 Dec 2024 23:32:32 +0100 Subject: [PATCH 03/11] Regenerate documentation --- docs/commands/phylum.md | 1 + docs/commands/phylum_exception.md | 27 ++++++++++++++ docs/commands/phylum_exception_add.md | 46 ++++++++++++++++++++++++ docs/commands/phylum_exception_list.md | 30 ++++++++++++++++ docs/commands/phylum_exception_remove.md | 46 ++++++++++++++++++++++++ docs/commands/phylum_firewall_log.md | 2 +- 6 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 docs/commands/phylum_exception.md create mode 100644 docs/commands/phylum_exception_add.md create mode 100644 docs/commands/phylum_exception_list.md create mode 100644 docs/commands/phylum_exception_remove.md diff --git a/docs/commands/phylum.md b/docs/commands/phylum.md index f6eff34a9..0b6543f69 100644 --- a/docs/commands/phylum.md +++ b/docs/commands/phylum.md @@ -39,6 +39,7 @@ Usage: phylum [OPTIONS] [COMMAND] * [phylum analyze](./phylum_analyze.md) * [phylum auth](./phylum_auth.md) +* [phylum exception](./phylum_exception.md) * [phylum extension](./phylum_extension.md) * [phylum firewall](./phylum_firewall.md) * [phylum group](./phylum_group.md) diff --git a/docs/commands/phylum_exception.md b/docs/commands/phylum_exception.md new file mode 100644 index 000000000..adc4e67a9 --- /dev/null +++ b/docs/commands/phylum_exception.md @@ -0,0 +1,27 @@ +# phylum exception + +Manage analysis exceptions + +```sh +Usage: phylum exception [OPTIONS] +``` + +## Options + +`-o`, `--org` `` +  Phylum organization + +`-v`, `--verbose`... +  Increase the level of verbosity (the maximum is -vvv) + +`-q`, `--quiet`... +  Reduce the level of verbosity (the maximum is -qq) + +`-h`, `--help` +  Print help + +## Commands + +* [phylum exception add](./phylum_exception_add.md) +* [phylum exception list](./phylum_exception_list.md) +* [phylum exception remove](./phylum_exception_remove.md) diff --git a/docs/commands/phylum_exception_add.md b/docs/commands/phylum_exception_add.md new file mode 100644 index 000000000..089eec3d6 --- /dev/null +++ b/docs/commands/phylum_exception_add.md @@ -0,0 +1,46 @@ +# phylum exception add + +Add a new analysis exception + +```sh +Usage: phylum exception add [OPTIONS] <--group |--project > +``` + +## Options + +`-g`, `--group` `` +  Group to add exception to + +`-p`, `--project` `` +  Project to add exceptions to + +`-e`, `--ecosystem` `` +  Ecosystem of the package to add an exception for +  Accepted values: `npm`, `rubygems`, `pypi`, `maven`, `nuget`, `golang`, `cargo` + +`-n`, `--name` `` +  Name and optional namespace of the package to add an exception for + +`--version` `` +  Version of the package to add an exception for + +`--purl` `` +  Package in PURL format + +`-r`, `--reason` `` +  Reason for adding this exception + +`-s`, `--no-suggestions` +  Do not query package firewall to make suggestions + +`-o`, `--org` `` +  Phylum organization + +`-v`, `--verbose`... +  Increase the level of verbosity (the maximum is -vvv) + +`-q`, `--quiet`... +  Reduce the level of verbosity (the maximum is -qq) + +`-h`, `--help` +  Print help diff --git a/docs/commands/phylum_exception_list.md b/docs/commands/phylum_exception_list.md new file mode 100644 index 000000000..2b4b3720c --- /dev/null +++ b/docs/commands/phylum_exception_list.md @@ -0,0 +1,30 @@ +# phylum exception list + +List active analysis exceptions + +```sh +Usage: phylum exception list [OPTIONS] <--group |--project > +``` + +## Options + +`-j`, `--json` +  Produce output in json format (default: false) + +`-g`, `--group` `` +  Group to list exceptions for + +`-p`, `--project` `` +  Project to list exceptions for + +`-o`, `--org` `` +  Phylum organization + +`-v`, `--verbose`... +  Increase the level of verbosity (the maximum is -vvv) + +`-q`, `--quiet`... +  Reduce the level of verbosity (the maximum is -qq) + +`-h`, `--help` +  Print help diff --git a/docs/commands/phylum_exception_remove.md b/docs/commands/phylum_exception_remove.md new file mode 100644 index 000000000..537b6b07f --- /dev/null +++ b/docs/commands/phylum_exception_remove.md @@ -0,0 +1,46 @@ +# phylum exception remove + +Remove an existing analysis exception + +```sh +Usage: phylum exception remove [OPTIONS] <--group |--project > +``` + +## Options + +`-g`, `--group` `` +  Group to add exception to + +`-p`, `--project` `` +  Project to add exceptions to + +`-e`, `--ecosystem` `` +  Ecosystem of the exception which should be removed +  Accepted values: `npm`, `rubygems`, `pypi`, `maven`, `nuget`, `golang`, `cargo` + +`-n`, `--name` `` +  Package name and optional namespace of the exception which should be removed + +`--version` `` +  Package version of the exception which should be removed + +`--purl` `` +  Package in PURL format + +`--id` `` +  Issue ID of the exception which should be removed + +`--tag` `` +  Issue tag of the exception which should be removed + +`-o`, `--org` `` +  Phylum organization + +`-v`, `--verbose`... +  Increase the level of verbosity (the maximum is -vvv) + +`-q`, `--quiet`... +  Reduce the level of verbosity (the maximum is -qq) + +`-h`, `--help` +  Print help diff --git a/docs/commands/phylum_firewall_log.md b/docs/commands/phylum_firewall_log.md index 83e00e81d..02827c37a 100644 --- a/docs/commands/phylum_firewall_log.md +++ b/docs/commands/phylum_firewall_log.md @@ -9,7 +9,7 @@ Usage: phylum firewall log [OPTIONS] ## Arguments `` -  Specify a group to use for analysis +  Firewall group to list log activity for ## Options From 6a87dca3cf6ee41485907993ad9e9be5330e4096 Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Wed, 18 Dec 2024 00:43:03 +0100 Subject: [PATCH 04/11] Remove last TODO An issue was created to handle this separately: https://github.com/phylum-dev/cli/issues/1558 --- cli/src/commands/exception.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cli/src/commands/exception.rs b/cli/src/commands/exception.rs index 38016db31..b31e08d6d 100644 --- a/cli/src/commands/exception.rs +++ b/cli/src/commands/exception.rs @@ -74,10 +74,6 @@ pub async fn handle_add(api: &PhylumApi, matches: &ArgMatches, config: Config) - None => purl_from_components(api, ecosystem, name, group, org, !no_suggestions).await?, }; - // TODO: Show short description of the issues for each version. - // => Create issue. - // => Maybe also provide full list while prompting for reason? - // // Get suggested versions from Aviary if no argument was supplied. let version = purl.version().or(version.map(String::as_str)); let mut suggested_versions = IndexSet::new(); From 797e4f6868522ceef48c05c2a36d8c2899d82324 Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Wed, 18 Dec 2024 23:19:00 +0100 Subject: [PATCH 05/11] Fix CLI arguments --- cli/Cargo.toml | 2 +- cli/src/app.rs | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index f31cf1097..800588222 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -38,6 +38,7 @@ git2 = { version = "0.19.0", default-features = false } git-version = "0.3.5" home = "0.5.3" ignore = { version = "0.4.20", optional = true } +indexmap = "2.7.0" lazy_static = "1.4.0" libc = "0.2.135" log = "0.4.6" @@ -70,7 +71,6 @@ vuln-reach = { git = "https://github.com/phylum-dev/vuln-reach", optional = true vulnreach_types = { path = "../vulnreach_types", optional = true } walkdir = "2.3.2" zip = { version = "2.1.0", default-features = false, features = ["deflate"] } -indexmap = "2.7.0" [target.'cfg(unix)'.dependencies] birdcage = { version = "0.8.1" } diff --git a/cli/src/app.rs b/cli/src/app.rs index e3a77b41b..a4fdd4716 100644 --- a/cli/src/app.rs +++ b/cli/src/app.rs @@ -712,8 +712,7 @@ pub fn add_subcommands(command: Command) -> Command { .long("name") .value_name("PACKAGE_NAME") .help( - "Name and optional namespace of the package to add an \ - exception for", + "Fully qualified name of the package to add an exception for", ), Arg::new("version") .long("version") @@ -751,12 +750,12 @@ pub fn add_subcommands(command: Command) -> Command { .short('g') .long("group") .value_name("GROUP_NAME") - .help("Group to add exception to"), + .help("Group to remove exception from"), Arg::new("project") .short('p') .long("project") .value_name("PROJECT_NAME") - .help("Project to add exceptions to"), + .help("Project to remove exceptions from"), Arg::new("ecosystem") .short('e') .long("ecosystem") @@ -770,8 +769,8 @@ pub fn add_subcommands(command: Command) -> Command { .long("name") .value_name("PACKAGE_NAME") .help( - "Package name and optional namespace of the exception which \ - should be removed", + "Fully qualified package name of the exception which should \ + be removed", ), Arg::new("version") .long("version") From 14640b006ddc5c89bb732ba0bbd81aaf65cd4ea2 Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Wed, 18 Dec 2024 23:21:34 +0100 Subject: [PATCH 06/11] Fix minor style issues --- cli/src/commands/exception.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/src/commands/exception.rs b/cli/src/commands/exception.rs index b31e08d6d..2f4bbae9f 100644 --- a/cli/src/commands/exception.rs +++ b/cli/src/commands/exception.rs @@ -108,7 +108,7 @@ pub async fn handle_add(api: &PhylumApi, matches: &ArgMatches, config: Config) - }; // Build suppression API object. - let suppressions = vec![Suppression::Package(IgnoredPackage { + let suppressions = [Suppression::Package(IgnoredPackage { purl: Cow::Owned(purl.to_string()), reason: Cow::Borrowed(&reason), })]; @@ -122,7 +122,7 @@ pub async fn handle_add(api: &PhylumApi, matches: &ArgMatches, config: Config) - None => api.group_suppress(org, group.unwrap(), &suppressions).await?, } - print_user_success!("Successfully added suppression for {}", purl.to_string()); + print_user_success!("Successfully added suppression for {}", purl); Ok(ExitCode::Ok) } @@ -186,7 +186,7 @@ pub async fn handle_remove(api: &PhylumApi, matches: &ArgMatches, config: Config }); } - let unsuppressions = vec![prompt_removal(&exceptions)?]; + let unsuppressions = [prompt_removal(&exceptions)?]; match project { Some(project_name) => { @@ -199,7 +199,7 @@ pub async fn handle_remove(api: &PhylumApi, matches: &ArgMatches, config: Config match &unsuppressions[0] { Suppression::Package(IgnoredPackage { purl, .. }) => { - print_user_success!("Successfully removed suppression for package {purl:?}"); + print_user_success!("Successfully removed suppression for package {purl}"); }, Suppression::Issue(IgnoredIssue { id, tag, .. }) => { print_user_success!("Successfully removed suppression for issue {tag:?} [{id}]"); From a1804a830fd66e0ef8b7a36f208fbc99130f9986 Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Wed, 18 Dec 2024 23:35:10 +0100 Subject: [PATCH 07/11] Fix incorrect ecosystem usage --- cli/src/app.rs | 44 +++++++++++++++--------------- cli/src/commands/exception.rs | 51 +++++++++++++++++------------------ cli/src/commands/firewall.rs | 12 ++++----- 3 files changed, 52 insertions(+), 55 deletions(-) diff --git a/cli/src/app.rs b/cli/src/app.rs index a4fdd4716..14ce580fb 100644 --- a/cli/src/app.rs +++ b/cli/src/app.rs @@ -620,16 +620,16 @@ pub fn add_subcommands(command: Command) -> Command { .value_name("GROUP_NAME") .help("Firewall group to list log activity for") .required(true), - Arg::new("ecosystem") - .long("ecosystem") - .value_name("ECOSYSTEM") - .help("Only show logs matching this ecosystem") - .value_parser(["npm", "rubygems", "pypi", "maven", "nuget", "cargo"]), - Arg::new("package") - .long("package") + Arg::new("package-type") + .long("package-type") + .value_name("PACKAGE_TYPE") + .help("Only show logs matching this package type") + .value_parser(["npm", "gem", "pypi", "maven", "nuget", "cargo"]), + Arg::new("purl") + .long("purl") .value_name("PURL") .help("Only show logs matching this PURL") - .conflicts_with("ecosystem"), + .conflicts_with("package-type"), Arg::new("action") .long("action") .value_name("ACTION") @@ -699,13 +699,12 @@ pub fn add_subcommands(command: Command) -> Command { .long("project") .value_name("PROJECT_NAME") .help("Project to add exceptions to"), - Arg::new("ecosystem") - .short('e') - .long("ecosystem") - .value_name("ECOSYSTEM") - .help("Ecosystem of the package to add an exception for") + Arg::new("package-type") + .long("package-type") + .value_name("PACKAGE_TYPE") + .help("Package type of the package to add an exception for") .value_parser([ - "npm", "rubygems", "pypi", "maven", "nuget", "golang", "cargo", + "npm", "gem", "pypi", "maven", "nuget", "golang", "cargo", ]), Arg::new("name") .short('n') @@ -722,7 +721,7 @@ pub fn add_subcommands(command: Command) -> Command { .long("purl") .value_name("PURL") .help("Package in PURL format") - .conflicts_with_all(["ecosystem", "name", "version"]), + .conflicts_with_all(["package-type", "name", "version"]), Arg::new("reason") .short('r') .long("reason") @@ -741,7 +740,7 @@ pub fn add_subcommands(command: Command) -> Command { .group(ArgGroup::new("subject").args(["group", "project"]).required(true)) .group( ArgGroup::new("package") - .args(["ecosystem", "name", "version", "purl"]) + .args(["package-type", "name", "version", "purl"]) .conflicts_with("issue"), ) .group(ArgGroup::new("issue").args(["id", "tag"])) @@ -756,13 +755,12 @@ pub fn add_subcommands(command: Command) -> Command { .long("project") .value_name("PROJECT_NAME") .help("Project to remove exceptions from"), - Arg::new("ecosystem") - .short('e') - .long("ecosystem") - .value_name("ECOSYSTEM") - .help("Ecosystem of the exception which should be removed") + Arg::new("package-type") + .long("package-type") + .value_name("PACKAGE_TYPE") + .help("Package type of the exception which should be removed") .value_parser([ - "npm", "rubygems", "pypi", "maven", "nuget", "golang", "cargo", + "npm", "gem", "pypi", "maven", "nuget", "golang", "cargo", ]), Arg::new("name") .short('n') @@ -780,7 +778,7 @@ pub fn add_subcommands(command: Command) -> Command { .long("purl") .value_name("PURL") .help("Package in PURL format") - .conflicts_with_all(["ecosystem", "name", "version"]), + .conflicts_with_all(["package-type", "name", "version"]), Arg::new("id") .long("id") .value_name("ISSUE_ID") diff --git a/cli/src/commands/exception.rs b/cli/src/commands/exception.rs index 2f4bbae9f..d4aa6b91e 100644 --- a/cli/src/commands/exception.rs +++ b/cli/src/commands/exception.rs @@ -59,7 +59,7 @@ pub async fn handle_list(api: &PhylumApi, matches: &ArgMatches, config: Config) /// Handle `phylum exception add` subcommand. pub async fn handle_add(api: &PhylumApi, matches: &ArgMatches, config: Config) -> CommandResult { let no_suggestions = matches.get_flag("no-suggestions"); - let ecosystem = matches.get_one::("ecosystem"); + let package_type = matches.get_one::("package-type"); let project = matches.get_one::("project"); let version = matches.get_one::("version"); let reason = matches.get_one::("reason"); @@ -71,7 +71,7 @@ pub async fn handle_add(api: &PhylumApi, matches: &ArgMatches, config: Config) - // Parse PURL argument or assemble it from its components. let mut purl = match purl { Some(purl) => Purl::from_str(purl)?, - None => purl_from_components(api, ecosystem, name, group, org, !no_suggestions).await?, + None => purl_from_components(api, package_type, name, group, org, !no_suggestions).await?, }; // Get suggested versions from Aviary if no argument was supplied. @@ -129,7 +129,7 @@ pub async fn handle_add(api: &PhylumApi, matches: &ArgMatches, config: Config) - /// Handle `phylum exception remove` subcommand. pub async fn handle_remove(api: &PhylumApi, matches: &ArgMatches, config: Config) -> CommandResult { - let ecosystem = matches.get_one::("ecosystem"); + let package_type = matches.get_one::("package-type"); let project = matches.get_one::("project"); let version = matches.get_one::("version"); let group = matches.get_one::("group"); @@ -156,16 +156,16 @@ pub async fn handle_remove(api: &PhylumApi, matches: &ArgMatches, config: Config } // Filter package suppressions with CLI args. - if ecosystem.is_some() || name.is_some() || version.is_some() || purl.is_some() { + if package_type.is_some() || name.is_some() || version.is_some() || purl.is_some() { let purl = purl.map(|purl| Purl::from_str(purl)); - let (ecosystem, namespace, name, version) = match purl { + let (package_type, namespace, name, version) = match purl { Some(Ok(ref purl)) => { let package_type = Cow::Owned(purl.package_type().to_string()); (Some(package_type), purl.namespace(), Some(purl.name()), purl.version()) }, Some(Err(err)) => return Err(err.into()), None => ( - ecosystem.map(Cow::Borrowed), + package_type.map(Cow::Borrowed), None, name.map(String::as_str), version.map(String::as_str), @@ -178,8 +178,8 @@ pub async fn handle_remove(api: &PhylumApi, matches: &ArgMatches, config: Config Err(_) => return false, }; - let package_type = purl.package_type().to_string(); - ecosystem.as_ref().is_none_or(|ecosystem| **ecosystem == package_type) + let purl_package_type = purl.package_type().to_string(); + package_type.as_ref().is_none_or(|package_type| **package_type == purl_package_type) && namespace.is_none_or(|namespace| Some(namespace) == purl.namespace()) && name.is_none_or(|name| name == purl.name()) && version.is_none_or(|version| Some(version) == purl.version()) @@ -212,18 +212,17 @@ pub async fn handle_remove(api: &PhylumApi, matches: &ArgMatches, config: Config /// Creat a PURL from its individual components. async fn purl_from_components( api: &PhylumApi, - cli_ecosystem: Option<&String>, + cli_package_type: Option<&String>, cli_name: Option<&String>, group: Option<&String>, org: Option<&str>, suggestions: bool, ) -> anyhow::Result { - // Prompt for ecosystem if it wasn't supplied as an argument. - let ecosystem = match cli_ecosystem { - Some(ecosystem) => ecosystem.into(), - None => Cow::Owned(prompt_ecosystem()?), + // Prompt for package type if it wasn't supplied as an argument. + let package_type = match cli_package_type { + Some(package_type) => PackageType::from_str(package_type)?, + None => prompt_package_type()?, }; - let ecosystem = PackageType::from_str(&ecosystem).unwrap(); // Get suggested names from Aviary if no argument was supplied. let mut suggested_names: IndexSet = IndexSet::new(); @@ -231,13 +230,13 @@ async fn purl_from_components( let spinner = Spinner::new(); let filter = FirewallLogFilter { - ecosystem: Some(ecosystem), + ecosystem: Some(package_type), action: Some(FirewallAction::AnalysisFailure), ..Default::default() }; if let Ok(logs) = api.firewall_log(org, group, filter).await { for log in logs.data { - let purl = Purl::builder(ecosystem, log.package.name) + let purl = Purl::builder(package_type, log.package.name) .with_namespace(log.package.namespace) .build()?; suggested_names.insert(purl); @@ -249,27 +248,27 @@ async fn purl_from_components( // Prompt for name if it wasn't supplied as an argument. let purl = match cli_name { - Some(name) => Purl::builder_with_combined_name(ecosystem, name).build()?, - None => prompt_name(ecosystem, &suggested_names)?, + Some(name) => Purl::builder_with_combined_name(package_type, name).build()?, + None => prompt_name(package_type, &suggested_names)?, }; Ok(purl) } -/// Ask for an ecosystem. -fn prompt_ecosystem() -> dialoguer::Result { - let ecosystems = ["cargo", "gem", "golang", "maven", "npm", "nuget", "pypi"]; +/// Ask for a package type. +fn prompt_package_type() -> dialoguer::Result { + let package_types = ["cargo", "gem", "golang", "maven", "npm", "nuget", "pypi"]; - let prompt = "[ENTER] Select and Confirm\nSelect ecosystem"; - let index = FuzzySelect::new().with_prompt(prompt).items(&ecosystems).interact()?; + let prompt = "[ENTER] Select and Confirm\nSelect package type"; + let index = FuzzySelect::new().with_prompt(prompt).items(&package_types).interact()?; println!(); - Ok(ecosystems[index].to_owned()) + Ok(PackageType::from_str(package_types[index]).unwrap()) } /// Ask for a package name. -fn prompt_name(ecosystem: PackageType, suggestions: &'_ IndexSet) -> anyhow::Result { +fn prompt_name(package_type: PackageType, suggestions: &'_ IndexSet) -> anyhow::Result { // Get space available for suggestions. let term_size = Term::stdout().size_checked().unwrap_or((u16::MAX, u16::MAX)); let max_suggestions = (term_size.0 as usize - 3).min(MAX_SUGGESTIONS); @@ -292,7 +291,7 @@ fn prompt_name(ecosystem: PackageType, suggestions: &'_ IndexSet) -> anyho Ok(index) if index < suggestions.len() && index < MAX_SUGGESTIONS => { suggestions[index].clone() }, - _ => Purl::builder_with_combined_name(ecosystem, &input).build()?, + _ => Purl::builder_with_combined_name(package_type, &input).build()?, }; println!("Using package {}\n", purl.combined_name()); diff --git a/cli/src/commands/firewall.rs b/cli/src/commands/firewall.rs index 105f29c74..db9cd8e96 100644 --- a/cli/src/commands/firewall.rs +++ b/cli/src/commands/firewall.rs @@ -30,16 +30,16 @@ pub async fn handle_log(api: &PhylumApi, matches: &ArgMatches, config: Config) - let group = matches.get_one::("group").unwrap(); // Get log filter args. - let ecosystem = matches.get_one::("ecosystem"); - let purl = matches.get_one::("package"); + let package_type = matches.get_one::("package-type"); let action = matches.get_one::("action"); let before = matches.get_one::("before"); let after = matches.get_one::("after"); + let purl = matches.get_one::("purl"); let limit = matches.get_one::("limit").unwrap(); // Parse PURL filter. let parsed_purl = purl.map(|purl| Purl::from_str(purl)); - let (ecosystem, namespace, name, version) = match &parsed_purl { + let (package_type, namespace, name, version) = match &parsed_purl { Some(Ok(purl)) => { (Some(*purl.package_type()), purl.namespace(), Some(purl.name()), purl.version()) }, @@ -48,20 +48,20 @@ pub async fn handle_log(api: &PhylumApi, matches: &ArgMatches, config: Config) - return Ok(ExitCode::Generic); }, None => { - let ecosystem = ecosystem.and_then(|ecosystem| PackageType::from_str(ecosystem).ok()); - (ecosystem, None, None, None) + let package_type = package_type.and_then(|pt| PackageType::from_str(pt).ok()); + (package_type, None, None, None) }, }; // Construct the filter. let filter = FirewallLogFilter { - ecosystem, namespace, version, name, before: before.map(String::as_str), after: after.map(String::as_str), limit: Some(*limit as i32), + ecosystem: package_type, action: action.copied(), }; From 082ac407352fcbbe592cdd362c6ea522c910ebea Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Wed, 18 Dec 2024 23:42:47 +0100 Subject: [PATCH 08/11] Regenerate documentation --- docs/commands/phylum_exception_add.md | 8 ++++---- docs/commands/phylum_exception_remove.md | 12 ++++++------ docs/commands/phylum_firewall_log.md | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/commands/phylum_exception_add.md b/docs/commands/phylum_exception_add.md index 089eec3d6..2fa1ed215 100644 --- a/docs/commands/phylum_exception_add.md +++ b/docs/commands/phylum_exception_add.md @@ -14,12 +14,12 @@ Usage: phylum exception add [OPTIONS] <--group |--project `   Project to add exceptions to -`-e`, `--ecosystem` `` -  Ecosystem of the package to add an exception for -  Accepted values: `npm`, `rubygems`, `pypi`, `maven`, `nuget`, `golang`, `cargo` +`--package-type` `` +  Package type of the package to add an exception for +  Accepted values: `npm`, `gem`, `pypi`, `maven`, `nuget`, `golang`, `cargo` `-n`, `--name` `` -  Name and optional namespace of the package to add an exception for +  Fully qualified name of the package to add an exception for `--version` ``   Version of the package to add an exception for diff --git a/docs/commands/phylum_exception_remove.md b/docs/commands/phylum_exception_remove.md index 537b6b07f..d64ec9f7e 100644 --- a/docs/commands/phylum_exception_remove.md +++ b/docs/commands/phylum_exception_remove.md @@ -9,17 +9,17 @@ Usage: phylum exception remove [OPTIONS] <--group |--project ` -  Group to add exception to +  Group to remove exception from `-p`, `--project` `` -  Project to add exceptions to +  Project to remove exceptions from -`-e`, `--ecosystem` `` -  Ecosystem of the exception which should be removed -  Accepted values: `npm`, `rubygems`, `pypi`, `maven`, `nuget`, `golang`, `cargo` +`--package-type` `` +  Package type of the exception which should be removed +  Accepted values: `npm`, `gem`, `pypi`, `maven`, `nuget`, `golang`, `cargo` `-n`, `--name` `` -  Package name and optional namespace of the exception which should be removed +  Fully qualified package name of the exception which should be removed `--version` ``   Package version of the exception which should be removed diff --git a/docs/commands/phylum_firewall_log.md b/docs/commands/phylum_firewall_log.md index 02827c37a..496d15b0b 100644 --- a/docs/commands/phylum_firewall_log.md +++ b/docs/commands/phylum_firewall_log.md @@ -16,11 +16,11 @@ Usage: phylum firewall log [OPTIONS] `-j`, `--json`   Produce output in json format (default: false) -`--ecosystem` `` -  Only show logs matching this ecosystem -  Accepted values: `npm`, `rubygems`, `pypi`, `maven`, `nuget`, `cargo` +`--package-type` `` +  Only show logs matching this package type +  Accepted values: `npm`, `gem`, `pypi`, `maven`, `nuget`, `cargo` -`--package` `` +`--purl` ``   Only show logs matching this PURL `--action` `` From e65c66047cc7c6e6c272b15cb7d62b3ab38eac86 Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Thu, 19 Dec 2024 17:51:39 +0100 Subject: [PATCH 09/11] Add dynamic namespace resolution --- cli/src/commands/exception.rs | 65 ++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 17 deletions(-) diff --git a/cli/src/commands/exception.rs b/cli/src/commands/exception.rs index d4aa6b91e..e02014655 100644 --- a/cli/src/commands/exception.rs +++ b/cli/src/commands/exception.rs @@ -1,6 +1,7 @@ //! Subcommand `phylum exception`. use std::borrow::Cow; +use std::slice; use std::str::FromStr; use clap::ArgMatches; @@ -13,11 +14,11 @@ use crate::api::PhylumApi; use crate::commands::{CommandResult, ExitCode}; use crate::config::Config; use crate::format::Format; -use crate::print_user_success; use crate::spinner::Spinner; use crate::types::{ FirewallAction, FirewallLogFilter, IgnoredIssue, IgnoredPackage, Preferences, Suppression, }; +use crate::{print_user_success, print_user_warning}; /// Maximum number of package names or versions proposed for exceptions. const MAX_SUGGESTIONS: usize = 25; @@ -158,18 +159,14 @@ pub async fn handle_remove(api: &PhylumApi, matches: &ArgMatches, config: Config // Filter package suppressions with CLI args. if package_type.is_some() || name.is_some() || version.is_some() || purl.is_some() { let purl = purl.map(|purl| Purl::from_str(purl)); - let (package_type, namespace, name, version) = match purl { - Some(Ok(ref purl)) => { - let package_type = Cow::Owned(purl.package_type().to_string()); - (Some(package_type), purl.namespace(), Some(purl.name()), purl.version()) - }, + let (packages, version) = match purl { + Some(Ok(ref purl)) => (vec![purl.clone()], purl.version()), Some(Err(err)) => return Err(err.into()), - None => ( - package_type.map(Cow::Borrowed), - None, - name.map(String::as_str), - version.map(String::as_str), - ), + None => { + let packages = + name.map(|name| possible_packages(package_type, name)).unwrap_or_default(); + (packages, version.map(String::as_str)) + }, }; exceptions.ignored_packages.retain(|pkg| { @@ -178,14 +175,22 @@ pub async fn handle_remove(api: &PhylumApi, matches: &ArgMatches, config: Config Err(_) => return false, }; - let purl_package_type = purl.package_type().to_string(); - package_type.as_ref().is_none_or(|package_type| **package_type == purl_package_type) - && namespace.is_none_or(|namespace| Some(namespace) == purl.namespace()) - && name.is_none_or(|name| name == purl.name()) - && version.is_none_or(|version| Some(version) == purl.version()) + version.is_none_or(|version| Some(version) == purl.version()) + && (packages.is_empty() + || packages.iter().any(|pkg| { + pkg.package_type() == purl.package_type() + && pkg.name() == purl.name() + && pkg.namespace().is_none_or(|ns| Some(ns) == purl.namespace()) + })) }); } + // Abort if no matching exceptions were found. + if exceptions.ignored_packages.is_empty() && exceptions.ignored_issues.is_empty() { + print_user_warning!("No existing exception matches the active filter."); + return Ok(ExitCode::Ok); + } + let unsuppressions = [prompt_removal(&exceptions)?]; match project { @@ -358,3 +363,29 @@ fn prompt_removal<'a>(preferences: &'a Preferences<'a>) -> dialoguer::Result Ok(Suppression::from(&preferences.ignored_packages[index])), } } + +/// Find all possible package type, namespace, and name combination for a +/// combined package name and optional package type. +fn possible_packages(package_type: Option<&String>, combined_name: &str) -> Vec { + let package_type = package_type.and_then(|pt| PackageType::from_str(pt).ok()); + let package_types = package_type.as_ref().map_or( + [ + PackageType::Cargo, + PackageType::Gem, + PackageType::Golang, + PackageType::Maven, + PackageType::Npm, + PackageType::NuGet, + PackageType::PyPI, + ] + .as_slice(), + |pt| slice::from_ref(pt), + ); + + package_types + .iter() + .filter_map(|package_type| { + Purl::builder_with_combined_name(*package_type, combined_name).build().ok() + }) + .collect() +} From 4f634c9cc01f0fd3ca9e0f415f56f0ee827c0109 Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Thu, 19 Dec 2024 19:48:44 +0100 Subject: [PATCH 10/11] Simplify filter logic --- cli/src/commands/exception.rs | 50 +++++++++-------------------------- 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/cli/src/commands/exception.rs b/cli/src/commands/exception.rs index e02014655..2335bd6ae 100644 --- a/cli/src/commands/exception.rs +++ b/cli/src/commands/exception.rs @@ -1,7 +1,6 @@ //! Subcommand `phylum exception`. use std::borrow::Cow; -use std::slice; use std::str::FromStr; use clap::ArgMatches; @@ -159,13 +158,18 @@ pub async fn handle_remove(api: &PhylumApi, matches: &ArgMatches, config: Config // Filter package suppressions with CLI args. if package_type.is_some() || name.is_some() || version.is_some() || purl.is_some() { let purl = purl.map(|purl| Purl::from_str(purl)); - let (packages, version) = match purl { - Some(Ok(ref purl)) => (vec![purl.clone()], purl.version()), + let (package_type, combined_name, version) = match purl { + Some(Ok(ref purl)) => { + (Some(*purl.package_type()), Some(purl.combined_name()), purl.version()) + }, Some(Err(err)) => return Err(err.into()), None => { - let packages = - name.map(|name| possible_packages(package_type, name)).unwrap_or_default(); - (packages, version.map(String::as_str)) + let package_type = match package_type { + Some(package_type) => Some(PackageType::from_str(&package_type)?), + None => None, + }; + let name = name.map(|name| Cow::Borrowed(name.as_str())); + (package_type, name, version.map(String::as_str)) }, }; @@ -176,12 +180,8 @@ pub async fn handle_remove(api: &PhylumApi, matches: &ArgMatches, config: Config }; version.is_none_or(|version| Some(version) == purl.version()) - && (packages.is_empty() - || packages.iter().any(|pkg| { - pkg.package_type() == purl.package_type() - && pkg.name() == purl.name() - && pkg.namespace().is_none_or(|ns| Some(ns) == purl.namespace()) - })) + && combined_name.as_ref().is_none_or(|name| *name == purl.combined_name()) + && package_type.is_none_or(|pt| pt == *purl.package_type()) }); } @@ -363,29 +363,3 @@ fn prompt_removal<'a>(preferences: &'a Preferences<'a>) -> dialoguer::Result Ok(Suppression::from(&preferences.ignored_packages[index])), } } - -/// Find all possible package type, namespace, and name combination for a -/// combined package name and optional package type. -fn possible_packages(package_type: Option<&String>, combined_name: &str) -> Vec { - let package_type = package_type.and_then(|pt| PackageType::from_str(pt).ok()); - let package_types = package_type.as_ref().map_or( - [ - PackageType::Cargo, - PackageType::Gem, - PackageType::Golang, - PackageType::Maven, - PackageType::Npm, - PackageType::NuGet, - PackageType::PyPI, - ] - .as_slice(), - |pt| slice::from_ref(pt), - ); - - package_types - .iter() - .filter_map(|package_type| { - Purl::builder_with_combined_name(*package_type, combined_name).build().ok() - }) - .collect() -} From 61ecc3e6999253568ed7f960ad368efb8347a312 Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Thu, 19 Dec 2024 19:54:48 +0100 Subject: [PATCH 11/11] Fix clippy lints --- cli/src/commands/exception.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/commands/exception.rs b/cli/src/commands/exception.rs index 2335bd6ae..ca2919eed 100644 --- a/cli/src/commands/exception.rs +++ b/cli/src/commands/exception.rs @@ -165,7 +165,7 @@ pub async fn handle_remove(api: &PhylumApi, matches: &ArgMatches, config: Config Some(Err(err)) => return Err(err.into()), None => { let package_type = match package_type { - Some(package_type) => Some(PackageType::from_str(&package_type)?), + Some(package_type) => Some(PackageType::from_str(package_type)?), None => None, }; let name = name.map(|name| Cow::Borrowed(name.as_str()));