Introduce %python_wheel_inject_sbom
See https://discuss.python.org/t/encoding-origin-in-wheel-and-dist-info-metadata-for-downstream-security-backports/97436
This commit is contained in:
parent
364d99f4e1
commit
7d4cb5437d
4 changed files with 237 additions and 1 deletions
124
macros.python-wheel-sbom
Normal file
124
macros.python-wheel-sbom
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
# The macros in this file are used to add SBOM to wheel files that we ship.
|
||||
# Majority of Python packages will not need to do that,
|
||||
# as they only use wheels as an intermediate artifact.
|
||||
# The macros will be used by packages installing wheel to %%python_wheel_dir
|
||||
# or by Python interpreters bundling their own (patched) wheels.
|
||||
#
|
||||
# The runtime dependencies are not Required by the python-rpm-macros package,
|
||||
# users of this macro need to specify them on their own or rely on the fact that
|
||||
# they are all available in the default buildroot.
|
||||
#
|
||||
# Usage: %%python_wheel_inject_sbom PATHS_TO_WHEELS
|
||||
#
|
||||
# The wheels are modified in-place.
|
||||
|
||||
|
||||
# Path of the SBOM file in the PEP 770 .dist-info/sboms directory
|
||||
# This filename is explicitly mentioned in https://cyclonedx.org/specification/overview/
|
||||
# section Recognized file patterns
|
||||
%__python_wheel_sbom_filename bom.json
|
||||
|
||||
|
||||
# The SBOM content to put to the file
|
||||
# This is a CycloneDX component as recommended in https://discuss.python.org/t/97436/7
|
||||
%__python_wheel_sbom_content %{expand:{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"components": [
|
||||
{
|
||||
"type": "library",
|
||||
"name": "%{name}",
|
||||
"version": "%{version}-%{release}",
|
||||
"purl": "%{__python_wheel_purl}"
|
||||
}
|
||||
]
|
||||
}}
|
||||
|
||||
|
||||
# The purl used above
|
||||
# We use the src package name (which is easier to get and more useful to consumers).
|
||||
# Note that epoch needs special handling, see https://github.com/package-url/purl-spec/issues/69
|
||||
# and https://redhatproductsecurity.github.io/security-data-guidelines/purl/
|
||||
%__python_wheel_purl pkg:rpm/%{__python_wheel_dist_purl_namespace}/%{name}@%{version}-%{release}?%{?epoch:epoch=%{epoch}&}arch=src
|
||||
|
||||
|
||||
# The purl namespace used above
|
||||
# https://lists.fedoraproject.org/archives/list/packaging@lists.fedoraproject.org/thread/GTRCTAF3R3SSBVEJYFCATKNRT7RYVFQI/
|
||||
# Distributors, define %%dist_purl_namespace to set this.
|
||||
# The rest of the code is fallback for distributions without it (relying on %%dist_name).
|
||||
%__python_wheel_dist_purl_namespace %{?dist_purl_namespace}%{!?dist_purl_namespace:%{lua:
|
||||
if macros.epel then
|
||||
-- being epel beats the %%dist_name value
|
||||
-- added in https://src.fedoraproject.org/rpms/epel-rpm-macros/pull-request/86
|
||||
print("epel")
|
||||
else
|
||||
local dist_map = {
|
||||
-- fedora is in the purl-spec examples https://github.com/package-url/purl-spec/blob/main/PURL-TYPES.rst#rpm
|
||||
-- added in https://src.fedoraproject.org/rpms/fedora-release/pull-request/385
|
||||
["Fedora Linux"] = "fedora",
|
||||
-- added in https://gitlab.com/redhat/centos-stream/rpms/centos-stream-release/-/merge_requests/7
|
||||
["CentOS Stream"] = "centos",
|
||||
-- documented at https://redhatproductsecurity.github.io/security-data-guidelines/purl/
|
||||
["Red Hat Enterprise Linux"] = "redhat",
|
||||
-- documented at https://wiki.almalinux.org/documentation/sbom-guide.html
|
||||
["AlmaLinux"] = "almalinux",
|
||||
-- from https://github.com/google/osv.dev/pull/2939
|
||||
["Rocky Linux"] = "rocky-linux",
|
||||
}
|
||||
print(dist_map[macros.dist_name] or "unknown")
|
||||
end
|
||||
}}
|
||||
|
||||
|
||||
# A Bash scriptlet to inject the SBOM file into the wheel(s)
|
||||
# The macro takes positional nargs+ with wheel paths
|
||||
# For each wheel, it
|
||||
# 1. aborts if the SBOM file is already there (it won't override)
|
||||
# 2. inserts the SBOM file to .dist-info/sboms
|
||||
# 3. amends .dist-info/RECORD with the added SBOM file
|
||||
%python_wheel_inject_sbom() %{expand:(
|
||||
%[%# ? "" : "%{error:%%%0: At least one argument (wheel path) is required}"]
|
||||
|
||||
set -eu -o pipefail
|
||||
export LANG=C.utf-8
|
||||
|
||||
tmpdir=$(mktemp -d)
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
pwd0=$(pwd)
|
||||
ret=0
|
||||
|
||||
for whl in %{*}; do
|
||||
cd "$tmpdir"
|
||||
if [[ "$whl" != /* ]]; then
|
||||
whl="$pwd0/$whl"
|
||||
fi
|
||||
|
||||
record=$(zipinfo -1 "$whl" | grep '\.dist-info/RECORD$')
|
||||
distinfo="${record%%/RECORD}"
|
||||
bom="$distinfo/sboms/%{__python_wheel_sbom_filename}"
|
||||
|
||||
if zipinfo -1 "$whl" | grep -qFx "$bom"; then
|
||||
echo -e "\\n\\nERROR %%%%%0: $whl already has $bom, aborting\\n\\n" >&2
|
||||
ret=1
|
||||
continue
|
||||
fi
|
||||
|
||||
unzip "$whl" "$record"
|
||||
mkdir "$distinfo/sboms"
|
||||
echo '%{__python_wheel_sbom_content}' > "$bom"
|
||||
checksum="sha256=$(sha256sum "$bom" | cut -f1 -d' ')"
|
||||
size="$(wc --bytes "$bom" | cut -f1 -d' ')"
|
||||
echo "$bom,$checksum,$size" >> "$record"
|
||||
|
||||
if [[ -n "${SOURCE_DATE_EPOCH:-}" ]]; then
|
||||
touch --date="@$SOURCE_DATE_EPOCH" "$bom" "$record"
|
||||
fi
|
||||
|
||||
zip -r "$whl" "$record" "$bom"
|
||||
rm -rf "$distinfo"
|
||||
cd "$pwd0"
|
||||
done
|
||||
|
||||
exit $ret
|
||||
)}
|
||||
|
||||
5
plan.fmf
5
plan.fmf
|
|
@ -17,6 +17,9 @@ discover:
|
|||
test: rpmlint ~/rpmbuild/RPMS/x86_64/pythontest-0-0.clamp0.x86_64.rpm | grep python-bytecode-inconsistent-mtime || exit 0 && exit 1
|
||||
- name: rpmlint_clamp_mtime_on
|
||||
test: rpmlint ~/rpmbuild/RPMS/x86_64/pythontest-0-0.clamp1.x86_64.rpm | grep python-bytecode-inconsistent-mtime || exit 0 && exit 1
|
||||
- name: python_wheel_inject_sbom
|
||||
path: /tests
|
||||
test: rpmbuild -ba testwheel.spec
|
||||
|
||||
prepare:
|
||||
- name: Install dependencies
|
||||
|
|
@ -27,6 +30,8 @@ prepare:
|
|||
- python-rpm-macros
|
||||
- python3-rpm-macros
|
||||
- python3-devel
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
- python3-pytest
|
||||
- python3.6
|
||||
- dnf
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ Source101: macros.python
|
|||
Source102: macros.python-srpm
|
||||
Source104: macros.python3
|
||||
Source105: macros.pybytecompile
|
||||
Source106: macros.python-wheel-sbom
|
||||
|
||||
# Lua files
|
||||
Source201: python.lua
|
||||
|
|
@ -55,7 +56,7 @@ elseif posix.stat('macros.python-srpm') then
|
|||
end
|
||||
}
|
||||
Version: %{__default_python3_version}
|
||||
Release: 5%{?dist}
|
||||
Release: 6%{?dist}
|
||||
|
||||
BuildArch: noarch
|
||||
|
||||
|
|
@ -149,6 +150,7 @@ grep -E '^#[^%%]*%%[^%%]' %{buildroot}%{rpmmacrodir}/macros.* && exit 1 || true
|
|||
%files
|
||||
%{rpmmacrodir}/macros.python
|
||||
%{rpmmacrodir}/macros.pybytecompile
|
||||
%{rpmmacrodir}/macros.python-wheel-sbom
|
||||
%{_rpmconfigdir}/redhat/import_all_modules.py
|
||||
%{_rpmconfigdir}/redhat/pathfix.py
|
||||
|
||||
|
|
@ -167,6 +169,9 @@ grep -E '^#[^%%]*%%[^%%]' %{buildroot}%{rpmmacrodir}/macros.* && exit 1 || true
|
|||
|
||||
|
||||
%changelog
|
||||
* Wed Aug 13 2025 Miro Hrončok <mhroncok@redhat.com> - 3.14-6
|
||||
- Introduce %%python_wheel_inject_sbom
|
||||
|
||||
* Mon Aug 11 2025 Lumír Balhar <lbalhar@redhat.com> - 3.14-5
|
||||
- import_all_modules: Add error handling for import failures
|
||||
|
||||
|
|
|
|||
102
tests/testwheel.spec
Normal file
102
tests/testwheel.spec
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
Name: testwheel
|
||||
Epoch: 42
|
||||
Version: 1
|
||||
Release: 0%{?dist}
|
||||
Summary: ...
|
||||
License: MIT
|
||||
BuildArch: noarch
|
||||
BuildRequires: python3-devel
|
||||
BuildRequires: python3-setuptools >= 61
|
||||
BuildRequires: python3-pip
|
||||
|
||||
%description
|
||||
This builds and installs a wheel which we can then use as a test for
|
||||
%%python_wheel_inject_sbom.
|
||||
|
||||
|
||||
%prep
|
||||
cat > pyproject.toml << EOF
|
||||
[project]
|
||||
name = "testwheel"
|
||||
version = "1"
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools >= 61"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
EOF
|
||||
|
||||
|
||||
%build
|
||||
export PIP_CONFIG_FILE=/dev/null
|
||||
%{python3} -m pip wheel . --no-build-isolation
|
||||
|
||||
# The macro should happily alter multiple wheels, let's make more
|
||||
for i in {1..5}; do
|
||||
mkdir ${i}
|
||||
cp -a *.whl ${i}
|
||||
done
|
||||
|
||||
# using relative paths should succeed
|
||||
%python_wheel_inject_sbom {1..5}/*.whl
|
||||
|
||||
# repetitive use should bail out and fail (SBOM is already there)
|
||||
%{python_wheel_inject_sbom {1..5}/*.whl} && exit 1 || true
|
||||
|
||||
# each wheel should already have it, all should fail individually as well
|
||||
for i in {1..5}; do
|
||||
%{python_wheel_inject_sbom ${i}/*.whl} && exit 1 || true
|
||||
done
|
||||
|
||||
|
||||
%install
|
||||
mkdir -p %{buildroot}%{python_wheel_dir}
|
||||
cp -a *.whl %{buildroot}%{python_wheel_dir}
|
||||
|
||||
# using absolute paths should work
|
||||
%python_wheel_inject_sbom %{buildroot}%{python_wheel_dir}/*.whl
|
||||
|
||||
# and fail when repeated
|
||||
%{python_wheel_inject_sbom %{buildroot}%{python_wheel_dir}/*.whl} && exit 1 || true
|
||||
|
||||
|
||||
%check
|
||||
%define venvsite venv/lib/python%{python3_version}/site-packages
|
||||
%{python3} -m venv venv
|
||||
venv/bin/pip install --no-index --no-cache-dir %{buildroot}%{python_wheel_dir}/*.whl
|
||||
|
||||
test -f %{venvsite}/testwheel-1.dist-info/RECORD
|
||||
test -f %{venvsite}/testwheel-1.dist-info/sboms/bom.json
|
||||
grep '^testwheel-1.dist-info/sboms/bom.json,' %{venvsite}/testwheel-1.dist-info/RECORD
|
||||
# a more specific grep. we don't care about CRLF line ends (pip uses those? without the sed the $ doesn't match line end)
|
||||
sed 's/\r//g' %{venvsite}/testwheel-1.dist-info/RECORD | grep -E '^testwheel-1.dist-info/sboms/bom.json,sha256=[a-f0-9]{64},[0-9]+$'
|
||||
|
||||
# this deliberately uses a different mechanism than the macro
|
||||
# if you are running this test on a different distro, adjust it
|
||||
%define ns %{?fedora:fedora}%{?eln:fedora}%{?epel:epel}%{!?eln:%{!?epel:%{?rhel:redhat}}}
|
||||
|
||||
PYTHONOPTIMIZE=0 %{python3} -c "
|
||||
import json
|
||||
with open('%{venvsite}/testwheel-1.dist-info/sboms/bom.json') as fp:
|
||||
sbom = json.load(fp)
|
||||
assert len(sbom['components']) == 1
|
||||
assert sbom['components'][0]['type'] == 'library'
|
||||
assert sbom['components'][0]['name'] == 'testwheel'
|
||||
assert sbom['components'][0]['version'] == '1-0%{?dist}'
|
||||
assert sbom['components'][0]['purl'] == 'pkg:rpm/%{ns}/testwheel@1-0%{?dist}?epoch=42&arch=src'
|
||||
"
|
||||
|
||||
# replace the installation with the original unaltered wheel
|
||||
venv/bin/pip install --force-reinstall --no-index --no-cache-dir *.whl
|
||||
test -f %{venvsite}/testwheel-1.dist-info/RECORD
|
||||
# no SBOM
|
||||
test ! -e %{venvsite}/testwheel-1.dist-info/sboms/bom.json
|
||||
grep '^testwheel-1.dist-info/sboms/bom.json,' %{venvsite}/testwheel-1.dist-info/RECORD && exit 1 || true
|
||||
|
||||
|
||||
%files
|
||||
%{python_wheel_dir}/*.whl
|
||||
|
||||
|
||||
%changelog
|
||||
* Wed Aug 13 2025 Miro Hrončok <mhroncok@redhat.com> - 42:1-0
|
||||
- A static changelog with a date, so we can clamp mtimes
|
||||
Loading…
Add table
Add a link
Reference in a new issue