Skip to content

Commit ac07f2d

Browse files
committed
build: added SBOM (Software Bill of Materials) generation
1 parent 76bf3c5 commit ac07f2d

5 files changed

Lines changed: 205 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ This is a log of major user-visible changes in each phpMyFAQ release.
2727
- added phpMyFAQ recent news widget to the admin dashboard (Thorsten)
2828
- added experimental support for API key authentication via OAuth2 (Thorsten)
2929
- added experimental per-tenant quota enforcement and API request rate limits (Thorsten)
30+
- added SBOM (Software Bill of Materials) generation (Thorsten)
3031
- improved audit and activity log with comprehensive security event tracking (Thorsten)
3132
- improved API errors with formatted RFC 7807 Problem Details JSON responses (Thorsten)
3233
- improved support for PDO (Thorsten)

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"build:prod": "vite build",
3838
"release:artifacts": "./scripts/prepare-release-artifacts.sh",
3939
"release:sign": "./scripts/sign-release-artifacts.sh",
40+
"sbom": "./scripts/generate-sbom.sh",
4041
"eslint": "eslint .",
4142
"lint": "prettier --check .",
4243
"lint:fix": "prettier --write .",

scripts/generate-sbom.sh

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/bin/sh
2+
3+
#
4+
# Generate CycloneDX Software Bill of Materials (SBOM) files for the
5+
# PHP (Composer) and JS/TS (pnpm) dependency graphs.
6+
#
7+
# Usage:
8+
# ./scripts/generate-sbom.sh [source-dir] [output-dir]
9+
#
10+
# Both arguments are optional:
11+
# source-dir defaults to the repository root
12+
# output-dir defaults to <repo-root>/build/release/<version>
13+
#
14+
# The release version is resolved from scripts/get-version.php unless
15+
# the VERSION environment variable is set.
16+
#
17+
# The resulting files follow the CycloneDX 1.5 JSON schema and are named
18+
# after the release version:
19+
# <output-dir>/phpMyFAQ-<version>-php.sbom.json (Composer only)
20+
# <output-dir>/phpMyFAQ-<version>-js.sbom.json (pnpm only)
21+
# <output-dir>/phpMyFAQ-<version>.sbom.json (both ecosystems)
22+
#
23+
# Requires pnpm (used to run @cyclonedx/cdxgen via `pnpm dlx`) and php
24+
# (used to resolve the project version).
25+
#
26+
# This Source Code Form is subject to the terms of the Mozilla Public License,
27+
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
28+
# obtain one at https://mozilla.org/MPL/2.0/.
29+
#
30+
# @package phpMyFAQ
31+
# @author Thorsten Rinne <thorsten@phpmyfaq.de>
32+
# @copyright 2008-2026 phpMyFAQ Team
33+
# @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
34+
# @link https://www.phpmyfaq.de
35+
# @version 2026-04-10
36+
#
37+
38+
set -eu
39+
40+
# shellcheck disable=SC3040
41+
if (set -o pipefail 2>/dev/null); then
42+
set -o pipefail
43+
fi
44+
IFS=$(printf ' \t\n')
45+
46+
# shellcheck disable=SC1007
47+
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
48+
# shellcheck disable=SC1007
49+
REPO_ROOT=$(CDPATH= cd -- "${SCRIPT_DIR}/.." && pwd)
50+
51+
if [ "$#" -gt 2 ]; then
52+
printf 'Usage: %s [source-dir] [output-dir]\n' "$0" >&2
53+
exit 1
54+
fi
55+
56+
SOURCE_DIR="${1:-${REPO_ROOT}}"
57+
58+
# shellcheck disable=SC1007
59+
SOURCE_DIR=$(CDPATH= cd -- "${SOURCE_DIR}" && pwd)
60+
61+
: "${PHP_BIN:=php}"
62+
: "${VERSION:=$("${PHP_BIN}" "${REPO_ROOT}/scripts/get-version.php")}"
63+
: "${CDXGEN_VERSION:=latest}"
64+
: "${CDXGEN_SPEC_VERSION:=1.5}"
65+
66+
OUTPUT_DIR="${2:-${REPO_ROOT}/build/release/${VERSION}}"
67+
68+
SBOM_PHP="${OUTPUT_DIR}/phpMyFAQ-${VERSION}.php.sbom.json"
69+
SBOM_JS="${OUTPUT_DIR}/phpMyFAQ-${VERSION}.js.sbom.json"
70+
SBOM_COMBINED="${OUTPUT_DIR}/phpMyFAQ-${VERSION}.sbom.json"
71+
72+
log() {
73+
printf '\n[%s] %s\n' "$(date '+%H:%M:%S')" "$*"
74+
}
75+
76+
fail() {
77+
printf '\n[ERROR] %s\n' "$*" >&2
78+
exit 1
79+
}
80+
81+
require_command() {
82+
command -v "$1" >/dev/null 2>&1 || fail "Required command '$1' not found in PATH"
83+
}
84+
85+
check_prerequisites() {
86+
require_command pnpm
87+
88+
if [ ! -f "${SOURCE_DIR}/composer.lock" ]; then
89+
fail "composer.lock not found in ${SOURCE_DIR}"
90+
fi
91+
if [ ! -f "${SOURCE_DIR}/pnpm-lock.yaml" ]; then
92+
fail "pnpm-lock.yaml not found in ${SOURCE_DIR}"
93+
fi
94+
}
95+
96+
run_cdxgen() {
97+
# Usage: run_cdxgen <project-type> <output-file>
98+
# <project-type> may be a single type or a comma-separated list,
99+
# which cdxgen interprets as a combined multi-ecosystem scan.
100+
_type="$1"
101+
_out="$2"
102+
pnpm dlx "@cyclonedx/cdxgen@${CDXGEN_VERSION}" \
103+
--type "${_type}" \
104+
--spec-version "${CDXGEN_SPEC_VERSION}" \
105+
--output "${_out}" \
106+
--project-name "phpMyFAQ-${VERSION}" \
107+
--project-version "${VERSION}" \
108+
--required-only \
109+
--no-recurse \
110+
"${SOURCE_DIR}"
111+
}
112+
113+
generate_php_sbom() {
114+
log "Generating PHP SBOM from ${SOURCE_DIR}/composer.lock"
115+
run_cdxgen composer "${SBOM_PHP}"
116+
}
117+
118+
generate_js_sbom() {
119+
log "Generating JS/TS SBOM from ${SOURCE_DIR}/pnpm-lock.yaml"
120+
run_cdxgen pnpm "${SBOM_JS}"
121+
}
122+
123+
generate_combined_sbom() {
124+
log "Generating combined PHP + JS/TS SBOM"
125+
run_cdxgen composer,pnpm "${SBOM_COMBINED}"
126+
}
127+
128+
main() {
129+
check_prerequisites
130+
mkdir -p "${OUTPUT_DIR}"
131+
generate_php_sbom
132+
generate_js_sbom
133+
generate_combined_sbom
134+
135+
log "SBOMs written to:"
136+
printf ' - %s\n' "${SBOM_PHP}" "${SBOM_JS}" "${SBOM_COMBINED}"
137+
}
138+
139+
main

