Skip to content

Commit 87f1488

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 7cf4dac commit 87f1488

File tree

2 files changed

+360
-0
lines changed

2 files changed

+360
-0
lines changed

.github/generate-sbom.py

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