Skip to content
Merged
Show file tree
Hide file tree
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
7 changes: 7 additions & 0 deletions ops/gmpctl/.gmpctl.default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,10 @@
# limitations under the License.

dir: "./data"

# Go modules to ignore during dependency upgrades (e.g., vulnfix).
vuln_ignored_modules:
# Until unfork, upgrading prometheus is non-trivial and its vulnerabilities
# are typically not related to us. Skip it in vulnerability fixes, and produce warning.
- "github.com/prometheus/prometheus"

18 changes: 17 additions & 1 deletion ops/gmpctl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,26 @@ It's a starting point for smaller or bigger automation on OSS side (e.g. releasi
* `gsed` (MacOS: `brew install gsed`)
* `gcloud` (https://docs.cloud.google.com/sdk/docs/install-sdk) (and `gcloud auth login`)

3. You can configure different work directory for gmpctl via `-c` flag. By default, `gmpctl` does the work in `ops/gmpctl/.data`
3. You can configure different work directory and settings for gmpctl via `-c` flag (default configuration file: `.gmpctl.default.yaml`).

Enjoy!

## Configuration

`gmpctl` reads configuration from a YAML file specified by the `-c` flag (defaults to `.gmpctl.default.yaml`).

Available options:

* `dir`: Base directory for gmpctl work, project clones, and git worktrees (default: `./data`).
* `vuln_ignored_modules`: List of Go module paths to ignore during module upgrades (e.g. `vulnfix`).
Example:

```yaml
dir: "./data"
vuln_ignored_modules:
- "github.com/prometheus/prometheus"
```

## Usage

Generally `gmpctl` does not need flags for general usage. It interactively asks you for
Expand Down
2 changes: 2 additions & 0 deletions ops/gmpctl/cmd_vulnfix.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"flag"
"fmt"
"os"
"strings"
)

