-
Notifications
You must be signed in to change notification settings - Fork 29
Add scan-image action to check for vulnerabilities #1372
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
name: Test scan-image action | ||
|
||
on: | ||
push: | ||
branches: | ||
- main | ||
paths: | ||
- "actions/scan-image/**" | ||
- ".github/workflows/test-scan-image.yaml" | ||
|
||
pull_request: | ||
paths: | ||
- "actions/scan-image/**" | ||
- ".github/workflows/test-scan-image.yaml" | ||
types: | ||
- edited | ||
- opened | ||
- ready_for_review | ||
- synchronize | ||
|
||
merge_group: | ||
|
||
permissions: | ||
id-token: write | ||
contents: read | ||
|
||
jobs: | ||
test: | ||
runs-on: ubuntu-latest | ||
steps: | ||
|
||
- name: (Test 1) Scan public image without vulnerabilities | ||
id: scan-image-1 | ||
uses: Lantero/shared-workflows/actions/scan-image@1c42928e7de3547403e934df7fa59a3e59865a75 # scan-action/v0.1.0 | ||
with: | ||
image_name: docker.io/hello-world | ||
image_source: public | ||
|
||
- name: (Test 2) Scan public image with some vulnerabilities (Pinned Trivy and Grype) | ||
id: scan-image-2 | ||
uses: Lantero/shared-workflows/actions/scan-image@1c42928e7de3547403e934df7fa59a3e59865a75 # scan-action/v0.1.0 | ||
with: | ||
image_name: docker.io/redis:7.4.0-alpine | ||
image_source: public | ||
trivy_version: v0.66.0 | ||
grype_version: v0.98.0 | ||
Comment on lines
+39
to
+46
Check failureCode scanning / zizmor commit with no history in referenced repository Error test
commit with no history in referenced repository
|
||
|
||
- name: (Test 3) Scan private DockerHub image for vulnerabilities | ||
id: scan-image-3 | ||
uses: Lantero/shared-workflows/actions/scan-image@1c42928e7de3547403e934df7fa59a3e59865a75 # scan-action/v0.1.0 | ||
with: | ||
image_name: ${{ github.repository }}@${{ steps.build.outputs.digest }} | ||
image_source: private_dockerhub | ||
Comment on lines
+48
to
+53
Check failureCode scanning / zizmor commit with no history in referenced repository Error test
commit with no history in referenced repository
|
||
|
||
- name: (Test 4) Scan private GAR image for vulnerabilities | ||
id: scan-image-4 | ||
uses: Lantero/shared-workflows/actions/scan-image@1c42928e7de3547403e934df7fa59a3e59865a75 # scan-action/v0.1.0 | ||
with: | ||
image_name: us-docker.pkg.dev/grafanalabs-global/docker-deployment-tools-prod/terraform-team:1.7.5-16-0-amd64 | ||
image_source: private_gar | ||
Comment on lines
+55
to
+60
Check failureCode scanning / zizmor commit with no history in referenced repository Error test
commit with no history in referenced repository
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
# Changelog | ||
|
||
## 0.1.0 (2025-10-03) | ||
|
||
### 🎉 Features | ||
|
||
* **scan-image:** add `snyk` and `trivy` vulnerability scanners with a fail condition configuration | ||
* **scan-image:** add [Dockerhub](https://hub.docker.com/) and [Google Artifact Registry](https://cloud.google.com/artifact-registry/docs) as available private sources |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# scan-image | ||
|
||
This is a composite GitHub Action used to scan your images in search of vulnerabilities. | ||
|
||
The goal is to provide developers a way to check if their PR changes, without having to | ||
wait for periodic scans (Faster feedback loop). This can also be used as part of deployment | ||
CI/CD jobs as a way to verify things before shipping to production environments. | ||
|
||
<!-- x-release-please-start-version --> | ||
|
||
```yaml | ||
name: Scan image for vulnerabilities | ||
jobs: | ||
scan-image: | ||
name: Scan image for vulnerabilities | ||
steps: | ||
- name: Scan image for vulnerabilities | ||
id: scan-image | ||
uses: grafana/shared-workflows/actions/scan-image@scan-image/v0.1.0 | ||
with: | ||
image_name: docker.io/hello-world | ||
fail_on: critical | ||
fail_on_threshold: 1 | ||
``` | ||
<!-- x-release-please-end-version --> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,209 @@ | ||
name: Scan Image | ||
description: Composite action to check an image for vulnerabilities | ||
|
||
inputs: | ||
image_name: | ||
description: "The name of the image to scan, including the registry e.g. docker.io/hello-world" | ||
required: true | ||
image_source: | ||
description: "The source of the image to scan" | ||
type: choice | ||
options: | ||
- public | ||
- private_dockerhub | ||
- private_gar | ||
default: public | ||
fail_on: | ||
description: "Whether to fail the workflow if vulnerabilities are found" | ||
type: choice | ||
options: | ||
- critical | ||
- high | ||
- medium | ||
- low | ||
default: critical | ||
fail_on_threshold: | ||
description: "The threshold of vulnerabilities to fail the workflow on" | ||
type: integer | ||
default: 1 | ||
trivy_version: | ||
description: "The version of Trivy to use (The latest will be used if not provided)" | ||
type: string | ||
grype_version: | ||
description: "The version of Grype to use (The latest will be used if not provided)" | ||
type: string | ||
|
||
outputs: {} | ||
|
||
runs: | ||
using: composite | ||
steps: | ||
|
||
- name: Login to DockerHub | ||
id: login-to-dockerhub | ||
if: inputs.image_source == 'private_dockerhub' | ||
uses: grafana/shared-workflows/actions/[email protected] | ||
|
||
- name: Extract GAR registry | ||
id: extract-gar-registry | ||
if: inputs.image_source == 'private_gar' | ||
shell: bash | ||
run: | | ||
IMAGE_NAME="${{ inputs.image_name }}" | ||
Check failureCode scanning / zizmor code injection via template expansion Error
code injection via template expansion
|
||
REGISTRY="${IMAGE_NAME%%/*}" | ||
echo "registry=${REGISTRY}" >> $GITHUB_OUTPUT | ||
|
||
- name: Login to GAR | ||
id: login-to-gar | ||
if: inputs.image_source == 'private_gar' | ||
uses: grafana/shared-workflows/actions/login-to-gar@login-to-gar/v1.0.0 | ||
with: | ||
registry: ${{ steps.extract-gar-registry.outputs.registry }} | ||
|
||
- name: Setup Trivy (Latest) | ||
id: setup-trivy-latest | ||
if: inputs.trivy_version == '' | ||
uses: aquasecurity/[email protected] | ||
Check failureCode scanning / zizmor unpinned action reference Error
unpinned action reference
|
||
|
||
- name: Setup Trivy (Pinned) | ||
id: setup-trivy-pinned | ||
if: inputs.trivy_version != '' | ||
uses: aquasecurity/[email protected] | ||
Check failureCode scanning / zizmor unpinned action reference Error
unpinned action reference
|
||
with: | ||
version: ${{ inputs.trivy_version }} | ||
|
||
- name: Run Trivy | ||
id: run-trivy | ||
shell: bash | ||
run: | | ||
trivy image ${{ inputs.image_name }} -f json -o trivy.json | ||
|
||
trivy convert --format cyclonedx --output trivy-cdx.json trivy.json | ||
|
||
- name: Setup Grype (Latest) | ||
id: setup-grype-latest | ||
if: inputs.grype_version == '' | ||
uses: anchore/scan-action/download-grype@v7 | ||
Check failureCode scanning / zizmor unpinned action reference Error
unpinned action reference
|
||
with: | ||
grype-version: | ||
|
||
- name: Setup Grype (Pinned) | ||
id: setup-grype-pinned | ||
if: inputs.grype_version != '' | ||
uses: anchore/scan-action/download-grype@v7 | ||
Check failureCode scanning / zizmor unpinned action reference Error
unpinned action reference
|
||
with: | ||
grype-version: ${{ inputs.grype_version }} | ||
|
||
- name: Run Grype | ||
id: run-grype | ||
shell: bash | ||
run: | | ||
${{ inputs.grype_version == '' && steps.setup-grype-latest.outputs.cmd || steps.setup-grype-pinned.outputs.cmd }} ${{ inputs.image_name }} -o cyclonedx-json > grype-cdx.json | ||
Check failureCode scanning / zizmor code injection via template expansion Error
code injection via template expansion
|
||
|
||
- name: Merge reports | ||
id: merge-reports | ||
shell: bash | ||
run: | | ||
jq -s ' | ||
(.[0].vulnerabilities // []) + (.[1].vulnerabilities // []) | ||
| map({ | ||
"id": .id, | ||
"url": ("https://www.cve.org/CVERecord?id=" + .id), | ||
"severity": ([.ratings[].severity] | map(select(. != null)) | .[0] // "N/A") | ||
}) | ||
| map(select(.id | startswith("CVE"))) | ||
| reduce .[] as $item ({}; | ||
.[$item.id] = { | ||
"url": $item.url, | ||
"severity": $item.severity | ||
} | ||
) | ||
' trivy-cdx.json grype-cdx.json > result.json | ||
cat result.json | ||
|
||
- name: Markdown summary | ||
id: markdown-summary | ||
shell: bash | ||
run: | | ||
# Check if result.json is empty or has no entries | ||
if [[ $(jq '. | length' result.json) -eq 0 ]]; then | ||
## No CVEs found | ||
echo "# CVE Report (${{ inputs.image_name }}) :white_check_mark: | ||
Check failureCode scanning / zizmor code injection via template expansion Error
code injection via template expansion
|
||
|
||
No CVEs found!" >> "$GITHUB_STEP_SUMMARY" | ||
else | ||
## CVEs found, create table | ||
MARKDOWN_CONTENT=" | ||
## CVE Report (${{ inputs.image_name }}) :warning: | ||
Check failureCode scanning / zizmor code injection via template expansion Error
code injection via template expansion
|
||
|
||
| CVE | Severity | | ||
| :--- | :--- | | ||
" | ||
# Use 'jq' to process the JSON file. | ||
# The output is formatted as tab-separated values (TSV) for easy reading into bash variables. | ||
# .key is the CVE ID, .value.severity is the severity string, and .value.url is the URL. | ||
while IFS=$'\t' read -r cve_id severity url; do | ||
# Append a row to the markdown content | ||
# The CVE ID is formatted as a link: [CVE-ID](URL) | ||
# The Severity is kept as plain text. | ||
MARKDOWN_CONTENT+="| **[$cve_id]($url)** | $severity | | ||
" | ||
done < <(jq -r '. | to_entries[] | "\(.key)\t\(.value.severity)\t\(.value.url)"' result.json) | ||
echo "$MARKDOWN_CONTENT" >> "$GITHUB_STEP_SUMMARY" | ||
fi | ||
|
||
- name: Fail condition | ||
id: fail-condition | ||
shell: bash | ||
run: | | ||
if [[ ${{ inputs.fail_on_threshold }} -lt 1 ]]; then | ||
Check failureCode scanning / zizmor code injection via template expansion Error
code injection via template expansion
|
||
echo "fail_on_threshold is less than 1, skipping fail condition check" | ||
exit 0 | ||
fi | ||
|
||
declare -A severity_levels=( | ||
["low"]=1 | ||
["medium"]=2 | ||
["high"]=3 | ||
["critical"]=4 | ||
) | ||
|
||
echo "Checking for vulnerabilities with severity >= ${{ inputs.fail_on }} (level ${{ inputs.fail_on_threshold }})" | ||
Check failureCode scanning / zizmor code injection via template expansion Error
code injection via template expansion
Check failureCode scanning / zizmor code injection via template expansion Error
code injection via template expansion
Check failureCode scanning / zizmor code injection via template expansion Error
code injection via template expansion
|
||
echo "Threshold: ${{ inputs.fail_on_threshold }} vulnerabilities" | ||
|
||
vulnerability_count=0 | ||
|
||
if [[ ! -f "result.json" ]] || [[ $(jq '. | length' result.json) -eq 0 ]]; then | ||
echo "No vulnerabilities found in scan results" | ||
exit 0 | ||
fi | ||
|
||
# Count vulnerabilities at or above the specified severity level | ||
while IFS=$'\t' read -r cve_id severity url; do | ||
# Convert severity to lowercase for comparison | ||
severity_lower=$(echo "$severity" | tr '[:upper:]' '[:lower:]') | ||
|
||
# Skip if severity is not in our defined levels (e.g., "N/A") | ||
if [[ -z "${severity_levels[$severity_lower]}" ]]; then | ||
echo "Skipping $cve_id with unknown severity: $severity" | ||
continue | ||
fi | ||
|
||
current_level=${severity_levels[$severity_lower]} | ||
|
||
if [[ $current_level -ge $${{ inputs.fail_on }} ]]; then | ||
Check failureCode scanning / zizmor code injection via template expansion Error
code injection via template expansion
|
||
echo "Found vulnerability: $cve_id (severity: $severity, level: $current_level)" | ||
((vulnerability_count++)) | ||
fi | ||
done < <(jq -r '. | to_entries[] | "\(.key)\t\(.value.severity)\t\(.value.url)"' result.json) | ||
|
||
echo "Found $vulnerability_count vulnerabilities at or above ${{ inputs.fail_on }} severity level" | ||
Check failureCode scanning / zizmor code injection via template expansion Error
code injection via template expansion
|
||
|
||
# Fail if vulnerability count meets or exceeds threshold | ||
if [[ $vulnerability_count -ge ${{ inputs.fail_on_threshold }} ]]; then | ||
Check failureCode scanning / zizmor code injection via template expansion Error
code injection via template expansion
Check failureCode scanning / zizmor code injection via template expansion Error
code injection via template expansion
Check failureCode scanning / zizmor code injection via template expansion Error
code injection via template expansion
|
||
echo "❌ FAIL: Found $vulnerability_count vulnerabilities (threshold: ${{ inputs.fail_on_threshold }})" | ||
Check failureCode scanning / zizmor code injection via template expansion Error
code injection via template expansion
|
||
echo "Vulnerabilities at or above '${{ inputs.fail_on }}' severity level exceed the specified threshold." | ||
exit 1 | ||
else | ||
echo "✅ PASS: Found $vulnerability_count vulnerabilities (threshold: ${{ inputs.fail_on_threshold }})" | ||
exit 0 | ||
fi |
Check failure
Code scanning / zizmor
commit with no history in referenced repository Error test