Skip to content

Commit 4535d8d

Browse files
committed
Generate a SBOM for the Enterprise image
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent 20046a9 commit 4535d8d

File tree

5 files changed

+218
-2
lines changed

5 files changed

+218
-2
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ jobs:
3131

3232
- run: make docker PRESET=Release ${{ matrix.edition.options }}
3333

34+
- run: ./enterprise/scripts/sbom.sh one "$GITHUB_SHA"
35+
if: matrix.edition.name == 'enterprise'
36+
3437
- name: Sandbox (headless)
3538
uses: ./.github/actions/sandbox
3639
with:

.github/workflows/deploy.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,12 +167,19 @@ jobs:
167167
with:
168168
images: ghcr.io/${{ github.repository_owner }}/one-enterprise
169169

170+
- run: |
171+
./enterprise/scripts/sbom.sh \
172+
"ghcr.io/${{ github.repository_owner }}/one-enterprise:${{ steps.meta.outputs.version }}" \
173+
"${{ steps.meta.outputs.version }}" \
174+
> /tmp/sbom.spdx.json
175+
170176
- run: >
171177
./enterprise/scripts/cosign.sh
172178
"ghcr.io/${{ github.repository_owner }}/one-enterprise"
173179
"${{ steps.meta.outputs.version }}"
174180
"https://token.actions.githubusercontent.com"
175181
"https://github.com/${{ github.repository }}/.github/workflows/deploy.yml@${{ github.ref }}"
182+
"/tmp/sbom.spdx.json"
176183
177184
release:
178185
needs: docker-multi-arch

