Skip to content

Commit c1292e7

Browse files
committed
fix: strip PEP 508 marker suffix in getDependencyName() for versionless requirements
getDependencyName() picked up operators from marker expressions (e.g., == in sys_platform == "win32") when a requirement had no version operator, producing malformed names and cache misses. Strip the marker suffix (;) before scanning for version operators in both getDependencyName() and the version matching block in getDependenciesImpl(). Implements TC-4085 Assisted-by: Claude Code
1 parent 96dae1e commit c1292e7

5 files changed

Lines changed: 116 additions & 14 deletions

File tree

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

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -173,22 +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();
184-
if (manifestVersion.contains(";")) {
185-
manifestVersion = manifestVersion.substring(0, manifestVersion.indexOf(";")).trim();
186-
}
183+
if (requirementSpec.contains("==")) {
184+
doubleEqualSignPosition = requirementSpec.indexOf("==");
185+
manifestVersion = requirementSpec.substring(doubleEqualSignPosition + 2).trim();
187186
if (manifestVersion.contains("#")) {
188187
var hashCharIndex = manifestVersion.indexOf("#");
189188
manifestVersion = manifestVersion.substring(0, hashCharIndex);
190189
}
191-
dependencyName = getDependencyName(dep);
190+
dependencyName = getDependencyName(requirementSpec);
192191
PythonDependency pythonDependency =
193192
cachedEnvironmentDeps.get(new StringInsensitive(dependencyName));
194193
if (pythonDependency != null) {
@@ -212,8 +211,7 @@ private List<Map<String, Object>> getDependenciesImpl(
212211
}
213212
}
214213
List<String> path = new ArrayList<>();
215-
String selectedDepName = getDependencyName(dep.toLowerCase());
216-
boolean hasMarker = dep.contains(";");
214+
String selectedDepName = getDependencyName(requirementSpec.toLowerCase());
217215
if (hasMarker && cachedEnvironmentDeps.get(new StringInsensitive(selectedDepName)) == null) {
218216
continue;
219217
}
@@ -380,15 +378,17 @@ protected String getDependencyNameShow(String pipShowOutput) {
380378
}
381379

382380
public static String getDependencyName(String dep) {
383-
int rightTriangleBracket = dep.indexOf(">");
384-
int leftTriangleBracket = dep.indexOf("<");
385-
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("=");
386386
int minimumIndex = getFirstSign(rightTriangleBracket, leftTriangleBracket, equalsSign);
387387
String depName;
388388
if (rightTriangleBracket == -1 && leftTriangleBracket == -1 && equalsSign == -1) {
389-
depName = dep;
389+
depName = requirement;
390390
} else {
391-
depName = dep.substring(0, minimumIndex);
391+
depName = requirement.substring(0, minimumIndex);
392392
}
393393
return depName.trim();
394394
}

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,45 @@ void test_marker_constrained_uninstalled_packages_are_skipped_in_component_analy
249249
assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom));
250250
}
251251

252+
/**
253+
* Verifies that marker-only packages (no version operator) that ARE installed appear in the SBOM.
254+
*/
255+
@Test
256+
@RestoreSystemProperties
257+
void test_marker_only_installed_packages_are_included_in_component_analysis() throws IOException {
258+
// Given a requirements.txt with a marker-only dep that IS installed
259+
var testFolder = "pip_requirements_txt_marker_installed";
260+
var targetRequirements =
261+
String.format("src/test/resources/tst_manifests/pip/%s/requirements.txt", testFolder);
262+
263+
String expectedSbom;
264+
try (var is =
265+
getResourceAsStreamDecision(
266+
this.getClass(),
267+
String.format("tst_manifests/pip/%s/expected_component_sbom.json", testFolder))) {
268+
expectedSbom = new String(is.readAllBytes());
269+
}
270+
271+
// When pip freeze and pip show include colorama (the marker-only package)
272+
String pipFreezeContent = "six==1.16.0\ncolorama==0.4.6\n";
273+
String pipShowContent =
274+
"Name: six\nVersion: 1.16.0\nSummary: Python 2 and 3 compatibility utilities\nRequires:"
275+
+ " \nRequired-by: \n---\nName: colorama\nVersion: 0.4.6\nSummary: Cross-platform"
276+
+ " colored terminal text\nRequires: \nRequired-by: ";
277+
System.setProperty(
278+
PROP_TRUSTIFY_DA_PIP_FREEZE,
279+
new String(Base64.getEncoder().encode(pipFreezeContent.getBytes())));
280+
System.setProperty(
281+
PROP_TRUSTIFY_DA_PIP_SHOW,
282+
new String(Base64.getEncoder().encode(pipShowContent.getBytes())));
283+
284+
var content = new PythonPipProvider(Path.of(targetRequirements)).provideComponent();
285+
286+
// Then SBOM contains both six and colorama
287+
assertThat(content.type).isEqualTo(Api.CYCLONEDX_MEDIA_TYPE);
288+
assertThat(dropIgnored(new String(content.buffer))).isEqualTo(dropIgnored(expectedSbom));
289+
}
290+
252291
@Test
253292
void Test_The_ProvideComponent_Path_Should_Throw_Exception() {
254293
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"

0 commit comments

Comments
 (0)