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
6 changes: 4 additions & 2 deletions cliv2/cmd/cliv2/instrumentation.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"
"time"

cli_utils "github.com/snyk/cli/cliv2/internal/utils"
"github.com/snyk/go-application-framework/pkg/analytics"
"github.com/snyk/go-application-framework/pkg/configuration"
"github.com/snyk/go-application-framework/pkg/instrumentation"
Expand Down Expand Up @@ -43,8 +44,9 @@ func addRuntimeDetails(instrumentor analytics.InstrumentationCollector, ua netwo
instrumentor.AddExtension("os-details", strings.TrimSpace(string(out)))
}

if out, err := exec.Command("ldd", "--version").Output(); err == nil {
instrumentor.AddExtension("c-runtime-details", strings.TrimSpace(string(out)))
cRuntimeDetails := cli_utils.GetGlibcDetails(cli_utils.ParserGlibcFull())
if cRuntimeDetails != "" {
instrumentor.AddExtension("c-runtime-details", cRuntimeDetails)
}
}

Expand Down
4 changes: 2 additions & 2 deletions cliv2/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ require (
github.com/snyk/cli-extension-sbom v0.0.0-20251113132837-5f6cc6d0cb26
github.com/snyk/container-cli v0.0.0-20250321132345-1e2e01681dd7
github.com/snyk/error-catalog-golang-public v0.0.0-20251205100923-e93b06d4a6c6
github.com/snyk/go-application-framework v0.0.0-20251218080318-c938eac5f436
github.com/snyk/go-application-framework v0.0.0-20251218160325-1dc83cd91def
github.com/snyk/go-httpauth v0.0.0-20240307114523-1f5ea3f55c65
github.com/snyk/snyk-iac-capture v0.6.5
github.com/snyk/snyk-ls v0.0.0-20251218112222-8c5878cbac7d
github.com/snyk/studio-mcp v1.2.3
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
golang.org/x/mod v0.29.0
)

