Chainctl Image Diff #69
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |