Skip to content

Commit 6ca858a

Browse files
authored
fix: exclude Poetry dev dependencies from SBOMs (guacsec#479)
Implements TC-4096
1 parent ccdac93 commit 6ca858a

10 files changed

Lines changed: 973 additions & 3 deletions

File tree

src/providers/python_poetry.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ export default class Python_poetry extends Base_pyproject {
4646
*/
4747
// eslint-disable-next-line no-unused-vars
4848
async _getDependencyData(manifestDir, _workspaceDir, parsed, opts) {
49-
let treeOutput = this._getPoetryShowTreeOutput(manifestDir, opts)
49+
let hasDevGroup = !!(parsed.tool?.poetry?.group?.dev || parsed.tool?.poetry?.['dev-dependencies'])
50+
let treeOutput = this._getPoetryShowTreeOutput(manifestDir, hasDevGroup, opts)
5051
let showAllOutput = this._getPoetryShowAllOutput(manifestDir, opts)
5152
let versionMap = this._parsePoetryShowAll(showAllOutput)
5253
return this._parsePoetryTree(treeOutput, versionMap)
@@ -55,15 +56,20 @@ export default class Python_poetry extends Base_pyproject {
5556
/**
5657
* Get poetry show --tree output.
5758
* @param {string} manifestDir
59+
* @param {boolean} hasDevGroup
5860
* @param {Object} opts
5961
* @returns {string}
6062
*/
61-
_getPoetryShowTreeOutput(manifestDir, opts) {
63+
_getPoetryShowTreeOutput(manifestDir, hasDevGroup, opts) {
6264
if (environmentVariableIsPopulated('TRUSTIFY_DA_POETRY_SHOW_TREE')) {
6365
return Buffer.from(process.env['TRUSTIFY_DA_POETRY_SHOW_TREE'], 'base64').toString('utf-8')
6466
}
6567
let poetryBin = getCustomPath('poetry', opts)
66-
return invokeCommand(poetryBin, ['show', '--tree', '--no-ansi'], { cwd: manifestDir }).toString()
68+
let args = ['show', '--tree', '--no-ansi']
69+
if (hasDevGroup) {
70+
args.push('--without', 'dev')
71+
}
72+
return invokeCommand(poetryBin, args, { cwd: manifestDir }).toString()
6773
}
6874

6975
/**

test/providers/python_pyproject.test.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,54 @@ suite('testing the python-pyproject data provider', () => {
177177
}).timeout(TIMEOUT)
178178
})
179179

180+
suite('poetry projects - modern dev dependencies excluded (TC-4096)', () => {
181+
test('dev dependencies excluded from stack analysis', async () => {
182+
let expectedSbom = fs.readFileSync('test/providers/tst_manifests/pyproject/poetry_dev_deps/expected_stack_sbom.json').toString()
183+
expectedSbom = JSON.stringify(JSON.parse(expectedSbom))
184+
let result = await poetryProvider.provideStack('test/providers/tst_manifests/pyproject/poetry_dev_deps/pyproject.toml')
185+
expect(result).to.deep.equal({
186+
ecosystem: 'pip',
187+
contentType: 'application/vnd.cyclonedx+json',
188+
content: expectedSbom
189+
})
190+
}).timeout(TIMEOUT)
191+
192+
test('dev dependencies excluded from component analysis', async () => {
193+
let expectedSbom = fs.readFileSync('test/providers/tst_manifests/pyproject/poetry_dev_deps/expected_component_sbom.json').toString().trim()
194+
expectedSbom = JSON.stringify(JSON.parse(expectedSbom))
195+
let result = await poetryProvider.provideComponent('test/providers/tst_manifests/pyproject/poetry_dev_deps/pyproject.toml')
196+
expect(result).to.deep.equal({
197+
ecosystem: 'pip',
198+
contentType: 'application/vnd.cyclonedx+json',
199+
content: expectedSbom
200+
})
201+
}).timeout(TIMEOUT)
202+
})
203+
204+
suite('poetry projects - legacy dev-dependencies excluded (TC-4096)', () => {
205+
test('legacy dev dependencies excluded from stack analysis', async () => {
206+
let expectedSbom = fs.readFileSync('test/providers/tst_manifests/pyproject/poetry_legacy_dev_deps/expected_stack_sbom.json').toString()
207+
expectedSbom = JSON.stringify(JSON.parse(expectedSbom))
208+
let result = await poetryProvider.provideStack('test/providers/tst_manifests/pyproject/poetry_legacy_dev_deps/pyproject.toml')
209+
expect(result).to.deep.equal({
210+
ecosystem: 'pip',
211+
contentType: 'application/vnd.cyclonedx+json',
212+
content: expectedSbom
213+
})
214+
}).timeout(TIMEOUT)
215+
216+
test('legacy dev dependencies excluded from component analysis', async () => {
217+
let expectedSbom = fs.readFileSync('test/providers/tst_manifests/pyproject/poetry_legacy_dev_deps/expected_component_sbom.json').toString().trim()
218+
expectedSbom = JSON.stringify(JSON.parse(expectedSbom))
219+
let result = await poetryProvider.provideComponent('test/providers/tst_manifests/pyproject/poetry_legacy_dev_deps/pyproject.toml')
220+
expect(result).to.deep.equal({
221+
ecosystem: 'pip',
222+
contentType: 'application/vnd.cyclonedx+json',
223+
content: expectedSbom
224+
})
225+
}).timeout(TIMEOUT)
226+
})
227+
180228
suite('poetry projects - poetry_only_deps manifest', () => {
181229
const fixtureDir = `${MANIFESTS}/poetry_only_deps`
182230

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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": "dev-deps-project",
9+
"version": "1.0.0",
10+
"purl": "pkg:pypi/dev-deps-project@1.0.0",
11+
"type": "application",
12+
"bom-ref": "pkg:pypi/dev-deps-project@1.0.0"
13+
}
14+
},
15+
"components": [
16+
{
17+
"name": "requests",
18+
"version": "2.33.1",
19+
"purl": "pkg:pypi/requests@2.33.1",
20+
"type": "library",
21+
"bom-ref": "pkg:pypi/requests@2.33.1"
22+
}
23+
],
24+
"dependencies": [
25+
{
26+
"ref": "pkg:pypi/dev-deps-project@1.0.0",
27+
"dependsOn": [
28+
"pkg:pypi/requests@2.33.1"
29+
]
30+
},
31+
{
32+
"ref": "pkg:pypi/requests@2.33.1",
33+
"dependsOn": []
34+
}
35+
]
36+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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": "dev-deps-project",
9+
"version": "1.0.0",
10+
"purl": "pkg:pypi/dev-deps-project@1.0.0",
11+
"type": "application",
12+
"bom-ref": "pkg:pypi/dev-deps-project@1.0.0"
13+
}
14+
},
15+
"components": [
16+
{
17+
"name": "requests",
18+
"version": "2.33.1",
19+
"purl": "pkg:pypi/requests@2.33.1",
20+
"type": "library",
21+
"bom-ref": "pkg:pypi/requests@2.33.1"
22+
},
23+
{
24+
"name": "certifi",
25+
"version": "2026.2.25",
26+
"purl": "pkg:pypi/certifi@2026.2.25",
27+
"type": "library",
28+
"bom-ref": "pkg:pypi/certifi@2026.2.25"
29+
},
30+
{
31+
"name": "charset-normalizer",
32+
"version": "3.4.7",
33+
"purl": "pkg:pypi/charset-normalizer@3.4.7",
34+
"type": "library",
35+
"bom-ref": "pkg:pypi/charset-normalizer@3.4.7"
36+
},
37+
{
38+
"name": "idna",
39+
"version": "3.11",
40+
"purl": "pkg:pypi/idna@3.11",
41+
"type": "library",
42+
"bom-ref": "pkg:pypi/idna@3.11"
43+
},
44+
{
45+
"name": "urllib3",
46+
"version": "2.6.3",
47+
"purl": "pkg:pypi/urllib3@2.6.3",
48+
"type": "library",
49+
"bom-ref": "pkg:pypi/urllib3@2.6.3"
50+
}
51+
],
52+
"dependencies": [
53+
{
54+
"ref": "pkg:pypi/dev-deps-project@1.0.0",
55+
"dependsOn": [
56+
"pkg:pypi/requests@2.33.1"
57+
]
58+
},
59+
{
60+
"ref": "pkg:pypi/requests@2.33.1",
61+
"dependsOn": [
62+
"pkg:pypi/certifi@2026.2.25",
63+
"pkg:pypi/charset-normalizer@3.4.7",
64+
"pkg:pypi/idna@3.11",
65+
"pkg:pypi/urllib3@2.6.3"
66+
]
67+
},
68+
{
69+
"ref": "pkg:pypi/certifi@2026.2.25",
70+
"dependsOn": []
71+
},
72+
{
73+
"ref": "pkg:pypi/charset-normalizer@3.4.7",
74+
"dependsOn": []
75+
},
76+
{
77+
"ref": "pkg:pypi/idna@3.11",
78+
"dependsOn": []
79+
},
80+
{
81+
"ref": "pkg:pypi/urllib3@2.6.3",
82+
"dependsOn": []
83+
}
84+
]
85+
}

0 commit comments

Comments
 (0)