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)