Skip to content

Commit d8ec262

Browse files
authored
feat(poetry): extract SHA-256 hashes from poetry.lock for SBOM generation (#534)
Extend _extractMarkerData() to extract files[].hash entries per package from poetry.lock and populate graph entries with SHA-256 hashes. Prefers sdist (.tar.gz) hash over wheel hashes. The hashes flow through the existing base_pyproject pipeline into CycloneDX SBOM components. Implements TC-4333 Assisted-by: Claude Code
1 parent f1e4e15 commit d8ec262

9 files changed

Lines changed: 789 additions & 508 deletions

File tree

src/providers/python_poetry.js

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,12 @@ export default class Python_poetry extends Base_pyproject {
121121
* → transitiveMarkers['click']['colorama'] = "sys_platform == 'win32'"
122122
* @param {string|null} lockDir
123123
* @param {object} parsed - parsed pyproject.toml
124-
* @returns {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>}}
124+
* @returns {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>, hashMap: Map<string, Array<{alg: string, content: string}>>}}
125125
*/
126126
_extractMarkerData(lockDir, parsed) {
127127
let directMarkers = new Map()
128128
let transitiveMarkers = new Map()
129+
let hashMap = new Map()
129130

130131
// Extract markers from PEP 621 dependency strings: "name[extras]>=ver ; marker"
131132
let deps = parsed.project?.dependencies || []
@@ -154,20 +155,42 @@ export default class Python_poetry extends Base_pyproject {
154155
transitiveMarkers.get(pkgKey).set(this._canonicalize(depName), markers)
155156
}
156157
}
158+
159+
let sha256Hex = this._extractSha256FromFiles(pkg.files)
160+
if (sha256Hex) {
161+
hashMap.set(pkgKey, [{alg: "SHA-256", content: sha256Hex}])
162+
}
157163
}
158164
}
159165
}
160166

161-
return { directMarkers, transitiveMarkers }
167+
return { directMarkers, transitiveMarkers, hashMap }
168+
}
169+
170+
/**
171+
* Extract a SHA-256 hex digest from poetry.lock files array.
172+
* Prefers the sdist (.tar.gz) hash; falls back to the first wheel hash.
173+
* @param {Array<{file: string, hash: string}>} [files]
174+
* @returns {string|null}
175+
*/
176+
_extractSha256FromFiles(files) {
177+
if (!Array.isArray(files) || files.length === 0) { return null }
178+
let sdist = files.find(f => f.file.endsWith('.tar.gz'))
179+
let entry = sdist || files[0]
180+
let hash = entry.hash
181+
if (hash && hash.startsWith('sha256:')) {
182+
return hash.slice(7)
183+
}
184+
return null
162185
}
163186

