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..8103f9fc 100644 --- a/src/main/java/io/github/guacsec/trustifyda/utils/PythonControllerBase.java +++ b/src/main/java/io/github/guacsec/trustifyda/utils/PythonControllerBase.java @@ -181,6 +181,9 @@ private List> getDependenciesImpl( if (dep.contains("==")) { doubleEqualSignPosition = dep.indexOf("=="); manifestVersion = dep.substring(doubleEqualSignPosition + 2).trim(); + if (manifestVersion.contains(";")) { + manifestVersion = manifestVersion.substring(0, manifestVersion.indexOf(";")).trim(); + } if (manifestVersion.contains("#")) { var hashCharIndex = manifestVersion.indexOf("#"); manifestVersion = manifestVersion.substring(0, hashCharIndex); @@ -210,6 +213,10 @@ private List> getDependenciesImpl( } List path = new ArrayList<>(); String selectedDepName = getDependencyName(dep.toLowerCase()); + boolean hasMarker = dep.contains(";"); + if (hasMarker && cachedEnvironmentDeps.get(new StringInsensitive(selectedDepName)) == null) { + continue; + } path.add(selectedDepName); bringAllDependencies( dependencies, selectedDepName, cachedEnvironmentDeps, includeTransitive, path); 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..9c87fb19 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 @@ -211,6 +211,44 @@ void test_the_provideComponent_with_properties(String testFolder) throws IOExcep assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom)); } + @Test + @RestoreSystemProperties + void test_marker_constrained_uninstalled_packages_are_skipped_in_component_analysis() + throws IOException { + var testFolder = "pip_requirements_txt_marker_skip"; + var targetRequirements = + String.format("src/test/resources/tst_manifests/pip/%s/requirements.txt", testFolder); + + // load expected SBOM + String expectedSbom; + try (var is = + getResourceAsStreamDecision( + this.getClass(), + String.format("tst_manifests/pip/%s/expected_component_sbom.json", testFolder))) { + expectedSbom = new String(is.readAllBytes()); + } + + // pip environment where only six and certifi are installed (pywin32 is Windows-only) + String pipFreezeContent = "six==1.16.0\ncertifi==2023.7.22\n"; + String pipShowContent = + "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: "; + 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()))); + + // when providing component content for a manifest with a Windows-only marker package + var content = new PythonPipProvider(Path.of(targetRequirements)).provideComponent(); + + // then SBOM contains six and certifi but not pywin32 + 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/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"