Reusable GitHub Actions workflows to scan Docker images for vulnerabilities and report findings as GitHub Issues.
- Overview
- How it works
- Reusable workflows
- scan_docker_images.py (local use)
- Variable naming convention
- Issue lifecycle
- End-to-end example
This repository provides two independent reusable workflows:
βββββββββββββββββββββββββββββββββββββββββββββββ
β generate-image-list β
β β
β Compose files ββ β
β Dockerfiles ββ scan_docker_images.py βββΊββββΊ docker_images.env
β *.yml / *.yamlββ β (committed to repo)
βββββββββββββββββββββββββββββββββββββββββββββββ
β
β (or bring your own docker_images.env)
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββ
β vuln-scanning β
β β
β docker_images.env βββΊ Trivy / Snyk scan βββΊββββΊ GitHub Issues
β β (HIGH / CRITICAL)
βββββββββββββββββββββββββββββββββββββββββββββββ
π generate-image-list β Scans your repository for Docker image references in Compose files and Dockerfiles, and writes them to a docker_images.env file that is committed back to your repo. Use this when you want image discovery to be fully automated.
π¬ vuln-scanning β Reads a docker_images.env file, scans each image for vulnerabilities using Trivy or Snyk, and creates or updates GitHub Issues for every HIGH or CRITICAL finding. If your repository already maintains its own image list, you can use this workflow on its own without generate-image-list.
The workflows are designed to be used together or independently, depending on your setup.
-
Image discovery (
generate-image-listworkflow)
scan_docker_images.pywalks the caller's repository and finds every Docker image reference in:docker-compose.yml,*.yml,*.yamlβ lines matchingimage: <ref>Dockerfile,Dockerfile.*β lines matchingFROM <ref>
References that use environment-variable substitution (
${VAR}) are skipped because they are already sourced from the.envfile. The discovered images are written todocker_images.env(or a custom path) and committed back to the caller's repository. -
Vulnerability scanning (
vuln-scanningworkflow)
Reads adocker_images.envfile containing*_IMAGEvariables (produced by step 1, or maintained manually), iterates over every image, and scans each one with either Trivy or Snyk. Results are saved as a JSON artifact. -
Issue management (
create-issueaction)
Compares the scan results against existing open GitHub Issues (filtered by a caller-supplied label). For each vulnerable image:- If no issue exists β a new issue is created with
HIGH/CRITICALlabels and a list of CVE IDs. - If an issue already exists β a new comment is added with the latest scan results.
- If no issue exists β a new issue is created with
Both workflows are independent and can be called on their own. Use generate-image-list only if you want image discovery to be automated; skip it if you already maintain a docker_images.env file yourself.
Scans the caller's repository and commits docker_images.env.
Caller workflow example:
jobs:
generate-image-list:
uses: thehyve/vulnerability-scanning/.github/workflows/generate-image-list.yml@main
secrets: inherit # forwards GH_PAT for private submodules (optional)
with:
output-file: docker_images.env # optional, this is the default
vuln-scanning-ref: main # optional, pin to a specific refInputs:
| Input | Required | Default | Description |
|---|---|---|---|
output-file |
No | docker_images.env |
Path (relative to repo root) to write the image list to |
vuln-scanning-ref |
No | main |
Branch, tag, or SHA of the thehyve/vulnerability-scanning repo to use |
Secrets:
| Secret | Required | Description |
|---|---|---|
GH_PAT |
Conditional | A Personal Access Token. Required only when the caller's repo contains private submodules in other repositories. Falls back to the built-in GITHUB_TOKEN automatically when absent. |
Creating a PAT (when needed):
Option A β Fine-grained PAT (recommended)
- Go to GitHub β Settings β Developer settings β Personal access tokens β Fine-grained tokens
- Click Generate new token
- Under Repository access, select the relevant repositories and grant:
- Calling repo (e.g.
opentargets-automation): Contents β Read and write - Each private submodule repo: Contents β Read
- Calling repo (e.g.
- Click Generate token and copy the value
Option B β Classic PAT (simpler, broader access)
- Go to GitHub β Settings β Developer settings β Personal access tokens β Tokens (classic)
- Click Generate new token
- Select the
reposcope (grants full read/write access to all private repos on your account) - Click Generate token and copy the value
Then store the token as a repository secret named GH_PAT:
- Go to your repository β Settings β Secrets and variables β Actions
- Click New repository secret
- Name:
GH_PAT, Value: the token you copied
Scans images listed in an .env file and creates GitHub Issues for findings.
Caller workflow example:
jobs:
vuln-scanning:
uses: thehyve/vulnerability-scanning/.github/workflows/vuln-scanning.yml@main
secrets:
token: ${{ secrets.GITHUB_TOKEN }}
snyk-token: ${{ secrets.SNYK_TOKEN }} # only required when scan-option is 'snyk'
with:
scan-option: trivy
env-file-path: docker_images.env
tag: vulnerability
assignee-user: octocat # optional
scan-report: scan_result.json # optionalInputs:
| Input | Required | Default | Description |
|---|---|---|---|
scan-option |
Yes | trivy |
Scanner to use: trivy or snyk |
env-file-path |
Yes | Path to the .env file containing *_IMAGE variables |
|
tag |
Yes | Label applied to all created GitHub Issues (used to correlate issues between runs) | |
assignee-user |
No | (none) | GitHub username to assign created issues to |
scan-report |
No | scan_result.json |
Path to write the raw scan output (JSON) |
Secrets:
| Secret | Required | Description |
|---|---|---|
token |
Yes | GitHub token used to create/update issues (typically GITHUB_TOKEN) |
snyk-token |
No | Snyk API token (required when scan-option is snyk) |
The script can also be run locally from the root of any repository:
# Scan the current directory and write docker_images.env
python3 scan_docker_images.py
# Write to a custom path
OUTPUT_FILE=infra/images.env python3 scan_docker_images.pyWhat is scanned:
*.yml/*.yamlβimage: <ref>directives (YAML anchors are supported)Dockerfile/Dockerfile.*βFROM <ref>directives (scratchand variable references are skipped)
Skipped directories: .git, .mypy_cache, node_modules, __pycache__, .venv
Given an image reference the script derives an environment-variable name as follows:
| Step | Example |
|---|---|
| Original reference | ghcr.io/opentargets/platform-api:25.4.2 |
| Strip tag | ghcr.io/opentargets/platform-api |
| Take last path component (repo name) | platform-api |
Replace non-word characters with _ |
platform_api |
| Upper-case | PLATFORM_API |
Append _IMAGE |
PLATFORM_API_IMAGE |
When multiple images share the same base name (e.g. two different tags of the same image), the tag is incorporated into both variable names to keep them unique:
# Found in: docker-compose.yml
CLICKHOUSE_SERVER_23_3_IMAGE=clickhouse/clickhouse-server:23.3
# Found in: docker-compose.prod.yml
CLICKHOUSE_SERVER_25_8_2_29_IMAGE=clickhouse/clickhouse-server:25.8.2.29Version-prefix tags (e.g. mysql:8 alongside mysql:8.0) are automatically merged β the shorter prefix is absorbed into the more specific tag.
| Condition | Action |
|---|---|
| π Vulnerability found, no existing issue | New issue created with HIGH / CRITICAL label(s) and CVE list |
| π Vulnerability found, issue already exists | Comment added to the existing issue with updated scan timestamp and CVE list |
| β No HIGH or CRITICAL vulnerabilities for an image | No issue created or updated |
Issues are matched by title (Vulnerability detected in <image>) and the caller-supplied tag label, so scans from different projects do not interfere with each other.
Both workflows together (image discovery + scanning):
# .github/workflows/scheduled-vuln-scan.yml
name: Scheduled vulnerability scan
on:
schedule:
- cron: "0 6 * * 1" # every Monday at 06:00 UTC
workflow_dispatch:
jobs:
generate-image-list:
uses: thehyve/vulnerability-scanning/.github/workflows/generate-image-list.yml@main
secrets: inherit
vuln-scanning:
needs: generate-image-list
uses: thehyve/vulnerability-scanning/.github/workflows/vuln-scanning.yml@main
secrets:
token: ${{ secrets.GITHUB_TOKEN }}
with:
scan-option: trivy
env-file-path: docker_images.env
tag: vuln-scan
assignee-user: your-github-usernameScanning only (repository already has its own docker_images.env):
# .github/workflows/scheduled-vuln-scan.yml
name: Scheduled vulnerability scan
on:
schedule:
- cron: "0 6 * * 1" # every Monday at 06:00 UTC
workflow_dispatch:
jobs:
vuln-scanning:
uses: thehyve/vulnerability-scanning/.github/workflows/vuln-scanning.yml@main
secrets:
token: ${{ secrets.GITHUB_TOKEN }}
with:
scan-option: trivy
env-file-path: docker_images.env
tag: vuln-scan
assignee-user: your-github-username