Skip to content

Commit 2563839

Browse files
committed
MDEV-34979 generate SBOM from server builds
This commit adds the capability to generate a Software Bill of Materials (SBOM) from server builds. It introduces a new WITH_SBOM variable, which defaults to ON for package builds (i.e if BUILD_CONFIG is used) and to OFF otherwise. When enabled, the build process will produce an sbom.json document in CycloneDX format, capturing information about various dependencies, which is gathered from various sources. We use git submodule information and CMake external projects properties to gather version information for 3rd party code, but also handle dependencies if external code is part of our repository (zlib, or Connect storage engine's minizip) The SBOM document is stored in the root build directory in sbom.json file, but is not currently installed.
1 parent 18dbeae commit 2563839

File tree

7 files changed

+453
-0
lines changed

7 files changed

+453
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ TAGS
3333
Testing/
3434
tmp/
3535
VERSION.dep
36+
cmake/submodule_info.cmake
3637
configure
3738
client/async_example
3839
client/mysql
@@ -113,6 +114,7 @@ plugin/auth_pam/config_auth_pam.h
113114
plugin/aws_key_management/aws-sdk-cpp
114115
plugin/aws_key_management/aws_sdk_cpp
115116
plugin/aws_key_management/aws_sdk_cpp-prefix
117+
sbom.json
116118
scripts/comp_sql
117119
scripts/make_binary_distribution
118120
scripts/msql2mysql

CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,13 @@ ENDIF()
581581

582582
INCLUDE(build_depends)
583583

584+
OPTION(WITH_SBOM "Generate Software Bill of Materials (SBOM)" "${SBOM_DEFAULT}")
585+
MARK_AS_ADVANCED(WITH_SBOM)
586+
IF(WITH_SBOM)
587+
INCLUDE(generate_sbom)
588+
GENERATE_SBOM()
589+
ENDIF()
590+
584591
INCLUDE(CPack)
585592

586593
IF(WIN32 AND SIGNCODE)

cmake/build_configurations/mysql_release.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ ENDIF()
8585
SET(WITH_INNODB_SNAPPY OFF CACHE STRING "")
8686
SET(WITH_NUMA 0 CACHE BOOL "")
8787
SET(CPU_LEVEL1_DCACHE_LINESIZE 0)
88+
# generate SBOMS
89+
SET(SBOM_DEFAULT 1)
8890

8991
IF(NOT EXISTS ${CMAKE_SOURCE_DIR}/.git)
9092
SET(GIT_EXECUTABLE GIT_EXECUTABLE-NOTFOUND CACHE FILEPATH "")

cmake/generate_sbom.cmake

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
INCLUDE(generate_submodule_info)
2+
INCLUDE(ExternalProject)
3+
4+
5+
# Extract user name and repository name from a github URL.
6+
FUNCTION (EXTRACT_REPO_NAME_AND_USER repo_url repo_name_var repo_user_var)
7+
IF(repo_url MATCHES "^git@")
8+
# normalize to https-style URLs
9+
STRING(REGEX REPLACE "^git@([^:]+):(.*)$" "https://\\1/\\2" repo_url "${repo_url}")
10+
ENDIF()
11+
# Extract the repository user
12+
STRING(REGEX REPLACE "https://([^/]+)/([^/]+)/.*" "\\2" repo_user "${repo_url}")
13+
14+
STRING(REGEX REPLACE ".*/([^/]*)$" "\\1" repo_name "${repo_url}")
15+
STRING(REGEX REPLACE "\\.git$" "" repo_name "${repo_name}")
16+
17+
SET(${repo_name_var} ${repo_name} PARENT_SCOPE)
18+
SET(${repo_user_var} ${repo_user} PARENT_SCOPE)
19+
ENDFUNCTION()
20+
21+
# Add a known 3rd party dependency for SBOM generation
22+
# Currently used for "vendored" (part of our repository) source code we know about
23+
# such as zlib, as well ExternalProject_Add() projects
24+
MACRO(ADD_THIRD_PARTY_DEPENDENCY name url tag rev version description)
25+
LIST(FIND ALL_THIRD_PARTY ${name} idx)
26+
IF (idx GREATER -1)
27+
MESSAGE(FATAL_ERROR "${name} is already in ALL_THIRD_PARTY")
28+
ENDIF()
29+
SET(${name}_URL ${url})
30+
SET(${name}_TAG ${tag})
31+
SET(${name}_REVISION ${rev})
32+
SET(${name}_DESCRIPTION "${description}")
33+
SET(${name}_VERSION "${version}")
34+
LIST(APPEND ALL_THIRD_PARTY ${name})
35+
ENDMACRO()
36+
37+
# Get CPE ID ( https://en.wikipedia.org/wiki/Common_Platform_Enumeration )
38+
# for given project name and version
39+
# Only "known" CPEs are handled here, e.g currently no CPE for rocksdb
40+
FUNCTION(SBOM_GET_CPE name version var)
41+
SET(cpe_prefix_map
42+
"zlib" "zlib:zlib"
43+
"mariadb-connector-c" "mariadb:connector\\\\/c"
44+
"wolfssl" "wolfssl:wolfssl"
45+
"minizip" "zlib:zlib"
46+
"pcre2" "pcre:pcre2"
47+
"fmt" "fmt:fmt"
48+
"boost" "boost:boost"
49+
"thrift" "apache:thrift"
50+
)
51+
LIST(FIND cpe_prefix_map "${name}" i)
52+
IF(i GREATER -1)
53+
MATH(EXPR next_idx "${i}+1")
54+
LIST(GET cpe_prefix_map ${next_idx} cpe_name_and_vendor)
55+
STRING(REGEX REPLACE "[^0-9\\.]" "" cleaned_version "${version}")
56+
SET(${var} "cpe:2.3:a:${cpe_name_and_vendor}:${cleaned_version}:*:*:*:*:*:*:*" PARENT_SCOPE)
57+
ELSE()
58+
SET(${var} "" PARENT_SCOPE)
59+
ENDIF()
60+
ENDFUNCTION()
61+
62+
# Add dependency on CMake ExternalProject.
63+
# Currently, only works for github hosted projects,
64+
# URL property of the external project needs to point to release source download
65+
MACRO(ADD_CMAKE_EXTERNAL_PROJECT_DEPENDENCY name)
66+
ExternalProject_GET_PROPERTY(${name} URL)
67+
STRING(REGEX REPLACE "https://github.com/([^/]+/[^/]+)/releases/download/([^/]+)/.*-([^-]+)\\..*" "\\1;\\2;\\3" parsed "${URL}")
68+
# Split the result into components
69+
LIST(LENGTH parsed parsed_length)
70+
IF(parsed_length EQUAL 3)
71+
LIST(GET parsed 0 project_path)
72+
LIST(GET parsed 1 tag)
73+
LIST(GET parsed 2 ver)
74+
ELSE()
75+
STRING(REGEX REPLACE "https://github.com/([^/]+/[^/]+)/archive/refs/tags/([^/]+)\\.(tar\\.gz|zip)$" "\\1;\\2;\\3" parsed "${URL}")
76+
LIST(LENGTH parsed parsed_length)
77+
IF(parsed_length GREATER 1)
78+
LIST(GET parsed 0 project_path)
79+
LIST(GET parsed 1 tag)
80+
STRING(REGEX REPLACE "[^0-9.]" "" ver "${tag}")
81+
ELSE()
82+
MESSAGE(FATAL_ERROR "Unexpected format for the download URL of project ${name} : (${URL})")
83+
ENDIF()
84+
ENDIF()
85+
ADD_THIRD_PARTY_DEPENDENCY(${name} "https://github.com/${project_path}" "${tag}" "${tag}" "${ver}" "")
86+
ENDMACRO()
87+
88+
89+
# Match third party component with supplier
90+
# CyclonDX documentation says it is
91+
# "The organization that supplied the component.
92+
# The supplier may often be the manufacturer, but may also be a distributor or repackager."
93+
#
94+
# Perhaps it can always be "MariaDB", but security team recommendation is different
95+
# more towards "author"
96+
FUNCTION (sbom_get_supplier repo_name repo_user varname)
97+
IF("${repo_name_SUPPLIER}")
98+
SET(${varname} "${repo_name_SUPPLIER}" PARENT_SCOPE)
99+
ELSEIF (repo_name MATCHES "zlib|minizip")
100+
# stuff that is checked into out repos
101+
SET(${varname} "MariaDB" PARENT_SCOPE)
102+
ELSEIF (repo_name MATCHES "boost")
103+
SET(${varname} "Boost.org" PARENT_SCOPE)
104+
ELSE()
105+
IF(repo_user MATCHES "mariadb-corporation|mariadb")
106+
set(repo_user "MariaDB")
107+
ENDIF()
108+
# Capitalize just first letter in repo_user
109+
STRING(SUBSTRING "${repo_user}" 0 1 first_letter)
110+
STRING(SUBSTRING "${repo_user}" 1 -1 rest)
111+
STRING(TOUPPER "${first_letter}" first_letter_upper)
112+
SET(${varname} "${first_letter_upper}${rest}" PARENT_SCOPE)
113+
ENDIF()
114+
ENDFUNCTION()
115+
116+
# Generate sbom.json in the top-level build directory
117+
FUNCTION(GENERATE_SBOM)
118+
IF(EXISTS ${PROJECT_SOURCE_DIR}/cmake/submodule_info.cmake)
119+
INCLUDE(${PROJECT_SOURCE_DIR}/cmake/submodule_info.cmake)
120+
ELSE()
121+
GENERATE_SUBMODULE_INFO(${PROJECT_BINARY_DIR}/cmake/submodule_info.cmake)
122+
INCLUDE(${PROJECT_BINARY_DIR}/cmake/submodule_info.cmake)
123+
ENDIF()
124+
# Remove irrelevant for the current build submodule information
125+
# That is, if we do not build say columnstore, do not include
126+
# dependency info into SBOM
127+
IF(NOT TARGET wolfssl)
128+
# using openssl, rather than wolfssl
129+
LIST(FILTER ALL_SUBMODULES EXCLUDE REGEX wolfssl)
130+
ENDIF()
131+
IF(NOT WITH_WSREP)
132+
# wsrep is not compiled
133+
LIST(FILTER ALL_SUBMODULES EXCLUDE REGEX wsrep)
134+
ENDIF()
135+
IF(NOT TARGET columnstore)
136+
LIST(FILTER ALL_SUBMODULES EXCLUDE REGEX columnstore)
137+
ENDIF()
138+
IF(NOT TARGET rocksdb)
139+
# Rocksdb is not compiled
140+
LIST(FILTER ALL_SUBMODULES EXCLUDE REGEX rocksdb)
141+
ENDIF()
142+
IF(NOT TARGET s3)
143+
# S3 aria is not compiled
144+
LIST(FILTER ALL_SUBMODULES EXCLUDE REGEX storage/maria/libmarias3)
145+
ENDIF()
146+
# libmariadb/docs is not a library, so remove it
147+
LIST(FILTER ALL_SUBMODULES EXCLUDE REGEX libmariadb/docs)
148+
149+
# It is possible to provide EXTRA_SBOM_DEPENDENCIES
150+
# and accompanying per-dependency data, to extend generared sbom
151+
# document.
152+
# Example below injects an extra "ncurses" dependency using several
153+
# command line parameters for CMake.
154+
# -DEXTRA_SBOM_DEPENDENCIES=ncurses
155+
# -Dncurses_URL=https://github.com/mirror/ncurses
156+
# -Dncurses_TAG=v6.4
157+
# -Dncurses_VERSION=6.4
158+
# -Dncurses_DESCRIPTION="A fake extra dependency"
159+
SET(ALL_THIRD_PARTY ${ALL_SUBMODULES} ${EXTRA_SBOM_DEPENDENCIES})
160+
161+
# Add dependencies on cmake ExternalProjects
162+
FOREACH(ext_proj libfmt pcre2)
163+
IF(TARGET ${ext_proj})
164+
ADD_CMAKE_EXTERNAL_PROJECT_DEPENDENCY(${ext_proj})
165+
ENDIF()
166+
ENDFOREACH()
167+
168+
# ZLIB
169+
IF(TARGET zlib OR TARGET connect)
170+
# Path to the zlib.h file
171+
SET(ZLIB_HEADER_PATH "${PROJECT_SOURCE_DIR}/zlib/zlib.h")
172+
# Variable to store the extracted version
173+
SET(ZLIB_VERSION "")
174+
# Read the version string from the file
175+
file(STRINGS "${ZLIB_HEADER_PATH}" ZLIB_VERSION_LINE REGEX "#define ZLIB_VERSION.*")
176+
# Extract the version number using a regex
177+
IF (ZLIB_VERSION_LINE)
178+
STRING(REGEX MATCH "\"([^\"]+)\"" ZLIB_VERSION_MATCH "${ZLIB_VERSION_LINE}")
179+
IF (ZLIB_VERSION_MATCH)
180+
STRING(REPLACE "\"" "" ZLIB_VERSION "${ZLIB_VERSION_MATCH}")
181+
IF(NOT ("${ZLIB_VERSION}" MATCHES "[0-9]+\\.[0-9]+\\.[0-9]+"))
182+
MESSAGE(FATAL_ERROR "Unexpected zlib version '${ZLIB_VERSION}' parsed from ${ZLIB_HEADER_PATH}")
183+
ENDIF()
184+
ELSE()
185+
MESSAGE(FATAL_ERROR "Could not extract ZLIB version from the line: ${ZLIB_VERSION_LINE}")
186+
ENDIF()
187+
ELSE()
188+
MESSAGE(FATAL_ERROR "ZLIB_VERSION definition not found in ${ZLIB_HEADER_PATH}")
189+
ENDIF()
190+
IF(TARGET zlib)
191+
ADD_THIRD_PARTY_DEPENDENCY(zlib "https://github.com/madler/zlib"
192+
"v${ZLIB_VERSION}" "v${ZLIB_VERSION}" "${ZLIB_VERSION}" "Vendored zlib included into server source")
193+
ENDIF()
194+
IF(TARGET ha_connect OR TARGET connect)
195+
SET(minizip_PURL "pkg:github/madler/zlib@${ZLIB_VERSION}?path=contrib/minizip")
196+
ADD_THIRD_PARTY_DEPENDENCY(minizip "https://github.com/madler/zlib?path=contrib/minizip"
197+
"v${ZLIB_VERSION}-minizip" "v${ZLIB_VERSION}-minizip" "${ZLIB_VERSION}"
198+
"Vendored minizip (zip.c, unzip.c, ioapi.c) in connect engine, copied from zlib/contributions")
199+
ENDIF()
200+
ENDIF()
201+
202+
IF(TARGET columnstore)
203+
# Determining if Columnstore builds Boost is tricky.
204+
# The presence of the external_boost target isn't reliable, it is always
205+
# present. Instead, we check indirectly by verifying if one of the libraries
206+
# built by the external project exists in the build directory.
207+
IF(TARGET external_boost AND TARGET boost_filesystem)
208+
GET_TARGET_PROPERTY(boost_filesystem_loc boost_filesystem IMPORTED_LOCATION)
209+
STRING(FIND "${boost_filesystem_loc}" "${CMAKE_BINARY_DIR}" idx)
210+
IF(idx EQUAL 0)
211+
# Now we can be reasonably sure, external_boost is indeed an external project
212+
ExternalProject_GET_PROPERTY(external_boost URL)
213+
# Extract the version from the URL using string manipulation.
214+
STRING(REGEX MATCH "[0-9]+\\.[0-9]+\\.[0-9]+" BOOST_VERSION ${URL})
215+
SET(tag boost-${BOOST_VERSION})
216+
ADD_THIRD_PARTY_DEPENDENCY(boost
217+
"https://github.com/boostorg/boost" "${tag}" "${tag}" "${BOOST_VERSION}"
218+
"Boost library, linked with columnstore engine")
219+
ENDIF()
220+
ENDIF()
221+
IF(TARGET external_thrift)
222+
ADD_CMAKE_EXTERNAL_PROJECT_DEPENDENCY(external_thrift)
223+
ENDIF()
224+
ENDIF()
225+
226+
SET(sbom_components "")
227+
SET(sbom_dependencies "\n {
228+
\"ref\": \"${CPACK_PACKAGE_NAME}\",
229+
\"dependsOn\": [" )
230+
231+
SET(first ON)
232+
FOREACH(dep ${ALL_THIRD_PARTY})
233+
# Extract the part after the last "/" from URL
234+
SET(revision ${${dep}_REVISION})
235+
SET(tag ${${dep}_TAG})
236+
SET(desc ${${dep}_DESCRIPTION})
237+
IF((tag STREQUAL "no-tag") OR (NOT tag))
238+
SET(tag ${revision})
239+
ENDIF()
240+
IF (NOT "${revision}" AND "${tag}")
241+
SET(revision ${tag})
242+
ENDIF()
243+
SET(version ${${dep}_VERSION})
244+
245+
IF (version)
246+
ELSEIF(tag)
247+
SET(version ${tag})
248+
ELSEIF(revision)
249+
SET(version ${revision})
250+
ENDIF()
251+
252+
EXTRACT_REPO_NAME_AND_USER("${${dep}_URL}" repo_name repo_user)
253+
254+
IF(first)
255+
SET(first OFF)
256+
ELSE()
257+
STRING(APPEND sbom_components ",")
258+
STRING(APPEND sbom_dependencies ",")
259+
ENDIF()
260+
SET(bom_ref "${repo_name}-${version}")
261+
IF(desc)
262+
SET(desc_line "\n \"description\": \"${desc}\",")
263+
ELSE()
264+
SET(desc_line "")
265+
ENDIF()
266+
STRING(TOLOWER "${repo_user}" repo_user_lower)
267+
STRING(TOLOWER "${repo_name}" repo_name_lower)
268+
IF (${repo_name_lower}_PURL)
269+
SET(purl "${${repo_name_lower}_PURL}")
270+
ELSE()
271+
SET(purl "pkg:github/${repo_user_lower}/${repo_name_lower}@${revision}")
272+
ENDIF()
273+
SBOM_GET_SUPPLIER(${repo_name_lower} ${repo_user_lower} supplier)
274+
SBOM_GET_CPE(${repo_name_lower} "${version}" cpe)
275+
IF(cpe)
276+
SET(cpe "\n \"cpe\": \"${cpe}\",")
277+
ENDIF()
278+
STRING(APPEND sbom_components "
279+
{
280+
\"bom-ref\": \"${bom_ref}\",
281+
\"type\": \"library\",
282+
\"name\": \"${repo_name}\",
283+
\"version\": \"${version}\",${desc_line}
284+
\"purl\": \"${purl}\",${cpe}
285+
\"supplier\": {
286+
\"name\": \"${supplier}\"
287+
}
288+
}")
289+
STRING(APPEND sbom_dependencies "
290+
\"${bom_ref}\"")
291+
STRING(APPEND reflist ",\n {\"ref\": \"${bom_ref}\"}")
292+
ENDFOREACH()
293+
STRING(APPEND sbom_dependencies "\n ]\n }${reflist}\n")
294+
STRING(UUID UUID NAMESPACE ee390ca3-e70f-4b35-808e-a512489156f5 NAME SBOM TYPE SHA1)
295+
STRING(TIMESTAMP TIMESTAMP "%Y-%m-%dT%H:%M:%SZ" UTC)
296+
EXTRACT_REPO_NAME_AND_USER("${GIT_REMOTE_ORIGIN_URL}" GITHUB_REPO_NAME GITHUB_REPO_USER)
297+
#github-purl needs lowercased user and project names
298+
STRING(TOLOWER "${GITHUB_REPO_NAME}" GITHUB_REPO_NAME)
299+
STRING(TOLOWER "${GITHUB_REPO_USER}" GITHUB_REPO_USER)
300+
IF(NOT DEFINED CPACK_PACKAGE_VERSION)
301+
SET(CPACK_PACKAGE_VERSION "${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}")
302+
ENDIF()
303+
configure_file(${CMAKE_CURRENT_LIST_DIR}/cmake/sbom.json.in ${CMAKE_BINARY_DIR}/sbom.json)
304+
ENDFUNCTION()

0 commit comments

Comments
 (0)