enterprise/Dockerfile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,20 @@ RUN ldd /usr/bin/sourcemeta-one-index | grep libcrypto
100100
RUN grep -q 'fips = fips_sect' /etc/ssl/openssl.cnf
101101
RUN test -f /usr/lib/*/ossl-modules/fips.so
102102

103+
# Remove packages not needed at runtime to reduce the image attack surface
104+
RUN apt-get --yes update && apt-get --yes purge --allow-remove-essential \
105+
apt adduser bash debconf debian-archive-keyring diffutils \
106+
e2fsprogs findutils gpgv grep gzip hostname init-system-helpers \
107+
login logsave mount ncurses-base ncurses-bin passwd perl-base \
108+
sed sysvinit-utils tzdata util-linux util-linux-extra \
109+
&& apt-get --yes autoremove --allow-remove-essential \
110+
&& rm -rf /var/lib/apt/lists/*
111+
RUN dpkg-query --show --showformat='${Package}\t${Version}\t${Homepage}\n' \
112+
> /usr/share/sourcemeta/one/dpkg-packages
113+
RUN dpkg --purge --force-remove-essential --force-depends \
114+
dpkg tar mawk \
115+
&& rm -rf /var/lib/dpkg
116+
103117
# We expect images that extend this one to use this directory
104118
ARG SOURCEMETA_ONE_WORKDIR=/source
105119
ENV SOURCEMETA_ONE_WORKDIR=${SOURCEMETA_ONE_WORKDIR}

enterprise/scripts/cosign.sh

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@
33
set -o errexit
44
set -o nounset
55

6-
if [ "$#" -ne 4 ]
6+
if [ "$#" -ne 5 ]
77
then
8-
echo "Usage: $0 <image> <version> <certificate-oidc-issuer> <certificate-identity>" 1>&2
8+
echo "Usage: $0 <image> <version> <certificate-oidc-issuer> <certificate-identity> <sbom-file>" 1>&2
99
exit 1
1010
fi
1111

1212
IMAGE="$1"
1313
VERSION="$2"
1414
CERTIFICATE_OIDC_ISSUER="$3"
1515
CERTIFICATE_IDENTITY="$4"
16+
SBOM_FILE="$5"
1617

1718
echo "Cosign: Extracting manifest digest for ${IMAGE}:${VERSION}" 1>&2
1819
MANIFEST=$(docker buildx imagetools inspect "${IMAGE}:${VERSION}" \
@@ -28,6 +29,9 @@ echo "Cosign: Manifest digest is ${DIGEST}" 1>&2
2829
echo "Cosign: Signing ${IMAGE}@${DIGEST}" 1>&2
2930
cosign sign --yes "${IMAGE}@${DIGEST}"
3031

32+
echo "Cosign: Attaching SBOM attestation to ${IMAGE}@${DIGEST}" 1>&2
33+
cosign attest --yes --predicate "$SBOM_FILE" --type spdx "${IMAGE}@${DIGEST}"
34+
3135
echo "Cosign: Verifying signature for ${IMAGE}@${DIGEST}" 1>&2
3236
echo "Cosign: OIDC issuer: ${CERTIFICATE_OIDC_ISSUER}" 1>&2
3337
echo "Cosign: Certificate identity: ${CERTIFICATE_IDENTITY}" 1>&2
@@ -37,3 +41,11 @@ cosign verify \
3741
"${IMAGE}@${DIGEST}"
3842

3943
echo "Cosign: Signature verified successfully" 1>&2
44+
45+
echo "Cosign: Verifying SBOM attestation for ${IMAGE}@${DIGEST}" 1>&2
46+
cosign verify-attestation --type spdx \
47+
--certificate-oidc-issuer "$CERTIFICATE_OIDC_ISSUER" \
48+
--certificate-identity "$CERTIFICATE_IDENTITY" \
49+
"${IMAGE}@${DIGEST}"
50+
51+
echo "Cosign: SBOM attestation verified successfully" 1>&2

enterprise/scripts/sbom.sh

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
#!/bin/sh
2+
3+
# Generates an SPDX 2.3 JSON Software Bill of Materials (SBOM) for the
4+
# Sourcemeta One Enterprise container image
5+
6+
set -o errexit
7+
set -o nounset
8+
9+
if [ $# -lt 2 ]; then
10+
echo "Usage: $0 <image> <version>" 1>&2
11+
exit 1
12+
fi
13+
14+
IMAGE="$1"
15+
VERSION="$2"
16+
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
17+
18+
if ! docker image inspect "$IMAGE" > /dev/null 2>&1; then
19+
docker pull "$IMAGE" 1>&2
20+
fi
21+
cd "$ROOT" && npm ci --ignore-scripts 1>&2
22+
23+
license_for() {
24+
case "$1" in
25+
core|blaze|hydra|codegen|jsonbinpack) echo "AGPL-3.0-or-later" ;;
26+
jsonschema) echo "AGPL-3.0-only" ;;
27+
uwebsockets) echo "Apache-2.0" ;;
28+
bootstrap|bootstrap-icons) echo "MIT" ;;
29+
pcre2) echo "BSD-3-Clause" ;;
30+
zlib) echo "Zlib" ;;
31+
curl) echo "curl" ;;
32+
nghttp2|cpr|c-ares|libpsl) echo "MIT" ;;
33+
*)
34+
echo "ERROR: No license mapping for dependency: $1" 1>&2
35+
exit 1
36+
;;
37+
esac
38+
}
39+
40+
is_blacklisted() {
41+
case "$1" in
42+
vendorpull|googletest|googlebenchmark) return 0 ;;
43+
jsontestsuite|yaml-test-suite|jsonschema-test-suite) return 0 ;;
44+
referencing-suite|uritemplate-test) return 0 ;;
45+
pyca-cryptography|ctrf) return 0 ;;
46+
mbedtls) return 0 ;;
47+
public/*|collections/*) return 0 ;;
48+
jsonschema-draft*|jsonschema-2019*|jsonschema-2020*) return 0 ;;
49+
openapi*) return 0 ;;
50+
*) return 1 ;;
51+
esac
52+
}
53+
54+
# ---------------------------------------------------------------------------
55+
# Part A: npm packages
56+
# ---------------------------------------------------------------------------
57+
NPM_SBOM="$(cd "$ROOT" && npm sbom --sbom-format spdx --sbom-type library --omit dev 2>/dev/null)"
58+
NPM_ROOT_SPDXID="SPDXRef-Package-sourcemeta.one-0.0.0"
59+
NPM_PACKAGES="$(printf '%s' "$NPM_SBOM" | jq --arg root "$NPM_ROOT_SPDXID" \
60+
'[.packages[] | select(.SPDXID == $root | not)]')"
61+
NPM_RELATIONSHIPS="$(printf '%s' "$NPM_SBOM" | jq \
62+
'[.relationships[]
63+
| select(.relationshipType == "DESCRIBES" | not)
64+
| .relatedSpdxElement = "SPDXRef-RootPackage"]')"
65+
66+
# ---------------------------------------------------------------------------
67+
# Part B: Vendored C++ dependencies from DEPENDENCIES files
68+
# ---------------------------------------------------------------------------
69+
SEEN_URLS_FILE="$(mktemp)"
70+
VENDOR_PACKAGES_FILE="$(mktemp)"
71+
VENDOR_RELATIONSHIPS_FILE="$(mktemp)"
72+
VENDOR_INDEX_FILE="$(mktemp)"
73+
DEPS_LIST_FILE="$(mktemp)"
74+
printf '1000' > "$VENDOR_INDEX_FILE"
75+
printf '[' > "$VENDOR_PACKAGES_FILE"
76+
printf '[' > "$VENDOR_RELATIONSHIPS_FILE"
77+
78+
VENDOR_FIRST_FILE="$(mktemp)"
79+
printf 'true' > "$VENDOR_FIRST_FILE"
80+
81+
trap 'rm -f "$SEEN_URLS_FILE" "$VENDOR_PACKAGES_FILE" "$VENDOR_RELATIONSHIPS_FILE" "$VENDOR_INDEX_FILE" "$VENDOR_FIRST_FILE" "$DEPS_LIST_FILE"' EXIT
82+
83+
find "$ROOT" -name DEPENDENCIES -type f -not -path '*/.git/*' | sort > "$DEPS_LIST_FILE"
84+
85+
while read -r deps_file; do
86+
while read -r name url version; do
87+
[ -z "$name" ] && continue
88+
is_blacklisted "$name" && continue
89+
grep -qFx "$url" "$SEEN_URLS_FILE" 2>/dev/null && continue
90+
printf '%s\n' "$url" >> "$SEEN_URLS_FILE"
91+
92+
vendor_license="$(license_for "$name")"
93+
94+
vendor_index="$(cat "$VENDOR_INDEX_FILE")"
95+
vendor_index=$((vendor_index + 1))
96+
printf '%s' "$vendor_index" > "$VENDOR_INDEX_FILE"
97+
vendor_spdxid="SPDXRef-Vendor-${vendor_index}"
98+
99+
vendor_first="$(cat "$VENDOR_FIRST_FILE")"
100+
if [ "$vendor_first" = true ]; then
101+
printf 'false' > "$VENDOR_FIRST_FILE"
102+
else
103+
printf ',' >> "$VENDOR_PACKAGES_FILE"
104+
printf ',' >> "$VENDOR_RELATIONSHIPS_FILE"
105+
fi
106+
107+
jq -n --arg name "$name" --arg spdxid "$vendor_spdxid" \
108+
--arg version "$version" --arg url "$url" \
109+
--arg license "$vendor_license" \
110+
'{ name: $name, SPDXID: $spdxid, versionInfo: $version,
111+
downloadLocation: $url, filesAnalyzed: false,
112+
licenseDeclared: $license }' >> "$VENDOR_PACKAGES_FILE"
113+
114+
jq -n --arg spdxid "$vendor_spdxid" \
115+
'{ spdxElementId: $spdxid,
116+
relatedSpdxElement: "SPDXRef-RootPackage",
117+
relationshipType: "DEPENDENCY_OF" }' >> "$VENDOR_RELATIONSHIPS_FILE"
118+
done < "$deps_file"
119+
done < "$DEPS_LIST_FILE"
120+
121+
printf ']' >> "$VENDOR_PACKAGES_FILE"
122+
printf ']' >> "$VENDOR_RELATIONSHIPS_FILE"
123+
124+
# ---------------------------------------------------------------------------
125+
# Part C: System packages from the Docker image
126+
# ---------------------------------------------------------------------------
127+
DPKG_OUTPUT="$(docker run --rm --entrypoint cat "$IMAGE" /usr/share/sourcemeta/one/dpkg-packages)"
128+
129+
DPKG_PACKAGES="$(printf '%s' "$DPKG_OUTPUT" | jq -R --argjson start 2000 '
130+
split("\t") | select(length >= 2) |
131+
{ name: .[0],
132+
SPDXID: ("SPDXRef-System-" + (input_line_number + $start | tostring)),
133+
versionInfo: .[1],
134+
downloadLocation: (if .[2] and (.[2] == "" | not) then .[2] else "NOASSERTION" end),
135+
filesAnalyzed: false,
136+
licenseDeclared: "NOASSERTION" }
137+
' | jq -s '.')"
138+
139+
DPKG_RELATIONSHIPS="$(printf '%s' "$DPKG_PACKAGES" | jq '[.[] | {
140+
spdxElementId: .SPDXID,
141+
relatedSpdxElement: "SPDXRef-RootPackage",
142+
relationshipType: "DEPENDENCY_OF"
143+
}]')"
144+
145+
# ---------------------------------------------------------------------------
146+
# Part D: Assemble the final SPDX 2.3 JSON document
147+
# ---------------------------------------------------------------------------
148+
jq -n \
149+
--arg version "$VERSION" \
150+
--argjson npm_packages "$NPM_PACKAGES" \
151+
--argjson npm_relationships "$NPM_RELATIONSHIPS" \
152+
--slurpfile vendor_packages "$VENDOR_PACKAGES_FILE" \
153+
--slurpfile vendor_relationships "$VENDOR_RELATIONSHIPS_FILE" \
154+
--argjson dpkg_packages "$DPKG_PACKAGES" \
155+
--argjson dpkg_relationships "$DPKG_RELATIONSHIPS" \
156+
'{
157+
spdxVersion: "SPDX-2.3",
158+
dataLicense: "CC0-1.0",
159+
SPDXID: "SPDXRef-DOCUMENT",
160+
name: "sourcemeta-one-enterprise",
161+
documentNamespace: ("https://sourcemeta.com/spdx/" + $version),
162+
creationInfo: {
163+
created: (now | strftime("%Y-%m-%dT%H:%M:%SZ")),
164+
creators: ["Tool: enterprise/scripts/sbom.sh"]
165+
},
166+
documentDescribes: ["SPDXRef-RootPackage"],
167+
packages: ([{
168+
name: "sourcemeta-one-enterprise",
169+
SPDXID: "SPDXRef-RootPackage",
170+
versionInfo: $version,
171+
downloadLocation: "https://github.com/sourcemeta/one",
172+
filesAnalyzed: false,
173+
licenseDeclared: "NOASSERTION"
174+
}] + $npm_packages + $vendor_packages[0] + $dpkg_packages),
175+
relationships: ([{
176+
spdxElementId: "SPDXRef-DOCUMENT",
177+
relatedSpdxElement: "SPDXRef-RootPackage",
178+
relationshipType: "DESCRIBES"
179+
}] + $npm_relationships + $vendor_relationships[0] + $dpkg_relationships)
180+
}'

0 commit comments

Comments
 (0)