Skip to content

Commit ca80085

Browse files
committed
[TC-2795] Add SBOM generating and uploading script
Signed-off-by: Jiří Kučera <[email protected]>
1 parent a79f708 commit ca80085

File tree

4 files changed

+313
-0
lines changed

4 files changed

+313
-0
lines changed

etc/gensbom/Containerfile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
ARG SYFT_REGISTRY=ghcr.io/anchore
2+
ARG SYFT_IMAGE=syft
3+
ARG SYFT_TAG=latest
4+
5+
FROM registry.access.redhat.com/ubi9/ubi:latest
6+
7+
ARG SYFT_REGISTRY
8+
ARG SYFT_IMAGE
9+
ARG SYFT_TAG
10+
11+
COPY --from=${SYFT_REGISTRY}/${SYFT_IMAGE}:${SYFT_TAG} /syft /usr/local/bin/syft
12+
COPY ./gensbom.sh /usr/local/bin/gensbom.sh
13+
14+
RUN dnf -y install gawk zip && \
15+
dnf clean all && \
16+
mkdir -p /gensbom
17+
18+
WORKDIR /gensbom
19+
20+
ENTRYPOINT ["/usr/local/bin/gensbom.sh"]

etc/gensbom/README.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Generating SBOMs From Container Images Using Syft
2+
3+
## Using the Container
4+
5+
**Prerequisites:**
6+
7+
* `podman`
8+
* [`config.json`](https://github.com/google/go-containerregistry/tree/main/pkg/authn#docker-config-auth)
9+
in the current working directory
10+
* `TPA_SERVICE_URL`, holding the URL to the running TPA service, e.g.
11+
`my.tpa.instance.abc:8765`
12+
* `TPA_AUTH_TOKEN`, holding the valid authorization token, e.g.
13+
`Bearer XXXXXXXXXX`
14+
15+
**Running:**
16+
17+
```
18+
# Assume `images.txt` is a list of images (one image per line) in the current
19+
# working directory
20+
21+
# Generate SBOMs from `images.txt` and ingest them:
22+
podman run --rm -v "${PWD}":/gensbom:Z -e 'TPA_*' gensbom:latest images.txt
23+
```
24+
25+
**Output:**
26+
27+
* `${PWD}/sboms` directory with the generated SBOMs
28+
* `${PWD}/sboms.zip` archive with the generated SBOMs
29+
30+
## Using the Script
31+
32+
**Prerequisites:**
33+
34+
* `awk`
35+
* Bash
36+
* `curl`
37+
* `sha512sum`
38+
* [`syft`](https://github.com/anchore/syft/releases)
39+
* `zip`
40+
* [`config.json`](https://github.com/google/go-containerregistry/tree/main/pkg/authn#docker-config-auth)
41+
in the current working directory
42+
* `TPA_SERVICE_URL`, holding the URL to the running TPA service, e.g.
43+
`my.tpa.instance.abc:8765`
44+
* `TPA_AUTH_TOKEN`, holding the valid authorization token, e.g.
45+
`Bearer XXXXXXXXXX`
46+
47+
**Running:**
48+
49+
```
50+
# Assume `images.txt` is a list of images (one image per line) in the current
51+
# working directory
52+
53+
# Generate SBOMs from `images.txt` and ingest them:
54+
./gensbom.sh images.txt
55+
```
56+
57+
**Output:**
58+
59+
* Same as in the previous case
60+
61+
## Troubleshooting
62+
63+
### Where Can I Get the `config.json` file?
64+
65+
For Quay.io:
66+
67+
1. Log in to your account
68+
1. Go to your user profile
69+
1. Click `Account Settings`
70+
1. Click `Generate Encrypted Password`
71+
1. Select `Docker Configuration`
72+
1. Click `Download {your_username}-auth.json`
73+
1. Run `mv {path-to-the-downloaded-file} config.json`
74+
75+
For other container image registries the process may be similar.
76+
77+
> [!WARNING]
78+
> Every time you generate encrypted password to your Quay.io account, do not
79+
> forget to update also `config.json`.
80+
81+
## Developer Section
82+
83+
### Building the Container Image
84+
85+
1. `cd` to the directory with the `Containerfile`
86+
1. Run `podman build -t gensbom -f Containerfile .`
87+
88+
The `Containerfile` build arguments:
89+
90+
* `SYFT_REGISTRY`, holding the container registry from which the `syft`
91+
container is pulled (default: `ghcr.io/anchore`)
92+
* `SYFT_IMAGE`, holding the `syft` container image name (default: `syft`)
93+
* `SYFT_TAG`, holding the `syft` container image tag (default: `latest`)
94+
95+
## References
96+
97+
* [Docker Config Auth](https://github.com/google/go-containerregistry/tree/main/pkg/authn#docker-config-auth)
98+
* [Syft](https://github.com/anchore/syft)
99+
* [Syft: Private Registry Authentication](https://github.com/anchore/syft/wiki/private-registry-authentication)

etc/gensbom/example-images.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
quay.io/keycloak/keycloak
2+
quay.io/keycloak/keycloak:26.4
3+
quay.io/keycloak/keycloak:26.3
4+
quay.io/keycloak/keycloak:nightly

etc/gensbom/gensbom.sh

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
#!/bin/bash
2+
3+
set -euo pipefail
4+
5+
WORKSPACE="${PWD}"
6+
SYFT_COMMAND="syft"
7+
8+
if [[ ! -t 0 || ! -t 1 || ! -t 2 ]]; then
9+
export NO_COLOR=1
10+
fi
11+
12+
if [[ "${NO_COLOR:-0}" =~ ^(n|no|0|f|false)$ ]]; then
13+
_BOLD="$(echo -ne '\e[1m')"
14+
_RED="$(echo -ne '\e[31m')"
15+
_BROWN="$(echo -ne '\e[33m')"
16+
_VIOLET="$(echo -ne '\e[35m')"
17+
_AZURE="$(echo -ne '\e[36m')"
18+
_YELLOW="$(echo -ne '\e[93m')"
19+
_RESET="$(echo -ne '\e[0m')"
20+
_Q1="${_VIOLET}"
21+
_Q0="${_RESET}"
22+
else
23+
_BOLD=""
24+
_RED=""
25+
_BROWN=""
26+
_VIOLET=""
27+
_AZURE=""
28+
_YELLOW=""
29+
_RESET=""
30+
_Q1="\`"
31+
_Q0="\`"
32+
fi
33+
34+
function inform() {
35+
echo "$0: $*"
36+
}
37+
38+
function warning() {
39+
echo "${_YELLOW}WARNING[$0]: $*${_RESET}" >&2
40+
}
41+
42+
function error() {
43+
echo "${_RED}ERROR[$0]: $*${_RESET}" >&2
44+
exit 1
45+
}
46+
47+
function usage() {
48+
cat <<-__EOF__
49+
Usage: ${_AZURE}$0 <FILE>${_RESET}
50+
51+
or as the container:
52+
53+
${_BROWN}# Read a list of images from the <FILE>${_RESET}
54+
${_AZURE}podman run --rm -v "\${PWD}":/gensbom:Z -e 'TPA_*' gensbom:latest <FILE>${_RESET}
55+
56+
In the current working directory:
57+
58+
1. Read the list of images (one image per line) from ${_VIOLET}<FILE>${_RESET}.
59+
2. For every image from the list:
60+
* generate an SBOM in CycloneDX 1.6 format using Syft
61+
* ingest the SBOM to the Trusted Profile Analyzer (TPA)
62+
3. The ${_Q1}sboms${_Q0} directory and the ${_Q1}sboms.zip${_Q0} archive contain the
63+
generated SBOMs.
64+
65+
${_YELLOW}WARNING:${_RESET} The ${_Q1}sboms${_Q0} directory and the ${_Q1}sboms.zip${_Q0} archive from the
66+
previous run will be removed!
67+
68+
${_BOLD}Authentication${_RESET}
69+
70+
${_VIOLET}config.json${_RESET}
71+
A file with the valid container registry credentials following the
72+
Docker format. This file must be in the current working directory
73+
74+
${_VIOLET}TPA_AUTH_TOKEN${_RESET}
75+
Authorization token for TPA
76+
77+
${_BOLD}Environment variables${_RESET}
78+
79+
${_VIOLET}TPA_SERVICE_URL${_RESET}
80+
URL with running TPA instance
81+
82+
${_VIOLET}TPA_AUTH_TOKEN${_RESET}
83+
Valid authorization token for TPA. Required if the TPA instance
84+
requires authorization
85+
86+
${_BOLD}See also${_RESET}
87+
88+
${_VIOLET}config.json${_RESET} format
89+
https://github.com/google/go-containerregistry/tree/main/pkg/authn#docker-config-auth
90+
https://github.com/anchore/syft/wiki/private-registry-authentication
91+
92+
__EOF__
93+
}
94+
95+
if [[ -z "${1:-}" ]]; then
96+
usage
97+
exit 0
98+
fi
99+
100+
_INPUT="$1"
101+
102+
if [[ ! -f "${_INPUT}" ]]; then
103+
error "\`${_INPUT}\` is not a file"
104+
fi
105+
106+
if [[ -z "${TPA_SERVICE_URL:-}" ]]; then
107+
error "TPA_SERVICE_URL environment variable is not set or it is empty"
108+
fi
109+
110+
_DOCKER_CONFIG="${WORKSPACE}/config.json"
111+
112+
if [[ ! -f "${_DOCKER_CONFIG}" ]]; then
113+
warning "Docker configuration with credentials (\`${_DOCKER_CONFIG}\`) not present"
114+
warning "Syft will not be able to access private container registries"
115+
else
116+
export DOCKER_CONFIG="${WORKSPACE}"
117+
fi
118+
119+
_AUTH_HEADER="Authorization: ${TPA_AUTH_TOKEN:-}"
120+
121+
if [[ -z "${TPA_AUTH_TOKEN:-}" ]]; then
122+
warning "TPA_AUTH_TOKEN environment variable is not set or it is empty"
123+
warning "Authorized communication with ${TPA_SERVICE_URL} will not be possible"
124+
_AUTH_HEADER=""
125+
fi
126+
127+
_SBOMS_DIR="${WORKSPACE}/sboms"
128+
_SBOMS_ARCHIVE="sboms.zip"
129+
130+
rm -rf "${_SBOMS_DIR}" "${WORKSPACE}/${_SBOMS_ARCHIVE}"
131+
mkdir -p "${_SBOMS_DIR}"
132+
133+
_SBOM_COUNTER=0
134+
135+
while IFS="" read -r _IMAGE || [[ -n "${_IMAGE}" ]]; do
136+
if [[ -z "${_IMAGE}" ]]; then
137+
continue
138+
fi
139+
140+
_SBOM="${_SBOMS_DIR}/$(printf 'sbom%010d' "${_SBOM_COUNTER}")-$(echo "${_IMAGE}" | tr '/:' '--').json"
141+
_SBOM_COUNTER=$(( _SBOM_COUNTER + 1 ))
142+
143+
if ! "${SYFT_COMMAND}" -v scan "${_IMAGE}" -o "cyclonedx-json=${_SBOM}"; then
144+
# Remove empty/corrupted file so it will not be included in the final archive
145+
rm -f "${_SBOM}"
146+
continue
147+
fi
148+
149+
if [[ ! -s "${_SBOM}" ]]; then
150+
warning "File \`${_SBOM}\` was not created"
151+
# Remove empty file so it will not be included in the final archive
152+
rm -f "${_SBOM}"
153+
continue
154+
fi
155+
156+
_SHA512="sha512:$(sha512sum "${_SBOM}" | awk '{print $1}')"
157+
inform "File ${_Q1}${_SBOM}${_Q0} with ${_Q1}${_SHA512}${_Q0} has been created"
158+
159+
if curl -sSfL \
160+
${_AUTH_HEADER:+-H "${_AUTH_HEADER}"} \
161+
"${TPA_SERVICE_URL}/api/v2/sbom/${_SHA512}/download" \
162+
>/dev/null 2>&1; \
163+
then
164+
# Do not ingest already ingested SBOM
165+
inform "File ${_Q1}${_SBOM}${_Q0} is already ingested"
166+
continue
167+
fi
168+
169+
if ! curl -sSfL \
170+
-X POST \
171+
${_AUTH_HEADER:+-H "${_AUTH_HEADER}"} \
172+
-H "Content-Type: application/json" \
173+
-d "@${_SBOM}" \
174+
"${TPA_SERVICE_URL}/api/v2/sbom"; \
175+
then
176+
warning "Failed to ingest \`${_SBOM}\`"
177+
else
178+
# Put few newlines after the `curl` output
179+
echo -ne '\n\n'
180+
fi
181+
done < "${_INPUT}"
182+
183+
pushd "${_SBOMS_DIR}" >/dev/null
184+
185+
if find -name '*.json' -type f | grep -q .; then
186+
find -name '*.json' -type f -print | zip "${_SBOMS_ARCHIVE}" -@
187+
mv "${_SBOMS_ARCHIVE}" "../${_SBOMS_ARCHIVE}"
188+
fi
189+
190+
popd >/dev/null

0 commit comments

Comments
 (0)