Skip to content
Open
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
92 changes: 92 additions & 0 deletions .github/workflows/bump-vuln-deps.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: Bump vulnerable dependencies

on:
schedule:
# Run daily at 05:30 UTC, just after the Go toolchain bumper.
- cron: "30 5 * * *"
workflow_dispatch:

permissions:
contents: write
pull-requests: write

jobs:
bump-vuln-deps:
runs-on:
group: databricks-protected-runner-group-large
labels: linux-ubuntu-latest-large

steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
# vulnbump lives in the tools module, which is what this job compiles.
go-version-file: tools/go.mod

- name: Build vulnbump
run: go -C tools/vulnbump build -o "$RUNNER_TEMP/vulnbump" .

- name: Bump vulnerable dependencies
id: bump
run: |
set -euo pipefail

# govulncheck is pinned as a tool dependency in tools/go.mod; -modfile
# resolves it from there while it scans the root module (the working
# directory). Only the root module ships; tools/ and
# bundle/internal/tf/codegen are build- and CI-only, so they are not
# scanned. Its vulnerability database is fetched from vuln.go.dev at
# runtime, so the pinned binary still uses the latest advisories.
#
# -scan module reports every advisory affecting a required module,
# regardless of whether the vulnerable symbol is reachable. In JSON
# mode govulncheck exits 0 on success whether or not it finds anything,
# and non-zero only on a real error; a failure must abort the job
# rather than be silently mistaken for "no vulnerabilities".
scan="$(mktemp)"
if ! go tool -modfile=tools/go.mod govulncheck -scan module -format json > "$scan"; then
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: would not this be enough? given you already have set -e

go tool -modfile=tools/go.mod govulncheck -scan module -format json > "$scan"

echo "::error::govulncheck failed"
exit 1
fi

summary_file="$(mktemp)"
"$RUNNER_TEMP/vulnbump" . < "$scan" > "$summary_file"

if git diff --quiet; then
echo "No vulnerable dependencies to bump."
echo "needed=false" >> "$GITHUB_OUTPUT"
else
echo "needed=true" >> "$GITHUB_OUTPUT"
{
echo "summary<<SUMMARY_EOF"
cat "$summary_file"
echo "SUMMARY_EOF"
} >> "$GITHUB_OUTPUT"
fi

- name: Show diff
if: steps.bump.outputs.needed == 'true'
run: git diff

- name: Create pull request
if: steps.bump.outputs.needed == 'true'
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
branch: auto/bump-vuln-deps
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need branch-suffix as well?

The branch suffix type when using the alternative branching strategy. Valid values are random, timestamp and short-commit-hash. See Alternative strategy for details.

commit-message: "Bump dependencies with known vulnerabilities"
title: "Bump dependencies with known vulnerabilities"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we could also write small file with list of upgraded deps and pass it to title

body: |
Bump dependencies flagged by `govulncheck -scan module` to their fixed versions.

Each CVE links to its Go advisory page.

${{ steps.bump.outputs.summary }}

Vulnerabilities in the Go standard library are left to the `Bump Go toolchain` workflow.

If a bump promotes a new direct dependency, double-check its license annotation in `go.mod` and `NOTICE`.
reviewers: simonfaltum,andrewnester,anton-107,denik,janniklasrose,pietern,shreyas-goenka
labels: dependencies
10 changes: 7 additions & 3 deletions tools/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ toolchain go1.26.3

require github.com/stretchr/testify v1.11.1

require gopkg.in/yaml.v3 v3.0.1
require (
golang.org/x/mod v0.35.0
gopkg.in/yaml.v3 v3.0.1
)

