Skip to content
Draft
Changes from all commits
Commits
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
117 changes: 103 additions & 14 deletions .github/workflows/cve-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,13 @@ jobs:
# Trivy CVE scan — scans bundled jars for known vulnerabilities.
#
# Behaviour:
# - On PRs: the scan blocks CI if CVEs are found (exit-code 1).
# SARIF upload is skipped because GitHub's Security tab only
# accepts results from default/protected branches.
# - On push to main/release branches: the scan is informational
# (exit-code 0) and results are uploaded as SARIF to the GitHub
# Security tab for ongoing tracking.
# - Trivy always writes SARIF with exit-code 0. The reporting step owns
# finding-based failures so GitHub opens the step with actionable details.
# - On PRs: the reporting step blocks CI if HIGH/CRITICAL CVEs are found.
# - On push to main/release branches and release tags: findings are
# informational, then SARIF is uploaded to the GitHub Security tab.
# - Missing or unparseable SARIF is still a failure on every event because
# it means the scan did not produce usable results.
# ------------------------------------------------------------------
cve-scan:
runs-on: ubuntu-24.04
Expand Down Expand Up @@ -220,19 +221,107 @@ jobs:
severity: 'HIGH,CRITICAL'
trivyignores: ${{ matrix.trivyignores }}
limit-severities-for-sarif: true
# Block PRs on CVE findings; on main/release branches report without failing
exit-code: ${{ github.event_name == 'pull_request' && '1' || '0' }}
# Let Trivy generate SARIF without failing on findings. GitHub opens the
# failed step by default, so PRs fail later in the reporting step that
# prints the actionable CVE details.
exit-code: '0'
format: 'sarif'
output: 'trivy-results.sarif'
trivy-image: ${{ env.TRIVY_IMAGE }}
- name: Print Trivy scan results
- name: Report Trivy scan results
if: always()
env:
# PRs block on findings; push runs report findings without failing so
# SARIF can be uploaded to GitHub Security for tracking.
FAIL_ON_FINDINGS: ${{ github.event_name == 'pull_request' && 'true' || 'false' }}
run: |
if [ -f trivy-results.sarif ]; then
echo "## Trivy CVE Scan Results — ${{ matrix.distribution }}"
jq -r '.runs[].results[] | "- \(.ruleId): \(.message.text)"' trivy-results.sarif 2>/dev/null || echo "No findings or unable to parse SARIF."
else
echo "No SARIF file found — scan may have failed to install."
results_file="trivy-results.sarif"
summary="${GITHUB_STEP_SUMMARY:-/dev/null}"

log() {
printf '%s\n' "$*" | tee -a "${summary}"
}

markdown_escape() {
value="$1"
value="${value//|/\\|}"
printf '%s' "${value}"
}

escape_annotation() {
value="$1"
value="${value//'%'/%25}"
value="${value//$'\r'/%0D}"
value="${value//$'\n'/%0A}"
printf '%s' "${value}"
}

extract_findings() {
jq -r '
def field($prefix):
(.message.text | split("\n") | map(select(startswith($prefix))) | first // "")
| ltrimstr($prefix);

.runs[].results[]?
| [
.ruleId,
field("Severity: "),
field("Package: "),
field("Installed Version: "),
field("Fixed Version: "),
field("Link: ")
]
| @tsv
' "${results_file}"
}

report_findings() {
log "Found ${finding_count} HIGH/CRITICAL vulnerabilities."
log ""
log "| CVE | Severity | Package | Installed | Fixed | Link |"
log "| --- | --- | --- | --- | --- | --- |"

while IFS=$'\t' read -r cve severity package installed fixed link; do
cve="$(markdown_escape "${cve}")"
severity="$(markdown_escape "${severity}")"
package="$(markdown_escape "${package}")"
installed="$(markdown_escape "${installed}")"
fixed="$(markdown_escape "${fixed}")"
link="$(markdown_escape "${link}")"
log "| ${cve} | ${severity} | \`${package}\` | \`${installed}\` | ${fixed} | ${link} |"
done <<< "${findings}"
}

if [ ! -f "${results_file}" ]; then
log "No SARIF file found — scan may have failed to run."
exit 1
fi

if ! findings="$(extract_findings)"; then
log "Unable to parse Trivy SARIF results."
exit 1
fi

log "## Trivy CVE Scan Results — ${{ matrix.distribution }}"

if [ -z "${findings}" ]; then
log "No HIGH or CRITICAL vulnerabilities found."
exit 0
fi

finding_count="$(printf '%s\n' "${findings}" | awk 'END { print NR }')"
finding_ids="$(printf '%s\n' "${findings}" | cut -f1 | awk 'BEGIN { sep="" } { printf "%s%s", sep, $0; sep=", " } END { print "" }')"

report_findings

if [ "${FAIL_ON_FINDINGS}" = "true" ]; then
# Surface findings in the PR checks UI, not just in the workflow logs.
annotation_message="Trivy found ${finding_count} HIGH/CRITICAL vulnerabilities in ${{ matrix.distribution }}: ${finding_ids}. See the 'Report Trivy scan results' step for details."
annotation="$(escape_annotation "${annotation_message}")"
echo "::error title=Trivy CVE scan failed::${annotation}"
log ""
log "Failing because ${finding_count} HIGH/CRITICAL vulnerabilities were found."
exit 1
fi
- name: Upload Trivy results to GitHub Security tab
if: always() && github.event_name == 'push'
Expand Down