diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80c446a..53a0a3b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,6 +126,8 @@ jobs: timeout-minutes: 30 permissions: contents: read + env: + GOVULNCHECK_VERSION: v1.6.0 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -135,8 +137,10 @@ jobs: with: go-version-file: go.mod cache: false - - name: Check reachable Go vulnerabilities - run: go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./... + - name: Check reachable Go vulnerabilities in the source + run: go run "golang.org/x/vuln/cmd/govulncheck@${GOVULNCHECK_VERSION}" ./... + - name: Test that the symbol-table guard rejects a stripped binary + run: scripts/assert-go-symbols-test.sh - name: Build GoReleaser snapshot artifacts uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2 with: @@ -145,6 +149,8 @@ jobs: args: release --snapshot --clean --skip=publish - name: Verify and prepare all release artifacts run: scripts/prepare-artifact-scan-rootfs.sh + - name: Check reachable Go vulnerabilities in each release binary + run: scripts/govulncheck-release-binaries.sh - name: Import release artifacts for scanning run: docker import artifact-scan/rootfs.tar stackstate-backup-cli:security-scan - name: Scan release artifacts with Trivy, Grype, and VEX diff --git a/.goreleaser.yml b/.goreleaser.yml index f8830c6..663b54c 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -17,7 +17,7 @@ builds: - windows_amd64 main: . ldflags: - - -s -w -X github.com/stackvista/stackstate-backup-cli/cmd/version.Version={{.Version}} + - -w -X github.com/stackvista/stackstate-backup-cli/cmd/version.Version={{.Version}} -X github.com/stackvista/stackstate-backup-cli/cmd/version.Commit={{.Commit}} -X github.com/stackvista/stackstate-backup-cli/cmd/version.Date={{.Date}} binary: sts-backup diff --git a/go.mod b/go.mod index 6fe9568..10543f2 100644 --- a/go.mod +++ b/go.mod @@ -80,12 +80,12 @@ require ( go.opentelemetry.io/otel/trace v1.44.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.53.0 // indirect - golang.org/x/net v0.56.0 // indirect + golang.org/x/crypto v0.55.0 // indirect + golang.org/x/net v0.57.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/term v0.44.0 // indirect - golang.org/x/text v0.39.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/term v0.45.0 // indirect + golang.org/x/text v0.41.0 // indirect golang.org/x/time v0.9.0 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect diff --git a/go.sum b/go.sum index 7461c93..dade845 100644 --- a/go.sum +++ b/go.sum @@ -208,8 +208,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= -golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -217,8 +217,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -231,25 +231,25 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= -golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= -golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= -golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= 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= diff --git a/scripts/assert-go-symbols-test.sh b/scripts/assert-go-symbols-test.sh new file mode 100755 index 0000000..ea5d26c --- /dev/null +++ b/scripts/assert-go-symbols-test.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +# Negative control for assert-go-symbols.sh: proves the guard rejects a stripped +# binary for the absence of symbols alone, on a fixture no advisory matches, so +# the result cannot come from the vulnerability database. +# +# Every target in .goreleaser.yml is covered because the two platform families +# fail differently: stripping leaves an ELF or PE file with no symbol section at +# all, while a stripped Mach-O still yields a readable table that `runtime.main` +# is missing from. A guard that only checked whether `go tool nm` succeeded would +# pass both darwin artifacts. + +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +guard="${script_dir}/assert-go-symbols.sh" +fixture_dir="${script_dir}/testdata/assessable-fixture" +targets=(darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 windows/amd64) + +work="$(mktemp -d)" +trap 'rm -rf "${work}"' EXIT + +export CGO_ENABLED=0 +failures=0 + +for target in "${targets[@]}"; do + goos="${target%/*}" + goarch="${target#*/}" + suffix="" + [[ "${goos}" == "windows" ]] && suffix=".exe" + + kept="${work}/${goos}-${goarch}-kept${suffix}" + stripped="${work}/${goos}-${goarch}-stripped${suffix}" + (cd "${fixture_dir}" && GOOS="${goos}" GOARCH="${goarch}" go build -ldflags '-w' -o "${kept}" main.go) + (cd "${fixture_dir}" && GOOS="${goos}" GOARCH="${goarch}" go build -ldflags '-s -w' -o "${stripped}" main.go) + + if "${guard}" "${kept}" >/dev/null; then + echo "PASS ${target}: built with -w only, accepted" + else + echo "FAIL ${target}: built with -w only, should have been accepted" >&2 + failures=1 + fi + + if "${guard}" "${stripped}" >/dev/null 2>&1; then + echo "FAIL ${target}: built with -s -w, should have been rejected" >&2 + failures=1 + else + echo "PASS ${target}: built with -s -w, rejected" + fi +done + +exit "${failures}" diff --git a/scripts/assert-go-symbols.sh b/scripts/assert-go-symbols.sh new file mode 100755 index 0000000..952fa95 --- /dev/null +++ b/scripts/assert-go-symbols.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# Binary-mode govulncheck can only tell which packages a binary links while its +# Go symbol table is present; without one it conservatively assumes every package +# named in an advisory is linked. A stripped binary therefore still exits +# non-zero today only because an advisory happens to match one of its modules, +# which is a property of the current vulnerability database rather than of the +# artifact — so assessability is asserted here instead of inferred from +# govulncheck's exit status. + +set -euo pipefail + +# Linked into every Go binary regardless of platform or build tags, so its +# absence means the symbol table was stripped rather than that the program +# differs. +required_symbol='runtime.main' + +if [[ "$#" -eq 0 ]]; then + echo "usage: $0 ..." >&2 + exit 2 +fi + +status=0 +for binary in "$@"; do + if ! symbols="$(go tool nm "${binary}" 2>&1)"; then + echo "FAIL ${binary}: no readable Go symbol table -- ${symbols%%$'\n'*}" >&2 + status=1 + continue + fi + if ! grep -q "[[:space:]]${required_symbol}\$" <<<"${symbols}"; then + echo "FAIL ${binary}: symbol table has no ${required_symbol}" >&2 + status=1 + continue + fi + echo "OK ${binary}: Go symbol table is usable" +done + +exit "${status}" diff --git a/scripts/govulncheck-release-binaries.sh b/scripts/govulncheck-release-binaries.sh new file mode 100755 index 0000000..b459ca8 --- /dev/null +++ b/scripts/govulncheck-release-binaries.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +# Binary-mode govulncheck can only judge which packages are linked while the +# release binaries keep their symbol table, so stripping it again (`-s` in the +# GoReleaser ldflags) makes this fail instead of publishing artifacts whose +# reachability nobody can assess. The symbol table is asserted separately rather +# than inferred from govulncheck's exit status, which would only catch a stripped +# binary while some advisory still matched one of its modules. + +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +rootfs_dir="${1:-artifact-scan/rootfs}" +govulncheck_version="${GOVULNCHECK_VERSION:-v1.6.0}" +expected_binaries=5 +binaries=() + +while IFS= read -r binary; do + binaries+=("${binary}") +done < <(find "${rootfs_dir}" -type f \( -name sts-backup -o -name sts-backup.exe \) -print | sort) + +if [[ "${#binaries[@]}" -ne "${expected_binaries}" ]]; then + echo "Expected ${expected_binaries} release binaries under '${rootfs_dir}', found ${#binaries[@]}" >&2 + exit 1 +fi + +"${script_dir}/assert-go-symbols.sh" "${binaries[@]}" + +status=0 +for binary in "${binaries[@]}"; do + echo "== ${binary}" + if ! go run "golang.org/x/vuln/cmd/govulncheck@${govulncheck_version}" -mode=binary "${binary}"; then + status=1 + fi +done + +exit "${status}" diff --git a/scripts/testdata/assessable-fixture/main.go b/scripts/testdata/assessable-fixture/main.go new file mode 100644 index 0000000..b910ddc --- /dev/null +++ b/scripts/testdata/assessable-fixture/main.go @@ -0,0 +1,5 @@ +// Command assessable-fixture is the smallest Go binary the assessability guard +// can be tested against, so the test does not pay for a full CLI cross-build. +package main + +func main() {}