Skip to content

Commit 4e2d14c

Browse files
committed
Merge remote-tracking branch 'upstream/8.0.x' into fix/gsp-spring-boot-standalone-8.0.x
2 parents d52f80f + c4f60ce commit 4e2d14c

19 files changed

Lines changed: 629 additions & 197 deletions

File tree

.github/scripts/ossIndexReport.sh

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
#!/bin/bash
2+
3+
#
4+
# Licensed to the Apache Software Foundation (ASF) under one
5+
# or more contributor license agreements. See the NOTICE file
6+
# distributed with this work for additional information
7+
# regarding copyright ownership. The ASF licenses this file
8+
# to you under the Apache License, Version 2.0 (the
9+
# "License"); you may not use this file except in compliance
10+
# with the License. You may obtain a copy of the License at
11+
#
12+
# https://www.apache.org/licenses/LICENSE-2.0
13+
#
14+
# Unless required by applicable law or agreed to in writing,
15+
# software distributed under the License is distributed on an
16+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17+
# KIND, either express or implied. See the License for the
18+
# specific language governing permissions and limitations
19+
# under the License.
20+
#
21+
22+
# Renders a Markdown vulnerability report from the log of an `ossIndexAudit --info` run
23+
# and writes it to stdout. Shared by the job summary and the pull request comment so both
24+
# present the scan identically.
25+
#
26+
# Usage: ossIndexReport.sh <scan-log> <scan-outcome> <title> [max-report-bytes]
27+
#
28+
# scan-log path to the tee'd `ossIndexAudit` output
29+
# scan-outcome `success` when the audit found nothing, anything else otherwise
30+
# title heading text for the report
31+
# max-report-bytes truncate the vulnerability listing to this many bytes; 0 (default)
32+
# leaves it untruncated. Used to stay under GitHub's comment size cap.
33+
34+
set -uo pipefail
35+
36+
SCAN_LOG="${1:?path to the ossIndexAudit log is required}"
37+
SCAN_OUTCOME="${2:?scan outcome is required}"
38+
TITLE="${3:?report title is required}"
39+
MAX_REPORT_BYTES="${4:-0}"
40+
41+
echo "## ${TITLE}"
42+
43+
if [ "$SCAN_OUTCOME" = "success" ]; then
44+
echo "✅ No vulnerabilities found."
45+
exit 0
46+
fi
47+
48+
# The audit prints a line per resolved coordinate; hold each one back and print it only when a
49+
# vulnerability follows, so a clean dependency contributes nothing. Report each
50+
# coordinate and each CVE once even though a CVE may be reported against several modules.
51+
REPORT=$(awk '
52+
BEGIN { in_section=0; in_vuln=0 }
53+
{ gsub(/\033\[[0-9;]*m/, "") }
54+
/^##\[ossIndexAudit:begin\]/ { in_section=1; next }
55+
/^##\[ossIndexAudit:end\]/ { in_section=0; in_vuln=0; next }
56+
!in_section { next }
57+
/^\[[0-9]+\/[0-9]+\] - pkg:maven\// {
58+
sub(/^\[[0-9]+\/[0-9]+\] - /, "")
59+
coord=$0
60+
next
61+
}
62+
/^ Vulnerability Title:/ { in_vuln=1; block=$0 "\n"; cve_id=""; next }
63+
in_vuln && /^ CVE:/ { match($0,/CVE-[0-9-]+/); if (RSTART) cve_id=substr($0,RSTART,RLENGTH); block=block $0 "\n"; next }
64+
in_vuln && /^ Reference:/ {
65+
block=block $0 "\n"
66+
if (cve_id && !seen_cve[cve_id]++) {
67+
if (coord != "" && !seen_coord[coord]++) { print ""; print coord }
68+
printf "%s",block
69+
}
70+
in_vuln=0
71+
next
72+
}
73+
in_vuln { block=block $0 "\n" }
74+
' "$SCAN_LOG" 2>/dev/null)
75+
76+
TRUNCATED=''
77+
if [ "$MAX_REPORT_BYTES" -gt 0 ] && [ "$(printf '%s' "$REPORT" | wc -c)" -gt "$MAX_REPORT_BYTES" ]; then
78+
REPORT=$(printf '%s' "$REPORT" | head -c "$MAX_REPORT_BYTES")
79+
TRUNCATED='yes'
80+
fi
81+
82+
if [ -z "$REPORT" ]; then
83+
REPORT='(no scan output captured — check the full log)'
84+
fi
85+
86+
echo "❌ Vulnerabilities detected."
87+
echo
88+
echo '```'
89+
printf '%s\n' "$REPORT"
90+
if [ -n "$TRUNCATED" ]; then
91+
echo
92+
echo '… report truncated; see the workflow run for the complete listing.'
93+
fi
94+
echo '```'
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#!/bin/bash
2+
3+
#
4+
# Licensed to the Apache Software Foundation (ASF) under one
5+
# or more contributor license agreements. See the NOTICE file
6+
# distributed with this work for additional information
7+
# regarding copyright ownership. The ASF licenses this file
8+
# to you under the Apache License, Version 2.0 (the
9+
# "License"); you may not use this file except in compliance
10+
# with the License. You may obtain a copy of the License at
11+
#
12+
# https://www.apache.org/licenses/LICENSE-2.0
13+
#
14+
# Unless required by applicable law or agreed to in writing,
15+
# software distributed under the License is distributed on an
16+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17+
# KIND, either express or implied. See the License for the
18+
# specific language governing permissions and limitations
19+
# under the License.
20+
#
21+
22+
# Creates a comment on a pull request, or updates the one a previous run left behind, so a
23+
# workflow that runs repeatedly on the same pull request keeps a single up to date comment
24+
# instead of appending a new one each time. The comment is identified by an HTML marker
25+
# written as its first line.
26+
#
27+
# Usage: postStickyComment.sh <pull-request-number> <marker> <body-file>
28+
#
29+
# Requires the `gh` CLI, a GH_TOKEN with `pull-requests: write`, and GITHUB_REPOSITORY.
30+
31+
set -euo pipefail
32+
33+
PR_NUMBER="${1:?pull request number is required}"
34+
MARKER="${2:?comment marker is required}"
35+
BODY_FILE="${3:?path to the comment body is required}"
36+
37+
REPO="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is not set}"
38+
39+
FULL_BODY_FILE=$(mktemp)
40+
trap 'rm -f "$FULL_BODY_FILE"' EXIT
41+
{
42+
printf '%s\n\n' "$MARKER"
43+
cat "$BODY_FILE"
44+
} > "$FULL_BODY_FILE"
45+
46+
# Match on the marker rather than on the comment author so a run cannot adopt an unrelated
47+
# comment the same bot left on the pull request.
48+
EXISTING_IDS=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" \
49+
--jq "[.[] | select((.body // \"\") | startswith(\"${MARKER}\")) | .id] | .[]")
50+
EXISTING_ID=$(printf '%s\n' "$EXISTING_IDS" | head -n 1)
51+
52+
if [ -n "$EXISTING_ID" ]; then
53+
echo "Updating existing comment ${EXISTING_ID} on pull request #${PR_NUMBER}"
54+
jq -n --rawfile body "$FULL_BODY_FILE" '{body: $body}' \
55+
| gh api -X PATCH "repos/${REPO}/issues/comments/${EXISTING_ID}" --input - --silent
56+
else
57+
echo "Creating comment on pull request #${PR_NUMBER}"
58+
jq -n --rawfile body "$FULL_BODY_FILE" '{body: $body}' \
59+
| gh api -X POST "repos/${REPO}/issues/${PR_NUMBER}/comments" --input - --silent
60+
fi