require (
Expand Down Expand Up @@ -226,7 +227,6 @@ require (
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/sync v0.18.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions cliv2/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1318,8 +1318,8 @@ github.com/snyk/dep-graph/go v0.0.0-20251128083058-1972edcff6cf h1:RZ3KGLcbH37DU
github.com/snyk/dep-graph/go v0.0.0-20251128083058-1972edcff6cf/go.mod h1:hTr91da/4ze2nk9q6ZW1BmfM2Z8rLUZSEZ3kK+6WGpc=
github.com/snyk/error-catalog-golang-public v0.0.0-20251205100923-e93b06d4a6c6 h1:8fGb/ipxqcmQHwC1ltd7Fch23fbQXP6kNohORe/GQpE=
github.com/snyk/error-catalog-golang-public v0.0.0-20251205100923-e93b06d4a6c6/go.mod h1:Ytttq7Pw4vOCu9NtRQaOeDU2dhBYUyNBe6kX4+nIIQ4=
github.com/snyk/go-application-framework v0.0.0-20251218080318-c938eac5f436 h1:G7gu1qgxsGnh79VCsik98wKV7kmUS4RQnV+YrKlyxgc=
github.com/snyk/go-application-framework v0.0.0-20251218080318-c938eac5f436/go.mod h1:T+dt4+4XFAJ4PmoGgt/hrx7LiY+vaz+m9V4UYe24Rpc=
github.com/snyk/go-application-framework v0.0.0-20251218160325-1dc83cd91def h1:4VHOC7ccmL5D718oUR6DECeCbbC16iWe7jI3OGLEh+0=
github.com/snyk/go-application-framework v0.0.0-20251218160325-1dc83cd91def/go.mod h1:T+dt4+4XFAJ4PmoGgt/hrx7LiY+vaz+m9V4UYe24Rpc=
github.com/snyk/go-httpauth v0.0.0-20240307114523-1f5ea3f55c65 h1:CEQuYv0Go6MEyRCD3YjLYM2u3Oxkx8GpCpFBd4rUTUk=
github.com/snyk/go-httpauth v0.0.0-20240307114523-1f5ea3f55c65/go.mod h1:88KbbvGYlmLgee4OcQ19yr0bNpXpOr2kciOthaSzCAg=
github.com/snyk/policy-engine v1.1.0 h1:vFbFZbs3B0Y3XuGSur5om2meo4JEcCaKfNzshZFGOUs=
Expand Down
69 changes: 69 additions & 0 deletions cliv2/internal/utils/glibcversion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package utils

import (
"fmt"
"os/exec"
"regexp"
"runtime"
"strings"
"sync"
)

var (
cachedVersion string
versionDetectOnce sync.Once
versionRegex = regexp.MustCompile(`(\d+\.\d+)`)
)

type GlibcParsers func(string) (string, error)

// DefaultGlibcVersion attempts to detect the glibc version on Linux systems
// The detection is performed only once and cached for subsequent calls
func DefaultGlibcVersion() string {
versionDetectOnce.Do(func() {
cachedVersion = GetGlibcDetails(ParserGlibcVersion())
})
return cachedVersion
}

// GetGlibcDetails attempts to detect the glibc version on Linux systems
func GetGlibcDetails(parser GlibcParsers) string {
if runtime.GOOS != "linux" {
return ""
}

// Method 1: Try ldd --version
if out, err := exec.Command("ldd", "--version").Output(); err == nil {
lines := strings.Split(string(out), "\n")
if len(lines) > 0 {
if result, err := parser(lines[0]); err == nil {
return result
}
}
}

// Method 2: Try getconf GNU_LIBC_VERSION
if out, err := exec.Command("getconf", "GNU_LIBC_VERSION").Output(); err == nil {
if result, err := parser(string(out)); err == nil {
return result
}
}

return ""
}

func ParserGlibcFull() GlibcParsers {
return func(details string) (string, error) {
return strings.TrimSpace(details), nil
}
}

func ParserGlibcVersion() GlibcParsers {
return func(details string) (string, error) {
if matches := versionRegex.FindStringSubmatch(details); len(matches) > 1 {
return matches[1], nil
}

return "", fmt.Errorf("failed to parse glibc version")
}
}
152 changes: 152 additions & 0 deletions cliv2/internal/utils/glibcversion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package utils

import (
"runtime"
"testing"

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

func Test_ParserGlibcVersion(t *testing.T) {
testCases := []struct {
name string
input string
expectedVer string
expectError bool
}{
{
name: "Standard ldd output format",
input: "ldd (GNU libc) 2.31",
expectedVer: "2.31",
expectError: false,
},
{
name: "Standard ldd output format with extra info",
input: "ldd (Ubuntu GLIBC 2.35-0ubuntu3.8) 2.35",
expectedVer: "2.35",
expectError: false,
},
{
name: "getconf format",
input: "glibc 2.28",
expectedVer: "2.28",
expectError: false,
},
{
name: "Version at start of line",
input: "2.27 (GNU libc)",
expectedVer: "2.27",
expectError: false,
},
{
name: "Multi-digit minor version",
input: "ldd (GNU libc) 2.117",
expectedVer: "2.117",
expectError: false,
},
{
name: "Invalid format - no version",
input: "musl libc (x86_64)",
expectedVer: "",
expectError: true,
},
{
name: "Invalid format - empty string",
input: "",
expectedVer: "",
expectError: true,
},
{
name: "Invalid format - only text",
input: "some random text",
expectedVer: "",
expectError: true,
},
{
name: "Invalid format - single number",
input: "version 2",
expectedVer: "",
expectError: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
parser := ParserGlibcVersion()
result, err := parser(tc.input)

if tc.expectError {
assert.Error(t, err)
assert.Equal(t, "", result)
} else {
assert.NoError(t, err)
assert.Equal(t, tc.expectedVer, result)
}
})
}
}

func Test_ParserGlibcFull(t *testing.T) {
testCases := []struct {
name string
input string
expected string
}{
{
name: "Standard ldd output",
input: "ldd (GNU libc) 2.31",
expected: "ldd (GNU libc) 2.31",
},
{
name: "Output with leading whitespace",
input: " ldd (GNU libc) 2.31",
expected: "ldd (GNU libc) 2.31",
},
{
name: "Output with trailing whitespace",
input: "ldd (GNU libc) 2.31 \n",
expected: "ldd (GNU libc) 2.31",
},
{
name: "Output with both leading and trailing whitespace",
input: " \t ldd (GNU libc) 2.31 \n\t ",
expected: "ldd (GNU libc) 2.31",
},
{
name: "Empty string",
input: "",
expected: "",
},
{
name: "Only whitespace",
input: " \n\t ",
expected: "",
},
{
name: "Multiline output - only first matters",
input: "ldd (GNU libc) 2.31\nCopyright info\nMore text",
expected: "ldd (GNU libc) 2.31\nCopyright info\nMore text",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
parser := ParserGlibcFull()
result, err := parser(tc.input)

assert.NoError(t, err)
assert.Equal(t, tc.expected, result)
})
}
}

func Test_GetGlibcDetails_NonLinux(t *testing.T) {
if runtime.GOOS == "linux" {
t.Skip("Test only applicable on non-Linux")
}

parser := ParserGlibcVersion()
result := GetGlibcDetails(parser)

assert.Equal(t, "", result, "Should return empty string on non-Linux")
}
25 changes: 25 additions & 0 deletions cliv2/internal/utils/helpers.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package utils

import (
"strings"

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

// Dedupe removes duplicate entries from a given slice.
// Returns a new, deduplicated slice.
//
Expand Down Expand Up @@ -36,3 +42,22 @@ func Contains(list []string, element string) bool {
}
return false
}

// SemverCompare compares two semantic version strings
func SemverCompare(v1 string, v2 string) int {
// ensure v1 and v2 start with "v"
if !strings.HasPrefix(v1, "v") {
v1 = "v" + v1
}
if !strings.HasPrefix(v2, "v") {
v2 = "v" + v2
}

if !semver.IsValid(v1) || !semver.IsValid(v2) {
// return 0 to comply with semver.Compare()
// "Falls back to 0 when either version is invalid semver."
return 0
}

return semver.Compare(v1, v2)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggestion: Let's be a bit paranoid and check if both versions are actually valid using isValid()

Copy link
Collaborator

Choose a reason for hiding this comment

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

just because the doc of Compare() says the following

An invalid semantic version string is considered less than a valid one. All invalid semantic version strings compare equal to each other.

}
46 changes: 45 additions & 1 deletion cliv2/pkg/basic_workflows/legacycli.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ package basic_workflows
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"os/exec"
"runtime"
"strconv"

"github.com/snyk/cli/cliv2/internal/proxy/interceptor"
"github.com/snyk/cli/cliv2/internal/utils"
"github.com/snyk/error-catalog-golang-public/snyk"
"github.com/snyk/error-catalog-golang-public/snyk_errors"

"github.com/pkg/errors"
"github.com/rs/zerolog"
Expand All @@ -27,7 +32,9 @@ var DATATYPEID_LEGACY_CLI_STDOUT workflow.Identifier = workflow.NewTypeIdentifie
var staticNodeJsBinary string // injected by Makefile

const (
PROXY_NOAUTH string = "proxy-noauth"
PROXY_NOAUTH string = "proxy-noauth"
MIN_GLIBC_VERSION_LINUX_AMD64 string = "2.28"
MIN_GLIBC_VERSION_LINUX_ARM64 string = "2.31"
)

func initLegacycli(engine workflow.Engine) error {
Expand Down Expand Up @@ -75,6 +82,11 @@ func legacycliWorkflow(
debugLoggerDefault := invocation.GetLogger() // uses log
ri := invocation.GetRuntimeInfo()

err = ValidateGlibcVersion(debugLogger, utils.DefaultGlibcVersion(), runtime.GOOS, runtime.GOARCH)
if err != nil {
return output, err
}

args := config.GetStringSlice(configuration.RAW_CMD_ARGS)
useStdIo := config.GetBool(configuration.WORKFLOW_USE_STDIO)
workingDirectory := config.GetString(configuration.WORKING_DIRECTORY)
Expand Down Expand Up @@ -189,3 +201,35 @@ func createInternalProxy(config configuration.Configuration, debugLogger *zerolo

return wrapperProxy, nil
}

// ValidateGlibcVersion checks if the glibc version is supported and returns an Error Catalog error if it is not.
// This check only applies to glibc-based Linux systems (amd64, arm64).
func ValidateGlibcVersion(debugLogger *zerolog.Logger, glibcVersion string, os string, arch string) error {
// Skip validation on non-Linux or if glibc not detected
if glibcVersion == "" || os != "linux" {
return nil
}

var minVersion string
switch arch {
case "arm64":
minVersion = MIN_GLIBC_VERSION_LINUX_ARM64
case "amd64":
minVersion = MIN_GLIBC_VERSION_LINUX_AMD64
default:
return nil
}

res := utils.SemverCompare(glibcVersion, minVersion)

if res < 0 {
return snyk.NewRequirementsNotMetError(
fmt.Sprintf("The installed glibc version, %s is not supported. Upgrade to a version of glibc >= %s", glibcVersion, minVersion),
snyk_errors.WithLinks([]string{"https://docs.snyk.io/developer-tools/snyk-cli/releases-and-channels-for-the-snyk-cli#runtime-requirements"}),
)
}

// We currently do not fail on Linux when glibc is not detected, which could lead to an ungraceful failure.
// Failing here would require detectGlibcVersion to always return a valid version, which is not the case.
return nil
}
Loading