diff --git a/.github/workflows/cve-scan.yml b/.github/workflows/cve-scan.yml index 043d2378bd43..6f2e5b2b9e7e 100644 --- a/.github/workflows/cve-scan.yml +++ b/.github/workflows/cve-scan.yml @@ -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 @@ -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'