var (
Expand Down Expand Up @@ -78,6 +79,7 @@ func vulnfix() error {
fmt.Sprintf("DIR=%v", dir),
fmt.Sprintf("BRANCH=%v", branch),
fmt.Sprintf("PROJECT=%v", proj.Name),
fmt.Sprintf("VULN_IGNORED_MODULES=%v", strings.Join(cfg.VulnIgnoredModules, ",")),
}
if *vulnfixSyncDockerfilesFrom {
opts = append(opts, "SYNC_DOCKERFILES_FROM=true")
Expand Down
4 changes: 2 additions & 2 deletions ops/gmpctl/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/text v0.40.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)

Expand Down
16 changes: 8 additions & 8 deletions ops/gmpctl/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions ops/gmpctl/lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ release-lib::vulnlist() {
go run "./..." \
-go-version="${go_version}" \
-only-fixed \
-dir="${dir}" | tee "${vuln_file}"
-dir="${dir}" \
-vuln-ignore-modules="${VULN_IGNORED_MODULES:-}" | tee "${vuln_file}"
if [[ -z $(cat "${vuln_file}") ]]; then
# Print this, otherwise error on the above might keep this file mistakenly empty.
echo "no vulnerabilities" >"${vuln_file}"
Expand Down Expand Up @@ -473,7 +474,6 @@ release-lib::manifests_regen() {

echo "🔄 Regenerating manifests..."
YQ="$(command -v yq)" HELM="$(command -v helm)" ADDLICENSE="$(command -v addlicense)" bash "${dir}/hack/presubmit.sh" manifests

if [[ -f "${dir}/.bingo/variables.env.bak" ]]; then
mv "${dir}/.bingo/variables.env.bak" "${dir}/.bingo/variables.env"
trap - EXIT
Expand Down
2 changes: 2 additions & 0 deletions ops/gmpctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ var (
type Config struct {
// Directory for the gmpctl work, notably for project clones and git worktrees.
Directory string `yaml:"dir"`
// VulnIgnoredModules contains Go module paths to ignore during upgrades (e.g. vulnfix).
VulnIgnoredModules []string `yaml:"vuln_ignored_modules"`
}

func loadConfig() (ret *Config, _ error) {
Expand Down
24 changes: 14 additions & 10 deletions ops/gmpctl/vulnfix.sh
Original file line number Diff line number Diff line change
Expand Up @@ -100,23 +100,27 @@ echo "*" >>"${DIR}/.gmpctl/.gitignore"
vuln_file="${DIR}/.gmpctl/vulnlist.txt"
pushd "${DIR}"

release-lib::idemp::vulnlist "${DIR}" "${vuln_file}"
max_attempts=5
for ((i=1; i<=max_attempts; i++)); do
release-lib::vulnlist "${DIR}" "${vuln_file}"
if [[ "no vulnerabilities" == $(cat "${vuln_file}") ]]; then
echo "✅ No more fixable Go vulnerabilities found!"
break
fi

if [[ "no vulnerabilities" != $(cat "${vuln_file}") ]]; then
# Attempt to update + go mod tidy.
echo "🔄 Iteration $i/$max_attempts: Applying dependency updates..."
release-lib::gomod_vulnfix "${DIR}" "${vuln_file}"
git add go.mod go.sum

if [ -d "${DIR}/vendor" ]; then
go mod vendor
git add --all
fi
done

# Check if that helped.
echo "⚠️ This will fail on older branches with vendoring; in this case, simply go to ${DIR}, run 'go mod vendor' and rerun."
if [[ "no vulnerabilities" != $(cat "${vuln_file}") ]]; then
release-lib::vulnlist "${DIR}" "${vuln_file}"
if [[ "no vulnerabilities" != $(cat "${vuln_file}") ]]; then
echo "❌ After go mod update some vulnerabilities are still found; go to ${DIR} and resolve it manually (select not reusing the ./vulnlist.txt file) and rerun."
exit 1
fi
fi
if [[ "no vulnerabilities" != $(cat "${vuln_file}") ]]; then
echo "❌ After go mod update some vulnerabilities are still found; go to ${DIR} and resolve it manually."
exit 1
fi
20 changes: 16 additions & 4 deletions ops/gmpctl/vulnupdatelist/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,16 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/Masterminds/semver/v3"
)

var (
goVersion = flag.String("go-version", "", "Go version to test vulnerabilities in (stdlib). Otherwise the `go env GOVERSION` is used")
dir = flag.String("dir", ".", "Where to run the script from")
onlyFixed = flag.Bool("only-fixed", false, "Don't print vulnerable modules without fixed version; note: fixed version often means sometimes that a new major version contains a fix.")
goVersion = flag.String("go-version", "", "Go version to test vulnerabilities in (stdlib). Otherwise the `go env GOVERSION` is used")
dir = flag.String("dir", ".", "Where to run the script from")
onlyFixed = flag.Bool("only-fixed", false, "Don't print vulnerable modules without fixed version; note: fixed version often means sometimes that a new major version contains a fix.")
vulnIgnoreModules = flag.String("vuln-ignore-modules", "", "Comma-separated list of Go module paths to ignore for upgrades due to known issues.")
)

// UpdateList presents the minimum version to upgrade to solve all CVEs with
Expand Down Expand Up @@ -89,8 +91,18 @@ func main() {
os.Exit(0)
}

ignoredModules := make(map[string]struct{})
if *vulnIgnoreModules != "" {
for _, m := range strings.Split(*vulnIgnoreModules, ",") {
m = strings.TrimSpace(m)
if m != "" {
ignoredModules[m] = struct{}{}
}
}
}

slog.Info("Parsing vulnerabilities and finding updates...")
updates, err := compileUpdateList(bytes.NewReader(vulnJSON), *onlyFixed)
updates, err := compileUpdateList(bytes.NewReader(vulnJSON), *onlyFixed, ignoredModules)
if err != nil {
log.Fatalf("Error parsing govulncheck output: %v", err)
}
Expand Down
34 changes: 20 additions & 14 deletions ops/gmpctl/vulnupdatelist/vuln.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ type FindingTrace struct {

// compileUpdateList decodes the JSON stream from govulncheck and extracts
// a list of modules that need to be updated to a fixed version.
func compileUpdateList(jsonData io.Reader, onlyFixed bool) ([]UpdateList, error) {
func compileUpdateList(jsonData io.Reader, onlyFixed bool, ignoredModules map[string]struct{}) ([]UpdateList, error) {
updates := make(map[string]UpdateList)
osvs := make(map[string]*OSV)
decoder := json.NewDecoder(jsonData)
Expand All @@ -109,10 +109,8 @@ func compileUpdateList(jsonData io.Reader, onlyFixed bool) ([]UpdateList, error)
// We assume OSVs are printed first.
osv := osvs[v.Finding.OSVID]
cveID := v.Finding.OSVID
allCVEs := v.Finding.OSVID
if osv != nil {
cveID = getCVEID(*osv)
allCVEs = osv.CVEs()
} else {
slog.Error("Malformed govulncheck input; a finding without an OSV entry.", "finding.osv", v.Finding.OSVID)
}
Expand All @@ -132,14 +130,8 @@ func compileUpdateList(jsonData io.Reader, onlyFixed bool) ([]UpdateList, error)
}
}

if onlyFixed && fixVersion == nil {
slog.Warn("IMPORTANT: Found Go vulnerability without a fixed version. Ignoring this module, given the -only-fixed flag...", "mod", module, "cve", cveID)
continue
}

up, ok := updates[module]
if !ok {
slog.Info("Found Go vulnerability with a fix; queuing...", "mod", module, "CVEs", allCVEs)
updates[module] = UpdateList{
CVEID: cveID,
Module: module,
Expand All @@ -149,8 +141,6 @@ func compileUpdateList(jsonData io.Reader, onlyFixed bool) ([]UpdateList, error)
continue
}

// Check if there are more CVE IDs corresponding to the vulnerability, which can give more context.
slog.Debug("Found Go vulnerability with a fix, the module was already queued; resolving version...", "mod", module, "CVEs", allCVEs)
up.AdditionalCVEs++
if fixVersion != nil {
if up.FixedVersion == nil || fixVersion.GreaterThan(up.FixedVersion) {
Expand All @@ -160,10 +150,26 @@ func compileUpdateList(jsonData io.Reader, onlyFixed bool) ([]UpdateList, error)
updates[module] = up
}

updateList := slices.Collect(maps.Values(updates))
sort.Slice(updateList, func(i, j int) bool {
return updateList[i].Module < updateList[j].Module
allUpdates := slices.Collect(maps.Values(updates))
sort.Slice(allUpdates, func(i, j int) bool {
return allUpdates[i].Module < allUpdates[j].Module
})

var updateList []UpdateList
for _, up := range allUpdates {
if _, ignored := ignoredModules[up.Module]; ignored {
slog.Warn("IMPORTANT: Found Go vulnerability in an ignored module; skipping upgrade...", "module", up.Module, "cve", up.CVEID)
continue
}

if onlyFixed && up.FixedVersion == nil {
slog.Warn("IMPORTANT: Found Go vulnerability without a fixed version. Ignoring this module, given the -only-fixed flag...", "mod", up.Module, "cve", up.CVEID)
continue
}

slog.Info("Found Go vulnerability with a fix; queuing...", "mod", up.Module, "fixedVersion", up.FixedVersion, "cve", up.CVEID)
updateList = append(updateList, up)
}
return updateList, nil
}

Expand Down
63 changes: 63 additions & 0 deletions ops/gmpctl/vulnupdatelist/vuln_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"strings"
"testing"

"github.com/stretchr/testify/require"
)

func TestCompileUpdateList_IgnoredModules(t *testing.T) {
mockJSON := `
{"osv":{"id":"GO-2024-0001","summary":"Test vuln 1"}}
{"finding":{"osv":"GO-2024-0001","fixed_version":"1.2.3","trace":[{"module":"github.com/prometheus/prometheus","version":"1.0.0"}]}}
{"osv":{"id":"GO-2024-0002","summary":"Test vuln 2"}}
{"finding":{"osv":"GO-2024-0002","fixed_version":"0.1.5","trace":[{"module":"golang.org/x/net","version":"0.1.0"}]}}
`

t.Run("without ignored modules", func(t *testing.T) {
updates, err := compileUpdateList(strings.NewReader(mockJSON), false, nil)
require.NoError(t, err)
require.Len(t, updates, 2)
Comment thread
bwplotka marked this conversation as resolved.

require.Equal(t, "github.com/prometheus/prometheus", updates[0].Module)
require.Equal(t, "GO-2024-0001", updates[0].CVEID)
require.Equal(t, "1.0.0", updates[0].Version)
require.NotNil(t, updates[0].FixedVersion)
require.Equal(t, "1.2.3", updates[0].FixedVersion.String())

require.Equal(t, "golang.org/x/net", updates[1].Module)
require.Equal(t, "GO-2024-0002", updates[1].CVEID)
require.Equal(t, "0.1.0", updates[1].Version)
require.NotNil(t, updates[1].FixedVersion)
require.Equal(t, "0.1.5", updates[1].FixedVersion.String())
})

t.Run("with ignored module", func(t *testing.T) {
ignored := map[string]struct{}{
"github.com/prometheus/prometheus": {},
}
updates, err := compileUpdateList(strings.NewReader(mockJSON), false, ignored)
require.NoError(t, err)
require.Len(t, updates, 1)
require.Equal(t, "golang.org/x/net", updates[0].Module)
require.Equal(t, "GO-2024-0002", updates[0].CVEID)
require.Equal(t, "0.1.0", updates[0].Version)
require.NotNil(t, updates[0].FixedVersion)
require.Equal(t, "0.1.5", updates[0].FixedVersion.String())
})
}
Loading