Skip to content

Commit a49edd0

Browse files
committed
feat: add hack script for fixing vulns on release branches
Signed-off-by: bwplotka <bwplotka@google.com>
1 parent f335d41 commit a49edd0

File tree

3 files changed

+348
-1
lines changed

3 files changed

+348
-1
lines changed

hack/release-lib.sh

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
#!/usr/bin/env bash
2+
# Copyright 2025 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
# Useful functions for release scripts.
17+
18+
set -o errexit
19+
set -o pipefail
20+
set -o nounset
21+
22+
if [[ -n "${DEBUG_MODE:-}" ]]; then
23+
set -o xtrace
24+
fi
25+
26+
SCRIPT_DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
27+
28+
release-lib::confirm() {
29+
local prompt_message="${1:-Are you sure?}"
30+
31+
# -p: Display the prompt string.
32+
# -r: Prevents backslash interpretation.
33+
# -n 1: Read only one character.
34+
read -p "$prompt_message [y/n]: " -r -n 1 response
35+
echo # Ensures the cursor moves to the next line after input.
36+
case "$response" in
37+
[yY])
38+
return 0
39+
;;
40+
[nN])
41+
echo "❌ The action has been cancelled as requested."
42+
return 1
43+
;;
44+
*)
45+
echo "Invalid input. Exiting script." >&2
46+
exit 1
47+
;;
48+
esac
49+
}
50+
51+
release-lib::idemp::clone() {
52+
local clone_dir=$1
53+
if [[ -z "${clone_dir}" ]]; then
54+
echo "❌ clone_dir variable is not set." >&2
55+
usage
56+
return 1
57+
fi
58+
if [[ -z "${PR_BRANCH}" ]]; then
59+
echo "❌ PR_BRANCH environment variable is not set." >&2
60+
usage
61+
return 1
62+
fi
63+
if [[ ! -d "${clone_dir}" ]]; then
64+
release-lib::clone "${clone_dir}"
65+
fi
66+
67+
pushd "${clone_dir}"
68+
if [[ "$(git symbolic-ref --short HEAD)" != "${PR_BRANCH}" ]]; then
69+
echo "❌ Malformed ${DIR}; expected ${PR_BRANCH} got $(git symbolic-ref --short HEAD); remove or fix manually the ${DIR} and rerun." >&2
70+
return 1
71+
fi
72+
popd
73+
}
74+
75+
release-lib::clone() {
76+
local clone_dir=$1
77+
if [[ -z "${clone_dir}" ]]; then
78+
echo "❌ clone_dir variable is not set." >&2
79+
usage
80+
return 1
81+
fi
82+
if [[ -z "${BRANCH}" ]]; then
83+
echo "❌ BRANCH environment variable is not set." >&2
84+
usage
85+
return 1
86+
fi
87+
if [[ -z "${REMOTE_URL}" ]]; then
88+
echo "❌ REMOTE_URL environment variable is not set." >&2
89+
usage
90+
return 1
91+
fi
92+
if [[ -z "${PR_BRANCH}" ]]; then
93+
echo "❌ PR_BRANCH environment variable is not set." >&2
94+
usage
95+
return 1
96+
fi
97+
# NOTE: We could add --single-branch but it would be a bit harder to use interactively.
98+
git clone -b "${BRANCH}" "${REMOTE_URL}" "${clone_dir}"
99+
if [[ "${BRANCH}" != "${PR_BRANCH}" ]]; then
100+
pushd "${clone_dir}"
101+
git checkout -b "${PR_BRANCH}"
102+
popd
103+
fi
104+
}
105+
106+
release-lib::remote_url_from_branch() {
107+
local branch=$1
108+
# Check if the BRANCH environment variable is set.
109+
if [[ -z "${branch}" ]]; then
110+
echo "❌ branch is required." >&2
111+
return 1
112+
fi
113+
114+
if [[ "${branch}" =~ ^release-(2|3)\.[0-9]+\.[0-9]+-gmp$ ]]; then
115+
echo "git@github.com:GoogleCloudPlatform/prometheus.git"
116+
elif [[ "${branch}" =~ ^release-0\.[0-9]+\.[0-9]+-gmp$ ]]; then
117+
echo "git@github.com:GoogleCloudPlatform/alertmanager.git"
118+
elif [[ "${branch}" =~ ^release/0\.[0-9]+$ ]]; then
119+
echo "git@github.com:GoogleCloudPlatform/prometheus-engine.git"
120+
else
121+
echo "❌ No matching remote URL found for branch='$BRANCH'" >&2
122+
return 1
123+
fi
124+
}
125+
126+
release-lib::idemp::vulnlist() {
127+
local dir=$1
128+
if [[ -z "${dir}" ]]; then
129+
echo "❌ dir is required." >&2
130+
return 1
131+
fi
132+
133+
if [[ ! -f "${dir}/vulnlist.txt" || -z $(cat "${dir}/vulnlist.txt") ]]; then
134+
release-lib::vulnlist "${dir}"
135+
else
136+
echo "⚠️ Using existing ${dir}/vulnlist.txt"
137+
fi
138+
}
139+
140+
release-lib::vulnlist() {
141+
local dir=$1
142+
if [[ -z "${dir}" ]]; then
143+
echo "❌ dir is required." >&2
144+
return 1
145+
fi
146+
147+
echo "🔄 Detecting Go vulnerabilities to fix..."
148+
# TODO(bwplotka): Capture correct Go version.
149+
# TODO(bwplotka): api.text is useful, document how to obtain it.
150+
pushd "${SCRIPT_DIR}/vulnupdatelist/"
151+
go run "./..." \
152+
-go-version=1.23.4 \
153+
-only-fixed \
154+
-dir="${dir}" \
155+
-nvd-api-key="$(cat "./api.text")" | tee "${dir}/vulnlist.txt"
156+
if [[ -z $(cat "${dir}/vulnlist.txt") ]]; then
157+
# Print this, otherwise error on the above might keep this file mistakenly empty.
158+
echo "no vulnerabilities" > "${dir}/vu
159+
}lnlist.txt"
160+
fi
161+
popd
162+
}
163+
164+
release-lib::gomod_vulnfix() {
165+
local dir=$1
166+
if [[ -z "${dir}" ]]; then
167+
echo "❌ dir is required." >&2
168+
return 1
169+
fi
170+
171+
local vuln_file="${dir}/vulnlist.txt"
172+
if [[ ! -f "${vuln_file}" ]]; then
173+
echo "❌ no ${vuln_file} file found" >&2
174+
return 1
175+
fi
176+
177+
if [[ "no vulnerabilities" == $(cat "${vuln_file}") ]]; then
178+
echo "${vuln_file} shows no vulnerabilities" >&2
179+
return 1
180+
fi
181+
182+
# Read the vulnerability file line by line.
183+
# The `|| [[ -n "$line" ]]` part handles the case where the last line doesn't have a newline.
184+
while IFS= read -r line || [[ -n "$line" ]]; do
185+
# Skip any empty lines in the input file.
186+
if [ -z "$line" ]; then
187+
continue
188+
fi
189+
190+
mod=$(echo "$line" | awk '{print $2}')
191+
mod_path=$(echo "${mod}" | cut -d'@' -f1)
192+
desired_version=$(echo "${mod}" | cut -d'@' -f2)
193+
194+
if [[ -z "${mod_path}" ]] || [[ -z "${desired_version}" ]]; then
195+
echo "⚠️ Skipping malformed line: $line"
196+
continue
197+
fi
198+
199+
echo "🔄 Updating module '${mod_path}' to version '${desired_version}'..."
200+
gsed -i.bak "s|\( ${mod_path} \).*|\1${desired_version}|" "${dir}/go.mod"
201+
done < "${vuln_file}"
202+
echo "🔄 Resolving ${dir}/go.mod..."
203+
pushd "${dir}"
204+
go mod tidy
205+
popd
206+
rm "${dir}/go.mod.bak"
207+
}
208+
209+
release-lib::idemp::git_commit_amend_match() {
210+
# Anything staged?
211+
if ! git diff-index --quiet --cached HEAD; then
212+
release-lib::git_commit_amend_match "${1}"
213+
fi
214+
}
215+
216+
release-lib::git_commit_amend_match() {
217+
local message="${1}"
218+
if [[ -z "${message}" ]]; then
219+
echo "❌ message is required." >&2
220+
return 1
221+
fi
222+
if [[ "$(git log -1 --pretty=%s)" == "${message}" ]]; then
223+
git commit -s --amend -m "${message}"
224+
else
225+
git commit -sm "${message}"
226+
fi
227+
}

