Skip to content

Chainctl Image Diff #69

Chainctl Image Diff

Chainctl Image Diff #69

name: Chainctl Image Diff
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * 0"
permissions:
contents: read
env:
# Images will be signed by either the CATALOG_SYNCER or APKO_BUILDER identity in your organization.
#
# Use these commands to find the ids of the identities in your org:
#
# chainctl iam account-associations describe YOUR_ORG -o json | jq -r '.[].chainguard.service_bindings.CATALOG_SYNCER'
# chainctl iam account-associations describe YOUR_ORG -o json | jq -r '.[].chainguard.service_bindings.APKO_BUILDER'
#
catalog_syncer: 720909c9f5279097d847ad02a2f24ba8f59de36a/6cfb09d006f29ece
apko_builder: 720909c9f5279097d847ad02a2f24ba8f59de36a/a9e14125ba29b4f8
jobs:
chainctl-image-diff:
name: Chainctl Image Diff
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
id-token: write
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
with:
egress-policy: audit
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# Install chainctl. Configure auth with the id of an assumed identity.
#
# You can create an assumed identity for a Github workflow running on a
# given branch like:
# chainctl iam identities create github my-gha-identity \
# --github-repo=my-org/repo-name \
# --github-ref=refs/heads/main \
# --role=viewer
- uses: chainguard-dev/setup-chainctl@2cddd35a2f120d9973e58094dc6878c93cf58c28 # v0.5.1
with:
identity: "326bcde5903252f3ae2fe01c691b5a50f7046b5d/10275f9d35604b8e"
# Install grype. It's required by chainctl image diff.
- uses: anchore/scan-action/download-grype@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0
id: download_grype
# Install cosign. We'll use this to verify image signatures.
- uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
# Run digestabot. Don't create the PR.
- uses: chainguard-dev/digestabot@afe360aa3b0c29d88844138e8fa0349384398967 # v1.3.1
id: digestabot
with:
token: ${{ secrets.GITHUB_TOKEN }}
create-pr: false
# Revert any changes made by digestabot.
- shell: bash
run: |
git reset --hard && git clean -fd
# Iterate over the updates described by the JSON output of digestabot. Run
# chainctl image diff and perform any updates that resolve vulnerabilities.
#
# Construct the PR body.
- shell: bash
id: chainctl_image_diff
run: |
# Start the PR body
echo 'body<<EOF' >> "${GITHUB_OUTPUT}"
# Include the grype binary in the path
export PATH="$(dirname '${{steps.download_grype.outputs.cmd}}'):${PATH}"
while read -r item; do
from_image=$(jq -r '.image + "@" + .digest' <<<$item)
to_image=$(jq -r '.image + "@" + .updated_digest' <<<$item)
file=$(jq -r '.file' <<<$item)
# Store the diff output in a filename derived from the images. We can
# use this to avoid diffing the same images more than once.
diff_file="/tmp/$(sha256sum <<<"${from_image}+${to_image}" | awk '{print $1}').json"
# Skip image updates we've already processed
if [[ -f "${diff_file}" ]]; then
echo "Skipping because we've already processed the change; from=${from_image} to=${to_image}"
continue
fi
# Use the signatures to figure out if they are Chainguard images. We
# can't run `chainctl images diff` on non-Chainguard images.
signed=0
for img in "${from_image}" "${to_image}"; do
echo "Verifying that ${img} is a Chainguard image with cosign"
if cosign verify --certificate-oidc-issuer=https://issuer.enforce.dev --certificate-identity-regexp='^https://issuer.enforce.dev/(${{env.catalog_syncer}}|${{env.apko_builder}})$' "${img}" &>/dev/null; then
signed=$((signed+1))
continue
fi
if cosign verify --certificate-oidc-issuer=https://token.actions.githubusercontent.com --certificate-identity-regexp='^https://github.com/chainguard-images/images(-private)?/.github/workflows/release.yaml@refs/heads/main$' "${img}" &>/dev/null; then
signed=$((signed+1))
continue
fi
echo "Skipping because ${img} is not signed by Chainguard"
break
done
if [[ ${signed} -lt 2 ]]; then
continue
fi
echo "Processing from=${from_image} to=${to_image}"
# Perform the diff, save it to the file
chainctl images diff -o json "${from_image}" "${to_image}" > "${diff_file}"
# Ignore updates that don't remove any vulnerabilities
if [[ -z $(jq -r '.vulnerabilities.removed // [] | .[].id' "${diff_file}") ]]; then
echo "Skipping because the update doesn't resolve any vulnerabilities; from=${from_image} to=${to_image}"
continue
fi
# Perform the update
sed -i -e "s|$from_image|$to_image|g" "$file"
# Construct the header for the image in the PR body
echo "## ${to_image%@*}" >> "${GITHUB_OUTPUT}"
echo '| | Digest |' >> "${GITHUB_OUTPUT}"
echo '| ---- | -------------------- |' >> "${GITHUB_OUTPUT}"
echo "| From | \`${from_image#*@}\` |" >> "${GITHUB_OUTPUT}"
echo "| To | \`${to_image#*@}\` |" >> "${GITHUB_OUTPUT}"
# Put the diff information inside a <details> block so it's
# collapsible
echo '' >> "${GITHUB_OUTPUT}"
echo '<details>' >> "${GITHUB_OUTPUT}"
echo '' >> "${GITHUB_OUTPUT}"
# Vulnerabilities added
vulnerabilities_added=$(jq -r '.vulnerabilities.added // [] | .[] | "|" + .id + "|" + .reference + "|" + .severity + "|"' "${diff_file}")
if [ ! -z "${vulnerabilities_added}" ]; then
echo '### Vulnerabilities Added' >> "${GITHUB_OUTPUT}"
echo '| Id | Reference | Severity |' >> "${GITHUB_OUTPUT}"
echo '| -- | --------- | -------- |' >> "${GITHUB_OUTPUT}"
echo "${vulnerabilities_added}" >> "${GITHUB_OUTPUT}"
fi
# Vulnerabilities removed
vulnerabilities_removed=$(jq -r '.vulnerabilities.removed // [] | .[] | "|" + .id + "|" + .reference + "|" + .severity + "|"' "${diff_file}")
if [ ! -z "${vulnerabilities_removed}" ]; then
echo '### Vulnerabilities Removed' >> "${GITHUB_OUTPUT}"
echo '| Id | Reference | Severity |' >> "${GITHUB_OUTPUT}"
echo '| -- | --------- | -------- |' >> "${GITHUB_OUTPUT}"
echo "${vulnerabilities_removed}" >> "${GITHUB_OUTPUT}"
fi
# Packages added
packages_added=$(jq -r '.packages.added // [] | .[] | select(.reference | startswith("pkg:apk/")) | "|" + .name + "|" + .version + "|" + .reference + "|"' "${diff_file}")
if [ ! -z "${packages_added}" ]; then
echo '### Packages Added' >> "${GITHUB_OUTPUT}"
echo '| Name | Version | Reference |' >> "${GITHUB_OUTPUT}"
echo '| ---- | ------- | --------- |' >> "${GITHUB_OUTPUT}"
echo "${packages_added}" >> "${GITHUB_OUTPUT}"
fi
# Packages removed
packages_removed=$(jq -r '.packages.removed // [] | .[] | select(.reference | startswith("pkg:apk/")) | "|" + .name + "|" + .version + "|" + .reference + "|"' "${diff_file}")
if [ ! -z "${packages_removed}" ]; then
echo '### Packages Removed' >> "${GITHUB_OUTPUT}"
echo '| Name | Version | Reference |' >> "${GITHUB_OUTPUT}"
echo '| ---- | ------- | --------- |' >> "${GITHUB_OUTPUT}"
echo "${packages_removed}" >> "${GITHUB_OUTPUT}"
fi
# Packages changed
packages_changed=$(jq -r '.packages.changed // [] | .[] | select(.current.reference | startswith("pkg:apk/")) | "|" + .name + "|" + .previous.version + "|" + .current.version + "|"' "${diff_file}")
if [ ! -z "${packages_changed}" ]; then
echo '### Packages Changed' >> "${GITHUB_OUTPUT}"
echo '| Name | Previous Version | Current Version |' >> "${GITHUB_OUTPUT}"
echo '| ---- | ---------------- | --------------- |' >> "${GITHUB_OUTPUT}"
echo "${packages_changed}" >> "${GITHUB_OUTPUT}"
fi
# If there are no changes, then note that in the body.
if [ -z "${packages_added}" ] && [ -z "${packages_removed}" ] && [ -z "${packages_changed}" ] && [ -z "${vulnerabilities_added}" ] && [ -z "${vulnerabilities_removed}" ]; then
echo 'No changes.' >> "${GITHUB_OUTPUT}"
echo >> "${GITHUB_OUTPUT}"
fi
# Close the details block
echo '' >> "${GITHUB_OUTPUT}"
echo '</details>' >> "${GITHUB_OUTPUT}"
echo '' >> "${GITHUB_OUTPUT}"
done < <(jq -c '.updates // [] | .[]' <<<'${{ steps.digestabot.outputs.json }}')
# Close the body
echo 'EOF' >> "${GITHUB_OUTPUT}"
# Check if we made any changes
- id: create_pr_update
shell: bash
run: |
git diff --stat
echo "create_pr_update=false" >> $GITHUB_OUTPUT
if [[ $(git diff --stat) != '' ]]; then
echo "create_pr_update=true" >> $GITHUB_OUTPUT
fi
# Create the pull request
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
if: ${{ steps.create_pr_update.outputs.create_pr_update == 'true' }}
id: pull_request
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'Update Digests (+ `chainctl image diff`)'
title: 'Update Digests (+ `chainctl image diff`)'
body: |
Results of `chainctl image diff` for each image.
${{ steps.chainctl_image_diff.outputs.body }}
branch: chainctl-image-diff
delete-branch: true