From 00edc78bf5a8e41ea75cdc78625b5e0b149c2582 Mon Sep 17 00:00:00 2001 From: Paul Dorsch <107068277+pauld-msft@users.noreply.github.com> Date: Mon, 19 Aug 2024 12:49:12 -0400 Subject: [PATCH] Pauldorsch/pipreport version fix (#1229) * check for valid python versions before adding to the dependency graph * bump version * compiled regex --- .../pip/PipReportComponentDetector.cs | 12 +- .../pip/PipReportUtilities.cs | 11 + ....ComponentDetection.Detectors.Tests.csproj | 3 + .../Mocks/TestResources.Designer.cs | 602 +++++++++--------- .../Mocks/TestResources.resx | 3 + ...report_single_pkg_invalid_pkg_version.json | 57 ++ .../PipReportComponentDetectorTests.cs | 25 + .../PipReportUtilitiesTests.cs | 30 + 8 files changed, 455 insertions(+), 288 deletions(-) create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/pip_report_single_pkg_invalid_pkg_version.json diff --git a/src/Microsoft.ComponentDetection.Detectors/pip/PipReportComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/pip/PipReportComponentDetector.cs index 967c5978f..f964ed515 100644 --- a/src/Microsoft.ComponentDetection.Detectors/pip/PipReportComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/pip/PipReportComponentDetector.cs @@ -77,7 +77,7 @@ private enum PipReportOverrideBehavior public override IEnumerable SupportedComponentTypes { get; } = new[] { ComponentType.Pip }; - public override int Version { get; } = 7; + public override int Version { get; } = 8; protected override bool EnableParallelism { get; set; } = true; @@ -332,6 +332,16 @@ private Dictionary BuildGraphFromInstallationReport( continue; } + if (!PipReportUtilities.IsCanonicalVersion(package.Metadata.Version)) + { + this.Logger.LogWarning( + "PipReport: Skipping package '{Package}' with non-canonical version '{Version}'. " + + "See https://www.python.org/dev/peps/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions", + normalizedPkgName, + package.Metadata.Version); + continue; + } + var node = new PipReportGraphNode( new PipComponent( normalizedPkgName, diff --git a/src/Microsoft.ComponentDetection.Detectors/pip/PipReportUtilities.cs b/src/Microsoft.ComponentDetection.Detectors/pip/PipReportUtilities.cs index ca87e5cf5..f108cf5da 100644 --- a/src/Microsoft.ComponentDetection.Detectors/pip/PipReportUtilities.cs +++ b/src/Microsoft.ComponentDetection.Detectors/pip/PipReportUtilities.cs @@ -9,6 +9,12 @@ internal class PipReportUtilities private const string ClassifierFieldSeparator = " :: "; private const string ClassifierFieldLicensePrefix = "License"; + // Python regular expression for version schema: + // https://www.python.org/dev/peps/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions + public static readonly Regex CanonicalVersionPatternMatch = new Regex( + @"^([1-9]\d*!)?(0|[1-9]\d*)(\.(0|[1-9]\d*))*((a|b|rc)(0|[1-9]\d*))?(\.post(0|[1-9]\d*))?(\.dev(0|[1-9]\d*))?(\+(?:(?[a-z0-9]+(?:[.][a-z0-9]+)*))?)?$", + RegexOptions.Compiled); + /// /// Normalize the package name format to the standard Python Packaging format. /// See https://packaging.python.org/en/latest/specifications/name-normalization/#name-normalization. @@ -68,4 +74,9 @@ public static string GetLicenseFromInstalledItem(PipInstallationReportItem compo return null; } + + public static bool IsCanonicalVersion(string version) + { + return CanonicalVersionPatternMatch.Match(version).Success; + } } diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/Microsoft.ComponentDetection.Detectors.Tests.csproj b/test/Microsoft.ComponentDetection.Detectors.Tests/Microsoft.ComponentDetection.Detectors.Tests.csproj index 54c1a93b9..6572a0488 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/Microsoft.ComponentDetection.Detectors.Tests.csproj +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/Microsoft.ComponentDetection.Detectors.Tests.csproj @@ -40,6 +40,9 @@ Always + + Always + Always diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/TestResources.Designer.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/TestResources.Designer.cs index b46a9e688..d0db6536e 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/TestResources.Designer.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/TestResources.Designer.cs @@ -1,287 +1,315 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Microsoft.ComponentDetection.Detectors.Tests.Mocks { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class TestResources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal TestResources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.ComponentDetection.Detectors.Tests.Mocks.TestResources", typeof(TestResources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to integrationTestCompileClasspath - Compile classpath for source set 'integration test'. - ///+--- commons-io:commons-io:2.5 - ///+--- org.kohsuke:github-api:1.94 - ///| +--- org.apache.commons:commons-lang3:3.7 - ///| +--- commons-codec:commons-codec:1.7 - ///| +--- com.fasterxml.jackson.core:jackson-databind:2.9.2 - ///| | +--- com.fasterxml.jackson.core:jackson-annotations:2.9.0 - ///| | \--- com.fasterxml.jackson.core:jackson-core:2.9.2 - ///| \--- commons-io:commons-io:1.4 -> 2.5 - ///+--- org.zeroturnaround:zt-zip: [rest of string was truncated]";. - /// - internal static string GradlewDependencyOutput { - get { - return ResourceManager.GetString("GradlewDependencyOutput", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to org.apache.maven:maven-compat:jar:3.6.1-SNAPSHOT - ///+- org.apache.maven:maven-model:jar:3.6.1-SNAPSHOT:compile - ///+- org.apache.maven:maven-model-builder:jar:3.6.1-SNAPSHOT:compile - ///| \- org.apache.maven:maven-builder-support:jar:3.6.1-SNAPSHOT:compile - ///+- org.apache.maven:maven-settings:jar:3.6.1-SNAPSHOT:compile - ///+- org.apache.maven:maven-settings-builder:jar:3.6.1-SNAPSHOT:compile - ///| \- org.sonatype.plexus:plexus-sec-dispatcher:jar:1.4:compile - ///| \- org.sonatype.plexus:plexus-cipher:jar:1.7:compile - ///+- [rest of string was truncated]";. - /// - internal static string MvnCliDependencyOutput { - get { - return ResourceManager.GetString("MvnCliDependencyOutput", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to { - /// "version": "1", - /// "pip_version": "24.0", - /// "install": [ - /// { - /// "download_info": { - /// "url": "https://files.pythonhosted.org/packages/72/c3/532326adbb2b76f709e3e582aeefd0a85bd7454599ff450d90dd9540f5ed/jupyterlab-4.2.0-py3-none-any.whl", - /// "archive_info": { - /// "hash": "sha256=0dfe9278e25a145362289c555d9beb505697d269c10e99909766af7c440ad3cc", - /// "hashes": { - /// "sha256": "0dfe9278e25a145362289c555d9beb505697d269c10e99909766af7c440ad3cc" - /// } - /// [rest of string was truncated]";. - /// - internal static string pip_report_jupyterlab { - get { - return ResourceManager.GetString("pip_report_jupyterlab", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to { - /// "version": "1", - /// "pip_version": "24.0", - /// "install": [ - /// { - /// "download_info": { - /// "url": "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", - /// "archive_info": { - /// "hash": "sha256=8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", - /// "hashes": { - /// "sha256": "8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - /// } - /// } /// [rest of string was truncated]";. - /// - internal static string pip_report_multi_pkg { - get { - return ResourceManager.GetString("pip_report_multi_pkg", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to { - /// "version": "1", - /// "pip_version": "24.0", - /// "install": [ - /// { - /// "download_info": { - /// "url": "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", - /// "archive_info": { - /// "hash": "sha256=70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", - /// "hashes": { - /// "sha256": "70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" - /// } - /// } [rest of string was truncated]";. - /// - internal static string pip_report_simple_extras { - get { - return ResourceManager.GetString("pip_report_simple_extras", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to { - /// "version": "1", - /// "pip_version": "24.0", - /// "install": [ - /// { - /// "download_info": { - /// "url": "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", - /// "archive_info": { - /// "hash": "sha256=8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", - /// "hashes": { - /// "sha256": "8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - /// } - /// } /// [rest of string was truncated]";. - /// - internal static string pip_report_single_pkg { - get { - return ResourceManager.GetString("pip_report_single_pkg", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to { - /// "version": "2", - /// "pip_version": "24.0", - /// "install": [ - /// { - /// "download_info": { - /// "url": "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", - /// "archive_info": { - /// "hash": "sha256=8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", - /// "hashes": { - /// "sha256": "8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - /// } - /// } /// [rest of string was truncated]";. - /// - internal static string pip_report_single_pkg_bad_version { - get { - return ResourceManager.GetString("pip_report_single_pkg_bad_version", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to { - /// "version": 3, - /// "targets": { - /// ".NETCoreApp,Version=v2.2": { - /// "CommandLineParser/2.8.0": { - /// "type": "package", - /// "compile": { - /// "lib/netstandard2.0/_._": {} - /// }, - /// "runtime": { - /// "lib/netstandard2.0/CommandLine.dll": {} - /// } - /// }, - /// "coverlet.msbuild/2.5.1": { - /// "type": "package", - /// "build": { - /// "build/netstandard2.0/coverlet.msbuild.props": {}, - /// "build/netstandard2.0/coverlet.msbuild.targets": {} - /// } - /// }, - /// "DotNet.Glob/2.1.1": { - /// "type": "package [rest of string was truncated]";. - /// - internal static string project_assets_2_2 { - get { - return ResourceManager.GetString("project_assets_2_2", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to { - /// "version": 3, - /// "targets": { - /// ".NETCoreApp,Version=v2.2": { - /// "coverlet.msbuild/2.5.1": { - /// "type": "package", - /// "build": { - /// "build/netstandard2.0/coverlet.msbuild.props": {}, - /// "build/netstandard2.0/coverlet.msbuild.targets": {} - /// } - /// }, - /// "DotNet.Glob/2.1.1": { - /// "type": "package", - /// "dependencies": { - /// "NETStandard.Library": "1.6.1" - /// }, - /// "compile": { - /// "lib/netstandard1.1/DotNet.Glob.dll": {} - /// }, - /// "runtime": { - /// "lib/netstandard1.1/DotNet.Glob.dll": {} /// [rest of string was truncated]";. - /// - internal static string project_assets_2_2_additional { - get { - return ResourceManager.GetString("project_assets_2_2_additional", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to { - /// "version": 3, - /// "targets": { - /// ".NETCoreApp,Version=v3.1": { - /// "Microsoft.Extensions.DependencyModel/3.0.0": { - /// "type": "package", - /// "dependencies": { - /// "System.Text.Json": "4.6.0" - /// }, - /// "compile": { - /// "lib/netstandard2.0/Microsoft.Extensions.DependencyModel.dll": {} - /// }, - /// "runtime": { - /// "lib/netstandard2.0/Microsoft.Extensions.DependencyModel.dll": {} - /// } - /// }, - /// "Microsoft. [rest of string was truncated]";. - /// - internal static string project_assets_3_1 { - get { - return ResourceManager.GetString("project_assets_3_1", resourceCulture); - } - } - } -} +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.ComponentDetection.Detectors.Tests.Mocks { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class TestResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal TestResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.ComponentDetection.Detectors.Tests.Mocks.TestResources", typeof(TestResources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to integrationTestCompileClasspath - Compile classpath for source set 'integration test'. + ///+--- commons-io:commons-io:2.5 + ///+--- org.kohsuke:github-api:1.94 + ///| +--- org.apache.commons:commons-lang3:3.7 + ///| +--- commons-codec:commons-codec:1.7 + ///| +--- com.fasterxml.jackson.core:jackson-databind:2.9.2 + ///| | +--- com.fasterxml.jackson.core:jackson-annotations:2.9.0 + ///| | \--- com.fasterxml.jackson.core:jackson-core:2.9.2 + ///| \--- commons-io:commons-io:1.4 -> 2.5 + ///+--- org.zeroturnaround:zt-zip: [rest of string was truncated]";. + /// + internal static string GradlewDependencyOutput { + get { + return ResourceManager.GetString("GradlewDependencyOutput", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to org.apache.maven:maven-compat:jar:3.6.1-SNAPSHOT + ///+- org.apache.maven:maven-model:jar:3.6.1-SNAPSHOT:compile + ///+- org.apache.maven:maven-model-builder:jar:3.6.1-SNAPSHOT:compile + ///| \- org.apache.maven:maven-builder-support:jar:3.6.1-SNAPSHOT:compile + ///+- org.apache.maven:maven-settings:jar:3.6.1-SNAPSHOT:compile + ///+- org.apache.maven:maven-settings-builder:jar:3.6.1-SNAPSHOT:compile + ///| \- org.sonatype.plexus:plexus-sec-dispatcher:jar:1.4:compile + ///| \- org.sonatype.plexus:plexus-cipher:jar:1.7:compile + ///+- [rest of string was truncated]";. + /// + internal static string MvnCliDependencyOutput { + get { + return ResourceManager.GetString("MvnCliDependencyOutput", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to { + /// "version": "1", + /// "pip_version": "24.0", + /// "install": [ + /// { + /// "download_info": { + /// "url": "https://files.pythonhosted.org/packages/72/c3/532326adbb2b76f709e3e582aeefd0a85bd7454599ff450d90dd9540f5ed/jupyterlab-4.2.0-py3-none-any.whl", + /// "archive_info": { + /// "hash": "sha256=0dfe9278e25a145362289c555d9beb505697d269c10e99909766af7c440ad3cc", + /// "hashes": { + /// "sha256": "0dfe9278e25a145362289c555d9beb505697d269c10e99909766af7c440ad3cc" + /// } + /// [rest of string was truncated]";. + /// + internal static string pip_report_jupyterlab { + get { + return ResourceManager.GetString("pip_report_jupyterlab", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to { + /// "version": "1", + /// "pip_version": "24.0", + /// "install": [ + /// { + /// "download_info": { + /// "url": "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", + /// "archive_info": { + /// "hash": "sha256=8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", + /// "hashes": { + /// "sha256": "8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + /// } + /// } + /// [rest of string was truncated]";. + /// + internal static string pip_report_multi_pkg { + get { + return ResourceManager.GetString("pip_report_multi_pkg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to { + /// "version": "1", + /// "pip_version": "24.0", + /// "install": [ + /// { + /// "download_info": { + /// "url": "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", + /// "archive_info": { + /// "hash": "sha256=70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", + /// "hashes": { + /// "sha256": "70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" + /// } + /// } [rest of string was truncated]";. + /// + internal static string pip_report_simple_extras { + get { + return ResourceManager.GetString("pip_report_simple_extras", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to { + /// "version": "1", + /// "pip_version": "24.0", + /// "install": [ + /// { + /// "download_info": { + /// "url": "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", + /// "archive_info": { + /// "hash": "sha256=8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", + /// "hashes": { + /// "sha256": "8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + /// } + /// } + /// [rest of string was truncated]";. + /// + internal static string pip_report_single_pkg { + get { + return ResourceManager.GetString("pip_report_single_pkg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to { + /// "version": "2", + /// "pip_version": "24.0", + /// "install": [ + /// { + /// "download_info": { + /// "url": "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", + /// "archive_info": { + /// "hash": "sha256=8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", + /// "hashes": { + /// "sha256": "8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + /// } + /// } + /// [rest of string was truncated]";. + /// + internal static string pip_report_single_pkg_bad_version { + get { + return ResourceManager.GetString("pip_report_single_pkg_bad_version", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to { + /// "version": "1", + /// "pip_version": "24.0", + /// "install": [ + /// { + /// "download_info": { + /// "url": "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", + /// "archive_info": { + /// "hash": "sha256=8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", + /// "hashes": { + /// "sha256": "8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + /// } + /// } + /// [rest of string was truncated]";. + /// + internal static string pip_report_single_pkg_invalid_pkg_version + { + get + { + return ResourceManager.GetString("pip_report_single_pkg_invalid_pkg_version", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to { + /// "version": 3, + /// "targets": { + /// ".NETCoreApp,Version=v2.2": { + /// "CommandLineParser/2.8.0": { + /// "type": "package", + /// "compile": { + /// "lib/netstandard2.0/_._": {} + /// }, + /// "runtime": { + /// "lib/netstandard2.0/CommandLine.dll": {} + /// } + /// }, + /// "coverlet.msbuild/2.5.1": { + /// "type": "package", + /// "build": { + /// "build/netstandard2.0/coverlet.msbuild.props": {}, + /// "build/netstandard2.0/coverlet.msbuild.targets": {} + /// } + /// }, + /// "DotNet.Glob/2.1.1": { + /// "type": "package [rest of string was truncated]";. + /// + internal static string project_assets_2_2 { + get { + return ResourceManager.GetString("project_assets_2_2", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to { + /// "version": 3, + /// "targets": { + /// ".NETCoreApp,Version=v2.2": { + /// "coverlet.msbuild/2.5.1": { + /// "type": "package", + /// "build": { + /// "build/netstandard2.0/coverlet.msbuild.props": {}, + /// "build/netstandard2.0/coverlet.msbuild.targets": {} + /// } + /// }, + /// "DotNet.Glob/2.1.1": { + /// "type": "package", + /// "dependencies": { + /// "NETStandard.Library": "1.6.1" + /// }, + /// "compile": { + /// "lib/netstandard1.1/DotNet.Glob.dll": {} + /// }, + /// "runtime": { + /// "lib/netstandard1.1/DotNet.Glob.dll": {} + /// [rest of string was truncated]";. + /// + internal static string project_assets_2_2_additional { + get { + return ResourceManager.GetString("project_assets_2_2_additional", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to { + /// "version": 3, + /// "targets": { + /// ".NETCoreApp,Version=v3.1": { + /// "Microsoft.Extensions.DependencyModel/3.0.0": { + /// "type": "package", + /// "dependencies": { + /// "System.Text.Json": "4.6.0" + /// }, + /// "compile": { + /// "lib/netstandard2.0/Microsoft.Extensions.DependencyModel.dll": {} + /// }, + /// "runtime": { + /// "lib/netstandard2.0/Microsoft.Extensions.DependencyModel.dll": {} + /// } + /// }, + /// "Microsoft. [rest of string was truncated]";. + /// + internal static string project_assets_3_1 { + get { + return ResourceManager.GetString("project_assets_3_1", resourceCulture); + } + } + } +} diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/TestResources.resx b/test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/TestResources.resx index 72ce43f8f..9bb9bca31 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/TestResources.resx +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/TestResources.resx @@ -139,6 +139,9 @@ pip_report_single_pkg_bad_version.json;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + pip_report_single_pkg_invalid_pkg_version.json;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + ..\Resources\project_assets_2_2.json;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/pip_report_single_pkg_invalid_pkg_version.json b/test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/pip_report_single_pkg_invalid_pkg_version.json new file mode 100644 index 000000000..0fb527e18 --- /dev/null +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/Mocks/pip_report_single_pkg_invalid_pkg_version.json @@ -0,0 +1,57 @@ +{ + "version": "1", + "pip_version": "24.0", + "install": [ + { + "download_info": { + "url": "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", + "archive_info": { + "hash": "sha256=8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", + "hashes": { + "sha256": "8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + } + } + }, + "is_direct": false, + "is_yanked": false, + "requested": true, + "metadata": { + "metadata_version": "2.1", + "name": "six", + "version": ".1", + "platform": [ + "UNKNOWN" + ], + "summary": "Python 2 and 3 compatibility utilities", + "description": ".. image:: https://img.shields.io/pypi/v/six.svg\n :target: https://pypi.org/project/six/\n :alt: six on PyPI\n\n.. image:: https://travis-ci.org/benjaminp/six.svg?branch=master\n :target: https://travis-ci.org/benjaminp/six\n :alt: six on TravisCI\n\n.. image:: https://readthedocs.org/projects/six/badge/?version=latest\n :target: https://six.readthedocs.io/\n :alt: six's documentation on Read the Docs\n\n.. image:: https://img.shields.io/badge/license-MIT-green.svg\n :target: https://github.com/benjaminp/six/blob/master/LICENSE\n :alt: MIT License badge\n\nSix is a Python 2 and 3 compatibility library. It provides utility functions\nfor smoothing over the differences between the Python versions with the goal of\nwriting Python code that is compatible on both Python versions. See the\ndocumentation for more information on what is provided.\n\nSix supports Python 2.7 and 3.3+. It is contained in only one Python\nfile, so it can be easily copied into your project. (The copyright and license\nnotice must be retained.)\n\nOnline documentation is at https://six.readthedocs.io/.\n\nBugs can be reported to https://github.com/benjaminp/six. The code can also\nbe found there.\n\n\n", + "home_page": "https://github.com/benjaminp/six", + "author": "Benjamin Peterson", + "author_email": "benjamin@python.org", + "license": "MIT", + "classifier": [ + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 3", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Topic :: Software Development :: Libraries", + "Topic :: Utilities" + ], + "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + } + } + ], + "environment": { + "implementation_name": "cpython", + "implementation_version": "3.12.3", + "os_name": "nt", + "platform_machine": "AMD64", + "platform_release": "11", + "platform_system": "Windows", + "platform_version": "10.0.22631", + "python_full_version": "3.12.3", + "platform_python_implementation": "CPython", + "python_version": "3.12", + "sys_platform": "win32" + } +} diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/PipReportComponentDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/PipReportComponentDetectorTests.cs index 2f1f46092..58eb48340 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/PipReportComponentDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/PipReportComponentDetectorTests.cs @@ -32,6 +32,7 @@ public class PipReportComponentDetectorTests : BaseDetectorTest(TestResources.pip_report_single_pkg); this.singlePackageReportBadVersion = JsonConvert.DeserializeObject(TestResources.pip_report_single_pkg_bad_version); + this.singlePackageReportInvalidPkgVersion = JsonConvert.DeserializeObject(TestResources.pip_report_single_pkg_invalid_pkg_version); this.multiPackageReport = JsonConvert.DeserializeObject(TestResources.pip_report_multi_pkg); this.jupyterPackageReport = JsonConvert.DeserializeObject(TestResources.pip_report_jupyterlab); this.simpleExtrasReport = JsonConvert.DeserializeObject(TestResources.pip_report_simple_extras); @@ -256,6 +258,29 @@ public async Task TestPipReportDetector_SimpleExtrasAsync() requestsComponent.Version.Should().Be("2.32.3"); } + [TestMethod] + public async Task TestPipReportDetector_BadPackageVersionAsync() + { + this.pipCommandService.Setup(x => x.GenerateInstallationReportAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((this.singlePackageReportInvalidPkgVersion, null)); + + var (result, componentRecorder) = await this.DetectorTestUtility + .WithFile("requirements.txt", string.Empty) + .ExecuteDetectorAsync(); + + result.ResultCode.Should().Be(ProcessingResultCode.Success); + + this.mockLogger.Verify(x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((o, t) => o.ToString().Contains("with non-canonical version")), + It.IsAny(), + (Func)It.IsAny())); + + var detectedComponents = componentRecorder.GetDetectedComponents(); + detectedComponents.Should().BeEmpty(); + } + [TestMethod] public async Task TestPipReportDetector_MultiComponentAsync() { diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/PipReportUtilitiesTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/PipReportUtilitiesTests.cs index ee1b6ff8a..a8cfcdfe9 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/PipReportUtilitiesTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/PipReportUtilitiesTests.cs @@ -28,4 +28,34 @@ public void NormalizePackageName_ExpectedEquivalent() PipReportUtilities.NormalizePackageNameFormat("friendly--bard").Should().Be(normalizedForm); PipReportUtilities.NormalizePackageNameFormat("FrIeNdLy-._.-bArD").Should().Be(normalizedForm); } + + [TestMethod] + [DataRow("1.0.0")] + [DataRow("2012.10")] + [DataRow("0.1rc2.post3")] + [DataRow("1.0.post2.dev3")] + [DataRow("1!2.0")] + [DataRow("1.0.post1")] + public void IsCanonicalVersion_ReturnsTrue_WhenVersionMatchesCanonicalVersionPattern(string version) + { + // Act + var isCanonicalVersion = PipReportUtilities.IsCanonicalVersion(version); + + // Assert + isCanonicalVersion.Should().BeTrue(); + } + + [TestMethod] + [DataRow("0.0.1-beta")] + [DataRow("1.0.0-alpha.1")] + [DataRow(".1")] + [DataRow("-pkg-version-")] + public void IsCanonicalVersion_ReturnsFalse_WhenVersionDoesNotMatchCanonicalVersionPattern(string version) + { + // Act + var isCanonicalVersion = PipReportUtilities.IsCanonicalVersion(version); + + // Assert + isCanonicalVersion.Should().BeFalse(); + } }