Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a1ae34d
ROX-30730: add action for iamge-vulnerability-check
tommartensen Dec 8, 2025
ffefc4f
remove superfluous step ID
tommartensen Dec 8, 2025
61ef929
Revert "remove superfluous step ID"
tommartensen Dec 9, 2025
6bb87b8
prepend only quay.io, require repository to be the passed in parameter
tommartensen Dec 9, 2025
9bbec44
quote bash command
tommartensen Dec 9, 2025
7d11292
refactor: markdown table, only fail if fixable CVEs
tommartensen Dec 9, 2025
bbbfed2
make script executable
tommartensen Dec 9, 2025
904332d
cleanups
tommartensen Dec 9, 2025
48015ea
update documentation
tommartensen Dec 9, 2025
c9fe987
update summary
tommartensen Dec 9, 2025
85a652f
add title prefix
tommartensen Dec 9, 2025
4c9379e
fix typo
tommartensen Dec 9, 2025
d6f9927
fix formatting
tommartensen Dec 9, 2025
7025a2a
handle no vulnerabilities found
tommartensen Dec 9, 2025
09af701
add message explaining status
tommartensen Dec 9, 2025
a6584fd
re-format
tommartensen Dec 9, 2025
6e4b676
add comments for the csv->markdown conversion
tommartensen Dec 9, 2025
75c2a6d
touches
tommartensen Dec 9, 2025
92b555b
update README
tommartensen Dec 9, 2025
05a2a6e
prefer 'scan' or 'check' terminology
tommartensen Dec 13, 2025
e6f53b0
addressing reviewer feedback
tommartensen Dec 13, 2025
02bdcb2
update script description
tommartensen Dec 13, 2025
eb950c6
update README to address review comments
tommartensen Dec 19, 2025
380f2f4
extract vuln table header from jq
tommartensen Dec 19, 2025
01751fb
put fixable findings first in table, then unfixable; fix counting of …
tommartensen Dec 19, 2025
7334b3b
s/vulnerabilities/findings
tommartensen Dec 19, 2025
b7c19e5
refactor fixable findings assertion to common function
tommartensen Dec 19, 2025
303fbdb
apply code suggestions
tommartensen Jan 5, 2026
35cf37d
resolve are_blocking_vulns_present hint
tommartensen Jan 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions common/common.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#!/usr/bin/env bash

set -euo pipefail

# The output appears in a dedicated emphasized GitHub step box.
# Multiple lines are not supported.
# Markdown is not supported.
Expand Down
85 changes: 85 additions & 0 deletions release/scan-image-vulnerabilities/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Scan Image Vulnerabilities

Scan a container image in Quay.io for vulnerabilities using `roxctl image scan` and fail if fixable critical or important vulnerabilities are found.

This action waits for an image to be available in Quay.io, scans it using roxctl, and generates a detailed vulnerability report in the GitHub step summary, while making the raw report available as JSON in the workspace as `scan-result.json`.

## Required permissions

```yaml
permissions:
# Needed for stackrox/central-login to create the JWT token.
id-token: write
```

## All options