164187
/**
165188
* Parse poetry show --tree output into a dependency graph structure.
166189
*
167190
* @param {string} treeOutput
168191
* @param {Map<string, string>} versionMap - canonical name -> resolved version
169-
* @param {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>}} markerData
170-
* @returns {{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[]}>}}
192+
* @param {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>, hashMap: Map<string, Array<{alg: string, content: string}>>}} markerData
193+
* @returns {{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[], hashes?: Array<{alg: string, content: string}>}>}}
171194
*/
172195
_parsePoetryTree(treeOutput, versionMap, markerData) {
173196
let lines = treeOutput.split(/\r?\n/)
@@ -196,7 +219,10 @@ export default class Python_poetry extends Base_pyproject {
196219

197220
directDeps.push(key)
198221
if (!graph.has(key)) {
199-
graph.set(key, { name, version, children: [] })
222+
let entry = { name, version, children: [] }
223+
let hashes = markerData.hashMap.get(key)
224+
if (hashes) { entry.hashes = hashes }
225+
graph.set(key, entry)
200226
}
201227
currentDirectDep = key
202228
stack = [{ key, depth: -1 }]
@@ -243,7 +269,10 @@ export default class Python_poetry extends Base_pyproject {
243269
}
244270

245271
if (!graph.has(depKey)) {
246-
graph.set(depKey, { name: depName, version, children: [] })
272+
let entry = { name: depName, version, children: [] }
273+
let hashes = markerData.hashMap.get(depKey)
274+
if (hashes) { entry.hashes = hashes }
275+
graph.set(depKey, entry)
247276
}
248277

249278
if (parentKey) {
Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,42 @@
11
{
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-
]
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+
"hashes": [
23+
{
24+
"alg": "SHA-256",
25+
"content": "18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517"
26+
}
27+
]
28+
}
29+
],
30+
"dependencies": [
31+
{
32+
"ref": "pkg:pypi/dev-deps-project@1.0.0",
33+
"dependsOn": [
34+
"pkg:pypi/requests@2.33.1"
35+
]
36+
},
37+
{
38+
"ref": "pkg:pypi/requests@2.33.1",
39+
"dependsOn": []
40+
}
41+
]
3642
}
Lines changed: 113 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,115 @@
11
{
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-
]
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+
"hashes": [
23+
{
24+
"alg": "SHA-256",
25+
"content": "18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517"
26+
}
27+
]
28+
},
29+
{
30+
"name": "certifi",
31+
"version": "2026.2.25",
32+
"purl": "pkg:pypi/certifi@2026.2.25",
33+
"type": "library",
34+
"bom-ref": "pkg:pypi/certifi@2026.2.25",
35+
"hashes": [
36+
{
37+
"alg": "SHA-256",
38+
"content": "e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"
39+
}
40+
]
41+
},
42+
{
43+
"name": "charset-normalizer",
44+
"version": "3.4.7",
45+
"purl": "pkg:pypi/charset-normalizer@3.4.7",
46+
"type": "library",
47+
"bom-ref": "pkg:pypi/charset-normalizer@3.4.7",
48+
"hashes": [
49+
{
50+
"alg": "SHA-256",
51+
"content": "ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5"
52+
}
53+
]
54+
},
55+
{
56+
"name": "idna",
57+
"version": "3.11",
58+
"purl": "pkg:pypi/idna@3.11",
59+
"type": "library",
60+
"bom-ref": "pkg:pypi/idna@3.11",
61+
"hashes": [
62+
{
63+
"alg": "SHA-256",
64+
"content": "795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"
65+
}
66+
]
67+
},
68+
{
69+
"name": "urllib3",
70+
"version": "2.6.3",
71+
"purl": "pkg:pypi/urllib3@2.6.3",
72+
"type": "library",
73+
"bom-ref": "pkg:pypi/urllib3@2.6.3",
74+
"hashes": [
75+
{
76+
"alg": "SHA-256",
77+
"content": "1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"
78+
}
79+
]
80+
}
81+
],
82+
"dependencies": [
83+
{
84+
"ref": "pkg:pypi/dev-deps-project@1.0.0",
85+
"dependsOn": [
86+
"pkg:pypi/requests@2.33.1"
87+
]
88+
},
89+
{
90+
"ref": "pkg:pypi/requests@2.33.1",
91+
"dependsOn": [
92+
"pkg:pypi/certifi@2026.2.25",
93+
"pkg:pypi/charset-normalizer@3.4.7",
94+
"pkg:pypi/idna@3.11",
95+
"pkg:pypi/urllib3@2.6.3"
96+
]
97+
},
98+
{
99+
"ref": "pkg:pypi/certifi@2026.2.25",
100+
"dependsOn": []
101+
},
102+
{
103+
"ref": "pkg:pypi/charset-normalizer@3.4.7",
104+
"dependsOn": []
105+
},
106+
{
107+
"ref": "pkg:pypi/idna@3.11",
108+
"dependsOn": []
109+
},
110+
{
111+
"ref": "pkg:pypi/urllib3@2.6.3",
112+
"dependsOn": []
113+
}
114+
]
85115
}

0 commit comments

Comments
 (0)