scripts/prepare-release-artifacts.sh

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ ARTIFACT_TAR="${RELEASE_DIR}/phpMyFAQ-${VERSION}.tar.gz"
6464
ARTIFACT_ZIP="${RELEASE_DIR}/phpMyFAQ-${VERSION}.zip"
6565
HASH_MANIFEST="${RELEASE_DIR}/hashes-${VERSION}.json"
6666
ARTIFACT_MANIFEST="${RELEASE_DIR}/ARTIFACTS.txt"
67+
SBOM_PHP="${RELEASE_DIR}/phpMyFAQ-${VERSION}.php.sbom.json"
68+
SBOM_JS="${RELEASE_DIR}/phpMyFAQ-${VERSION}.js.sbom.json"
69+
SBOM_COMBINED="${RELEASE_DIR}/phpMyFAQ-${VERSION}.sbom.json"
6770
TCPDF_PATH="${CHECKOUT_DIR}/phpmyfaq/src/libs/tecnickcom/tcpdf"
6871

6972
log() {
@@ -162,6 +165,11 @@ create_packages() {
162165
(cd "${PACKAGE_DIR}" && zip -rq "${ARTIFACT_ZIP}" phpmyfaq)
163166
}
164167

168+
generate_sboms() {
169+
log "Generating CycloneDX SBOMs"
170+
VERSION="${VERSION}" "${SCRIPT_DIR}/generate-sbom.sh" "${CHECKOUT_DIR}" "${RELEASE_DIR}"
171+
}
172+
165173
write_checksums() {
166174
log "Creating checksum files"
167175
${MD5BIN} "${ARTIFACT_TAR}" > "${ARTIFACT_TAR}.md5"
@@ -189,12 +197,18 @@ Artifacts:
189197
- $(basename "${ARTIFACT_ZIP}")
190198
- $(basename "${ARTIFACT_TAR}")
191199
- $(basename "${HASH_MANIFEST}")
200+
- $(basename "${SBOM_PHP}")
201+
- $(basename "${SBOM_JS}")
202+
- $(basename "${SBOM_COMBINED}")
192203
193204
Reserved for signing phase:
194205
- SHA256SUMS
195206
- SHA256SUMS.asc
196207
- $(basename "${ARTIFACT_ZIP}").asc
197208
- $(basename "${ARTIFACT_TAR}").asc
209+
- $(basename "${SBOM_PHP}").asc
210+
- $(basename "${SBOM_JS}").asc
211+
- $(basename "${SBOM_COMBINED}").asc
198212
EOF
199213
}
200214

@@ -209,11 +223,19 @@ main() {
209223
generate_hash_manifest
210224
stage_for_packaging
211225
create_packages
226+
generate_sboms
212227
write_checksums
213228
write_manifest
214229

215230
log "Prepared release artifacts:"
216-
printf ' - %s\n' "${ARTIFACT_TAR}" "${ARTIFACT_ZIP}" "${HASH_MANIFEST}" "${ARTIFACT_MANIFEST}"
231+
printf ' - %s\n' \
232+
"${ARTIFACT_TAR}" \
233+
"${ARTIFACT_ZIP}" \
234+
"${HASH_MANIFEST}" \
235+
"${SBOM_PHP}" \
236+
"${SBOM_JS}" \
237+
"${SBOM_COMBINED}" \
238+
"${ARTIFACT_MANIFEST}"
217239
}
218240

219241
main

scripts/sign-release-artifacts.sh

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,16 @@ fi
4343
RELEASE_DIR="${REPO_ROOT}/build/release/${VERSION}"
4444
ZIP_FILE="${RELEASE_DIR}/phpMyFAQ-${VERSION}.zip"
4545
TAR_FILE="${RELEASE_DIR}/phpMyFAQ-${VERSION}.tar.gz"
46+
SBOM_PHP_FILE="${RELEASE_DIR}/phpMyFAQ-${VERSION}.php.sbom.json"
47+
SBOM_JS_FILE="${RELEASE_DIR}/phpMyFAQ-${VERSION}.js.sbom.json"
48+
SBOM_COMBINED_FILE="${RELEASE_DIR}/phpMyFAQ-${VERSION}.sbom.json"
4649
SHA256_FILE="${RELEASE_DIR}/SHA256SUMS"
4750
SHA256_ASC_FILE="${RELEASE_DIR}/SHA256SUMS.asc"
4851
ZIP_ASC_FILE="${ZIP_FILE}.asc"
4952
TAR_ASC_FILE="${TAR_FILE}.asc"
53+
SBOM_PHP_ASC_FILE="${SBOM_PHP_FILE}.asc"
54+
SBOM_JS_ASC_FILE="${SBOM_JS_FILE}.asc"
55+
SBOM_COMBINED_ASC_FILE="${SBOM_COMBINED_FILE}.asc"
5056
ARTIFACT_MANIFEST="${RELEASE_DIR}/ARTIFACTS.txt"
5157

5258
log() {
@@ -83,6 +89,9 @@ check_prerequisites() {
8389
[ -d "${RELEASE_DIR}" ] || fail "Release directory ${RELEASE_DIR} does not exist"
8490
[ -f "${ZIP_FILE}" ] || fail "Missing artifact ${ZIP_FILE}"
8591
[ -f "${TAR_FILE}" ] || fail "Missing artifact ${TAR_FILE}"
92+
[ -f "${SBOM_PHP_FILE}" ] || fail "Missing SBOM ${SBOM_PHP_FILE}"
93+
[ -f "${SBOM_JS_FILE}" ] || fail "Missing SBOM ${SBOM_JS_FILE}"
94+
[ -f "${SBOM_COMBINED_FILE}" ] || fail "Missing SBOM ${SBOM_COMBINED_FILE}"
8695

8796
if [ "${SKIP_GPG:-0}" != "1" ]; then
8897
require_command gpg
@@ -97,6 +106,9 @@ generate_checksums() {
97106
cd "${RELEASE_DIR}"
98107
${SHA256_CMD} "$(basename "${ZIP_FILE}")" > "${SHA256_FILE}"
99108
${SHA256_CMD} "$(basename "${TAR_FILE}")" >> "${SHA256_FILE}"
109+
${SHA256_CMD} "$(basename "${SBOM_PHP_FILE}")" >> "${SHA256_FILE}"
110+
${SHA256_CMD} "$(basename "${SBOM_JS_FILE}")" >> "${SHA256_FILE}"
111+
${SHA256_CMD} "$(basename "${SBOM_COMBINED_FILE}")" >> "${SHA256_FILE}"
100112
)
101113
}
102114

@@ -125,7 +137,13 @@ sign_artifacts() {
125137
fi
126138

127139
log "Signing SHA256SUMS and release artifacts"
128-
rm -f "${SHA256_ASC_FILE}" "${ZIP_ASC_FILE}" "${TAR_ASC_FILE}"
140+
rm -f \
141+
"${SHA256_ASC_FILE}" \
142+
"${ZIP_ASC_FILE}" \
143+
"${TAR_ASC_FILE}" \
144+
"${SBOM_PHP_ASC_FILE}" \
145+
"${SBOM_JS_ASC_FILE}" \
146+
"${SBOM_COMBINED_ASC_FILE}"
129147

130148
GPG_ARGS="$(gpg_base_args)"
131149
GPG_USER_ARGS="$(gpg_local_user_args)"
@@ -136,6 +154,12 @@ sign_artifacts() {
136154
gpg ${GPG_ARGS} ${GPG_USER_ARGS} --armor --detach-sign --output "${ZIP_ASC_FILE}" "${ZIP_FILE}"
137155
# shellcheck disable=SC2086
138156
gpg ${GPG_ARGS} ${GPG_USER_ARGS} --armor --detach-sign --output "${TAR_ASC_FILE}" "${TAR_FILE}"
157+
# shellcheck disable=SC2086
158+
gpg ${GPG_ARGS} ${GPG_USER_ARGS} --armor --detach-sign --output "${SBOM_PHP_ASC_FILE}" "${SBOM_PHP_FILE}"
159+
# shellcheck disable=SC2086
160+
gpg ${GPG_ARGS} ${GPG_USER_ARGS} --armor --detach-sign --output "${SBOM_JS_ASC_FILE}" "${SBOM_JS_FILE}"
161+
# shellcheck disable=SC2086
162+
gpg ${GPG_ARGS} ${GPG_USER_ARGS} --armor --detach-sign --output "${SBOM_COMBINED_ASC_FILE}" "${SBOM_COMBINED_FILE}"
139163
}
140164

141165
verify_outputs() {
@@ -157,6 +181,9 @@ verify_outputs() {
157181
gpg --verify "${SHA256_ASC_FILE}" "${SHA256_FILE}"
158182
gpg --verify "${ZIP_ASC_FILE}" "${ZIP_FILE}"
159183
gpg --verify "${TAR_ASC_FILE}" "${TAR_FILE}"
184+
gpg --verify "${SBOM_PHP_ASC_FILE}" "${SBOM_PHP_FILE}"
185+
gpg --verify "${SBOM_JS_ASC_FILE}" "${SBOM_JS_FILE}"
186+
gpg --verify "${SBOM_COMBINED_ASC_FILE}" "${SBOM_COMBINED_FILE}"
160187
}
161188

162189
update_manifest() {
@@ -174,6 +201,9 @@ Artifacts:
174201
- $(basename "${ZIP_FILE}")
175202
- $(basename "${TAR_FILE}")
176203
- $(basename "${SHA256_FILE}")
204+
- $(basename "${SBOM_PHP_FILE}")
205+
- $(basename "${SBOM_JS_FILE}")
206+
- $(basename "${SBOM_COMBINED_FILE}")
177207
EOF
178208

179209
if [ -f "${RELEASE_DIR}/hashes-${VERSION}.json" ]; then
@@ -189,6 +219,9 @@ EOF
189219
- $(basename "${SHA256_ASC_FILE}")
190220
- $(basename "${ZIP_ASC_FILE}")
191221
- $(basename "${TAR_ASC_FILE}")
222+
- $(basename "${SBOM_PHP_ASC_FILE}")
223+
- $(basename "${SBOM_JS_ASC_FILE}")
224+
- $(basename "${SBOM_COMBINED_ASC_FILE}")
192225
EOF
193226
fi
194227
}
@@ -204,7 +237,13 @@ main() {
204237
printf ' - %s\n' "${SHA256_FILE}"
205238

206239
if [ "${SKIP_GPG:-0}" != "1" ]; then
207-
printf ' - %s\n' "${SHA256_ASC_FILE}" "${ZIP_ASC_FILE}" "${TAR_ASC_FILE}"
240+
printf ' - %s\n' \
241+
"${SHA256_ASC_FILE}" \
242+
"${ZIP_ASC_FILE}" \
243+
"${TAR_ASC_FILE}" \
244+
"${SBOM_PHP_ASC_FILE}" \
245+
"${SBOM_JS_ASC_FILE}" \
246+
"${SBOM_COMBINED_ASC_FILE}"
208247
fi
209248
}
210249

0 commit comments

Comments
 (0)