hack/release-vulnfix.sh

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#!/usr/bin/env bash
2+
# Copyright 2025 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
set -o errexit
17+
set -o pipefail
18+
set -o nounset
19+
20+
if [[ -n "${DEBUG_MODE:-}" ]]; then
21+
set -o xtrace
22+
fi
23+
24+
# TODO(bwplotka): Clean err on missing deps e.g. gsed.
25+
# TODO(bwplotka): Consider automation for npm and docker images (Go, debian, similar to bump-go.sh)
26+
27+
SCRIPT_DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
28+
source "${SCRIPT_DIR}/release-lib.sh"
29+
30+
usage() {
31+
local me
32+
me=$(basename "${BASH_SOURCE[0]}")
33+
cat <<_EOM
34+
usage: ${me}
35+
36+
Attempt a minimal dependency upgrade to solve fixable vulnerabilities.
37+
38+
NOTE: The script is idempotent; to force it to recreate local artifacts (e.g. local clones, remote branches it created), remove the artifact you want to recreate.
39+
40+
Example use:
41+
* BRANCH=release/0.15 CHECKOUT_DIR=~/Repos/tmp-release bash hack/release-vulnfix.sh
42+
* BRANCH=release-2.45.3-gmp CHECKOUT_DIR=~/Repos/tmp-release bash hack/release-vulnfix.sh
43+
* BRANCH=release-0.27.0-gmp CHECKOUT_DIR=~/Repos/tmp-release bash hack/release-vulnfix.sh
44+
45+
Variables:
46+
* BRANCH (required) - Release branch to work on; Project is auto-detected from this.
47+
* CHECKOUT_DIR (required) - Local working directory e.g. for local clones.
48+
* PR_BRANCH (default: $USER/$BRANCH-vulnfix) - Upstream branch to push to.
49+
_EOM
50+
}
51+
52+
if (( $# > 0 )); then
53+
case $1 in
54+
help)
55+
usage
56+
;;
57+
esac
58+
fi
59+
60+
# Check if the BRANCH environment variable is set.
61+
if [[ -z "${BRANCH}" ]]; then
62+
echo "❌ BRANCH environment variable is not set." >&2
63+
usage
64+
return 1
65+
fi
66+
67+
REMOTE_URL=$(release-lib::remote_url_from_branch "${BRANCH}")
68+
PROJECT=$(tmp=${REMOTE_URL##*/}; echo ${tmp%.git})
69+
PR_BRANCH=${PR_BRANCH:-"${USER}/${BRANCH}-vulnfix"}
70+
71+
echo "🔄 Assuming ${PROJECT} with remote ${REMOTE_URL}; changes will be pushed to ${PR_BRANCH}"
72+
73+
# Check if the BRANCH environment variable is set.
74+
if [[ -z "${CHECKOUT_DIR}" ]]; then
75+
echo "❌ CHECKOUT_DIR environment variable is not set." >&2
76+
usage
77+
return 1
78+
fi
79+
80+
DIR="${CHECKOUT_DIR}/${PROJECT}"
81+
release-lib::idemp::clone "${DIR}"
82+
83+
pushd "${DIR}"
84+
85+
# TODO: Make every command idempotent inside each function?
86+
# TODO: Make it multi-module aware?
87+
release-lib::idemp::vulnlist "${DIR}"
88+
89+
if [[ "no vulnerabilities" != $(cat "${DIR}/vulnlist.txt") ]]; then
90+
# Attempt to update + go mod tidy.
91+
release-lib::gomod_vulnfix "${DIR}"
92+
git add go.mod go.sum
93+
94+
# Check if that helped.
95+
echo "⚠️ This will fail on older branches with vendoring; in this case, simply go to ${DIR}, run 'go mod vendor' and rerun."
96+
release-lib::vulnlist "${DIR}"
97+
if [[ "no vulnerabilities" != $(cat "${DIR}/vulnlist.txt") ]]; then
98+
echo "❌ After go mod update some vulnerabilities are still found; go to ${DIR} and resolve it manually and remove the ./vulnlist.txt file and rerun." >&2
99+
exit 1
100+
fi
101+
fi
102+
103+
# Commit if anything is staged.
104+
msg="google patch[deps]: fix Go ${BRANCH} vulnerabilities"
105+
if [[ "${PROJECT}" == "prometheus-engine" ]]; then
106+
msg="fix: fix ${BRANCH} vulnerabilities"
107+
fi
108+
release-lib::idemp::git_commit_amend_match "${msg}"
109+
110+
# Anything to push?
111+
if [[ "$(git rev-parse HEAD)" != "$(git fetch && git rev-parse "origin/${PR_BRANCH}")" ]]; then
112+
# TODO: Potentially use ghclient for PR ops
113+
if release-lib::confirm "About to FORCE git push from ${DIR} to origin/${PR_BRANCH}; are you sure?"; then
114+
# TODO: Consider using leasing to check for stales etc
115+
git push --force origin "${PR_BRANCH}"
116+
fi
117+
else
118+
echo "⚠️ Nothing to do; no vulnerabilities and nothing to commit"
119+
exit 1
120+
fi

hack/vulnupdatelist/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ type UpdateList struct {
4848
func (u UpdateList) String() string {
4949
fixedVersion := "???"
5050
if u.FixedVersion != nil {
51-
fixedVersion = u.FixedVersion.String()
51+
fixedVersion = "v" + u.FixedVersion.String()
5252
}
5353
if u.AdditionalCVEs > 0 {
5454
return fmt.Sprintf("%s %s@%s %s(+%d more) now@%s", u.CVE.Severity, u.Module, fixedVersion, u.CVE.ID, u.AdditionalCVEs, u.Version)

0 commit comments

Comments
 (0)