Keep your GHCR-hosted images secure and updated with an effortless GitHub workflow. Follow along to see how!
⚙️ How It Works:
Here’s the gist of what we’re building:
- ✅ Query: Fetch all your images from GHCR.
- 🔍 Scan: Use Trivy to spot vulnerabilities.
- 🔧 Patch: Apply fixes with Copacetic and update the images on GHCR.
Gathering Images with Glue Code
To patch all your images, we first need to know what’s in your GHCR. Both Trivy and Copacetic have GitHub Actions, making automation a breeze—we can loop through every image in a workflow.
🧐 The Challenge:
Dynamically generating the list of images and tags.
💡 The Solution:
A bit of Go "glue code" that hits GitHub’s REST API, grabs all your images and tags under your username, and outputs them to a matrix.json
file. This file feeds into the workflow via the fromJson function, driving the patching process.
🔄 Continuous Patching Workflow
This is where the magic happens—a GitHub workflow that ties everything together. It starts by generating the image list, then loops through each one to scan, patch, and update it on GHCR.
The workflow splits into two jobs:
- 1️⃣ Setup: Prepares the list of images.
- 2️⃣ Patch: Runs the scanning, patching, and updating.
1️⃣ The Setup Job
The setup job kicks things off by creating the matrix of images to patch:
setup: runs-on: ubuntu-latest permissions: packages: read outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - name: Checkout to repository uses: actions/checkout@v3 - name: Generate image list run: ./listImages - name: Set matrix data id: set-matrix run: echo "matrix=$(jq -c . < ./matrix.json)" >> $GITHUB_OUTPUT
🔹 What's Happening Here?
- ✔️ Checks out your repo to access necessary files.
- ✔️ Runs listImages, a Go program that builds matrix.json.
- ✔️ Stores the matrix in a
GITHUB_OUTPUT
variable for the next job.
2️⃣ The Patch Job
The patch job takes the matrix and runs the full patching cycle for each image:
patch: runs-on: ubuntu-latest permissions: packages: read contents: write needs: setup strategy: fail-fast: false matrix: images: ${{ fromJson(needs.setup.outputs.matrix) }}
🔹 Key Features
- ✔️ Depends on setup (via needs: setup).
- ✔️ Prevents failure from stopping all patches (fail-fast: false).
- ✔️ Loads image list from matrix.json using fromJson().
🔍 Patch Step 1: Scan with Trivy
- name: Generate Trivy Report uses: aquasecurity/trivy-action@0.29.0 with: scan-type: "image" format: "json" output: "report.json" ignore-unfixed: true vuln-type: "os" image-ref: ${{ env.REGISTRY}}/${{ matrix.images }}
- ✅ Scans the image for OS vulnerabilities and outputs a report.json file.
📊 Patch Step 2: Count Vulnerabilities
- name: Check vulnerability count id: vuln_count run: | report_file="report.json" vuln_count=$(jq 'if .Results then [.Results[] | select(.Class=="os-pkgs" and .Vulnerabilities!=null) | .Vulnerabilities[]] | length else 0 end' "$report_file") echo "vuln_count=$vuln_count" >> $GITHUB_OUTPUT
- ✅ Extracts the number of fixable vulnerabilities and saves the count as
vuln_count
.
🏷️ Patch Step 3: Create Patch Tag
- name: Create patch tag id: patch_tag run: | imageName=$(echo ${{ matrix.images }} | cut -d ':' -f1) current_tag=$(echo ${{ matrix.images }} | cut -d ':' -f2) if [[ $current_tag == *-[0-9] ]]; then numeric_tag=$(echo "$current_tag" | awk -F'-' '{print $NF}') non_numeric_tag=$(echo "$current_tag" | sed "s#-$numeric_tag##g") incremented_tag=$((numeric_tag+1)) new_tag="$non_numeric_tag-$incremented_tag" else new_tag="$current_tag-1" fi echo "tag=$new_tag" >> $GITHUB_OUTPUT echo "imageName=$imageName" >> $GITHUB_OUTPUT
- ✅ Generates a new patch tag (e.g.,
app:0.1.0
→app:0.1.0-1
→app:0.1.0-2
) using the static incremental tagging strategy.
🔧 Patch Step 4: Patch with Copacetic
- name: Run copa action if: steps.vuln_count.outputs.vuln_count != '0' id: copa uses: project-copacetic/copa-action@v1.2.1 with: image: ${{ env.REGISTRY}}/${{ matrix.images }} image-report: "report.json" patched-tag: ${{ steps.patch_tag.outputs.tag }}
- ✅ Applies patches only if vulnerabilities exist.
📤 Patch Step 5: Push to GHCR
- name: Login to GHCR if: steps.copa.conclusion == 'success' id: login uses: docker/login-action@3.3.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GHCR_TOKEN }} - name: Push patched image if: steps.login.conclusion == 'success' run: | docker push ${{ steps.copa.outputs.patched-image }}
- ✅ Authenticates with GHCR and pushes the patched image if patching was successful.
Ready to try it?
Check the repo's README to set up your own continuous patching workflow! 🎉
Note: This solution only works for public registries, like GHCR. To use copa to patch locally, see Option 2 in the Copa docs. And for private see Option 1.
Top comments (0)