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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "go.mod", "date": "2026-10-21", "symptom": "After the v1 BSON driver removal (ako/mxcli#1497), go.mod no longer named github.com/klauspost/compress and `go list -m github.com/klauspost/compress` reported v1.19.2, the version a customer's Sonatype scan had blocked on for GHSA-fpmh-frq8-7rj8. No build, test or CI job failed", "cause": "The v1.20.3 floor from ako/mxcli#1493 was a hand-written `// indirect` require line for a module no mxcli package imports (it arrives through mongo-driver/v2, used only for its BSON packages). `go mod tidy` removes any requirement that no package in the module's import graph needs, so the first tidy after it, the one #1497 ran to drop v1, deleted the line and MVS fell back to the v1.19.2 mongo-driver/v2@v2.9.2 declares", "file": "`internal/depfloor/floor.go` (new, `//go:build depfloor`, blank-imports github.com/klauspost/compress/zstd); `internal/depfloor/floor_test.go` (new, TestGoModKeepsSecurityFloors); `go.mod`/`go.sum` (klauspost/compress v1.20.3, now direct)", "insight": "A security floor on a module the program never imports is not a go.mod edit, it is an import: tidy treats every build tag except `ignore` as enabled, so a blank import behind a tag no build sets keeps the requirement through tidy while compiling nothing (`go version -m bin/mxcli` still lists no klauspost/compress). Measured both ways: a hand-added require line is gone after one `go mod tidy`; with floor.go deleted tidy drops the line and the test fails 'go.mod does not require'; with floor.go present but no version pinned tidy re-adds it at the graph's v1.19.2 and the test fails 'below the floor'. The guard reads go.mod, which is what the scanner's graph is built from, and a direct requirement at or above the floor guarantees MVS selects at least that version", "refs": ["GHSA-fpmh-frq8-7rj8", "ako/mxcli#1493", "ako/mxcli#1497"]}
8 changes: 4 additions & 4 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:

- uses: actions/setup-go@v7
with:
go-version: '1.27.2'
go-version: '1.27.3'

- name: Cache ANTLR4 JAR
uses: actions/cache@v6
Expand Down Expand Up @@ -89,7 +89,7 @@ jobs:

- uses: actions/setup-go@v7
with:
go-version: '1.27.2'
go-version: '1.27.3'

# mxbuild 9 draws through System.Drawing; without libgdiplus every mx call
# dies with "The type initializer for 'Gdip' threw an exception."
Expand Down Expand Up @@ -144,7 +144,7 @@ jobs:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: '1.27.2'
go-version: '1.27.3'
- name: Cache ANTLR4 JAR
uses: actions/cache@v6
with:
Expand Down Expand Up @@ -175,7 +175,7 @@ jobs:

- uses: actions/setup-go@v7
with:
go-version: '1.27.2'
go-version: '1.27.3'

- uses: oven-sh/setup-bun@v2

Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/push-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: '1.27.2'
go-version: '1.27.3'
- name: Test the tunnel seam
shell: bash
# Scoped with -run to the seam's own tests. The full test binaries are still
Expand Down Expand Up @@ -78,7 +78,7 @@ jobs:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: '1.27.2'
go-version: '1.27.3'
- name: Test the Windows process helpers
shell: bash
# Scoped with -run: the rest of the docker package's tests assert POSIX
Expand Down Expand Up @@ -122,7 +122,7 @@ jobs:
submodules: true
- uses: actions/setup-go@v7
with:
go-version: '1.27.2'
go-version: '1.27.3'
- name: Cache ANTLR4 JAR
uses: actions/cache@v6
with:
Expand Down Expand Up @@ -256,7 +256,7 @@ jobs:
submodules: ${{ matrix.suite == 'roundtrip' || matrix.suite == 'parity' }}
- uses: actions/setup-go@v7
with:
go-version: '1.27.2'
go-version: '1.27.3'
- name: Cache ANTLR4 JAR
uses: actions/cache@v6
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
fetch-depth: 0
- uses: actions/setup-go@v7
with:
go-version: '1.27.2'
go-version: '1.27.3'
- uses: oven-sh/setup-bun@v2
- name: Cache ANTLR4 JAR
uses: actions/cache@v6
Expand Down
22 changes: 6 additions & 16 deletions CHANGELOG.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module github.com/mendixlabs/mxcli

go 1.26.0

toolchain go1.27.2
toolchain go1.27.3

