From 0b54f8c26f8fbe7581aebdfd1e6ae007cb27615a Mon Sep 17 00:00:00 2001 From: Michael Chernicoff Date: Tue, 30 Jul 2024 14:17:15 -0400 Subject: [PATCH] fix: Hipcheck now works with scoped npm packages. --- hipcheck/src/cli.rs | 7 +-- hipcheck/src/session/pm.rs | 90 +++++++++++++++++++++++++++++++++++--- hipcheck/src/target.rs | 9 +++- 3 files changed, 96 insertions(+), 10 deletions(-) diff --git a/hipcheck/src/cli.rs b/hipcheck/src/cli.rs index 151703bd..08a31515 100644 --- a/hipcheck/src/cli.rs +++ b/hipcheck/src/cli.rs @@ -568,9 +568,10 @@ impl ToTargetSeed for CheckNpmArgs { _ => pm::extract_package_version(raw_package)?, }; + // If the package is scoped, replace the leading '@' in the scope with %40 for proper pURL formatting let purl = Url::parse(&match version.as_str() { - "no version" => format!("pkg:npm/{}", name), - _ => format!("pkg:npm/{}@{}", name, version), + "no version" => format!("pkg:npm/{}", str::replace(&name, '@', "%40")), + _ => format!("pkg:npm/{}@{}", str::replace(&name, '@', "%40"), version), }) .unwrap(); @@ -1248,7 +1249,7 @@ mod tests { #[test] fn test_deductive_check_npm_purl() { - let package = "express@4.19.2".to_string(); + let package = "@expressjs/express@4.19.2".to_string(); let cmd = get_check_cmd_from_cli(vec!["hc", "check", "pkg:npm/%40expressjs/express@4.19.2"]); assert!(matches!(cmd, Ok(CheckCommand::Npm(..)))); diff --git a/hipcheck/src/session/pm.rs b/hipcheck/src/session/pm.rs index 8b20b5eb..9388c1e6 100644 --- a/hipcheck/src/session/pm.rs +++ b/hipcheck/src/session/pm.rs @@ -291,10 +291,22 @@ impl PackageManager { pub fn extract_package_version(raw_package: &str) -> Result<(String, String)> { // Get the package and version from package argument in form package@version because it has @ symbol in it let mut package_and_version = raw_package.split('@'); - let package_value = match package_and_version.next() { - Some(package) => Ok(package), - _ => Err(Error::msg("unable to get package from package@version")), + + // Check if the package is scoped, in the form "@scope/package". If it is, include the scope and package as the package name + let package_value = match raw_package.starts_with('@') { + true => { + package_and_version.next(); + match package_and_version.next() { + Some(package) => Ok(format!("@{}", package)), + _ => Err(Error::msg("unable to get package from package@version")), + } + } + false => match package_and_version.next() { + Some(package) => Ok(package.to_string()), + _ => Err(Error::msg("unable to get package from package@version")), + }, }; + Ok(( package_value.unwrap().to_string(), //this wont panic because we check for it above package_and_version @@ -320,7 +332,17 @@ pub fn extract_package_version_from_url(url: Url) -> Result<(String, String)> { .path_segments() .ok_or_else(|| hc_error!("Unable to get path"))?; let package_value = match path_segments.next() { - Some(package) => Ok(package), + Some(first) => { + // Check if the package is scoped, in the form "@scope/package". If it is, include the scope and package as the package name + if first.starts_with('@') { + match path_segments.next() { + Some(package) => Ok(format!("{}/{}", first, package)), + _ => Err(Error::msg("unable to get package from uri")), + } + } else { + Ok(first.to_string()) + } + } _ => Err(Error::msg("unable to get package from uri")), }; // An empty string or no string at all should both give "no version" as the version @@ -330,8 +352,8 @@ pub fn extract_package_version_from_url(url: Url) -> Result<(String, String)> { None => "no version", }; Ok(( - package_value.unwrap().to_string(), //this will graceful error if empty because of panic checking above - version.to_string(), //we check for this in match so we can format url correctly + package_value.unwrap(), //this will graceful error if empty because of panic checking above + version.to_string(), //we check for this in match so we can format url correctly )) } else if package_type.contains(PYPI) { //pypi gets the second and third segments @@ -876,6 +898,62 @@ mod tests { } } + #[test] + fn test_extract_repo_for_npm_6() { + let npm_package = "@types/ua-parser-js@0.7.36"; + let link2 = "https://github.com/DefinitelyTyped/DefinitelyTyped.git"; + + let target_seed = CheckNpmArgs { + package: npm_package.to_string(), + } + .to_target_seed() + .unwrap(); + if let TargetSeed::Package(package) = target_seed { + assert_eq!( + package, + Package { + purl: Url::parse("pkg:npm/%40types/ua-parser-js@0.7.36").unwrap(), + name: "@types/ua-parser-js".to_string(), + version: "0.7.36".to_string(), + host: PackageHost::Npm + } + ); + + let npm_git = Url::parse(link2).unwrap(); + assert_eq!(extract_repo_for_npm(&package).unwrap(), npm_git); + } else { + panic!() + } + } + + #[test] + fn test_extract_repo_for_npm_7() { + let npm_package = "https://registry.npmjs.org/@types/ua-parser-js/0.7.36"; + let link2 = "https://github.com/DefinitelyTyped/DefinitelyTyped.git"; + + let target_seed = CheckNpmArgs { + package: npm_package.to_string(), + } + .to_target_seed() + .unwrap(); + if let TargetSeed::Package(package) = target_seed { + assert_eq!( + package, + Package { + purl: Url::parse("pkg:npm/%40types/ua-parser-js@0.7.36").unwrap(), + name: "@types/ua-parser-js".to_string(), + version: "0.7.36".to_string(), + host: PackageHost::Npm + } + ); + + let npm_git = Url::parse(link2).unwrap(); + assert_eq!(extract_repo_for_npm(&package).unwrap(), npm_git); + } else { + panic!() + } + } + #[test] /// Tests scm:git: prefix removal case. fn test_extract_repo_for_maven_2() { diff --git a/hipcheck/src/target.rs b/hipcheck/src/target.rs index fe5b0900..fee9be1f 100644 --- a/hipcheck/src/target.rs +++ b/hipcheck/src/target.rs @@ -70,8 +70,15 @@ impl TargetType { } "npm" => { // Construct NPM package w/ optional version from pURL as the updated target string + let mut package = String::new(); + + // Include scope if provided + if let Some(scope) = purl.namespace() { + package.push_str(scope); + package.push('/'); + } let name = purl.name(); - let mut package = name.to_string(); + package.push_str(name); // Include version if provided if let Some(version) = purl.version() { package.push('@');