.github/workflows/vulnerability-scan.yml

Lines changed: 136 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,52 @@ on:
1919
# Run every Monday at 03:00 UTC
2020
- cron: '0 3 * * 1'
2121
workflow_dispatch:
22-
# Do not scan concurrently; OSS Index has per-account rate limits
22+
# Opt in per pull request by applying the "vulnerability scan" label. Applying a label
23+
# requires write access, so only a committer can start a scan.
24+
pull_request:
25+
types: [labeled, synchronize, reopened]
26+
# Used only to tell a fork pull request that it cannot be scanned. A `pull_request` run
27+
# raised from a fork gets neither the Sonatype credentials nor a token that can comment,
28+
# so the notice has to come from `pull_request_target`. That job checks out the base
29+
# branch and never runs anything from the pull request.
30+
pull_request_target:
31+
types: [labeled, synchronize, reopened]
32+
# Do not scan concurrently; OSS Index has per-account rate limits.
33+
# The event name is part of the group so the `pull_request` scan and the `pull_request_target`
34+
# notice for the same pull request cannot cancel one another.
2335
concurrency:
24-
group: ${{ github.workflow }}-${{ github.ref }}
25-
cancel-in-progress: false
36+
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
37+
cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }}
2638
jobs:
2739
scan-grails-core:
2840
name: "OSS Index Scan - grails-core"
41+
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
2942
runs-on: ubuntu-24.04
3043
permissions:
3144
contents: read
3245
steps:
3346
- name: "📥 Checkout repository"
3447
uses: actions/checkout@v6
48+
- name: "☕️ Determine Java version from .sdkmanrc"
49+
# Read the JDK the build targets out of the file that already declares it, so a
50+
# baseline bump does not leave a stale literal behind here. Only the major is kept:
51+
# a scan is outside the reproducible-build surface, so CI takes the runner's current
52+
# release of that major rather than pinning the patch version.
53+
id: sdkmanrc
54+
run: |
55+
set -euo pipefail
56+
sdkman_java=$(grep -E '^java=' .sdkmanrc | cut -d= -f2 || true)
57+
if [ -z "${sdkman_java}" ]; then
58+
echo "❌ Could not determine the java version from .sdkmanrc" >&2
59+
exit 1
60+
fi
61+
echo "Scanning with JDK ${sdkman_java%%.*} (from .sdkmanrc: ${sdkman_java})"
62+
echo "java-version=${sdkman_java%%.*}" >> "$GITHUB_OUTPUT"
3563
- name: "☕️ Setup JDK"
3664
uses: actions/setup-java@v4
3765
with:
3866
distribution: liberica
39-
java-version: 17
67+
java-version: ${{ steps.sdkmanrc.outputs.java-version }}
4068
- name: "🐘 Setup Gradle"
4169
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
4270
with:
@@ -50,30 +78,109 @@ jobs:
5078
run: ./gradlew ossIndexAudit --continue --info 2>&1 | tee /tmp/ossindex-scan.log; exit ${PIPESTATUS[0]}
5179
- name: "📋 Publish Vulnerability Summary"
5280
if: always()
81+
run: >
82+
.github/scripts/ossIndexReport.sh
83+
/tmp/ossindex-scan.log
84+
'${{ steps.scan.outcome }}'
85+
'🔍 OSS Index Vulnerability Scan — grails-core'
86+
>> $GITHUB_STEP_SUMMARY
87+
88+
scan-pull-request:
89+
name: "OSS Index Scan - pull request"
90+
# `synchronize` and `reopened` re-scan a pull request that already carries the label.
91+
if: >-
92+
github.event_name == 'pull_request'
93+
&& contains(github.event.pull_request.labels.*.name, 'vulnerability scan')
94+
&& github.event.pull_request.head.repo.full_name == github.repository
95+
runs-on: ubuntu-24.04
96+
permissions:
97+
contents: read
98+
pull-requests: write
99+
steps:
100+
- name: "📥 Checkout pull request"
101+
uses: actions/checkout@v6
102+
- name: "☕️ Determine Java version from .sdkmanrc"
103+
# Read the JDK the build targets out of the file that already declares it, so a
104+
# baseline bump does not leave a stale literal behind here. Only the major is kept:
105+
# a scan is outside the reproducible-build surface, so CI takes the runner's current
106+
# release of that major rather than pinning the patch version.
107+
id: sdkmanrc
53108
run: |
54-
echo "## 🔍 OSS Index Vulnerability Scan — grails-core" >> $GITHUB_STEP_SUMMARY
55-
if [ "${{ steps.scan.outcome }}" = "success" ]; then
56-
echo "✅ No vulnerabilities found." >> $GITHUB_STEP_SUMMARY
57-
else
58-
echo "❌ Vulnerabilities detected." >> $GITHUB_STEP_SUMMARY
59-
echo "" >> $GITHUB_STEP_SUMMARY
60-
echo '```' >> $GITHUB_STEP_SUMMARY
61-
awk '
62-
BEGIN { in_section=0; in_vuln=0 }
63-
{ gsub(/\033\[[0-9;]*m/, "") }
64-
/^##\[ossIndexAudit:begin\]/ { in_section=1; next }
65-
/^##\[ossIndexAudit:end\]/ { in_section=0; in_vuln=0; next }
66-
!in_section { next }
67-
/^\[[0-9]+\/[0-9]+\] - pkg:maven\// {
68-
sub(/^\[[0-9]+\/[0-9]+\] - /, "")
69-
if (!seen_coord[$0]++) { print ""; print }
70-
next
71-
}
72-
/^ Vulnerability Title:/ { in_vuln=1; block=$0 "\n"; cve_id=""; next }
73-
in_vuln && /^ CVE:/ { match($0,/CVE-[0-9-]+/); if (RSTART) cve_id=substr($0,RSTART,RLENGTH); block=block $0 "\n"; next }
74-
in_vuln && /^ Reference:/ { block=block $0 "\n"; if (cve_id && !seen_cve[cve_id]++) printf "%s",block; in_vuln=0; next }
75-
in_vuln { block=block $0 "\n" }
76-
' /tmp/ossindex-scan.log >> $GITHUB_STEP_SUMMARY \
77-
|| echo "(no scan output captured — check the full log)" >> $GITHUB_STEP_SUMMARY
78-
echo '```' >> $GITHUB_STEP_SUMMARY
109+
set -euo pipefail
110+
sdkman_java=$(grep -E '^java=' .sdkmanrc | cut -d= -f2 || true)
111+
if [ -z "${sdkman_java}" ]; then
112+
echo "❌ Could not determine the java version from .sdkmanrc" >&2
113+
exit 1
79114
fi
115+
echo "Scanning with JDK ${sdkman_java%%.*} (from .sdkmanrc: ${sdkman_java})"
116+
echo "java-version=${sdkman_java%%.*}" >> "$GITHUB_OUTPUT"
117+
- name: "☕️ Setup JDK"
118+
uses: actions/setup-java@v4
119+
with:
120+
distribution: liberica
121+
java-version: ${{ steps.sdkmanrc.outputs.java-version }}
122+
- name: "🐘 Setup Gradle"
123+
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
124+
with:
125+
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
126+
- name: "🔍 Run OSS Index Vulnerability Scan"
127+
env:
128+
SONATYPE_GUIDE_USERNAME: ${{ secrets.SONATYPE_GUIDE_USERNAME }}
129+
SONATYPE_GUIDE_TOKEN: ${{ secrets.SONATYPE_GUIDE_TOKEN }}
130+
continue-on-error: true
131+
id: scan
132+
run: ./gradlew ossIndexAudit --continue --info 2>&1 | tee /tmp/ossindex-scan.log; exit ${PIPESTATUS[0]}
133+
- name: "📋 Build Vulnerability Report"
134+
if: always()
135+
# 60000 bytes keeps the listing clear of GitHub's 65536 character comment limit.
136+
run: >
137+
.github/scripts/ossIndexReport.sh
138+
/tmp/ossindex-scan.log
139+
'${{ steps.scan.outcome }}'
140+
'🔍 OSS Index Vulnerability Scan — pull request'
141+
60000
142+
> /tmp/ossindex-report.md
143+
- name: "📝 Publish Vulnerability Summary"
144+
if: always()
145+
run: cat /tmp/ossindex-report.md >> $GITHUB_STEP_SUMMARY
146+
- name: "💬 Comment Vulnerability Report"
147+
if: always()
148+
env:
149+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
150+
run: >
151+
.github/scripts/postStickyComment.sh
152+
'${{ github.event.pull_request.number }}'
153+
'<!-- grails-vulnerability-scan -->'
154+
/tmp/ossindex-report.md
155+
156+
notify-fork-pull-request:
157+
name: "OSS Index Scan - unavailable"
158+
# A fork pull request cannot reach the Sonatype credentials, so say so instead of
159+
# leaving the label looking as though a scan ran.
160+
if: >-
161+
github.event_name == 'pull_request_target'
162+
&& contains(github.event.pull_request.labels.*.name, 'vulnerability scan')
163+
&& github.event.pull_request.head.repo.full_name != github.repository
164+
runs-on: ubuntu-24.04
165+
permissions:
166+
pull-requests: write
167+
steps:
168+
# Checks out the base branch, not the pull request; nothing from the fork is executed.
169+
- name: "📥 Checkout base branch"
170+
uses: actions/checkout@v6
171+
- name: "📋 Build Notice"
172+
run: |
173+
{
174+
echo "## 🔍 OSS Index Vulnerability Scan — pull request"
175+
echo "⚠️ Vulnerability scanning is not available because this branch is not on \`${{ github.repository }}\`."
176+
echo
177+
echo "The scan needs Sonatype Guide credentials, which GitHub withholds from workflow runs raised by a fork. Push the branch to \`${{ github.repository }}\` and open a pull request from there to have it scanned."
178+
} > /tmp/ossindex-report.md
179+
- name: "💬 Comment Notice"
180+
env:
181+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
182+
run: >
183+
.github/scripts/postStickyComment.sh
184+
'${{ github.event.pull_request.number }}'
185+
'<!-- grails-vulnerability-scan -->'
186+
/tmp/ossindex-report.md

