Skip to content

Commit 1aee31c

Browse files
ruromeroclaude
andauthored
fix: skip PEP 508 marker-constrained packages in pip provider (guacsec#400)
## Summary - Skip packages with PEP 508 environment markers (`;` separator) that are not installed in the current pip environment, instead of throwing `PackageNotInstalledException` - Strip marker suffixes from version strings before version-match comparison to prevent mismatches - Add test with mocked pip freeze/show data verifying pywin32 (Windows-only marker) is excluded while certifi (marker, installed) and six (no marker) are included - Fix `getDependencyName()` to strip PEP 508 marker suffix before scanning for version operators, preventing malformed names for marker-only requirements ([TC-4085](https://redhat.atlassian.net/browse/TC-4085)) - Use marker-stripped requirement spec for version matching in `getDependenciesImpl()` to prevent false-positive `==` detection from marker expressions Closes: [TC-4044](https://redhat.atlassian.net/browse/TC-4044) ## Test plan - [x] New unit test `test_marker_constrained_uninstalled_packages_are_skipped_in_component_analysis` passes - [x] New unit test `test_marker_only_installed_packages_are_included_in_component_analysis` passes - [x] New unit test `get_Dependency_Name_with_markers` passes - [x] All existing Python provider tests continue to pass - [x] `mvn spotless:apply` produces no changes 🤖 Generated with [Claude Code](https://claude.com/claude-code) [TC-4085]: https://redhat.atlassian.net/browse/TC-4085?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [TC-4044]: https://redhat.atlassian.net/browse/TC-4044?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --------- Signed-off-by: Ruben Romero Montes <rromerom@redhat.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0b4df10 commit 1aee31c

7 files changed

Lines changed: 181 additions & 10 deletions

File tree

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

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -173,19 +173,21 @@ private List<Map<String, Object>> getDependenciesImpl(
173173
boolean matchManifestVersions = Environment.getBoolean(PROP_MATCH_MANIFEST_VERSIONS, true);
174174

175175
for (String dep : linesOfRequirements) {
176+
boolean hasMarker = dep.contains(";");
177+
String requirementSpec = hasMarker ? dep.substring(0, dep.indexOf(";")).trim() : dep;
176178
if (matchManifestVersions) {
177179
String dependencyName;
178180
String manifestVersion;
179181
String installedVersion = "";
180182
int doubleEqualSignPosition;
181-
if (dep.contains("==")) {
182-
doubleEqualSignPosition = dep.indexOf("==");
183-
manifestVersion = dep.substring(doubleEqualSignPosition + 2).trim();
183+
if (requirementSpec.contains("==")) {
184+
doubleEqualSignPosition = requirementSpec.indexOf("==");
185+
manifestVersion = requirementSpec.substring(doubleEqualSignPosition + 2).trim();
184186
if (manifestVersion.contains("#")) {
185187
var hashCharIndex = manifestVersion.indexOf("#");
186188
manifestVersion = manifestVersion.substring(0, hashCharIndex);
187189
}
188-
dependencyName = getDependencyName(dep);
190+
dependencyName = getDependencyName(requirementSpec);
189191
PythonDependency pythonDependency =
190192
cachedEnvironmentDeps.get(new StringInsensitive(dependencyName));
191193
if (pythonDependency != null) {
@@ -209,7 +211,10 @@ private List<Map<String, Object>> getDependenciesImpl(
209211
}
210212
}
211213
List<String> path = new ArrayList<>();
212-
String selectedDepName = getDependencyName(dep.toLowerCase());
214+
String selectedDepName = getDependencyName(requirementSpec.toLowerCase());
215+
if (hasMarker && cachedEnvironmentDeps.get(new StringInsensitive(selectedDepName)) == null) {
216+
continue;
217+
}
213218
path.add(selectedDepName);
214219
bringAllDependencies(
215220
dependencies, selectedDepName, cachedEnvironmentDeps, includeTransitive, path);
@@ -373,15 +378,17 @@ protected String getDependencyNameShow(String pipShowOutput) {
373378
}
374379

375380
public static String getDependencyName(String dep) {
376-
int rightTriangleBracket = dep.indexOf(">");
377-
int leftTriangleBracket = dep.indexOf("<");
378-
int equalsSign = dep.indexOf("=");
381+
int markerSeparator = dep.indexOf(";");
382+
String requirement = markerSeparator == -1 ? dep : dep.substring(0, markerSeparator);
383+
int rightTriangleBracket = requirement.indexOf(">");
384+
int leftTriangleBracket = requirement.indexOf("<");
385+
int equalsSign = requirement.indexOf("=");
379386
int minimumIndex = getFirstSign(rightTriangleBracket, leftTriangleBracket, equalsSign);
380387
String depName;
381388
if (rightTriangleBracket == -1 && leftTriangleBracket == -1 && equalsSign == -1) {
382-
depName = dep;
389+
depName = requirement;
383390
} else {
384-
depName = dep.substring(0, minimumIndex);
391+
depName = requirement.substring(0, minimumIndex);
385392
}
386393
return depName.trim();
387394
}

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
3636
import org.junit.jupiter.api.extension.ExtendWith;
3737
import org.junit.jupiter.params.ParameterizedTest;
38+
import org.junit.jupiter.params.provider.Arguments;
3839
import org.junit.jupiter.params.provider.MethodSource;
3940
import org.junitpioneer.jupiter.RestoreSystemProperties;
4041
import org.junitpioneer.jupiter.SetSystemProperty;
@@ -211,6 +212,55 @@ void test_the_provideComponent_with_properties(String testFolder) throws IOExcep
211212
assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom));
212213
}
213214

215+
static Stream<Arguments> markerTestCases() {
216+
return Stream.of(
217+
Arguments.of(
218+
"pip_requirements_txt_marker_skip",
219+
"six==1.16.0\ncertifi==2023.7.22\n",
220+
"Name: certifi\nVersion: 2023.7.22\nSummary: Python package for providing Mozilla's CA"
221+
+ " Bundle.\nRequires: \nRequired-by: \n---\nName: six\nVersion: 1.16.0\nSummary:"
222+
+ " Python 2 and 3 compatibility utilities\nRequires: \nRequired-by: "),
223+
Arguments.of(
224+
"pip_requirements_txt_marker_installed",
225+
"six==1.16.0\ncolorama==0.4.6\n",
226+
"Name: six\nVersion: 1.16.0\nSummary: Python 2 and 3 compatibility utilities\nRequires:"
227+
+ " \nRequired-by: \n---\nName: colorama\nVersion: 0.4.6\nSummary: Cross-platform"
228+
+ " colored terminal text\nRequires: \nRequired-by: "));
229+
}
230+
231+
/**
232+
* Verifies that PEP 508 marker-constrained packages are handled correctly: skipped when not
233+
* installed (marker didn't match) and included when installed (marker matched or marker-only).
234+
*/
235+
@ParameterizedTest
236+
@MethodSource("markerTestCases")
237+
@RestoreSystemProperties
238+
void test_marker_constrained_packages_in_component_analysis(
239+
String testFolder, String pipFreezeContent, String pipShowContent) throws IOException {
240+
var targetRequirements =
241+
String.format("src/test/resources/tst_manifests/pip/%s/requirements.txt", testFolder);
242+
243+
String expectedSbom;
244+
try (var is =
245+
getResourceAsStreamDecision(
246+
this.getClass(),
247+
String.format("tst_manifests/pip/%s/expected_component_sbom.json", testFolder))) {
248+
expectedSbom = new String(is.readAllBytes());
249+
}
250+
251+
System.setProperty(
252+
PROP_TRUSTIFY_DA_PIP_FREEZE,
253+
new String(Base64.getEncoder().encode(pipFreezeContent.getBytes())));
254+
System.setProperty(
255+
PROP_TRUSTIFY_DA_PIP_SHOW,
256+
new String(Base64.getEncoder().encode(pipShowContent.getBytes())));
257+
258+
var content = new PythonPipProvider(Path.of(targetRequirements)).provideComponent();
259+
260+
assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE);
261+
assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom));
262+
}
263+
214264
@Test
215265
void Test_The_ProvideComponent_Path_Should_Throw_Exception() {
216266
assertThatIllegalArgumentException()

src/test/java/io/github/guacsec/trustifyda/utils/PythonControllerRealEnvTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,19 @@ void get_Dependency_Name_requirements() {
288288
assertEquals("something", PythonControllerRealEnv.getDependencyName("something>=2.0.5"));
289289
}
290290

291+
/** Verifies getDependencyName strips PEP 508 marker suffix from requirements. */
292+
@Test
293+
void get_Dependency_Name_with_markers() {
294+
assertEquals(
295+
"colorama",
296+
PythonControllerRealEnv.getDependencyName("colorama ; sys_platform == \"win32\""));
297+
assertEquals(
298+
"colorama", PythonControllerRealEnv.getDependencyName("colorama;sys_platform==\"win32\""));
299+
assertEquals(
300+
"certifi",
301+
PythonControllerRealEnv.getDependencyName("certifi==2023.7.22 ; python_version >= \"3\""));
302+
}
303+
291304
@Test
292305
void automaticallyInstallPackageOnEnvironment() {
293306
assertFalse(pythonControllerRealEnv.automaticallyInstallPackageOnEnvironment());
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/colorama@0.4.6",
26+
"name" : "colorama",
27+
"version" : "0.4.6",
28+
"purl" : "pkg:pypi/colorama@0.4.6"
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/colorama@0.4.6"
37+
]
38+
},
39+
{
40+
"ref" : "pkg:pypi/six@1.16.0",
41+
"dependsOn" : [ ]
42+
},
43+
{
44+
"ref" : "pkg:pypi/colorama@0.4.6",
45+
"dependsOn" : [ ]
46+
}
47+
]
48+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
six==1.16.0
2+
colorama ; sys_platform == "win32"
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)