-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaction.yaml
More file actions
256 lines (234 loc) · 11.6 KB
/
Copy pathaction.yaml
File metadata and controls
256 lines (234 loc) · 11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
---
name: Publish Container Image
description: This action publishes a container image
inputs:
image-registry-uri:
description: The URI of the container image registry
required: true
image-registry-username:
description: The username used to login to the container image registry
required: true
image-registry-password:
description: The password used to login to the container image registry
required: true
image-repository:
description: |
Path to the image, for example `stackable/kafka` or
`k8s/sig-storage/csi-provisioner`
required: true
image-manifest-tag:
description: |
Human-readable tag (usually the version) with architecture information,
for example: `3.4.1-stackable0.0.0-dev-amd64`
required: true
# NOTE (@Techassi): This ideally shouldn't be needed because we should be able
# to construct the source image uri from the other inputs as well, but there
# is some weird stuff happening with bake and how docker tags work. Part of
# the issue is that the hostname where the image is pushed to, is part of the
# tag itself. This shouldn't be the case and OCI should fix it in their spec
# as well.
#
# Another part of the issue is the difference in the namespace/repository
# path.
# On Nexus, we use:
# - stackable/kafka
# - k8s/sig-storage/csi-provisioner
#
# On Harbor we use:
# - sdp/kafka
# - sdp/sig-storage/csi-provisioner
source-image-uri:
description: |
The source image uri, which gets re-tagged by this action to be pushed to
the appropriate registry.
required: true
cosign-retries:
description: The number of times cosign operations should be retried
default: "3"
cosign-retry-timeout:
description: |
Duration to wait before a new cosign operation is retried, format: `NUMBER[SUFFIX]`.
SUFFIX may be 's' for seconds (the default), 'm' for minutes, 'h' for hours or 'd' for days.
See `sleep --help` for the full details.
default: "30s"
runs:
using: composite
steps:
- name: Set up Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Set up syft
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
- name: Login to Container Registry (${{ inputs.image-registry-uri }})
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ${{ inputs.image-registry-uri }}
username: ${{ inputs.image-registry-username }}
password: ${{ inputs.image-registry-password }}
- name: Re-tag container image
id: re-tag
shell: bash
env:
TARGET_IMAGE_URI: ${{ inputs.image-registry-uri }}/${{ inputs.image-repository }}:${{ inputs.image-manifest-tag }}
SOURCE_IMAGE_URI: ${{ inputs.source-image-uri }}
run: |
set -euo pipefail
docker tag "$SOURCE_IMAGE_URI" "$TARGET_IMAGE_URI"
# Output for the next step
echo "IMAGE_MANIFEST_URI=$TARGET_IMAGE_URI" | tee -a "$GITHUB_OUTPUT"
- name: Push the container image to ${{ inputs.image-registry-uri }}
id: push
shell: bash
env:
IMAGE_MANIFEST_URI: ${{ steps.re-tag.outputs.IMAGE_MANIFEST_URI }}
run: |
set -euo pipefail
docker image push "$IMAGE_MANIFEST_URI"
# Output for the next step
IMAGE_REPOSITORY_DIGEST=$("$GITHUB_ACTION_PATH/../.scripts/actions/get_repo_digest.sh" "$IMAGE_MANIFEST_URI")
echo "IMAGE_REPOSITORY_DIGEST=$IMAGE_REPOSITORY_DIGEST" | tee -a "$GITHUB_OUTPUT"
- name: Sign the container image (${{ env.IMAGE_REPOSITORY_DIGEST }})
shell: bash
env:
RETRY_TIMEOUT: ${{ inputs.cosign-retry-timeout }}
RETRY_COUNT: ${{ inputs.cosign-retries }}
RETRY_ARGS: --verbose
IMAGE_REPOSITORY_DIGEST: ${{ steps.push.outputs.IMAGE_REPOSITORY_DIGEST }}
run: |
set -euo pipefail
# This generates a signature and publishes it to the registry, next to
# the image. This step uses the keyless signing flow with Github Actions
# as the identity provider.
"$GITHUB_ACTION_PATH/../.scripts/actions/retry.sh" cosign sign --yes "${IMAGE_REPOSITORY_DIGEST}"
- name: Generate SBOM for the container image (${{ env.IMAGE_REPOSITORY_DIGEST }})
shell: bash
env:
RETRY_TIMEOUT: ${{ inputs.cosign-retry-timeout }}
RETRY_COUNT: ${{ inputs.cosign-retries }}
RETRY_ARGS: --verbose
IMAGE_REPOSITORY_DIGEST: ${{ steps.push.outputs.IMAGE_REPOSITORY_DIGEST }}
IMAGE_MANIFEST_TAG: ${{ inputs.image-manifest-tag }}
IMAGE_REPOSITORY: ${{ inputs.image-repository }}
REGISTRY_URI: ${{ inputs.image-registry-uri }}
run: |
set -euo pipefail
# Extract the digest from the image repo digest (right side of '@')
DIGEST=${IMAGE_REPOSITORY_DIGEST#*@}
# URL encode the digest and image repository, needed for the purl
URLENCODED_DIGEST=$(jq -rn --arg input "$DIGEST" '$input | @uri')
URLENCODED_IMAGE_REPOSITORY=$(jq -rn --arg input "$IMAGE_REPOSITORY" '$input | @uri')
# Last item, split by /
# Example: sdp/kafka -> kafka
SOURCE_NAME=$(echo "$IMAGE_REPOSITORY" | awk -F'/' '{print $NF}')
# Extract architecture from image tag
ARCH=$(echo "$IMAGE_MANIFEST_TAG" | awk -F'-' '{print $NF}')
if [ "$ARCH" != "amd64" ] && [ "$ARCH" != "arm64" ]; then
echo "Invalid architecture obtained from image tag. IMAGE_MANIFEST_TAG: $IMAGE_MANIFEST_TAG, ARCH: $ARCH"
exit 1
fi
# Construct the package url (purl)
PURL="pkg:oci/${SOURCE_NAME}@${URLENCODED_DIGEST}?arch=${ARCH}&repository_url=${REGISTRY_URI}%2F${URLENCODED_IMAGE_REPOSITORY}"
# Get metadata from the image
# NOTE (@Techassi): Maybe we should run this command only once
IMAGE_METADATA_DESCRIPTION=$(docker inspect --format='{{.Config.Labels.description}}' "$IMAGE_REPOSITORY_DIGEST")
IMAGE_METADATA_NAME=$(docker inspect --format='{{.Config.Labels.name}}' "$IMAGE_REPOSITORY_DIGEST")
# Generate the SBOM.
# The "-file" cataloger tag disables the file catalogers so no file-type
# components (RPM-owned files like /etc/aliases) are emitted. They carry
# no purl, so vulnerability scanners cannot use them, and for large
# images they would make up most of the component count.
syft scan \
--output cyclonedx-json@1.5=sbom_raw.json \
--select-catalogers "-cargo-auditable-binary-cataloger,+sbom-cataloger,-file" \
--scope all-layers \
--source-name "$SOURCE_NAME" \
--source-version "$IMAGE_MANIFEST_TAG" "$IMAGE_REPOSITORY_DIGEST"
# Merge SBOM components using https://github.com/stackabletech/mergebom
curl --fail -L -o mergebom https://repo.stackable.tech/repository/packages/mergebom/stable-$(uname -m)
curl --fail -L -o mergebom_signature.bundle https://repo.stackable.tech/repository/packages/mergebom/stable-$(arch)_signature.bundle
# Verify signature
"$GITHUB_ACTION_PATH/../.scripts/actions/retry.sh" cosign verify-blob \
--certificate-identity 'https://github.com/stackabletech/mergebom/.github/workflows/build_binary.yaml@refs/heads/main' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--bundle mergebom_signature.bundle \
mergebom
# Run mergebom
chmod +x ./mergebom
./mergebom sbom_raw.json sbom.json
# Strip syft's per-component bookkeeping properties (layer IDs, evidence
# annotations, package IDs, ...) after mergebom has consumed them. Keep
# the paths a component was found at, the path inside an archive it was
# unpacked from, and the cataloger name as these are useful for debugging.
jq '.components |= map(
if .properties then
.properties |= map(select(
(.name | startswith("syft:") | not)
or (.name | test("^syft:location:[0-9]+:path$"))
or .name == "syft:metadata:virtualPath"
or .name == "syft:package:foundBy"
))
else . end
| if .properties == [] then del(.properties) else . end)' \
sbom.json > sbom.slim.json
# TODO (@Techassi): Replace author with manufacturer, because it is
# automated, see https://cyclonedx.org/docs/1.6/json/#metadata_component_manufacturer
jq -s \
--arg description "$IMAGE_METADATA_NAME. $IMAGE_METADATA_DESCRIPTION" \
--arg purl "$PURL" \
'{
"metadata": {
"component": {
"description": $description,
"supplier": {
"name": "Stackable GmbH",
"url": ["https://stackable.tech/"]
},
"author": "Stackable GmbH",
"purl": $purl,
"publisher": "Stackable GmbH"
}
}
} * .[0]' sbom.slim.json > sbom.merged.json
# Attest the SBOM to the image
"$GITHUB_ACTION_PATH/../.scripts/actions/retry.sh" cosign attest \
--yes \
--predicate sbom.merged.json \
--type cyclonedx \
"${IMAGE_REPOSITORY_DIGEST}"
# Derive an SPDX SBOM from the merged CycloneDX SBOM. We only generate
# CycloneDX, SPDX is produced by conversion so both formats describe the
# same merged, deduplicated inventory.
syft convert sbom.merged.json --output spdx-json=sbom.spdx.raw.json
# Post-process the converted SPDX SBOM:
# 1. Normalise deprecated SPDX license identifiers that strict
# validators reject. syft still emits the deprecated standalone form
# "GPL-2.0-with-classpath-exception"; rewrite it to the valid WITH
# expression. Extend the `fix` chain if further deprecated ids show up.
# 2. Restore top-level metadata that the CycloneDX -> SPDX conversion
# drops: credit Stackable as an SBOM creator, and set the name,
# version, originator, supplier and purl of the root container
# package.
jq --arg source_name "$SOURCE_NAME" --arg version "$IMAGE_MANIFEST_TAG" --arg purl "$PURL" '
def fix: gsub("GPL-2.0-with-classpath-exception"; "GPL-2.0-only WITH Classpath-exception-2.0");
(.. | .licenseDeclared? // empty) |= (if type == "string" then fix else . end)
| (.. | .licenseConcluded? // empty) |= (if type == "string" then fix else . end)
| .creationInfo.creators += ["Organization: Stackable GmbH"]
| (first(.relationships[]? | select(.relationshipType == "DESCRIBES") | .relatedSpdxElement)) as $root
| .packages |= map(
if .SPDXID == $root then
.name = $source_name
| .versionInfo = $version
| .supplier = "Organization: Stackable GmbH"
| .originator = "Organization: Stackable GmbH"
| .externalRefs = ((.externalRefs // []) + [{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": $purl
}])
else . end)
' sbom.spdx.raw.json > sbom.spdx.json
# Attest the SPDX SBOM to the image
"$GITHUB_ACTION_PATH/../.scripts/actions/retry.sh" cosign attest \
--yes \
--predicate sbom.spdx.json \
--type spdxjson \
"${IMAGE_REPOSITORY_DIGEST}"