build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/VulnerabilityScanPlugin.groovy

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,20 @@ class VulnerabilityScanPlugin implements Plugin<Project> {
6565
// Each entry must name the CVE and the removal condition, and be revisited whenever a fix ships.
6666
extension.excludeCoordinates = [
6767
// CVE-2018-14335: Sonatype flags this against ALL H2 versions; no fixed release exists upstream.
68-
// Remove if H2 ever ships a patched release that OSS Index no longer flags.
68+
// See https://github.com/h2database/h2database/issues/1294
6969
'com.h2database:h2:2.4.240',
7070
// CVE-2026-14683/14684/14685/14686: pulled transitively via micrometer/actuator (LatencyUtils).
7171
// The fix is HdrHistogram 2.2.3, which is not yet published to Maven Central (2.2.2 is the latest
7272
// release). Remove once 2.2.3+ is available and resolves on the classpath.
7373
'org.hdrhistogram:HdrHistogram:2.2.2',
74-
// CVE-2026-0603: Hibernate ORM 5.x is EOL and the CVE persists through the latest 5.6.x (5.6.15.Final).
75-
// Only grails-data-hibernate5 consumers pull it. Remove when those modules move off Hibernate 5.
76-
'org.hibernate:hibernate-core:5.6.11.Final',
77-
// CVE-2026-47838: spring-security-web 7.1.0 is the version managed by Spring Boot 4.1.0 and is the
78-
// latest release; no patched version exists upstream yet. Remove once a fixed release ships and
79-
// spring-boot.version is bumped.
80-
'org.springframework.security:spring-security-web:7.1.0',
74+
// CVE-2026-47838: OSS Index flags the 7.x line, but Spring's advisory
75+
// (https://spring.io/security/cve-2026-47838) lists only 5.7.x-6.5.10 as affected, fixed in
76+
// 6.5.11 - the 7.0.x/7.1.x lines are not listed, the advisory being a continuation of
77+
// CVE-2026-22747 which 7.x already carries. 7.1.1 is the version managed by Spring Boot 4.1.1
78+
// and the latest release, so there is nothing to bump to. Remove once OSS Index narrows its
79+
// range. Note this coordinate is version-pinned: a spring-boot.version bump moves
80+
// spring-security and orphans the entry, so update it in lockstep.
81+
'org.springframework.security:spring-security-web:7.1.1',
8182
] as Set
8283

8384
project.tasks.named(TASK_NAME) { task ->

0 commit comments

Comments
 (0)