Chainctl Image Diff #15
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 | |
| 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@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 | |
| with: | |
| egress-policy: audit | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.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@272698817627c158bbd813cb783b62a4b9bbbc67 # v0.3.1 | |
| with: | |
| identity: "326bcde5903252f3ae2fe01c691b5a50f7046b5d/10275f9d35604b8e" | |
| # Install grype. It's required by chainctl image diff. | |
| - uses: anchore/scan-action/download-grype@2c901ab7378897c01b8efaa2d0c9bf519cc64b9e # v6.1.0 | |
| id: download_grype | |
| # Run digestabot. Don't create the PR. | |
| - uses: chainguard-dev/digestabot@43222237fd8a07dc41a06ca13e931c95ce2cedac # v1.2.2 | |
| 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 | |
| # If the image isn't hosted in cgr.dev, use the vendor annotation to | |
| # identify whether an image is a Chainguard one. We can't run | |
| # `chainctl images diff` on non-Chainguard images. | |
| # | |
| # The `chainctl images diff` command below will verify | |
| # the signatures when it fetches the SBOM, so we don't need to | |
| # use signatures here for a verifiable proof that the image came from | |
| # Chainguard. | |
| if [[ ! "${from_image}" =~ ^cgr.dev/.+$ ]]; then | |
| from_vendor=$(crane manifest "${from_image}" | jq -r '.annotations["org.opencontainers.image.vendor"] // ""') | |
| if [[ "${from_vendor}" != "Chainguard" ]]; then | |
| echo "Skipping ${from_image} because it isn't a Chainguard image" | |
| continue | |
| fi | |
| fi | |
| if [[ ! "${to_image}" =~ ^cgr.dev/.+$ ]]; then | |
| to_vendor=$(crane manifest "${to_image}" | jq -r '.annotations["org.opencontainers.image.vendor"] // ""') | |
| if [[ "${to_vendor}" != "Chainguard" ]]; then | |
| echo "Skipping ${to_image} because it isn't a Chainguard image" | |
| continue | |
| fi | |
| 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@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 | |
| 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 |