diff --git a/src/main/java/io/github/guacsec/trustifyda/utils/PythonControllerBase.java b/src/main/java/io/github/guacsec/trustifyda/utils/PythonControllerBase.java index 68ff5c86..b08c204e 100644 --- a/src/main/java/io/github/guacsec/trustifyda/utils/PythonControllerBase.java +++ b/src/main/java/io/github/guacsec/trustifyda/utils/PythonControllerBase.java @@ -173,19 +173,21 @@ private List> getDependenciesImpl( boolean matchManifestVersions = Environment.getBoolean(PROP_MATCH_MANIFEST_VERSIONS, true); for (String dep : linesOfRequirements) { + boolean hasMarker = dep.contains(";"); + String requirementSpec = hasMarker ? dep.substring(0, dep.indexOf(";")).trim() : dep; if (matchManifestVersions) { String dependencyName; String manifestVersion; String installedVersion = ""; int doubleEqualSignPosition; - if (dep.contains("==")) { - doubleEqualSignPosition = dep.indexOf("=="); - manifestVersion = dep.substring(doubleEqualSignPosition + 2).trim(); + if (requirementSpec.contains("==")) { + doubleEqualSignPosition = requirementSpec.indexOf("=="); + manifestVersion = requirementSpec.substring(doubleEqualSignPosition + 2).trim(); if (manifestVersion.contains("#")) { var hashCharIndex = manifestVersion.indexOf("#"); manifestVersion = manifestVersion.substring(0, hashCharIndex); } - dependencyName = getDependencyName(dep); + dependencyName = getDependencyName(requirementSpec); PythonDependency pythonDependency = cachedEnvironmentDeps.get(new StringInsensitive(dependencyName)); if (pythonDependency != null) { @@ -209,7 +211,10 @@ private List> getDependenciesImpl( } } List path = new ArrayList<>(); - String selectedDepName = getDependencyName(dep.toLowerCase()); + String selectedDepName = getDependencyName(requirementSpec.toLowerCase()); + if (hasMarker && cachedEnvironmentDeps.get(new StringInsensitive(selectedDepName)) == null) { + continue; + } path.add(selectedDepName); bringAllDependencies( dependencies, selectedDepName, cachedEnvironmentDeps, includeTransitive, path); @@ -373,15 +378,17 @@ protected String getDependencyNameShow(String pipShowOutput) { } public static String getDependencyName(String dep) { - int rightTriangleBracket = dep.indexOf(">"); - int leftTriangleBracket = dep.indexOf("<"); - int equalsSign = dep.indexOf("="); + int markerSeparator = dep.indexOf(";"); + String requirement = markerSeparator == -1 ? dep : dep.substring(0, markerSeparator); + int rightTriangleBracket = requirement.indexOf(">"); + int leftTriangleBracket = requirement.indexOf("<"); + int equalsSign = requirement.indexOf("="); int minimumIndex = getFirstSign(rightTriangleBracket, leftTriangleBracket, equalsSign); String depName; if (rightTriangleBracket == -1 && leftTriangleBracket == -1 && equalsSign == -1) { - depName = dep; + depName = requirement; } else { - depName = dep.substring(0, minimumIndex); + depName = requirement.substring(0, minimumIndex); } return depName.trim(); } diff --git a/src/test/java/io/github/guacsec/trustifyda/providers/Python_Provider_Test.java b/src/test/java/io/github/guacsec/trustifyda/providers/Python_Provider_Test.java index 109b0a65..2f6cce69 100644 --- a/src/test/java/io/github/guacsec/trustifyda/providers/Python_Provider_Test.java +++ b/src/test/java/io/github/guacsec/trustifyda/providers/Python_Provider_Test.java @@ -35,6 +35,7 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junitpioneer.jupiter.RestoreSystemProperties; import org.junitpioneer.jupiter.SetSystemProperty; @@ -211,6 +212,55 @@ void test_the_provideComponent_with_properties(String testFolder) throws IOExcep assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); } + static Stream markerTestCases() { + return Stream.of( + Arguments.of( + "pip_requirements_txt_marker_skip", + "six==1.16.0\ncertifi==2023.7.22\n", + "Name: certifi\nVersion: 2023.7.22\nSummary: Python package for providing Mozilla's CA" + + " Bundle.\nRequires: \nRequired-by: \n---\nName: six\nVersion: 1.16.0\nSummary:" + + " Python 2 and 3 compatibility utilities\nRequires: \nRequired-by: "), + Arguments.of( + "pip_requirements_txt_marker_installed", + "six==1.16.0\ncolorama==0.4.6\n", + "Name: six\nVersion: 1.16.0\nSummary: Python 2 and 3 compatibility utilities\nRequires:" + + " \nRequired-by: \n---\nName: colorama\nVersion: 0.4.6\nSummary: Cross-platform" + + " colored terminal text\nRequires: \nRequired-by: ")); + } + + /** + * Verifies that PEP 508 marker-constrained packages are handled correctly: skipped when not + * installed (marker didn't match) and included when installed (marker matched or marker-only). + */ + @ParameterizedTest + @MethodSource("markerTestCases") + @RestoreSystemProperties + void test_marker_constrained_packages_in_component_analysis( + String testFolder, String pipFreezeContent, String pipShowContent) throws IOException { + var targetRequirements = + String.format("src/test/resources/tst_manifests/pip/%s/requirements.txt", testFolder); + + String expectedSbom; + try (var is = + getResourceAsStreamDecision( + this.getClass(), + String.format("tst_manifests/pip/%s/expected_component_sbom.json", testFolder))) { + expectedSbom = new String(is.readAllBytes()); + } + + System.setProperty( + PROP_TRUSTIFY_DA_PIP_FREEZE, + new String(Base64.getEncoder().encode(pipFreezeContent.getBytes()))); + System.setProperty( + PROP_TRUSTIFY_DA_PIP_SHOW, + new String(Base64.getEncoder().encode(pipShowContent.getBytes()))); + + var content = new PythonPipProvider(Path.of(targetRequirements)).provideComponent(); + + assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE); + assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); + } + @Test void Test_The_ProvideComponent_Path_Should_Throw_Exception() { assertThatIllegalArgumentException() diff --git a/src/test/java/io/github/guacsec/trustifyda/utils/PythonControllerRealEnvTest.java b/src/test/java/io/github/guacsec/trustifyda/utils/PythonControllerRealEnvTest.java index 378cd1da..2b382110 100644 --- a/src/test/java/io/github/guacsec/trustifyda/utils/PythonControllerRealEnvTest.java +++ b/src/test/java/io/github/guacsec/trustifyda/utils/PythonControllerRealEnvTest.java @@ -288,6 +288,19 @@ void get_Dependency_Name_requirements() { assertEquals("something", PythonControllerRealEnv.getDependencyName("something>=2.0.5")); } + /** Verifies getDependencyName strips PEP 508 marker suffix from requirements. */ + @Test + void get_Dependency_Name_with_markers() { + assertEquals( + "colorama", + PythonControllerRealEnv.getDependencyName("colorama ; sys_platform == \"win32\"")); + assertEquals( + "colorama", PythonControllerRealEnv.getDependencyName("colorama;sys_platform==\"win32\"")); + assertEquals( + "certifi", + PythonControllerRealEnv.getDependencyName("certifi==2023.7.22 ; python_version >= \"3\"")); + } + @Test void automaticallyInstallPackageOnEnvironment() { assertFalse(pythonControllerRealEnv.automaticallyInstallPackageOnEnvironment()); diff --git a/src/test/resources/tst_manifests/pip/pip_requirements_txt_marker_installed/expected_component_sbom.json b/src/test/resources/tst_manifests/pip/pip_requirements_txt_marker_installed/expected_component_sbom.json new file mode 100644 index 00000000..072b60c7 --- /dev/null +++ b/src/test/resources/tst_manifests/pip/pip_requirements_txt_marker_installed/expected_component_sbom.json @@ -0,0 +1,48 @@ +{ + "bomFormat" : "CycloneDX", + "specVersion" : "1.4", + "version" : 1, + "metadata" : { + "timestamp" : "2025-04-09T12:38:18Z", + "component" : { + "type" : "application", + "bom-ref" : "pkg:pypi/default-pip-root@0.0.0", + "name" : "default-pip-root", + "version" : "0.0.0", + "purl" : "pkg:pypi/default-pip-root@0.0.0" + } + }, + "components" : [ + { + "type" : "library", + "bom-ref" : "pkg:pypi/six@1.16.0", + "name" : "six", + "version" : "1.16.0", + "purl" : "pkg:pypi/six@1.16.0" + }, + { + "type" : "library", + "bom-ref" : "pkg:pypi/colorama@0.4.6", + "name" : "colorama", + "version" : "0.4.6", + "purl" : "pkg:pypi/colorama@0.4.6" + } + ], + "dependencies" : [ + { + "ref" : "pkg:pypi/default-pip-root@0.0.0", + "dependsOn" : [ + "pkg:pypi/six@1.16.0", + "pkg:pypi/colorama@0.4.6" + ] + }, + { + "ref" : "pkg:pypi/six@1.16.0", + "dependsOn" : [ ] + }, + { + "ref" : "pkg:pypi/colorama@0.4.6", + "dependsOn" : [ ] + } + ] +} diff --git a/src/test/resources/tst_manifests/pip/pip_requirements_txt_marker_installed/requirements.txt b/src/test/resources/tst_manifests/pip/pip_requirements_txt_marker_installed/requirements.txt new file mode 100644 index 00000000..7cf1e7e2 --- /dev/null +++ b/src/test/resources/tst_manifests/pip/pip_requirements_txt_marker_installed/requirements.txt @@ -0,0 +1,2 @@ +six==1.16.0 +colorama ; sys_platform == "win32" diff --git a/src/test/resources/tst_manifests/pip/pip_requirements_txt_marker_skip/expected_component_sbom.json b/src/test/resources/tst_manifests/pip/pip_requirements_txt_marker_skip/expected_component_sbom.json new file mode 100644 index 00000000..af333b2a --- /dev/null +++ b/src/test/resources/tst_manifests/pip/pip_requirements_txt_marker_skip/expected_component_sbom.json @@ -0,0 +1,48 @@ +{ + "bomFormat" : "CycloneDX", + "specVersion" : "1.4", + "version" : 1, + "metadata" : { + "timestamp" : "2025-04-09T12:38:18Z", + "component" : { + "type" : "application", + "bom-ref" : "pkg:pypi/default-pip-root@0.0.0", + "name" : "default-pip-root", + "version" : "0.0.0", + "purl" : "pkg:pypi/default-pip-root@0.0.0" + } + }, + "components" : [ + { + "type" : "library", + "bom-ref" : "pkg:pypi/six@1.16.0", + "name" : "six", + "version" : "1.16.0", + "purl" : "pkg:pypi/six@1.16.0" + }, + { + "type" : "library", + "bom-ref" : "pkg:pypi/certifi@2023.7.22", + "name" : "certifi", + "version" : "2023.7.22", + "purl" : "pkg:pypi/certifi@2023.7.22" + } + ], + "dependencies" : [ + { + "ref" : "pkg:pypi/default-pip-root@0.0.0", + "dependsOn" : [ + "pkg:pypi/six@1.16.0", + "pkg:pypi/certifi@2023.7.22" + ] + }, + { + "ref" : "pkg:pypi/six@1.16.0", + "dependsOn" : [ ] + }, + { + "ref" : "pkg:pypi/certifi@2023.7.22", + "dependsOn" : [ ] + } + ] +} diff --git a/src/test/resources/tst_manifests/pip/pip_requirements_txt_marker_skip/requirements.txt b/src/test/resources/tst_manifests/pip/pip_requirements_txt_marker_skip/requirements.txt new file mode 100644 index 00000000..f8c35ddd --- /dev/null +++ b/src/test/resources/tst_manifests/pip/pip_requirements_txt_marker_skip/requirements.txt @@ -0,0 +1,3 @@ +six==1.16.0 +certifi==2023.7.22 ; python_version >= "3" +pywin32==306 ; platform_system == "Windows"