| Input | Description | Required | Default |
| ----------------------------------------- | -------------------------------------------------- | -------- | --------- |
| [image](#image) | Image name (without registry prefix) | Yes | |
| [version](#version) | Image version tag | Yes | |
| [wait-limit](#wait-limit) | Maximum time to wait for image (seconds) | No | `"7200"` |
| [summary-prefix](#summary-prefix) | Title prefix for the GitHub step summary | Yes | |
| [quay-bearer-token](#quay-bearer-token) | Quay.io bearer token for wait-for-image | Yes | |
| [central-url](#central-url) | ACS Central URL | Yes | |

### Detailed options

#### image

Image name without the registry prefix. The action will automatically prepend `quay.io/` to construct the full image reference.

Example: `"rhacs-eng/main"`

#### version

Image version tag to scan.

Example: `"3.76.1"`

#### wait-limit

Maximum time in seconds to wait for the image to be available on Quay.io before failing.

Default: `"7200"` (2 hours)

#### summary-prefix

Prefix for the vulnerability report in the GitHub step summary. Use this to help users of the action classify images into groups when multiple matrix scans are performed in a workflow.

Example: `"Upstream Image Scan Results"`

#### quay-bearer-token

Bearer token for authenticating with the Quay.io API. This is required by the wait-for-image action to check if the image is available.

#### central-url

URL of the ACS/RHACS Central instance to use for scanning.

Example: `"https://central.example.com"`

## Usage

The action integrates with the [stackrox/central-login](https://github.com/stackrox/central-login) action, which uses OIDC login for authentication of the `roxctl` CLI.
The ACS Central needs to be configured to allow exchanging tokens from GitHub Actions workflow runs.

Additionally, an image integration for Quay.io must be configured in the ACS Central so that it can pull images requested for scanning.

```yaml
name: Scan image for vulnerabilities

jobs:
scan:
runs-on: ubuntu-latest
permissions:
id-token: write # Required for ACS Central OIDC login
steps:
- uses: stackrox/actions/release/scan-image-vulnerabilities@v1
with:
image: rhacs-eng/main
version: 3.76.1
summary-prefix: "Main Image Scan"
quay-bearer-token: ${{ secrets.QUAY_BEARER_TOKEN }}
central-url: https://central.example.com
```
53 changes: 53 additions & 0 deletions release/scan-image-vulnerabilities/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: "Scan Image Vulnerabilities"
description: "Scan an image for vulnerabilities and fail if fixable critical or important ones are found"

inputs:
image:
description: "Image name (without registry prefix)"
required: true
version:
description: "Image version tag"
required: true
wait-limit:
description: "Maximum time to wait for image (seconds)"
required: false
default: "7200"
summary-prefix:
description: "Title prefix for the GitHub step summary"
required: true
quay-bearer-token:
description: "Quay.io bearer token for wait-for-image"
required: true
central-url:
description: "ACS Central URL"
required: true

runs:
using: composite
steps:
- name: "Wait for image to be built: quay.io/${{ inputs.image }}:${{ inputs.version }}"
uses: stackrox/actions/release/wait-for-image@v1
with:
token: ${{ inputs.quay-bearer-token }}
image: ${{ inputs.image }}:${{ inputs.version }}
limit: ${{ inputs.wait-limit }}

- name: Central login
uses: stackrox/central-login@v1
with:
endpoint: ${{ inputs.central-url }}

- name: Install roxctl
uses: stackrox/roxctl-installer-action@v1
with:
central-endpoint: ${{ inputs.central-url }}
central-token: ${{ env.ROX_API_TOKEN }}

- name: Scan image for fixable vulnerabilities
shell: bash
run: |
set -uo pipefail
"${{ github.action_path }}/../../common/common.sh" \
"${{ github.action_path }}/scan-image-vulnerabilities.sh" \
"quay.io/${{ inputs.image }}:${{ inputs.version }}" \
"${{ inputs.summary-prefix }}"
122 changes: 122 additions & 0 deletions release/scan-image-vulnerabilities/scan-image-vulnerabilities.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#!/usr/bin/env bash
#
# Scans an image for vulnerabilities and prints a summary of the findings.
# For the purpose of this script, a finding is an image component in a version that is affected by a CVE.
# There may be multiple findings for the same image component in a version, if the component has multiple CVEs.
#
# Local run:
#
# test/local-env.sh release/scan-image-vulnerabilities/scan-image-vulnerabilities.sh <image> <summary-prefix>
#
set -euo pipefail

function main() {
local IMAGE="${1:-}"
local SUMMARY_PREFIX="${2:-}"
local result_path="scan-result.json"

check_not_empty \
IMAGE \
SUMMARY_PREFIX

scan_image "$IMAGE" "$result_path"

# Count the total and fixable number of findings for each severity.
# Use associative arrays to store counts by severity.
# Arrays are used via nameref parameters in print_summary function.
# shellcheck disable=SC2034
declare -A total_counts
# shellcheck disable=SC2034
declare -A fixable_counts

# shellcheck disable=SC2034
for severity in CRITICAL IMPORTANT MODERATE LOW; do
total_counts[$severity]="$(count_total_findings "$severity" "$result_path")"
fixable_counts[$severity]="$(count_fixable_findings "$severity" "$result_path")"
done

gh_summary "### $SUMMARY_PREFIX $IMAGE"
print_summary total_counts fixable_counts

# Print the findings table in a collapsible section.
# For the table to render correctly, we need to add a newline after the summary.
gh_summary "<details><summary>Click to expand details</summary>\n"
gh_summary "$(print_details_table "$result_path")"
gh_summary "</details>"

# Fail the build if any fixable findings of relevant severity are present.
if are_blocking_vulns_present fixable_counts; then
exit 1
fi
}

# Scans the image for vulnerabilities.
function scan_image() {
local image="$1"
local result_path="$2"
roxctl image scan --output=json --force \
--image="${image}" | tee "$result_path"
gh_output result-path "$result_path"
}

# Counts the number of findings for a given severity.
function count_total_findings() {
local severity="$1"
local result_path="$2"
jq "[.result.vulnerabilities // [] | .[] | select(.cveSeverity == \"$severity\")] | length" "$result_path"
}

# Counts the number of fixable findings for a given severity.
function count_fixable_findings() {
local severity="$1"
local result_path="$2"
jq "[.result.vulnerabilities // [] | .[] | select(.cveSeverity == \"$severity\" and .componentFixedVersion != \"\")] | length" "$result_path"
}

# Prints the vulnerability status and an overview table of the findings counts.
function print_summary() {
local -n total_counts_ref=$1
local -n fixable_counts_ref=$2

if are_blocking_vulns_present fixable_counts_ref; then
local message="Found fixable critical or important vulnerabilities."

gh_log "error" "$message See the step summary for details."
gh_summary "Status: ❌"
gh_summary "> $message"
else
gh_summary "Status: ✅"
gh_summary "> No fixable critical or important vulnerabilities found."
fi

gh_summary ""
gh_summary "| Severity | Total | Fixable |"
gh_summary "| --- | --- | --- |"
for severity in CRITICAL IMPORTANT MODERATE LOW; do
gh_summary "| $severity | ${total_counts_ref[$severity]} | ${fixable_counts_ref[$severity]} |"
done
}

# Checks if any fixable findings of relevant severity are present.
function are_blocking_vulns_present() {
local -n fixable_counts_map="$1"
(( fixable_counts_map[CRITICAL] > 0 || fixable_counts_map[IMPORTANT] > 0 ))
}

# Prints a markdown table of the findings, sorted by severity with fixable findings first.
# Each row contains left, right and column separators added by jq's join function.
function print_details_table() {
local result_path="$1"
echo "| COMPONENT | VERSION | CVE | SEVERITY | FIXED_VERSION | LINK |"
echo "| --- | --- | --- | --- | --- | --- |"
jq -r '
.result.vulnerabilities // []
| sort_by([
(if ((.componentFixedVersion // "") != "") then 0 else 1 end),
({"CRITICAL":0,"IMPORTANT":1,"MODERATE":2,"LOW":3}[.cveSeverity] // 4)
])
| (.[] | [.componentName // "", .componentVersion // "", .cveId // "", .cveSeverity // "", .componentFixedVersion // "", .cveInfo // ""] | "| " + join(" | ") + " |")
' "$result_path"
}

main "$@"
Loading