Skip to content

Commit fb7d0e3

Browse files
ruromeroclaude
andcommitted
fix: skip marker-constrained uninstalled packages in Python requirements
When a requirements.txt contains packages with PEP 508 environment markers (e.g., `pywin32==306 ; platform_system == "Windows"`), pip only installs packages whose markers match the current platform. Previously, the JavaScript client scanned ALL packages from the manifest regardless of markers, throwing an error when a marker- constrained package was not installed. This fix uses tree-sitter to detect `marker_spec` nodes on each requirement and skips packages that have a marker but are absent from the installed packages cache (`pip freeze`). Packages with markers that ARE installed are still included in the SBOM. Implements TC-4043 Assisted-by: Claude Code Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f2d5d72 commit fb7d0e3

4 files changed

Lines changed: 96 additions & 3 deletions

File tree

src/providers/python_controller.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export default class Python_controller {
9797

9898
/**
9999
* Parse the requirements.txt file using tree-sitter and return structured requirement data.
100-
* @return {Promise<{name: string, version: string|null}[]>}
100+
* @return {Promise<{name: string, version: string|null, hasMarker: boolean}[]>}
101101
*/
102102
async #parseRequirements() {
103103
const content = fs.readFileSync(this.pathToRequirements).toString();
@@ -109,7 +109,8 @@ export default class Python_controller {
109109
const version = versionMatches.length > 0
110110
? versionMatches[0].captures.find(c => c.name === 'version').node.text
111111
: null;
112-
return { name, version };
112+
const hasMarker = reqNode.children.some(c => c.type === 'marker_spec');
113+
return { name, version, hasMarker };
113114
}));
114115
}
115116

@@ -224,7 +225,10 @@ export default class Python_controller {
224225
CachedEnvironmentDeps[packageName.replace("_", "-")] = pipDepTreeEntryForCache
225226
})
226227
}
227-
parsedRequirements.forEach(({ name: depName, version: manifestVersion }) => {
228+
parsedRequirements.forEach(({ name: depName, version: manifestVersion, hasMarker }) => {
229+
if(hasMarker && CachedEnvironmentDeps[depName.toLowerCase()] === undefined) {
230+
return
231+
}
228232
if(matchManifestVersions === "true" && manifestVersion != null) {
229233
let installedVersion
230234
if(CachedEnvironmentDeps[depName.toLowerCase()] !== undefined) {

test/providers/python_pip.test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,41 @@ suite('testing the python-pip data provider with virtual environment', () => {
115115
})
116116

117117
}).beforeAll(() => {clock = useFakeTimers(new Date('2023-10-01T00:00:00.000Z'))}).afterAll(()=> clock.restore());
118+
119+
suite('testing python-pip PEP 508 marker handling', () => {
120+
const markerTestCase = 'pip_requirements_txt_marker_skip'
121+
122+
/** Verify that packages with environment markers (PEP 508) that are not installed
123+
* in the current environment are silently skipped, while marker-constrained
124+
* packages that ARE installed are still included in the SBOM. */
125+
test('verify marker-constrained uninstalled packages are skipped in component analysis', async () => {
126+
// given: pip environment where only six and certifi are installed (pywin32 is Windows-only)
127+
const pipFreezeOutput = 'six==1.16.0\ncertifi==2023.7.22\n'
128+
const pipShowOutput =
129+
'Name: certifi\nVersion: 2023.7.22\nSummary: Python package for providing Mozilla\'s CA Bundle.\nRequires: \nRequired-by: ' +
130+
'\n---\n' +
131+
'Name: six\nVersion: 1.16.0\nSummary: Python 2 and 3 compatibility utilities\nRequires: \nRequired-by: '
132+
133+
process.env['TRUSTIFY_DA_PIP_FREEZE'] = Buffer.from(pipFreezeOutput).toString('base64')
134+
process.env['TRUSTIFY_DA_PIP_SHOW'] = Buffer.from(pipShowOutput).toString('base64')
135+
136+
try {
137+
// when: component analysis is run against a manifest with a Windows-only marker package
138+
let expectedSbom = fs.readFileSync(`test/providers/tst_manifests/pip/${markerTestCase}/expected_component_sbom.json`).toString().trim()
139+
expectedSbom = JSON.stringify(JSON.parse(expectedSbom))
140+
141+
let result = await pythonPip.provideComponent(`test/providers/tst_manifests/pip/${markerTestCase}/requirements.txt`, {})
142+
143+
// then: SBOM contains six and certifi but not pywin32
144+
expect(result).to.deep.equal({
145+
ecosystem: 'pip',
146+
contentType: 'application/vnd.cyclonedx+json',
147+
content: expectedSbom
148+
})
149+
} finally {
150+
delete process.env['TRUSTIFY_DA_PIP_FREEZE']
151+
delete process.env['TRUSTIFY_DA_PIP_SHOW']
152+
}
153+
}).timeout(10000)
154+
155+
}).beforeAll(() => clock = useFakeTimers(new Date('2023-10-01T00:00:00.000Z'))).afterAll(() => clock.restore());
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": "2023-10-01T00:00:00.000Z",
7+
"component": {
8+
"name": "default-pip-root",
9+
"version": "0.0.0",
10+
"purl": "pkg:pypi/default-pip-root@0.0.0",
11+
"type": "application",
12+
"bom-ref": "pkg:pypi/default-pip-root@0.0.0"
13+
}
14+
},
15+
"components": [
16+
{
17+
"name": "certifi",
18+
"version": "2023.7.22",
19+
"purl": "pkg:pypi/certifi@2023.7.22",
20+
"type": "library",
21+
"bom-ref": "pkg:pypi/certifi@2023.7.22"
22+
},
23+
{
24+
"name": "six",
25+
"version": "1.16.0",
26+
"purl": "pkg:pypi/six@1.16.0",
27+
"type": "library",
28+
"bom-ref": "pkg:pypi/six@1.16.0"
29+
}
30+
],
31+
"dependencies": [
32+
{
33+
"ref": "pkg:pypi/default-pip-root@0.0.0",
34+
"dependsOn": [
35+
"pkg:pypi/certifi@2023.7.22",
36+
"pkg:pypi/six@1.16.0"
37+
]
38+
},
39+
{
40+
"ref": "pkg:pypi/certifi@2023.7.22",
41+
"dependsOn": []
42+
},
43+
{
44+
"ref": "pkg:pypi/six@1.16.0",
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)