Skip to content

Commit 5001ad7

Browse files
aclark4lifeCopilot
andcommitted
Generate CycloneDX SBOM at release time via CI
- Add .github/generate-sbom.py: reads the real version from src/PIL/_version.py and emits a CycloneDX 1.6 JSON SBOM covering the 8 C extension modules, 3 vendored thirdparty libraries, and 13 optional native library dependencies. - Add 'sbom' job to wheels.yml: runs on tag pushes, generates the SBOM, uploads it as a workflow artifact, and attaches it to the GitHub release via 'gh release upload'. - Remove the static pillow.cdx.json; CI now owns the generated file. Can also be run locally: python .github/generate-sbom.py Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3f8212c commit 5001ad7

File tree

3 files changed

+357
-459
lines changed

3 files changed

+357
-459
lines changed

.github/generate-sbom.py

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
#!/usr/bin/env python3
2+
"""Generate a CycloneDX 1.6 SBOM for Pillow's C extensions and their
3+
vendored/optional native library dependencies.
4+
5+
Usage:
6+
python .github/generate-sbom.py [output-file]
7+
8+
Output defaults to pillow-{version}.cdx.json in the current directory.
9+
"""
10+
from __future__ import annotations
11+
12+
import json
13+
import sys
14+
import uuid
15+
from datetime import datetime, timezone
16+
from pathlib import Path
17+
18+
19+
def get_version() -> str:
20+
version_file = Path(__file__).parent.parent / "src" / "PIL" / "_version.py"
21+
return version_file.read_text(encoding="utf-8").split('"')[1]
22+
23+
24+
def generate(version: str) -> dict:
25+
serial = str(uuid.uuid4())
26+
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
27+
purl = f"pkg:pypi/pillow@{version}"
28+
29+
metadata_component = {
30+
"bom-ref": purl,
31+
"type": "library",
32+
"name": "Pillow",
33+
"version": version,
34+
"description": "Python Imaging Library (fork)",
35+
"licenses": [{"license": {"id": "MIT-CMU"}}],
36+
"purl": purl,
37+
"externalReferences": [
38+
{"type": "website", "url": "https://python-pillow.github.io"},
39+
{"type": "vcs", "url": "https://github.com/python-pillow/Pillow"},
40+
{"type": "documentation", "url": "https://pillow.readthedocs.io"},
41+
],
42+
}
43+
44+
c_extensions = [
45+
(
46+
"PIL._imaging",
47+
"Core image processing extension "
48+
"(decode, encode, map, display, outline, path, libImaging)",
49+
),
50+
("PIL._imagingft", "FreeType font rendering extension"),
51+
("PIL._imagingcms", "LittleCMS2 colour management extension"),
52+
("PIL._webp", "WebP image format extension"),
53+
("PIL._avif", "AVIF image format extension"),
54+
("PIL._imagingtk", "Tk/Tcl display extension"),
55+
("PIL._imagingmath", "Image math operations extension (via pybind11)"),
56+
("PIL._imagingmorph", "Image morphology extension"),
57+
]
58+
59+
ext_components = [
60+
{
61+
"bom-ref": f"{purl}#c-ext/{name}",
62+
"type": "library",
63+
"name": name,
64+
"version": version,
65+
"description": desc,
66+
"licenses": [{"license": {"id": "MIT-CMU"}}],
67+
"purl": f"{purl}#c-ext/{name}",
68+
}
69+
for name, desc in c_extensions
70+
]
71+
72+
vendored_components = [
73+
{
74+
"bom-ref": "pkg:github/HOST-Oman/libraqm@0.10.3",
75+
"type": "library",
76+
"name": "raqm",
77+
"version": "0.10.3",
78+
"description": "Complex text layout library "
79+
"(vendored in src/thirdparty/raqm/)",
80+
"licenses": [{"license": {"id": "MIT"}}],
81+
"purl": "pkg:github/HOST-Oman/libraqm@0.10.3",
82+
"externalReferences": [
83+
{"type": "vcs", "url": "https://github.com/HOST-Oman/libraqm"},
84+
],
85+
},
86+
{
87+
"bom-ref": f"{purl}#thirdparty/fribidi-shim",
88+
"type": "library",
89+
"name": "fribidi-shim",
90+
"version": "1.x",
91+
"description": "FriBiDi runtime-loading shim "
92+
"(vendored in src/thirdparty/fribidi-shim/); "
93+
"loads libfribidi dynamically",
94+
"licenses": [{"license": {"id": "LGPL-2.1-or-later"}}],
95+
"externalReferences": [
96+
{"type": "website", "url": "https://github.com/fribidi/fribidi"},
97+
],
98+
},
99+
{
100+
"bom-ref": "pkg:github/python/pythoncapi-compat",
101+
"type": "library",
102+
"name": "pythoncapi_compat",
103+
"description": "Backport header for new CPython C-API functions "
104+
"(vendored in src/thirdparty/pythoncapi_compat.h)",
105+
"licenses": [{"license": {"id": "MIT-0"}}],
106+
"externalReferences": [
107+
{
108+
"type": "vcs",
109+
"url": "https://github.com/python/pythoncapi-compat",
110+
},
111+
],
112+
},
113+
]
114+
115+
native_deps = [
116+
{
117+
"bom-ref": "pkg:generic/libjpeg",
118+
"type": "library",
119+
"name": "libjpeg / libjpeg-turbo",
120+
"description": "JPEG codec (required by default; disable with "
121+
"-C jpeg=disable). Tested with libjpeg 6b/8/9-9d "
122+
"and libjpeg-turbo 8.",
123+
"externalReferences": [
124+
{"type": "website", "url": "https://libjpeg-turbo.org"},
125+
{"type": "website", "url": "https://ijg.org"},
126+
],
127+
},
128+
{
129+
"bom-ref": "pkg:generic/zlib",
130+
"type": "library",
131+
"name": "zlib",
132+
"description": "Deflate/PNG compression (required by default; "
133+
"disable with -C zlib=disable).",
134+
"externalReferences": [
135+
{"type": "website", "url": "https://zlib.net"},
136+
],
137+
},
138+
{
139+
"bom-ref": "pkg:generic/libtiff",
140+
"type": "library",
141+
"name": "libtiff",
142+
"description": "TIFF codec (optional). Tested with libtiff 4.0-4.7.1.",
143+
"externalReferences": [
144+
{"type": "website", "url": "https://libtiff.gitlab.io/libtiff/"},
145+
],
146+
},
147+
{
148+
"bom-ref": "pkg:generic/freetype2",
149+
"type": "library",
150+
"name": "FreeType",
151+
"description": "Font rendering (optional, used by PIL._imagingft). "
152+
"Required for text/font support.",
153+
"externalReferences": [
154+
{"type": "website", "url": "https://freetype.org"},
155+
],
156+
},
157+
{
158+
"bom-ref": "pkg:generic/littlecms2",
159+
"type": "library",
160+
"name": "Little CMS 2",
161+
"description": "Colour management (optional, used by PIL._imagingcms). "
162+
"Tested with lcms2 2.7-2.18.",
163+
"externalReferences": [
164+
{"type": "website", "url": "https://www.littlecms.com"},
165+
],
166+
},
167+
{
168+
"bom-ref": "pkg:generic/libwebp",
169+
"type": "library",
170+
"name": "libwebp",
171+
"description": "WebP codec (optional, used by PIL._webp).",
172+
"externalReferences": [
173+
{
174+
"type": "website",
175+
"url": "https://chromium.googlesource.com/webm/libwebp",
176+
},
177+
],
178+
},
179+
{
180+
"bom-ref": "pkg:generic/openjpeg",
181+
"type": "library",
182+
"name": "OpenJPEG",
183+
"description": "JPEG 2000 codec (optional). "
184+
"Tested with openjpeg 2.0.0-2.5.4.",
185+
"externalReferences": [
186+
{"type": "website", "url": "https://www.openjpeg.org"},
187+
],
188+
},
189+
{
190+
"bom-ref": "pkg:generic/libavif",
191+
"type": "library",
192+
"name": "libavif",
193+
"description": "AVIF codec (optional, used by PIL._avif). "
194+
"Requires libavif >= 1.0.0.",
195+
"externalReferences": [
196+
{"type": "website", "url": "https://github.com/AOMediaCodec/libavif"},
197+
],
198+
},
199+
{
200+
"bom-ref": "pkg:generic/harfbuzz",
201+
"type": "library",
202+
"name": "HarfBuzz",
203+
"description": "Text shaping (optional, required by libraqm "
204+
"for complex text layout).",
205+
"externalReferences": [
206+
{"type": "website", "url": "https://harfbuzz.github.io"},
207+
],
208+
},
209+
{
210+
"bom-ref": "pkg:generic/fribidi",
211+
"type": "library",
212+
"name": "FriBiDi",
213+
"description": "Unicode bidi algorithm library (optional, "
214+
"loaded at runtime by fribidi-shim).",
215+
"externalReferences": [
216+
{"type": "website", "url": "https://github.com/fribidi/fribidi"},
217+
],
218+
},
219+
{
220+
"bom-ref": "pkg:generic/libimagequant",
221+
"type": "library",
222+
"name": "libimagequant",
223+
"description": "Improved colour quantization (optional). "
224+
"Tested with 2.6-4.4.1. NOTE: GPLv3 licensed.",
225+
"licenses": [{"license": {"id": "GPL-3.0-only"}}],
226+
"externalReferences": [
227+
{"type": "website", "url": "https://pngquant.org/lib/"},
228+
],
229+
},
230+
{
231+
"bom-ref": "pkg:generic/libxcb",
232+
"type": "library",
233+
"name": "libxcb",
234+
"description": "X11 screen-grab support (optional, "
235+
"used by PIL._imagingtk on Linux).",
236+
"externalReferences": [
237+
{"type": "website", "url": "https://xcb.freedesktop.org"},
238+
],
239+
},
240+
{
241+
"bom-ref": "pkg:pypi/pybind11",
242+
"type": "library",
243+
"name": "pybind11",
244+
"description": "C++/Python binding library "
245+
"(build-time dependency for PIL._imagingmath).",
246+
"externalReferences": [
247+
{"type": "website", "url": "https://pybind11.readthedocs.io"},
248+
],
249+
},
250+
]
251+
252+
dependencies = [
253+
{
254+
"ref": purl,
255+
"dependsOn": [e["bom-ref"] for e in ext_components],
256+
},
257+
{
258+
"ref": f"{purl}#c-ext/PIL._imaging",
259+
"dependsOn": [
260+
"pkg:generic/libjpeg",
261+
"pkg:generic/zlib",
262+
"pkg:generic/libtiff",
263+
"pkg:generic/openjpeg",
264+
],
265+
},
266+
{
267+
"ref": f"{purl}#c-ext/PIL._imagingft",
268+
"dependsOn": [
269+
"pkg:generic/freetype2",
270+
"pkg:github/HOST-Oman/libraqm@0.10.3",
271+
f"{purl}#thirdparty/fribidi-shim",
272+
"pkg:generic/harfbuzz",
273+
"pkg:generic/fribidi",
274+
],
275+
},
276+
{
277+
"ref": f"{purl}#c-ext/PIL._imagingcms",
278+
"dependsOn": ["pkg:generic/littlecms2"],
279+
},
280+
{
281+
"ref": f"{purl}#c-ext/PIL._webp",
282+
"dependsOn": ["pkg:generic/libwebp"],
283+
},
284+
{
285+
"ref": f"{purl}#c-ext/PIL._avif",
286+
"dependsOn": ["pkg:generic/libavif"],
287+
},
288+
{
289+
"ref": f"{purl}#c-ext/PIL._imagingmath",
290+
"dependsOn": ["pkg:pypi/pybind11"],
291+
},
292+
{
293+
"ref": "pkg:github/HOST-Oman/libraqm@0.10.3",
294+
"dependsOn": [
295+
f"{purl}#thirdparty/fribidi-shim",
296+
"pkg:generic/harfbuzz",
297+
],
298+
},
299+
]
300+
301+
return {
302+
"bomFormat": "CycloneDX",
303+
"specVersion": "1.6",
304+
"serialNumber": f"urn:uuid:{serial}",
305+
"version": 1,
306+
"metadata": {
307+
"timestamp": now,
308+
"tools": [
309+
{
310+
"type": "application",
311+
"name": "generate-sbom.py",
312+
"vendor": "Pillow",
313+
}
314+
],
315+
"component": metadata_component,
316+
},
317+
"components": ext_components + vendored_components + native_deps,
318+
"dependencies": dependencies,
319+
}
320+
321+
322+
if __name__ == "__main__":
323+
version = get_version()
324+
output = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(f"pillow-{version}.cdx.json")
325+
sbom = generate(version)
326+
output.write_text(json.dumps(sbom, indent=2) + "\n", encoding="utf-8")
327+
print(f"Wrote {output} (Pillow {version}, {len(sbom['components'])} components)")

.github/workflows/wheels.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,36 @@ jobs:
276276
artifacts_path: dist
277277
anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }}
278278

279+
sbom:
280+
if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
281+
needs: count-dists
282+
runs-on: ubuntu-latest
283+
name: Generate and publish SBOM
284+
permissions:
285+
contents: write
286+
steps:
287+
- uses: actions/checkout@v6
288+
with:
289+
persist-credentials: false
290+
291+
- uses: actions/setup-python@v6
292+
with:
293+
python-version: "3.x"
294+
295+
- name: Generate CycloneDX SBOM
296+
run: python .github/generate-sbom.py
297+
298+
- name: Upload SBOM as workflow artifact
299+
uses: actions/upload-artifact@v7
300+
with:
301+
name: sbom
302+
path: "*.cdx.json"
303+
304+
- name: Attach SBOM to GitHub release
305+
env:
306+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
307+
run: gh release upload "${{ github.ref_name }}" *.cdx.json
308+
279309
pypi-publish:
280310
if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
281311
needs: count-dists

0 commit comments

Comments
 (0)