require (
github.com/alecthomas/chroma/v2 v2.26.1
Expand All @@ -14,6 +14,7 @@ require (
github.com/fsnotify/fsnotify v1.10.1
github.com/jackc/pgx/v5 v5.9.2
github.com/jpillora/chisel v1.11.8
github.com/klauspost/compress v1.20.3
github.com/mattn/go-runewidth v0.0.24
github.com/microsoft/go-mssqldb v1.11.2
github.com/pmezard/go-difflib v1.0.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ github.com/jpillora/requestlog v1.0.0 h1:bg++eJ74T7DYL3DlIpiwknrtfdUA9oP/M4fL+Pp
github.com/jpillora/requestlog v1.0.0/go.mod h1:HTWQb7QfDc2jtHnWe2XEIEeJB7gJPnVdpNn52HXPvy8=
github.com/jpillora/sizestr v1.0.0 h1:4tr0FLxs1Mtq3TnsLDV+GYUWG7Q26a6s+tV5Zfw2ygw=
github.com/jpillora/sizestr v1.0.0/go.mod h1:bUhLv4ctkknatr6gR42qPxirmd5+ds1u7mzD+MZ33f0=
github.com/klauspost/compress v1.20.3 h1:zWNoFqq/lSuYQRim3dUXlXeA0IqwuZGicdG5ZTfY5j4=
github.com/klauspost/compress v1.20.3/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
Expand Down
25 changes: 25 additions & 0 deletions internal/depfloor/floor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: Apache-2.0

//go:build depfloor

// Package depfloor keeps security floors in go.mod for modules mxcli requires
// but never compiles.
//
// A customer's dependency scan reads the module graph, so a vulnerable version
// there blocks a release even when no mxcli build contains the code. Raising
// such a module needs a require line in go.mod, but `go mod tidy` removes a
// requirement no package imports. Tidy considers every build tag, so the
// imports below keep each requirement alive. Nothing sets the depfloor tag,
// so no build compiles this file and the binary is unchanged.
//
// TestGoModKeepsSecurityFloors checks the versions. To raise a floor, run
// `go get <module>@<version>` and then `go mod tidy`. To drop one, remove its
// import here and its row in the test.
package depfloor

import (
// GHSA-fpmh-frq8-7rj8: zstd DecodeAll crash on crafted input, fixed in
// v1.20.2. Arrives through go.mongodb.org/mongo-driver/v2, which declares
// v1.19.2 and which mxcli uses only for its BSON packages.
_ "github.com/klauspost/compress/zstd"
)
98 changes: 98 additions & 0 deletions internal/depfloor/floor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// SPDX-License-Identifier: Apache-2.0

package depfloor_test

import (
"os"
"path/filepath"
"strconv"
"strings"
"testing"
)

// floors are the lowest versions go.mod may require. Each one is a version a
// customer's dependency scan blocked a release on. The scan reads the module
// graph, not the binary, so a module counts even when no mxcli build compiles
// it.
var floors = []struct {
module, min, why string
}{
{"github.com/klauspost/compress", "v1.20.2", "GHSA-fpmh-frq8-7rj8 (zstd DecodeAll crash), fixed in v1.20.2"},
{"go.mongodb.org/mongo-driver/v2", "v2.9.2", "CVE-2026-81521, fixed in v2.8.2, regressed in v2.9.0 and v2.9.1, restored in v2.9.2"},
}

// TestGoModKeepsSecurityFloors fails if go.mod no longer requires a floored
// module at or above its floor.
//
// klauspost/compress is the case this exists for. mxcli imports none of its
// packages; it arrives through mongo-driver/v2, which declares v1.19.2. A
// hand-written require line is removed by the next `go mod tidy`, which is how
// the v1.20.3 floor from ako/mxcli#1493 was lost in ako/mxcli#1497 without any
// build or test noticing. floor.go keeps the requirement alive across tidy.
func TestGoModKeepsSecurityFloors(t *testing.T) {
b, err := os.ReadFile(filepath.Join("..", "..", "go.mod"))
if err != nil {
t.Fatalf("read go.mod: %v", err)
}
required := map[string]string{}
for _, line := range strings.Split(string(b), "\n") {
f := strings.Fields(strings.TrimPrefix(strings.TrimSpace(line), "require "))
if len(f) >= 2 && strings.HasPrefix(f[1], "v") {
required[f[0]] = f[1]
}
}
for _, fl := range floors {
got, ok := required[fl.module]
switch {
case !ok:
t.Errorf("go.mod does not require %s, so the module graph falls back to whatever a dependency declares.\n"+
"Floor %s: %s.\n"+
"Keep its import in internal/depfloor/floor.go and run `go mod tidy`; a hand-written require line does not survive tidy.",
fl.module, fl.min, fl.why)
case less(got, fl.min):
t.Errorf("go.mod requires %s %s, below the floor %s: %s", fl.module, got, fl.min, fl.why)
}
}
}

// less reports whether semantic version a sorts before b. It compares only
// vMAJOR.MINOR.PATCH, which is all the floors use; a pre-release suffix sorts
// as its release.
func less(a, b string) bool {
pa, pb := parts(a), parts(b)
for i := range pa {
if pa[i] != pb[i] {
return pa[i] < pb[i]
}
}
return false
}

func parts(v string) [3]int {
var p [3]int
v = strings.TrimPrefix(v, "v")
if i := strings.IndexAny(v, "-+"); i >= 0 {
v = v[:i]
}
for i, s := range strings.SplitN(v, ".", 3) {
p[i], _ = strconv.Atoi(s)
}
return p
}

func TestLess(t *testing.T) {
for _, c := range []struct {
a, b string
want bool
}{
{"v1.19.2", "v1.20.2", true},
{"v1.20.2", "v1.20.2", false},
{"v1.20.3", "v1.20.2", false},
{"v1.9.0", "v1.10.0", true},
{"v2.9.2-rc1", "v2.9.2", false},
} {
if got := less(c.a, c.b); got != c.want {
t.Errorf("less(%s, %s) = %v, want %v", c.a, c.b, got, c.want)
}
}
}
Loading