require (
4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
Expand Down Expand Up @@ -214,13 +217,13 @@ require (
go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa // indirect
golang.org/x/telemetry v0.0.0-20260421165255-392afab6f40e // indirect
golang.org/x/term v0.39.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/tools v0.44.0 // indirect
golang.org/x/vuln v1.3.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
Expand All @@ -234,5 +237,6 @@ tool (
github.com/golangci/golangci-lint/v2/cmd/golangci-lint
github.com/google/yamlfmt/cmd/yamlfmt
golang.org/x/tools/cmd/deadcode
golang.org/x/vuln/cmd/govulncheck
gotest.tools/gotestsum
)
9 changes: 7 additions & 2 deletions tools/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,8 @@ github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqt
github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e/go.mod h1:h+wZwLjUTJnm/P2rwlbJdRPZXOzaT36/FwnPnY2inzc=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786 h1:rcv+Ippz6RAtvaGgKxc+8FQIpxHgsF+HBzPyYL2cyVU=
github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
Expand All @@ -324,6 +326,7 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
Expand Down Expand Up @@ -802,8 +805,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4=
golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE=
golang.org/x/telemetry v0.0.0-20260421165255-392afab6f40e h1:OXgN37M6hqjaAvb7CJK9vJ+7Z/6lvIm5bXho5poo/Wk=
golang.org/x/telemetry v0.0.0-20260421165255-392afab6f40e/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
Expand Down Expand Up @@ -875,6 +878,8 @@ golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnps
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
golang.org/x/vuln v1.3.0 h1:hZYzR8uRhYhDSX88d+40TWbKAVw7BIvRWm26rtEn8jw=
golang.org/x/vuln v1.3.0/go.mod h1:MIY2PaR1y52stzZM3uHBboUAdVJvSVMl5nP3OQrwQaE=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
1 change: 1 addition & 0 deletions tools/vulnbump/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/vulnbump
171 changes: 171 additions & 0 deletions tools/vulnbump/bumps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package main

import (
"encoding/json"
"errors"
"fmt"
"io"
"maps"
"slices"
"strings"

"golang.org/x/mod/semver"
)

// stdlibModule is govulncheck's module name for the Go standard library.
// Standard-library fixes map to a toolchain version and are handled by the
// separate "Bump Go toolchain" workflow, so we skip them here.
const stdlibModule = "stdlib"

// advisoryBaseURL is the canonical advisory page for a GO-YYYY-NNNN ID. The
// page cross-links the CVE alias and the per-package fixed version.
const advisoryBaseURL = "https://pkg.go.dev/vuln/"

// advisory identifies a Go vulnerability and its CVE alias.
type advisory struct {
ID string // GO-YYYY-NNNN, always present.
CVE string // CVE-YYYY-NNNN, empty until one is assigned (it can lag the Go advisory by days).
}

// label is the link text for an advisory: the CVE when known, else the Go ID.
func (a advisory) label() string {
if a.CVE != "" {
return a.CVE
}
return a.ID
}

// bump is a single dependency upgrade: the highest fixed version across all
// advisories affecting a module, plus the advisories it resolves.
type bump struct {
Module string
FixedVersion string
Advisories []advisory
}

// govulncheckFinding mirrors the "finding" message emitted by
// `govulncheck -format json`. See https://pkg.go.dev/golang.org/x/vuln/internal/govulncheck#Finding.
type govulncheckFinding struct {
OSV string `json:"osv"`
FixedVersion string `json:"fixed_version"`
Trace []struct {
Module string `json:"module"`
} `json:"trace"`
}

// govulncheckOSV mirrors the "osv" message emitted by `govulncheck -format json`,
// which carries the full advisory record including its CVE aliases.
type govulncheckOSV struct {
ID string `json:"id"`
Aliases []string `json:"aliases"`
}

// parseBumps reads the concatenated JSON stream from `govulncheck -format json`
// and reduces it to one bump per module, choosing the highest fixed version and
// collecting the advisories it resolves. The standard library is excluded.
func parseBumps(r io.Reader) ([]bump, error) {
// One accumulator per module: the highest fixed version seen and the set of
// advisories resolved by upgrading to it.
type acc struct {
version string
advisories map[string]struct{}
}
byModule := map[string]*acc{}
// govulncheck emits a separate "osv" message per advisory; collect the CVE
// aliases so findings (which carry only the Go ID) can be labelled with it.
cveByID := map[string]string{}

dec := json.NewDecoder(r)
for {
var msg struct {
Finding *govulncheckFinding `json:"finding"`
OSV *govulncheckOSV `json:"osv"`
}
err := dec.Decode(&msg)
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, fmt.Errorf("decode govulncheck output: %w", err)
}

if o := msg.OSV; o != nil {
cveByID[o.ID] = firstCVE(o.Aliases)
}

f := msg.Finding
if f == nil || f.FixedVersion == "" || len(f.Trace) == 0 {
continue
}
// trace[0] is the most specific frame; in module scan mode it carries
// the vulnerable module itself.
module := f.Trace[0].Module
if module == "" || module == stdlibModule {
continue
}

a := byModule[module]
if a == nil {
a = &acc{advisories: map[string]struct{}{}}
byModule[module] = a
}
if a.version == "" || semver.Compare(f.FixedVersion, a.version) > 0 {
a.version = f.FixedVersion
}
a.advisories[f.OSV] = struct{}{}
}

bumps := make([]bump, 0, len(byModule))
for module, a := range byModule {
var advisories []advisory
for _, id := range slices.Sorted(maps.Keys(a.advisories)) {
advisories = append(advisories, advisory{ID: id, CVE: cveByID[id]})
}
bumps = append(bumps, bump{
Module: module,
FixedVersion: a.version,
Advisories: advisories,
})
}
slices.SortFunc(bumps, func(a, b bump) int {
return strings.Compare(a.Module, b.Module)
})
return bumps, nil
}

// firstCVE returns the first CVE alias in the list, or "" if there is none.
func firstCVE(aliases []string) string {
for _, a := range aliases {
if strings.HasPrefix(a, "CVE-") {
return a
}
}
return ""
}

// renderSummary formats the bumps as Markdown list items for the PR body. It
// uses reference-style links so the list lines stay short instead of wrapping,
// with the advisory link definitions collected in a block at the end.
func renderSummary(bumps []bump) string {
var list, refs strings.Builder
seen := map[string]struct{}{}

for _, bump := range bumps {
labels := make([]string, len(bump.Advisories))
for i, adv := range bump.Advisories {
labels[i] = "[" + adv.label() + "]"
if _, ok := seen[adv.label()]; ok {
continue
}
seen[adv.label()] = struct{}{}
fmt.Fprintf(&refs, "[%s]: %s%s\n", adv.label(), advisoryBaseURL, adv.ID)
}
fmt.Fprintf(&list, "- %s → %s (fixes %s)\n",
bump.Module, bump.FixedVersion, strings.Join(labels, ", "))
}

if refs.Len() == 0 {
return list.String()
}
return list.String() + "\n" + refs.String()
}
Loading
Loading