Skip to content

Commit 310c568

Browse files
ruromeroclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 473aeb5 commit 310c568

File tree

4 files changed

+96
-0
lines changed

4 files changed

+96
-0
lines changed

src/main/java/io/github/guacsec/trustifyda/utils/PythonControllerBase.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ private List<Map<String, Object>> getDependenciesImpl(
181181
if (dep.contains("==")) {
182182
doubleEqualSignPosition = dep.indexOf("==");
183183
manifestVersion = dep.substring(doubleEqualSignPosition + 2).trim();
184+
if (manifestVersion.contains(";")) {
185+
manifestVersion = manifestVersion.substring(0, manifestVersion.indexOf(";")).trim();
186+
}
184187
if (manifestVersion.contains("#")) {
185188
var hashCharIndex = manifestVersion.indexOf("#");
186189
manifestVersion = manifestVersion.substring(0, hashCharIndex);
@@ -210,6 +213,10 @@ private List<Map<String, Object>> getDependenciesImpl(
210213
}
211214
List<String> path = new ArrayList<>();
212215
String selectedDepName = getDependencyName(dep.toLowerCase());
216+
boolean hasMarker = dep.contains(";");
217+
if (hasMarker && cachedEnvironmentDeps.get(new StringInsensitive(selectedDepName)) == null) {
218+
continue;
219+
}
213220
path.add(selectedDepName);
214221
bringAllDependencies(
215222
dependencies, selectedDepName, cachedEnvironmentDeps, includeTransitive, path);

src/test/java/io/github/guacsec/trustifyda/providers/Python_Provider_Test.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,44 @@ void test_the_provideComponent_with_properties(String testFolder) throws IOExcep
211211
assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom));
212212
}
213213

214+
@Test
215+
@RestoreSystemProperties
216+
void test_marker_constrained_uninstalled_packages_are_skipped_in_component_analysis()
217+
throws IOException {
218+
var testFolder = "pip_requirements_txt_marker_skip";
219+
var targetRequirements =
220+
String.format("src/test/resources/tst_manifests/pip/%s/requirements.txt", testFolder);
221+
222+
// load expected SBOM
223+
String expectedSbom;
224+
try (var is =
225+
getResourceAsStreamDecision(
226+
this.getClass(),
227+
String.format("tst_manifests/pip/%s/expected_component_sbom.json", testFolder))) {
228+
expectedSbom = new String(is.readAllBytes());
229+
}
230+
231+
// pip environment where only six and certifi are installed (pywin32 is Windows-only)
232+
String pipFreezeContent = "six==1.16.0\ncertifi==2023.7.22\n";
233+
String pipShowContent =
234+
"Name: certifi\nVersion: 2023.7.22\nSummary: Python package for providing Mozilla's CA"
235+
+ " Bundle.\nRequires: \nRequired-by: \n---\nName: six\nVersion: 1.16.0\nSummary:"
236+
+ " Python 2 and 3 compatibility utilities\nRequires: \nRequired-by: ";
237+
System.setProperty(
238+
PROP_TRUSTIFY_DA_PIP_FREEZE,
239+
new String(Base64.getEncoder().encode(pipFreezeContent.getBytes())));
240+
System.setProperty(
241+
PROP_TRUSTIFY_DA_PIP_SHOW,
242+
new String(Base64.getEncoder().encode(pipShowContent.getBytes())));
243+
244+
// when providing component content for a manifest with a Windows-only marker package
245+
var content = new PythonPipProvider(Path.of(targetRequirements)).provideComponent();
246+
247+
// then SBOM contains six and certifi but not pywin32
248+
assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE);
249+
assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom));
250+
}
251+
214252
@Test
215253
void Test_The_ProvideComponent_Path_Should_Throw_Exception() {
216254
assertThatIllegalArgumentException()
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"bomFormat" : "CycloneDX",
3+
"specVersion" : "1.4",
4+
"version" : 1,
5+
"metadata" : {
6+
"timestamp" : "2025-04-09T12:38:18Z",
7+
"component" : {
8+
"type" : "application",
9+
"bom-ref" : "pkg:pypi/default-pip-root@0.0.0",
10+
"name" : "default-pip-root",
11+
"version" : "0.0.0",
12+
"purl" : "pkg:pypi/default-pip-root@0.0.0"
13+
}
14+
},
15+
"components" : [
16+
{
17+
"type" : "library",
18+
"bom-ref" : "pkg:pypi/six@1.16.0",
19+
"name" : "six",
20+
"version" : "1.16.0",
21+
"purl" : "pkg:pypi/six@1.16.0"
22+
},
23+
{
24+
"type" : "library",
25+
"bom-ref" : "pkg:pypi/certifi@2023.7.22",
26+
"name" : "certifi",
27+
"version" : "2023.7.22",
28+
"purl" : "pkg:pypi/certifi@2023.7.22"
29+
}
30+
],
31+
"dependencies" : [
32+
{
33+
"ref" : "pkg:pypi/default-pip-root@0.0.0",
34+
"dependsOn" : [
35+
"pkg:pypi/six@1.16.0",
36+
"pkg:pypi/certifi@2023.7.22"
37+
]
38+
},
39+
{
40+
"ref" : "pkg:pypi/six@1.16.0",
41+
"dependsOn" : [ ]
42+
},
43+
{
44+
"ref" : "pkg:pypi/certifi@2023.7.22",
45+
"dependsOn" : [ ]
46+
}
47+
]
48+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
six==1.16.0
2+
certifi==2023.7.22 ; python_version >= "3"
3+
pywin32==306 ; platform_system == "Windows"

0 commit comments

Comments
 (0)