diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b6821a01..b92f348bf 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 + +- Support for C#'s `packages.*.config` lockfile type + ## 7.1.5 - 2024-11-26 ### Fixed diff --git a/cli/tests/common.rs b/cli/tests/common.rs index 2b903a80d..b58d97397 100644 --- a/cli/tests/common.rs +++ b/cli/tests/common.rs @@ -193,7 +193,7 @@ impl<'a> TestExtension<'a> { } #[cfg(feature = "extensions")] -impl<'a> Drop for TestExtension<'a> { +impl Drop for TestExtension<'_> { fn drop(&mut self) { self.test_cli.run(["extension", "uninstall", "test-ext"]).success(); fs::remove_dir_all(&self.extension_path).unwrap(); diff --git a/docs/commands/phylum_analyze.md b/docs/commands/phylum_analyze.md index a29888f8f..4129b866f 100644 --- a/docs/commands/phylum_analyze.md +++ b/docs/commands/phylum_analyze.md @@ -27,7 +27,7 @@ Usage: phylum analyze [OPTIONS] [DEPENDENCY_FILE]... `-t`, `--type` ``   Dependency file type used for all lockfiles (default: auto) -  Accepted values: `npm`, `yarn`, `pnpm`, `gem`, `pip`, `poetry`, `pipenv`, `mvn`, `gradle`, `msbuild`, `nugetlock`, `gomod`, `go`, `cargo`, `spdx`, `cyclonedx`, `auto` +  Accepted values: `npm`, `yarn`, `pnpm`, `gem`, `pip`, `poetry`, `pipenv`, `mvn`, `gradle`, `msbuild`, `nugetlock`, `nugetconfig`, `gomod`, `go`, `cargo`, `spdx`, `cyclonedx`, `auto` `--skip-sandbox`   Run lockfile generation without sandbox protection diff --git a/docs/commands/phylum_init.md b/docs/commands/phylum_init.md index 5dbec0546..dee40ceeb 100644 --- a/docs/commands/phylum_init.md +++ b/docs/commands/phylum_init.md @@ -21,7 +21,7 @@ Usage: phylum init [OPTIONS] [PROJECT_NAME] `-t`, `--type` ``   Dependency file type used for all lockfiles (default: auto) -  Accepted values: `npm`, `yarn`, `pnpm`, `gem`, `pip`, `poetry`, `pipenv`, `mvn`, `gradle`, `msbuild`, `nugetlock`, `gomod`, `go`, `cargo`, `spdx`, `cyclonedx`, `auto` +  Accepted values: `npm`, `yarn`, `pnpm`, `gem`, `pip`, `poetry`, `pipenv`, `mvn`, `gradle`, `msbuild`, `nugetlock`, `nugetconfig`, `gomod`, `go`, `cargo`, `spdx`, `cyclonedx`, `auto` `-f`, `--force`   Overwrite existing configurations without confirmation diff --git a/docs/commands/phylum_parse.md b/docs/commands/phylum_parse.md index 829838079..21342675a 100644 --- a/docs/commands/phylum_parse.md +++ b/docs/commands/phylum_parse.md @@ -15,7 +15,7 @@ Usage: phylum parse [OPTIONS] [DEPENDENCY_FILE]... `-t`, `--type` ``   Dependency file type used for all lockfiles (default: auto) -  Accepted values: `npm`, `yarn`, `pnpm`, `gem`, `pip`, `poetry`, `pipenv`, `mvn`, `gradle`, `msbuild`, `nugetlock`, `gomod`, `go`, `cargo`, `spdx`, `cyclonedx`, `auto` +  Accepted values: `npm`, `yarn`, `pnpm`, `gem`, `pip`, `poetry`, `pipenv`, `mvn`, `gradle`, `msbuild`, `nugetlock`, `nugetconfig`, `gomod`, `go`, `cargo`, `spdx`, `cyclonedx`, `auto` `--skip-sandbox`   Run lockfile generation without sandbox protection diff --git a/docs/supported_lockfiles.md b/docs/supported_lockfiles.md index cfa0c513e..aec077e3a 100644 --- a/docs/supported_lockfiles.md +++ b/docs/supported_lockfiles.md @@ -13,6 +13,7 @@ The Phylum CLI supports processing many different lockfiles: | `gem` | `Gemfile.lock` | | `msbuild` | `*.csproj` | | `nugetlock` | `packages.lock.json`
`packages.*.lock.json` | +| `nugetconfig` | `packages.config`
`packages.*.config` | | `mvn` | `effective-pom.xml` | | `gradle` | `gradle.lockfile`
`gradle/dependency-locks/*.lockfile` | | `go` | `go.sum` | diff --git a/lockfile/src/csharp.rs b/lockfile/src/csharp.rs index f7332d5ef..700275236 100644 --- a/lockfile/src/csharp.rs +++ b/lockfile/src/csharp.rs @@ -155,6 +155,61 @@ impl Parse for CSProj { } } +pub struct PackagesConfig; + +impl Parse for PackagesConfig { + /// Parses `packages.*.config` files into a [`Vec`] of packages. + fn parse(&self, data: &str) -> anyhow::Result> { + let data = data.trim_start_matches(UTF8_BOM); + let parsed: PackagesConfigXml = quick_xml::de::from_str(data)?; + Ok(parsed.into()) + } + + fn is_path_lockfile(&self, path: &Path) -> bool { + let file_name = match path.file_name().and_then(|f| f.to_str()) { + Some(file_name) => file_name, + None => return false, + }; + + // Accept both `packages.config` and `packages..config`. + file_name.starts_with("packages.") && file_name.ends_with(".config") + } + + fn is_path_manifest(&self, _path: &Path) -> bool { + false + } +} + +/// XML format of the `packages.*.config` file. +#[derive(Deserialize)] +struct PackagesConfigXml { + #[serde(rename = "package", default)] + packages: Vec, +} + +impl From for Vec { + fn from(packages_config: PackagesConfigXml) -> Self { + packages_config + .packages + .into_iter() + .map(|package| { + let version = + package.version.map_or(PackageVersion::Unknown, PackageVersion::FirstParty); + Package { version, name: package.id, package_type: PackageType::Nuget } + }) + .collect() + } +} + +/// Package entry in a `packages.*.config` file. +#[derive(Deserialize)] +struct PackagesConfigXmlPackage { + #[serde(alias = "@id")] + id: String, + #[serde(alias = "@version")] + version: Option, +} + #[cfg(test)] mod tests { use super::*; @@ -302,4 +357,38 @@ mod tests { assert_eq!(pkgs[1].name, "NUnit3TestAdapter"); assert_eq!(pkgs[1].version, PackageVersion::FirstParty("3.13.0".into())); } + + #[test] + fn packages_config() { + let pkgs = + PackagesConfig.parse(include_str!("../../tests/fixtures/packages.config")).unwrap(); + assert_eq!(pkgs.len(), 4); + + let expected_pkgs = [ + Package { + name: "AddressParser".into(), + version: PackageVersion::FirstParty("0.0.20".into()), + package_type: PackageType::Nuget, + }, + Package { + name: "JetBrains.ReSharper.SDK".into(), + version: PackageVersion::FirstParty("8.2.921-EAP".into()), + package_type: PackageType::Nuget, + }, + Package { + name: "boost".into(), + version: PackageVersion::FirstParty("1.78.0".into()), + package_type: PackageType::Nuget, + }, + Package { + name: "noversion".into(), + version: PackageVersion::Unknown, + package_type: PackageType::Nuget, + }, + ]; + + for expected_pkg in expected_pkgs { + assert!(pkgs.contains(&expected_pkg), "missing package {expected_pkg:?}"); + } + } } diff --git a/lockfile/src/lib.rs b/lockfile/src/lib.rs index 8381942b5..7b9878fa4 100644 --- a/lockfile/src/lib.rs +++ b/lockfile/src/lib.rs @@ -17,7 +17,7 @@ use thiserror::Error; use walkdir::WalkDir; pub use crate::cargo::Cargo; -pub use crate::csharp::{CSProj, PackagesLock}; +pub use crate::csharp::{CSProj, PackagesConfig, PackagesLock}; pub use crate::cyclonedx::CycloneDX; pub use crate::golang::{GoMod, GoSum}; pub use crate::java::{GradleLock, Pom}; @@ -61,6 +61,7 @@ pub enum LockfileFormat { #[serde(alias = "nuget")] Msbuild, NugetLock, + NugetConfig, GoMod, Go, Cargo, @@ -103,6 +104,7 @@ impl LockfileFormat { LockfileFormat::Gradle => "gradle", LockfileFormat::Msbuild => "msbuild", LockfileFormat::NugetLock => "nugetlock", + LockfileFormat::NugetConfig => "nugetconfig", LockfileFormat::GoMod => "gomod", LockfileFormat::Go => "go", LockfileFormat::Cargo => "cargo", @@ -125,6 +127,7 @@ impl LockfileFormat { LockfileFormat::Gradle => &GradleLock, LockfileFormat::Msbuild => &CSProj, LockfileFormat::NugetLock => &PackagesLock, + LockfileFormat::NugetConfig => &PackagesConfig, LockfileFormat::GoMod => &GoMod, LockfileFormat::Go => &GoSum, LockfileFormat::Cargo => &Cargo, @@ -134,44 +137,32 @@ impl LockfileFormat { } /// Iterate over all supported lockfile formats. - pub fn iter() -> LockfileFormatIter { - LockfileFormatIter(0) - } -} - -/// An iterator of all supported lockfile formats. -pub struct LockfileFormatIter(u8); - -impl Iterator for LockfileFormatIter { - type Item = LockfileFormat; - - fn next(&mut self) -> Option { + pub fn iter() -> impl Iterator { // NOTE: Without explicit override, the lockfile generator will always pick the // first matching format for the manifest. To ensure best possible support, // common formats should be returned **before** less common ones (i.e. NPM // before Yarn). + const FORMATS: &[LockfileFormat] = &[ + LockfileFormat::Npm, + LockfileFormat::Yarn, + LockfileFormat::Pnpm, + LockfileFormat::Gem, + LockfileFormat::Pip, + LockfileFormat::Poetry, + LockfileFormat::Pipenv, + LockfileFormat::Maven, + LockfileFormat::Gradle, + LockfileFormat::Msbuild, + LockfileFormat::NugetLock, + LockfileFormat::NugetConfig, + LockfileFormat::GoMod, + LockfileFormat::Go, + LockfileFormat::Cargo, + LockfileFormat::Spdx, + LockfileFormat::CycloneDX, + ]; - let item = match self.0 { - 0 => LockfileFormat::Npm, - 1 => LockfileFormat::Yarn, - 2 => LockfileFormat::Pnpm, - 3 => LockfileFormat::Gem, - 4 => LockfileFormat::Pip, - 5 => LockfileFormat::Poetry, - 6 => LockfileFormat::Pipenv, - 7 => LockfileFormat::Maven, - 8 => LockfileFormat::Gradle, - 9 => LockfileFormat::Msbuild, - 10 => LockfileFormat::NugetLock, - 11 => LockfileFormat::GoMod, - 12 => LockfileFormat::Go, - 13 => LockfileFormat::Cargo, - 14 => LockfileFormat::Spdx, - 15 => LockfileFormat::CycloneDX, - _ => return None, - }; - self.0 += 1; - Some(item) + FORMATS.iter().copied() } } @@ -519,6 +510,9 @@ mod tests { ("pnpm-lock.yaml", LockfileFormat::Pnpm), ("sample.csproj", LockfileFormat::Msbuild), ("packages.lock.json", LockfileFormat::NugetLock), + ("packages.project.lock.json", LockfileFormat::NugetLock), + ("packages.config", LockfileFormat::NugetConfig), + ("packages.project.config", LockfileFormat::NugetConfig), ("gradle.lockfile", LockfileFormat::Gradle), ("default.lockfile", LockfileFormat::Gradle), ("effective-pom.xml", LockfileFormat::Maven), @@ -528,7 +522,9 @@ mod tests { ("go.sum", LockfileFormat::Go), ("Cargo.lock", LockfileFormat::Cargo), (".spdx.json", LockfileFormat::Spdx), + ("file.spdx.json", LockfileFormat::Spdx), (".spdx.yaml", LockfileFormat::Spdx), + ("file.spdx.yaml", LockfileFormat::Spdx), ("bom.json", LockfileFormat::CycloneDX), ("bom.xml", LockfileFormat::CycloneDX), ]; @@ -555,6 +551,7 @@ mod tests { ("nuget", LockfileFormat::Msbuild), ("msbuild", LockfileFormat::Msbuild), ("nugetlock", LockfileFormat::NugetLock), + ("nugetconfig", LockfileFormat::NugetConfig), ("gomod", LockfileFormat::GoMod), ("go", LockfileFormat::Go), ("cargo", LockfileFormat::Cargo), @@ -585,6 +582,7 @@ mod tests { ("gradle", LockfileFormat::Gradle), ("msbuild", LockfileFormat::Msbuild), ("nugetlock", LockfileFormat::NugetLock), + ("nugetconfig", LockfileFormat::NugetConfig), ("gomod", LockfileFormat::GoMod), ("go", LockfileFormat::Go), ("cargo", LockfileFormat::Cargo), @@ -629,6 +627,7 @@ mod tests { (LockfileFormat::Gradle, 1), (LockfileFormat::Msbuild, 2), (LockfileFormat::NugetLock, 1), + (LockfileFormat::NugetConfig, 1), (LockfileFormat::GoMod, 1), (LockfileFormat::Go, 1), (LockfileFormat::Cargo, 3), diff --git a/tests/fixtures/packages.config b/tests/fixtures/packages.config new file mode 100644 index 000000000..76aea3c40 --- /dev/null +++ b/tests/fixtures/packages.config @@ -0,0 +1,7 @@ + + + + + + +