From 310c5688e8e7640c8fc5c77357272e7cba933255 Mon Sep 17 00:00:00 2001 From: Ruben Romero Montes Date: Thu, 9 Apr 2026 13:01:11 +0200 Subject: [PATCH] fix: skip PEP 508 marker-constrained packages not installed in pip environment When a requirements.txt contains packages with environment markers (e.g., `pywin32==306 ; platform_system == "Windows"`), pip only installs packages whose markers match the current platform. The component analysis now detects the semicolon marker separator and silently skips packages that have markers but are not present in the pip freeze cache, instead of throwing a PackageNotInstalledException. Also strips marker suffixes from version strings before version-match comparison. Co-Authored-By: Claude Opus 4.6 --- .../utils/PythonControllerBase.java | 7 +++ .../providers/Python_Provider_Test.java | 38 +++++++++++++++ .../expected_component_sbom.json | 48 +++++++++++++++++++ .../requirements.txt | 3 ++ 4 files changed, 96 insertions(+) create mode 100644 src/test/resources/tst_manifests/pip/pip_requirements_txt_marker_skip/expected_component_sbom.json create mode 100644 src/test/resources/tst_manifests/pip/pip_requirements_txt_marker_skip/requirements.txt 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"