diff --git a/.circleci/config.yml b/.circleci/config.yml index 75cb2551d6..79f8b396ed 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -216,6 +216,9 @@ commands: - ~\AppData\Local\Temp\chocolatey - install-deps-python: os: win + - run: + name: Install uv + command: pip install uv install-deps-windows-signing: steps: @@ -300,6 +303,8 @@ commands: command: | sudo apt-get update sudo apt-get install xdg-utils docker.io -y + curl -LsSf https://astral.sh/uv/install.sh | sh + echo 'export PATH="$HOME/.local/bin:$PATH"' >> $BASH_ENV install-deps-python: parameters: @@ -334,7 +339,7 @@ commands: parameters: items: type: string - default: go gradle python pipenv elixir composer gradle@9 maven sbt dotnet + default: go gradle python pipenv elixir composer gradle@9 maven sbt dotnet uv steps: - run: name: Installing Rosetta @@ -385,6 +390,8 @@ commands: wget https://services.gradle.org/distributions/gradle-9.0.0-bin.zip -O /tmp/gradle.zip && \ unzip -d /usr/local /tmp/gradle.zip && \ ln -s /usr/local/gradle-9.0.0/bin/gradle /usr/local/bin + wget -qO- https://astral.sh/uv/install.sh | sh + ln -s $HOME/.local/bin/uv /usr/local/bin/uv failed-release-notification: steps: @@ -641,20 +648,6 @@ workflows: requires: - prepare-build - - regression-tests: - name: regression-tests - context: - - team_hammerhead-cli - - devex_cli_docker_hub - filters: - branches: - ignore: - - main - - '/release.*/' - requires: - - build linux amd64 - test_snyk_command: ./binary-releases/snyk-linux - - acceptance-tests: name: acceptance-tests linux static arm64 go_target_os: linux @@ -832,6 +825,7 @@ workflows: install_deps_extension: windows-full dont_skip_tests: 0 shards: 8 + shard_calc_cmd: '$([int]$env:CIRCLE_NODE_INDEX + 1)' pre_test_cmds: Import-Module $env:ChocolateyInstall\helpers\chocolateyProfile.psm1; RefreshEnv - sign: @@ -1261,6 +1255,7 @@ jobs: open-source-additional-arguments: --exclude=test,dist iac-scan: disabled release-branch: main + open-source-scan-reachability: true docs-only-check: executor: docker-amd64 @@ -1446,6 +1441,9 @@ jobs: shards: type: integer default: 4 + shard_calc_cmd: + type: string + default: '$(expr $CIRCLE_NODE_INDEX + 1)' executor: << parameters.executor >> parallelism: << parameters.shards >> environment: @@ -1474,7 +1472,7 @@ jobs: no_output_timeout: 30m command: | << parameters.pre_test_cmds >> - npm run test:acceptance -- --selectProjects coreCli --shard=$(expr $CIRCLE_NODE_INDEX + 1)/<< parameters.shards >> + npm run test:acceptance -- --selectProjects coreCli --shard=<< parameters.shard_calc_cmd >>/<< parameters.shards >> environment: TEST_SNYK_FIPS: << parameters.fips >> TEST_SNYK_COMMAND: << parameters.test_snyk_command >> @@ -1486,36 +1484,6 @@ jobs: - store_artifacts: path: test/reports - regression-tests: - parameters: - test_snyk_command: - type: string - default: ./binary-files/snyk-linux - executor: docker-amd64 - # working_directory: /mnt/ramdisk/snyk - steps: - - prepare-workspace - - run: - name: Install ShellSpec Deps - command: | - ./test/smoke/install-shellspec.sh --yes - sudo ln -s ~/.local/lib/shellspec/shellspec /usr/local/bin/shellspec - - run: - name: Installing test fixture dependencies - working_directory: ./test/fixtures/basic-npm - command: npm i - - run: - name: Installing Snyk CLI - command: | - sudo ln -s $(realpath << parameters.test_snyk_command >>) /usr/local/bin/snyk - snyk --version - - run: - name: Running ShellSpec tests - working_directory: ./test/smoke - command: | - echo "Checkout the README in test/smoke folder for more details about this step" - shellspec -f d -e REGRESSION_TEST=1 -e PIP_BREAK_SYSTEM_PACKAGES=1 - sign: parameters: go_os: diff --git a/.eslintrc.json b/.eslintrc.json index ab1bf13600..2d21293606 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -55,7 +55,17 @@ "jest/no-conditional-expect": "warn", "jest/no-try-expect": "warn", "jest/no-identical-title": "warn", - "@typescript-eslint/ban-ts-comment": "warn" + "@typescript-eslint/ban-ts-comment": "warn", + "jest/no-standalone-expect": [ + "error", + { + "additionalTestBlockFunctions": [ + "testIf", + "describeIf", + "testIf.each" + ] + } + ] } } ] diff --git a/.github/workflows/iac-cli-alert.yml b/.github/workflows/iac-cli-alert.yml deleted file mode 100644 index 593d340403..0000000000 --- a/.github/workflows/iac-cli-alert.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Infrastructure as Code CLI alert - -on: - schedule: - - cron: '0 * * * *' - -jobs: - check_tests: - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./packages/iac-cli-alert - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v4 - with: - node-version: '18.19.1' - cache: 'npm' - - run: npm ci - - run: npm start - env: - TEAM_CLI_GITHUB_PAT: ${{ secrets.TEAM_CLI_GITHUB_PAT }} - IAC_SMOKE_TESTS_SLACK_WEBHOOK_URL: ${{ secrets.IAC_SMOKE_TESTS_SLACK_WEBHOOK_URL }} diff --git a/.github/workflows/iac-smoke-tests.yml b/.github/workflows/iac-smoke-tests.yml deleted file mode 100644 index c38c523aba..0000000000 --- a/.github/workflows/iac-smoke-tests.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Infrastructure as Code Smoke Tests - -on: - release: - types: [published] - workflow_call: - inputs: - is_skip_alert: - type: boolean - required: true - default: false - -jobs: - run_iac_smoke_tests: - name: Run IaC smoke tests - runs-on: ${{ matrix.os }}-latest - strategy: - fail-fast: false - matrix: - os: [ubuntu, macos, windows] - - steps: - - uses: actions/checkout@v2 - with: - ref: ${{ github.ref }} - - - uses: actions/setup-node@v4 - with: - node-version: 16.16.0 - - - name: Install dependencies - run: | - npm install - - - name: Build Snyk CLI - run: | - npm run build:prod - - - name: Run IaC smoke tests - non-Windows - id: run_smoke_tests_non_windows - if: ${{ matrix.os != 'windows' }} - env: - IAC_SMOKE_TESTS_SNYK_TOKEN: ${{ secrets.IAC_SMOKE_TESTS_SNYK_TOKEN }} - TEST_SNYK_COMMAND: ${{ format('node {0}/dist/cli/index.js', github.workspace) }} - run: | - npx jest --runInBand --testPathPattern '/test/smoke(/jest)?/iac/' - - - name: Run IaC smoke tests - Windows - id: run_smoke_tests_windows - if: ${{ matrix.os == 'windows' }} - shell: pwsh - env: - IAC_SMOKE_TESTS_SNYK_TOKEN: ${{ secrets.IAC_SMOKE_TESTS_SNYK_TOKEN }} - TEST_SNYK_COMMAND: ${{ format('node {0}\dist\cli\index.js', github.workspace) }} - run: | - npx jest --runInBand --testPathPattern '/test/smoke(/jest)?/iac/' diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml deleted file mode 100644 index 9412b8ec9e..0000000000 --- a/.github/workflows/smoke-tests.yml +++ /dev/null @@ -1,199 +0,0 @@ -name: Smoke Tests - -on: - push: - branches: [feat/smoke-test, smoke/**] - release: - types: [published] - schedule: - - cron: '0 23 * * *' - workflow_dispatch: - -jobs: - smoke_test: - # The type of runner that the job will run on - runs-on: ${{ matrix.os }}-latest - strategy: - fail-fast: false # we care about other platforms and channels building - matrix: - os: [ubuntu, macos, windows] - snyk_install_method: [binary, npm, yarn, brew] - node_version: [18, 20] - exclude: - # Skip yarn for Windows, as it's a bit crazy to get it working in CI environment. Unless we see evidence we need it, I'd avoid it - - snyk_install_method: yarn - os: windows - # For binary, use only the Node 18 - - snyk_install_method: binary - node_version: 18 - # No need to run brew tests on some Platforms - - snyk_install_method: brew - os: ubuntu - - snyk_install_method: brew - os: windows - include: - - snyk_install_method: binary - os: ubuntu - snyk_cli_dl_file: snyk-linux - - snyk_install_method: binary - os: macos - snyk_cli_dl_file: snyk-macos - - snyk_install_method: alpine-binary - os: ubuntu - node_version: 18 - snyk_cli_dl_file: snyk-alpine - - snyk_install_method: npm-root-user - os: ubuntu - node_version: 18 - - snyk_install_method: docker-bundle - os: macos - node_version: 18 - snyk_cli_dl_file: snyk-for-docker-desktop-darwin-x64.tar.gz - - steps: - - uses: actions/checkout@v2 - - - uses: actions/setup-node@v4 # Needed for fixtures installation - with: - node-version: ${{ matrix.node_version }} - - - name: Install Snyk with npm - if: ${{ matrix.snyk_install_method == 'npm' }} - run: | - echo "node_version: ${{ matrix.node_version }}" - node -v - echo "install snyk with npm" - npm install -g snyk - - - name: Install Snyk with Yarn globally - if: ${{ matrix.snyk_install_method == 'yarn' }} - run: | - npm install yarn -g - echo "Yarn global path" - yarn global bin - echo 'export PATH="$PATH:$(yarn global bin)"' >> ~/.bash_profile - yarn global add snyk - - - name: npm install for fixture project - working-directory: test/fixtures/basic-npm - run: | - npm install - - - name: Run alpine test - if: ${{ matrix.snyk_install_method == 'alpine-binary' }} - env: - TEST_SNYK_TOKEN: ${{ secrets.TEST_SNYK_TOKEN }} - TEST_SNYK_API: ${{ secrets.TEST_SNYK_API }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - docker build -t snyk-cli-alpine -f ./test/smoke/alpine/Dockerfile ./test - docker run -eCI=1 -eTEST_SNYK_TOKEN -eGITHUB_TOKEN -eTEST_SNYK_API snyk-cli-alpine - - - name: Install snyk from Docker bundle - if: ${{ matrix.snyk_install_method == 'docker-bundle' && matrix.os == 'macos' }} - run: | - pushd "$(mktemp -d)" - curl 'https://downloads.snyk.io/cli/latest/${{ matrix.snyk_cli_dl_file }}' | tar -xz - pushd ./docker - ls -la - sudo ln -s "$(pwd)/snyk-mac.sh" ./snyk - export PATH="$(pwd):${PATH}" - echo "$(pwd)" >> "${GITHUB_PATH}" - popd - popd - which snyk - snyk version - - - name: Run npm test with Root user - if: ${{ matrix.snyk_install_method == 'npm-root-user' }} - env: - TEST_SNYK_TOKEN: ${{ secrets.TEST_SNYK_TOKEN }} - TEST_SNYK_API: ${{ secrets.TEST_SNYK_API }} - run: | - docker build -t snyk-docker-root -f ./test/smoke/docker-root/Dockerfile ./test - docker run -eCI=1 -eTEST_SNYK_TOKEN -eTEST_SNYK_API snyk-docker-root - - - name: Install Snyk with binary - Non-Windows - if: ${{ matrix.snyk_install_method == 'binary' && matrix.os != 'windows' }} - run: | - curl -Lo ./snyk-cli 'https://downloads.snyk.io/cli/latest/${{ matrix.snyk_cli_dl_file }}' - chmod -R +x ./snyk-cli - sudo mv ./snyk-cli /usr/local/bin/snyk - snyk --version - - - name: Install Snyk with binary - Windows - if: ${{ matrix.snyk_install_method == 'binary' && matrix.os == 'windows' }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - shell: powershell - run: | - echo "install snyk with binary" - echo $env:PATH - sh ./test/smoke/install-snyk-binary-win.sh - - - name: Install Shellspec - non-windows - if: ${{ matrix.os != 'windows' && matrix.snyk_install_method != 'alpine-binary' }} - run: | - ./test/smoke/install-shellspec.sh --yes - sudo ln -s ${HOME}/.local/lib/shellspec/shellspec /usr/local/bin/shellspec - ls -la ${HOME}/.local/lib/shellspec - echo "shellspec symlink:" - ls -la /usr/local/bin/shellspec - /usr/local/bin/shellspec --version - which shellspec - shellspec --version - - - name: Install test utilities with homebrew on macOS - if: ${{ matrix.os == 'macos' }} - # We need "timeout" and "jq" util and we'll use brew to check our brew package as well - run: | - brew install coreutils - brew install jq - - - name: Install Snyk CLI with homebrew on macOS - if: ${{ matrix.snyk_install_method == 'brew' && matrix.os == 'macos'}} - run: | - brew tap snyk/tap - brew install snyk - - - name: Install scoop on Windows - if: ${{ matrix.os == 'windows'}} - run: | - iwr -useb get.scoop.sh -outfile 'install-scoop.ps1' - .\install-scoop.ps1 -RunAsAdmin - scoop install jq - - - name: Install jq on Ubuntu - if: ${{ matrix.os == 'ubuntu' && matrix.snyk_install_method != 'alpine-binary' && matrix.snyk_install_method != 'npm-root-user' }} - run: | - sudo apt-get install jq - - - name: Install Shellspec - Windows - shell: powershell - if: ${{ matrix.os == 'windows' }} - run: | - Get-Host | Select-Object Version - Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux - sh ./test/smoke/install-shellspec.sh --yes - - - name: Run shellspec tests - non-Windows - if: ${{ matrix.os != 'windows' && matrix.snyk_install_method != 'alpine-binary' && matrix.snyk_install_method != 'npm-root-user' }} - working-directory: test/smoke - shell: bash -l {0} # run bash with --login flag to load .bash_profile that's used by yarn install method - env: - TEST_SNYK_TOKEN: ${{ secrets.TEST_SNYK_TOKEN }} - TEST_SNYK_API: ${{ secrets.TEST_SNYK_API }} - run: | - which snyk - snyk version - shellspec -f d --skip-message quiet --no-warning-as-failure - - - name: Run shellspec tests - Windows - if: ${{ matrix.os == 'windows' }} - working-directory: test/smoke - shell: powershell - env: - TEST_SNYK_TOKEN: ${{ secrets.TEST_SNYK_TOKEN }} - TEST_SNYK_API: ${{ secrets.TEST_SNYK_API }} - run: | - sh ./run-shellspec-win.sh diff --git a/.gitignore b/.gitignore index baa16920b3..32546b5f14 100644 --- a/.gitignore +++ b/.gitignore @@ -42,7 +42,7 @@ test/acceptance/workspaces/**/Package.resolved test/**/.gradle .iac-data .dccache -!test/smoke/.iac-data +!test/jest/unit/iac/fixtures/.iac-data test-output test-results tap-output @@ -53,4 +53,5 @@ test/fixtures/**/*/.build test/fixtures/**/*/Package.resolved scripts/Brewfile.lock.json test/fixtures/**/go.sum -.cursor \ No newline at end of file +.cursor +.windsurf \ No newline at end of file diff --git a/.gitleaksignore b/.gitleaksignore index 986398450a..3127037fa8 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -87,3 +87,4 @@ test/jest/acceptance/instrumentation.spec.ts:snyk-api-token:19 1b65935bc7c69b1029d7c63808af211ae6030c98:test/fixtures/sast/shallow_sast_webgoat/JWTFinalEndpointTest.java:jwt:31 test/jest/acceptance/snyk-code/snyk-code-integration.spec.ts:snyk-api-token:181 test/jest/acceptance/error-catalog.spec.ts:snyk-api-token:32 +test/jest/acceptance/snyk-redteam/redteam.spec.ts:snyk-api-token:30 diff --git a/binary-releases/RELEASE_NOTES.md b/binary-releases/RELEASE_NOTES.md index fa2cf2a023..388ca7d63c 100644 --- a/binary-releases/RELEASE_NOTES.md +++ b/binary-releases/RELEASE_NOTES.md @@ -1,7 +1,29 @@ -## [1.1301.2](https://github.com/snyk/snyk/compare/v1.1301.0...v1.1301.1) (2025-12-16) +## [1.1302.0](https://github.com/snyk/snyk/compare/v1.1301.2...v1.1302.0) (2026-01-14) The Snyk CLI is being deployed to different deployment channels, users can select the stability level according to their needs. For details please see [this documentation](https://docs.snyk.io/snyk-cli/releases-and-channels-for-the-snyk-cli) +### Features + +* **aibom:** Improved Exit Code handling ([d8fed82](https://github.com/snyk/snyk/commit/d8fed82450a92f4d06858b11d3e55b741c742567)) +* **container:** Added support for OCI images with manifests missing platform fields ([dae56aa](https://github.com/snyk/snyk/commit/dae56aada1b0ba22f9e681b71bd7437e99e83c94)) +* **container:** Added container scan support for cgo and stripped Go binaries ([9b2ee6e](https://github.com/snyk/snyk/commit/9b2ee6ee055eceacb7fcf9fde656a7369f9b48f1)) +* **container:** Added pnpm lockfile support ([47db111](https://github.com/snyk/snyk/commit/47db111dd0737224b28678268d1d712b525ca0a7)) +* **mcp-scan:** Added experimental mcp-scan command ([54b8376](https://github.com/snyk/snyk/commit/54b83769e33f8a0adf499e5c2b725bc076146864)) +* **sbom:** Improved PackageURLs in SBOM documents for go.mod projects ([c145efc](https://github.com/snyk/snyk/commit/c145efc1fc1b0d29fc3ce13d8f02391a195ccc0c)) +* **sbom test:** Added support for deb, apk and rpm ([9fd6f84](https://github.com/snyk/snyk/commit/9fd6f84af10495f4ccb91c79ad6c6089ecda5976)) +* **test:** Added PackageURL information to go.mod dependency graphs ([d90b54e](https://github.com/snyk/snyk/commit/d90b54e17801e74fa324f0f5c71aa2ec827eb49c)) +* **test:** Added support for poetry development dependencies ([6977004](https://github.com/snyk/snyk/commit/697700425d0eb71ca6965698658a09bce2cc4f77)) + ### Bug Fixes -* **mcp:** Fix MCP compliance issue ([51d3f8d](https://github.com/snyk/cli/commit/51d3f8d8224bf04d96303ea85000e63302bda77a)) +* **container:** Resolves false positive vulnerabilities for RHEL 10 container images ([d4afe60](https://github.com/snyk/snyk/commit/d4afe602fd14cf7284d88575669c58f2e153cc1f)) +* **general:** Upgraded multiple dependencies ([e185c92](https://github.com/snyk/snyk/commit/e185c92cbbb9102a8b50fb68f5ec9a3ddce280e1)) +* **general:** Fixed Exit Code handling when using incompatible glibc versions ([66fbb50](https://github.com/snyk/snyk/commit/66fbb50f4ed9b7a60641c214bdbc628556a5b50c)) +* **general:** Improved file filtering support with .gitignore ([a16b853](https://github.com/snyk/snyk/commit/a16b85338efb4209917198a890ca307b8c1a0ac8)) +* **mcp:** Added rule file to .gitignore if not previously ignored ([cc78694](https://github.com/snyk/snyk/commit/cc78694b5eab5cf41c004dfec89bd72ab5076723)) +* **test:** Improved upload speed when using --reachability ([da21315](https://github.com/snyk/snyk/commit/da2131536111d43de1a83150b3b7b9da025cc6e1)) +* **test:** Fixed npm v2 dependency resolution when using shadowing aliases ([237a4f5](https://github.com/snyk/snyk/commit/237a4f5ceb5b2b1e75cc2ce682e80f2abf7d8562)) +* **test:** Fixed --exclude support for pnpm workspaces ([293d9b1](https://github.com/snyk/snyk/commit/293d9b1be73185324712c5a5c4952afda855dabe)) +* **test:** Fixed SARIF output for Gradle projects to include the complete path in artifactLocation ([ec1262e](https://github.com/snyk/snyk/commit/ec1262e0d76f485872ec84414b84439070d1a4cc)) + + diff --git a/cliv2/Makefile b/cliv2/Makefile index 5b2a0fe4e2..1924b8e699 100644 --- a/cliv2/Makefile +++ b/cliv2/Makefile @@ -5,7 +5,7 @@ GOOS = $(shell go env GOOS) GOARCH = $(shell go env GOARCH) GOHOSTOS = $(shell go env GOHOSTOS) GOHOSTARCH = $(shell go env GOHOSTARCH) -LS_COMMIT_HASH = $(shell cat go.mod | grep snyk-ls | cut -d "-" -f 4) +LS_COMMIT_HASH = $(shell cat go.mod | grep snyk-ls | cut -d "-" -f 4 | head -1) FIPS_CRYPTO_BACKEND_DEFAULT = systemcrypto FIPS_CRYPTO_BACKEND = HASH = sha diff --git a/cliv2/cmd/cliv2/behavior/maperrortoexitcode.go b/cliv2/cmd/cliv2/behavior/maperrortoexitcode.go new file mode 100644 index 0000000000..eed0d4bd21 --- /dev/null +++ b/cliv2/cmd/cliv2/behavior/maperrortoexitcode.go @@ -0,0 +1,26 @@ +package behavior + +import ( + "github.com/snyk/error-catalog-golang-public/aibom" + "github.com/snyk/error-catalog-golang-public/code" + "github.com/snyk/error-catalog-golang-public/snyk_errors" + + "github.com/snyk/cli/cliv2/internal/constants" +) + +var MapErrorCatalogToExitCode func(err *snyk_errors.Error, defaultValue int) int = mapErrorToExitCode + +// mapErrorToExitCode maps error catalog errors to exit codes. Please extend the switch statement if new error codes need to be mapped. +func mapErrorToExitCode(err *snyk_errors.Error, defaultValue int) int { + var errorCatalogToExitCodeMap = map[string]int{ + code.NewUnsupportedProjectError("").ErrorCode: constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS, + aibom.NewNoSupportedFilesError("").ErrorCode: constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS, + // Add new mappings here + } + + if exitCode, ok := errorCatalogToExitCodeMap[err.ErrorCode]; ok { + return exitCode + } + + return defaultValue +} diff --git a/cliv2/cmd/cliv2/exitcode.go b/cliv2/cmd/cliv2/exitcode.go index 77eeb240b0..44cd35bfbe 100644 --- a/cliv2/cmd/cliv2/exitcode.go +++ b/cliv2/cmd/cliv2/exitcode.go @@ -1,18 +1,21 @@ package main import ( + "context" "encoding/json" "errors" "fmt" + "os/exec" "strings" - "github.com/snyk/error-catalog-golang-public/code" + "github.com/snyk/error-catalog-golang-public/snyk_errors" "github.com/snyk/go-application-framework/pkg/apiclients/testapi" "github.com/snyk/go-application-framework/pkg/local_workflows/content_type" "github.com/snyk/go-application-framework/pkg/local_workflows/json_schemas" "github.com/snyk/go-application-framework/pkg/utils/ufm" "github.com/snyk/go-application-framework/pkg/workflow" + "github.com/snyk/cli/cliv2/cmd/cliv2/behavior" "github.com/snyk/cli/cliv2/internal/constants" cli_errors "github.com/snyk/cli/cliv2/internal/errors" ) @@ -146,8 +149,8 @@ func handleTestSummary(engine workflow.Engine, data workflow.Data) (int, error) // handleDataErrors processes data errors and returns the appropriate exit code and error func handleDataErrors(data workflow.Data) (int, error) { for _, dataError := range data.GetErrorList() { - if dataError.ErrorCode == code.NewUnsupportedProjectError("").ErrorCode { - return constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS, dataError + if exitCode := mapErrorToExitCode(dataError); exitCode != unsetExitCode { + return exitCode, dataError } } return unsetExitCode, nil @@ -168,3 +171,26 @@ func createErrorWithExitCode(exitCode int, err error) error { } return errors.Join(err, errorWithExitCode) } + +// mapErrorToExitCode maps specific errors to an exit code. Unmapped errors will return unsetExitCode. +func mapErrorToExitCode(err error) int { + // no need to map if the error already contains an exit code in some form + exitCodeError := cli_errors.ErrorWithExitCode{} + var exitError *exec.ExitError + if errors.Is(err, exitCodeError) || errors.As(err, &exitError) { + return unsetExitCode + } + + // map external errors for example from golang runtime or other libraries that require a specific exit code + if errors.Is(err, context.DeadlineExceeded) { + return constants.SNYK_EXIT_CODE_EX_UNAVAILABLE + } + + // map error catalog errors + errCatalogError := snyk_errors.Error{} + if errors.As(err, &errCatalogError) { + return behavior.MapErrorCatalogToExitCode(&errCatalogError, unsetExitCode) + } + + return unsetExitCode +} diff --git a/cliv2/cmd/cliv2/exitcode_test.go b/cliv2/cmd/cliv2/exitcode_test.go new file mode 100644 index 0000000000..5afa306d42 --- /dev/null +++ b/cliv2/cmd/cliv2/exitcode_test.go @@ -0,0 +1,101 @@ +package main + +import ( + "context" + "errors" + "os" + "os/exec" + "testing" + + "github.com/snyk/error-catalog-golang-public/code" + "github.com/snyk/error-catalog-golang-public/snyk_errors" + + "github.com/snyk/cli/cliv2/internal/constants" + cli_errors "github.com/snyk/cli/cliv2/internal/errors" +) + +func TestMapErrorToExitCode(t *testing.T) { + t.Run("nil error returns unset", func(t *testing.T) { + exitCode := mapErrorToExitCode(nil) + if exitCode != unsetExitCode { + t.Errorf("expected exit code %d, got %d", unsetExitCode, exitCode) + } + }) + + t.Run("ErrorWithExitCode returns unset", func(t *testing.T) { + exitCodeErr := &cli_errors.ErrorWithExitCode{ExitCode: 42} + exitCode := mapErrorToExitCode(exitCodeErr) + if exitCode != unsetExitCode { + t.Errorf("expected exit code %d, got %d", unsetExitCode, exitCode) + } + }) + + t.Run("wrapped ErrorWithExitCode returns unset", func(t *testing.T) { + exitCodeErr := &cli_errors.ErrorWithExitCode{ExitCode: 42} + wrappedErr := errors.Join(exitCodeErr, errors.New("additional context")) + exitCode := mapErrorToExitCode(wrappedErr) + if exitCode != unsetExitCode { + t.Errorf("expected exit code %d, got %d", unsetExitCode, exitCode) + } + }) + + t.Run("exec.ExitError returns unset", func(t *testing.T) { + execErr := &exec.ExitError{ProcessState: &os.ProcessState{}} + exitCode := mapErrorToExitCode(execErr) + if exitCode != unsetExitCode { + t.Errorf("expected exit code %d, got %d", unsetExitCode, exitCode) + } + }) + + t.Run("wrapped exec.ExitError returns unset", func(t *testing.T) { + execErr := &exec.ExitError{ProcessState: &os.ProcessState{}} + wrappedErr := errors.Join(execErr, errors.New("command failed")) + exitCode := mapErrorToExitCode(wrappedErr) + if exitCode != unsetExitCode { + t.Errorf("expected exit code %d, got %d", unsetExitCode, exitCode) + } + }) + + t.Run("context.DeadlineExceeded returns EX_UNAVAILABLE", func(t *testing.T) { + exitCode := mapErrorToExitCode(context.DeadlineExceeded) + if exitCode != constants.SNYK_EXIT_CODE_EX_UNAVAILABLE { + t.Errorf("expected exit code %d, got %d", constants.SNYK_EXIT_CODE_EX_UNAVAILABLE, exitCode) + } + }) + + t.Run("wrapped context.DeadlineExceeded returns EX_UNAVAILABLE", func(t *testing.T) { + wrappedErr := errors.Join(context.DeadlineExceeded, errors.New("timeout occurred")) + exitCode := mapErrorToExitCode(wrappedErr) + if exitCode != constants.SNYK_EXIT_CODE_EX_UNAVAILABLE { + t.Errorf("expected exit code %d, got %d", constants.SNYK_EXIT_CODE_EX_UNAVAILABLE, exitCode) + } + }) + + t.Run("wrapped unsupported project error returns UNSUPPORTED_PROJECTS", func(t *testing.T) { + unsupportedErr := code.NewUnsupportedProjectError("test project") + wrappedErr := errors.Join(unsupportedErr, errors.New("additional context")) + exitCode := mapErrorToExitCode(wrappedErr) + if exitCode != constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS { + t.Errorf("expected exit code %d, got %d", constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS, exitCode) + } + }) + + t.Run("other error catalog error returns unset", func(t *testing.T) { + otherErr := snyk_errors.Error{ + ErrorCode: "OTHER_ERROR_CODE", + Detail: "some other error", + } + exitCode := mapErrorToExitCode(otherErr) + if exitCode != unsetExitCode { + t.Errorf("expected exit code %d, got %d", unsetExitCode, exitCode) + } + }) + + t.Run("generic error returns unset", func(t *testing.T) { + genericErr := errors.New("some generic error") + exitCode := mapErrorToExitCode(genericErr) + if exitCode != unsetExitCode { + t.Errorf("expected exit code %d, got %d", unsetExitCode, exitCode) + } + }) +} diff --git a/cliv2/cmd/cliv2/instrumentation.go b/cliv2/cmd/cliv2/instrumentation.go index c24ef5c2a4..2f4d258433 100644 --- a/cliv2/cmd/cliv2/instrumentation.go +++ b/cliv2/cmd/cliv2/instrumentation.go @@ -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" @@ -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) } } diff --git a/cliv2/cmd/cliv2/main.go b/cliv2/cmd/cliv2/main.go index 30cd664901..51697a1cd8 100644 --- a/cliv2/cmd/cliv2/main.go +++ b/cliv2/cmd/cliv2/main.go @@ -22,20 +22,23 @@ import ( "github.com/snyk/cli-extension-dep-graph/pkg/depgraph" "github.com/snyk/cli-extension-iac-rules/iacrules" "github.com/snyk/cli-extension-iac/pkg/iac" + "github.com/snyk/cli-extension-mcp-scan/pkg/mcpscan" "github.com/snyk/cli-extension-os-flows/pkg/osflows" "github.com/snyk/cli-extension-sbom/pkg/sbom" - "github.com/snyk/cli/cliv2/cmd/cliv2/behavior/legacy" - "github.com/snyk/cli/cliv2/internal/cliv2" - "github.com/snyk/cli/cliv2/internal/constants" "github.com/snyk/container-cli/pkg/container" "github.com/snyk/error-catalog-golang-public/cli" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/snyk/go-application-framework/pkg/analytics" "github.com/snyk/go-application-framework/pkg/app" "github.com/snyk/go-application-framework/pkg/configuration" "github.com/snyk/go-application-framework/pkg/instrumentation" "github.com/snyk/go-application-framework/pkg/logging" - "github.com/spf13/cobra" - "github.com/spf13/pflag" + + "github.com/snyk/cli/cliv2/cmd/cliv2/behavior/legacy" + "github.com/snyk/cli/cliv2/internal/cliv2" + "github.com/snyk/cli/cliv2/internal/constants" cliv2utils "github.com/snyk/cli/cliv2/internal/utils" @@ -45,6 +48,9 @@ import ( workflows "github.com/snyk/go-application-framework/pkg/local_workflows/connectivity_check_extension" + "github.com/snyk/go-httpauth/pkg/httpauth" + "github.com/snyk/snyk-iac-capture/pkg/capture" + ignoreworkflow "github.com/snyk/go-application-framework/pkg/local_workflows/ignore_workflow" "github.com/snyk/go-application-framework/pkg/local_workflows/output_workflow" "github.com/snyk/go-application-framework/pkg/networking" @@ -52,8 +58,6 @@ import ( "github.com/snyk/go-application-framework/pkg/ui" "github.com/snyk/go-application-framework/pkg/utils" "github.com/snyk/go-application-framework/pkg/workflow" - "github.com/snyk/go-httpauth/pkg/httpauth" - "github.com/snyk/snyk-iac-capture/pkg/capture" snykls "github.com/snyk/snyk-ls/ls_extension" @@ -185,7 +189,7 @@ func runWorkflowAndProcessData(engine workflow.Engine, logger *zerolog.Logger, n output, err := engine.Invoke(workflow.NewWorkflowIdentifier(name), workflow.WithInstrumentationCollector(ic)) if err != nil { - logger.Print("Failed to execute the command!", err) + logger.Print("Failed to execute the command! ", err) return err } @@ -300,6 +304,12 @@ func runCodeTestCommand(cmd *cobra.Command, args []string) error { } globalConfiguration.Set(output_workflow.OUTPUT_CONFIG_KEY_FILE_WRITERS, fileWriters) + // ensure that json is translated to sarif for the default writer as well + defaultWriterLookup := map[string]string{ + output_workflow.JSON_MIME_TYPE: output_workflow.SARIF_MIME_TYPE, + } + globalConfiguration.Set(output_workflow.OUTPUT_CONFIG_KEY_DEFAULT_WRITER_LUT, defaultWriterLookup) + return runCommand(cmd, args) } @@ -552,6 +562,7 @@ func MainWithErrorCode() int { globalEngine.AddExtensionInitializer(workflows.InitConnectivityCheckWorkflow) globalEngine.AddExtensionInitializer(localworkflows.InitCodeWorkflow) globalEngine.AddExtensionInitializer(ignoreworkflow.InitIgnoreWorkflows) + globalEngine.AddExtensionInitializer(mcpscan.Init) // init engine err = globalEngine.Init() @@ -621,6 +632,11 @@ func MainWithErrorCode() int { } err = legacyCLITerminated(err, errorList) + + // ensure to apply exit code mapping based on errors + if exitCode := mapErrorToExitCode(err); exitCode != unsetExitCode { + err = createErrorWithExitCode(exitCode, err) + } } displayError(err, globalEngine.GetUserInterface(), globalConfiguration, ctx) diff --git a/cliv2/go.mod b/cliv2/go.mod index 1dc65cbfa8..018e4a7dd2 100644 --- a/cliv2/go.mod +++ b/cliv2/go.mod @@ -10,22 +10,24 @@ require ( github.com/google/uuid v1.6.0 github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.34.0 - github.com/snyk/cli-extension-ai-bom v0.0.0-20251020202637-f2a2552d8e69 - github.com/snyk/cli-extension-dep-graph v0.0.0-20251111071234-1ea3ded4c7d2 + github.com/snyk/cli-extension-ai-bom v0.0.0-20251211082900-ca53e09267c4 + github.com/snyk/cli-extension-dep-graph v0.11.0 github.com/snyk/cli-extension-iac v0.0.0-20250829110702-b41ac109dab0 github.com/snyk/cli-extension-iac-rules v0.0.0-20250829110455-1260348bc188 - github.com/snyk/cli-extension-os-flows v0.0.0-20251204103758-6d63a313a38e - github.com/snyk/cli-extension-sbom v0.0.0-20251113132837-5f6cc6d0cb26 + github.com/snyk/cli-extension-mcp-scan v0.0.0-20251217093101-0705cbe3593b + github.com/snyk/cli-extension-os-flows v0.0.0-20260108122334-2d86a741f1f7 + github.com/snyk/cli-extension-sbom v0.0.0-20260106140701-6c590485fbe4 github.com/snyk/container-cli v0.0.0-20250321132345-1e2e01681dd7 - github.com/snyk/error-catalog-golang-public v0.0.0-20251024131459-25bdd340f134 - github.com/snyk/go-application-framework v0.0.0-20251118111357-8c9e565ff018 + github.com/snyk/error-catalog-golang-public v0.0.0-20251222142433-dbdc288a6e98 + github.com/snyk/go-application-framework v0.0.0-20260106115317-a1fb6f13accd 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-20251126093614-d999dd468f2e - github.com/snyk/studio-mcp v1.1.1 + github.com/snyk/snyk-ls v0.0.0-20260108085345-39b92d542121 + github.com/snyk/studio-mcp v1.3.0 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 ( @@ -69,6 +71,7 @@ require ( github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/chzyer/readline v1.5.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 // indirect github.com/containerd/console v1.0.3 // indirect @@ -81,6 +84,7 @@ require ( github.com/docker/distribution v2.8.2+incompatible // indirect github.com/dop251/goja v0.0.0-20251008123653-cf18d89f3cf6 // indirect github.com/dop251/goja_nodejs v0.0.0-20250409162600-f7acab6894b0 // indirect + github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect @@ -143,6 +147,7 @@ require ( github.com/klauspost/compress v1.17.9 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/manifoldco/promptui v0.9.0 // indirect github.com/mark3labs/mcp-go v0.31.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -178,11 +183,13 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/samber/lo v1.52.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.3.1 // indirect github.com/snyk/code-client-go v1.24.4 // indirect + github.com/snyk/dep-graph/go v0.0.0-20251128083058-1972edcff6cf // indirect github.com/snyk/policy-engine v1.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/sourcegraph/go-lsp v0.0.0-20240223163137-f80c5dd31dfd // indirect @@ -220,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 @@ -253,3 +259,5 @@ replace github.com/mattn/go-localereader v0.0.1 => github.com/mattn/go-localerea //replace github.com/snyk/cli-extension-os-flows => ../../cli-extension-os-flows // replace github.com/snyk/cli-extension-ai-bom => ../../cli-extension-ai-bom + +// replace github.com/snyk/studio-mcp => ../../studio-mcp diff --git a/cliv2/go.sum b/cliv2/go.sum index 069bb90ce5..c125a522c1 100644 --- a/cliv2/go.sum +++ b/cliv2/go.sum @@ -731,8 +731,14 @@ github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQ github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= @@ -789,6 +795,8 @@ github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/elazarl/goproxy/ext v0.0.0-20230808193330-2592e75ae04a h1:6hp3+W5oJSkbk/m2XquFdhih2H4wxxR0Nl6GfPL8kss= github.com/elazarl/goproxy/ext v0.0.0-20230808193330-2592e75ae04a/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= +github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg= +github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -1138,6 +1146,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mark3labs/mcp-go v0.31.0 h1:4UxSV8aM770OPmTvaVe/b1rA2oZAjBMhGBfUgOGut+4= github.com/mark3labs/mcp-go v0.31.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= @@ -1286,36 +1296,40 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= -github.com/snyk/cli-extension-ai-bom v0.0.0-20251020202637-f2a2552d8e69 h1:fRmPAb9M4fbq3bq3TY8cB7dquxL7zc/dpkwHHxM0eTg= -github.com/snyk/cli-extension-ai-bom v0.0.0-20251020202637-f2a2552d8e69/go.mod h1:YvlGYA6i/aXDY68ps/X/XvkN8JGZ8T6eKFNdPE2y3oI= -github.com/snyk/cli-extension-dep-graph v0.0.0-20251111071234-1ea3ded4c7d2 h1:GG3jrRzdgRbK6rDPBeUm/NHnu6+9oI/mpAVz1xs+SzM= -github.com/snyk/cli-extension-dep-graph v0.0.0-20251111071234-1ea3ded4c7d2/go.mod h1:k1ZvlY0rv527wfkmLOggYhaH78cy4R86C7/jW3BwFgk= +github.com/snyk/cli-extension-ai-bom v0.0.0-20251211082900-ca53e09267c4 h1:/fFPiyMeU6egp39QkubOsDBn86WgHO/OlRGH8J0CAhg= +github.com/snyk/cli-extension-ai-bom v0.0.0-20251211082900-ca53e09267c4/go.mod h1:YvlGYA6i/aXDY68ps/X/XvkN8JGZ8T6eKFNdPE2y3oI= +github.com/snyk/cli-extension-dep-graph v0.11.0 h1:giyMIGHH6VeUG1sfvrDP8vyIwbkqE7iQEV4hwGk6bbc= +github.com/snyk/cli-extension-dep-graph v0.11.0/go.mod h1:jw4rmBwqM1Z+rPHUagXlSsw/S78MXNodpie4+EjYVmw= github.com/snyk/cli-extension-iac v0.0.0-20250829110702-b41ac109dab0 h1:ecGoMisVTnz5xRnt9yXW2hlRrIyYM123yMt1NeNEo6s= github.com/snyk/cli-extension-iac v0.0.0-20250829110702-b41ac109dab0/go.mod h1:tLxyhtrRiEvbSLQ6PbCsl29ZXK6s2aunRuL6cSe/8cE= github.com/snyk/cli-extension-iac-rules v0.0.0-20250829110455-1260348bc188 h1:UoyD7cB9XZVHPTRugsmCt6rBvAw5IoiBjI8go2qj1pk= github.com/snyk/cli-extension-iac-rules v0.0.0-20250829110455-1260348bc188/go.mod h1:qUc1yjKJe6tt/8/MJasnog3VBXd/b619MSFVfKAlDxE= -github.com/snyk/cli-extension-os-flows v0.0.0-20251204103758-6d63a313a38e h1:Y+hdlfpNMez/qYI3WYgd6OLsFWzXKVU/rSQu+PHNEag= -github.com/snyk/cli-extension-os-flows v0.0.0-20251204103758-6d63a313a38e/go.mod h1:7cBuPp3HdioLGDBd7TvBPPUxTrehTi0dYPRh3K7fKm4= -github.com/snyk/cli-extension-sbom v0.0.0-20251113132837-5f6cc6d0cb26 h1:KEiRBMdOJHefM4GKL3C3FfvH4J2G/vBFnwkonylV5+o= -github.com/snyk/cli-extension-sbom v0.0.0-20251113132837-5f6cc6d0cb26/go.mod h1:zyKDBaETfZyI7BfIjPnezH3QX2seQrR/d7NM5W6LV9s= +github.com/snyk/cli-extension-mcp-scan v0.0.0-20251217093101-0705cbe3593b h1:d8s+TntutaQlPcB+5I2781ALWEgGfQh2XQjPrt0oRy8= +github.com/snyk/cli-extension-mcp-scan v0.0.0-20251217093101-0705cbe3593b/go.mod h1:dRgGvQssSQ1U//nQ0D+H8JXnjz1ZhG9GWbz8GEaFRMQ= +github.com/snyk/cli-extension-os-flows v0.0.0-20260108122334-2d86a741f1f7 h1:njG2hz4K4FGTf2C95wXKO+p8Y4CD13ESHL8JKL68y6M= +github.com/snyk/cli-extension-os-flows v0.0.0-20260108122334-2d86a741f1f7/go.mod h1:TWWxoMwavH+jAluWZtaaDbdOuwt8C5n51xnEuWvrv1g= +github.com/snyk/cli-extension-sbom v0.0.0-20260106140701-6c590485fbe4 h1:hfrkX61nZ0+RsVBBbsJNwjfga+tB0Nesa95a4dteqRc= +github.com/snyk/cli-extension-sbom v0.0.0-20260106140701-6c590485fbe4/go.mod h1:jIACVV10j4pW7LFrlYYtjn9mZm2JnXeFBM6/aTNJgvM= github.com/snyk/code-client-go v1.24.4 h1:19rmeqZFvjQMKaAmSZ0CdYZb1d0ENsDad2Cp32jeWOA= github.com/snyk/code-client-go v1.24.4/go.mod h1:uMlmMToe4uuNhNLs+yxjM3WFbytna+ytDWhpbnNwTSk= github.com/snyk/container-cli v0.0.0-20250321132345-1e2e01681dd7 h1:/2+2piwQtB9fEJCkXEOjboZjY+77lQfnvqBZ/60xNHk= github.com/snyk/container-cli v0.0.0-20250321132345-1e2e01681dd7/go.mod h1:38w+dcAQp9eG3P5t2eNS9eG0reut10AeJjLv5lJ5lpM= -github.com/snyk/error-catalog-golang-public v0.0.0-20251024131459-25bdd340f134 h1:IKwMDrwicB07NDS+VrI6I8qowqdDpKI0nBEvMnbSu+w= -github.com/snyk/error-catalog-golang-public v0.0.0-20251024131459-25bdd340f134/go.mod h1:Ytttq7Pw4vOCu9NtRQaOeDU2dhBYUyNBe6kX4+nIIQ4= -github.com/snyk/go-application-framework v0.0.0-20251118111357-8c9e565ff018 h1:1NErKWe//TRxFzw/qG2kfyS+LQXyPncNRXArVxD52AQ= -github.com/snyk/go-application-framework v0.0.0-20251118111357-8c9e565ff018/go.mod h1:HXON5jD2A4GarLrQyUSLBGR7jJy7LfzzHmjdkLe3VCk= +github.com/snyk/dep-graph/go v0.0.0-20251128083058-1972edcff6cf h1:RZ3KGLcbH37DUFNA0zDnfcET3jz6lxTOywON5L1xxK0= +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-20251222142433-dbdc288a6e98 h1:ucaLtBucnO9U8mrUmKovxObkflB1aQUQTB+orFt9rug= +github.com/snyk/error-catalog-golang-public v0.0.0-20251222142433-dbdc288a6e98/go.mod h1:Ytttq7Pw4vOCu9NtRQaOeDU2dhBYUyNBe6kX4+nIIQ4= +github.com/snyk/go-application-framework v0.0.0-20260106115317-a1fb6f13accd h1:mGFCdZOB+e7CdNbJ1+FezEVW9i7tc4PgC72bMwJmbxU= +github.com/snyk/go-application-framework v0.0.0-20260106115317-a1fb6f13accd/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= github.com/snyk/policy-engine v1.1.0/go.mod h1:SSZiMz6TiggRAk33duOueWeSG0Xwl0QoZo8hfPcEAh0= github.com/snyk/snyk-iac-capture v0.6.5 h1:992DXCAJSN97KtUh8T5ndaWwd/6ZCal2bDkRXqM1u/E= github.com/snyk/snyk-iac-capture v0.6.5/go.mod h1:e47i55EmM0F69ZxyFHC4sCi7vyaJW6DLoaamJJCzWGk= -github.com/snyk/snyk-ls v0.0.0-20251126093614-d999dd468f2e h1:HkawkOJEGRj9HcbtiXVu2VEet0e/RrJNplWfeSW6fLg= -github.com/snyk/snyk-ls v0.0.0-20251126093614-d999dd468f2e/go.mod h1:WgxyI7pnavACJzAfVuigRiannqtmbtx+LyEI1qb5DpI= -github.com/snyk/studio-mcp v1.1.1 h1:D9su5ulUcCzKPKaTBwIYOqGooFF6AhbQBPGO629XJ6o= -github.com/snyk/studio-mcp v1.1.1/go.mod h1:zspP6KgoeYZxobgbsq/XxhaI3m/JdLj/mi59QeRo9Xs= +github.com/snyk/snyk-ls v0.0.0-20260108085345-39b92d542121 h1:Ge5797JfA1+R2uCON2J83js2Y5rPZm4+bvfi3TyiyIw= +github.com/snyk/snyk-ls v0.0.0-20260108085345-39b92d542121/go.mod h1:STDRX94wGUjny7cg2a1gxbsmoXNuHQJISQWQKGOjRZ0= +github.com/snyk/studio-mcp v1.3.0 h1:6eqjiaab9hILdoY4awAF2z+xj03VOB93DbLhlzYyRB4= +github.com/snyk/studio-mcp v1.3.0/go.mod h1:+OuCQy/pOysingGYHNUojmpSVse/q36KODBaJlOvsNQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/sourcegraph/go-lsp v0.0.0-20240223163137-f80c5dd31dfd h1:Dq5WSzWsP1TbVi10zPWBI5LKEBDg4Y1OhWEph1wr5WQ= @@ -1657,6 +1671,7 @@ golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1721,6 +1736,7 @@ golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/cliv2/internal/cliv2/cliv2.go b/cliv2/internal/cliv2/cliv2.go index 3a8f56ad87..a139c3607b 100644 --- a/cliv2/internal/cliv2/cliv2.go +++ b/cliv2/internal/cliv2/cliv2.go @@ -91,6 +91,11 @@ func NewCLIv2(config configuration.Configuration, debugLogger *log.Logger, ri ru globalConfig: config, } + subProcessEnv := config.GetStringSlice(configuration.SUBPROCESS_ENVIRONMENT) + if len(subProcessEnv) != 0 { + cli.env = subProcessEnv + } + return &cli, nil } @@ -548,8 +553,6 @@ func DeriveExitCode(err error) int { if returnCode < 0 || returnCode == constants.SNYK_EXIT_CODE_TS_CLI_TERMINATED { returnCode = constants.SNYK_EXIT_CODE_ERROR } - } else if errors.Is(err, context.DeadlineExceeded) { - returnCode = constants.SNYK_EXIT_CODE_EX_UNAVAILABLE } else if errors.As(err, &errorWithExitCode) { returnCode = errorWithExitCode.ExitCode } else { diff --git a/cliv2/internal/cliv2/cliv2_test.go b/cliv2/internal/cliv2/cliv2_test.go index 1db8df2808..3b3ff6ab43 100644 --- a/cliv2/internal/cliv2/cliv2_test.go +++ b/cliv2/internal/cliv2/cliv2_test.go @@ -46,6 +46,53 @@ func getRuntimeInfo(t *testing.T) runtimeinfo.RuntimeInfo { return runtimeinfo.New(runtimeinfo.WithVersion(cliv1.CLIV1Version())) } +func Test_NewCLIv2_SubprocessEnv_OverridesIfSet_AndDefaultsToOsEnv(t *testing.T) { + t.Run("uses configured subprocess environment if set", func(t *testing.T) { + cacheDir := getCacheDir(t) + config := configuration.NewWithOpts(configuration.WithAutomaticEnv()) + config.Set(configuration.CACHE_PATH, cacheDir) + config.Set(configuration.SUBPROCESS_ENVIRONMENT, []string{"FOO=bar"}) + + cli, err := cliv2.NewCLIv2(config, discardLogger, getRuntimeInfo(t)) + assert.NoError(t, err) + + cmd, err := cli.PrepareV1Command( + context.Background(), + "someExecutable", + []string{"--help"}, + getProxyInfoForTest(), + "name", + "version", + ) + assert.NoError(t, err) + assert.Contains(t, cmd.Env, "FOO=bar") + }) + + t.Run("uses os.Environ when subprocess environment is not defined", func(t *testing.T) { + cacheDir := getCacheDir(t) + config := configuration.NewWithOpts(configuration.WithAutomaticEnv()) + config.Set(configuration.CACHE_PATH, cacheDir) + + envKey := "SNYK_CLIV2_TEST_ENV" + envValue := "present" + t.Setenv(envKey, envValue) + + cli, err := cliv2.NewCLIv2(config, discardLogger, getRuntimeInfo(t)) + assert.NoError(t, err) + + cmd, err := cli.PrepareV1Command( + context.Background(), + "someExecutable", + []string{"--help"}, + getProxyInfoForTest(), + "name", + "version", + ) + assert.NoError(t, err) + assert.Contains(t, cmd.Env, envKey+"="+envValue) + }) +} + func Test_PrepareV1EnvironmentVariables_Fill_and_Filter(t *testing.T) { orgid := "orgid" testapi := "https://api.snyky.io" @@ -514,9 +561,6 @@ func Test_setTimeout(t *testing.T) { err = cli.Execute(getProxyInfoForTest(), []string{"2"}) assert.ErrorIs(t, err, context.DeadlineExceeded) - - // ensure that -1 is correctly mapped if timeout is set - assert.Equal(t, constants.SNYK_EXIT_CODE_EX_UNAVAILABLE, cliv2.DeriveExitCode(err)) } func TestDeriveExitCode(t *testing.T) { @@ -527,7 +571,6 @@ func TestDeriveExitCode(t *testing.T) { }{ {name: "no error", err: nil, expected: constants.SNYK_EXIT_CODE_OK}, {name: "error with exit code", err: &cli_errors.ErrorWithExitCode{ExitCode: 42}, expected: 42}, - {name: "context.DeadlineExceeded", err: context.DeadlineExceeded, expected: constants.SNYK_EXIT_CODE_EX_UNAVAILABLE}, {name: "other error", err: errors.New("some other error"), expected: constants.SNYK_EXIT_CODE_ERROR}, } diff --git a/cliv2/internal/constants/constants.go b/cliv2/internal/constants/constants.go index 1f2ffa5b97..13d60adfbe 100644 --- a/cliv2/internal/constants/constants.go +++ b/cliv2/internal/constants/constants.go @@ -4,6 +4,7 @@ const SNYK_EXIT_CODE_OK = 0 const SNYK_EXIT_CODE_VULNERABILITIES_FOUND = 1 const SNYK_EXIT_CODE_ERROR = 2 const SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS = 3 +const SNYK_EXIT_CODE_EX_TEMPFAIL = 75 // EX_TEMPFAIL, Temporary failure, indicating something that is not really an error. For example that a mailer could not create a connection, and the request should be reattempted later. const SNYK_EXIT_CODE_EX_UNAVAILABLE = 69 const SNYK_EXIT_CODE_TS_CLI_TERMINATED = 44 const SNYK_INTEGRATION_NAME = "CLI_V1_PLUGIN" diff --git a/cliv2/internal/utils/glibcversion.go b/cliv2/internal/utils/glibcversion.go new file mode 100644 index 0000000000..1f350e3fb8 --- /dev/null +++ b/cliv2/internal/utils/glibcversion.go @@ -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") + } +} diff --git a/cliv2/internal/utils/glibcversion_test.go b/cliv2/internal/utils/glibcversion_test.go new file mode 100644 index 0000000000..754dd5ac1e --- /dev/null +++ b/cliv2/internal/utils/glibcversion_test.go @@ -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") +} diff --git a/cliv2/internal/utils/helpers.go b/cliv2/internal/utils/helpers.go index 8497b4c3a3..2df88d48a6 100644 --- a/cliv2/internal/utils/helpers.go +++ b/cliv2/internal/utils/helpers.go @@ -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. // @@ -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) +} diff --git a/cliv2/pkg/basic_workflows/legacycli.go b/cliv2/pkg/basic_workflows/legacycli.go index 16c2c48657..acc35313b8 100644 --- a/cliv2/pkg/basic_workflows/legacycli.go +++ b/cliv2/pkg/basic_workflows/legacycli.go @@ -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" @@ -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 { @@ -75,6 +82,16 @@ func legacycliWorkflow( debugLoggerDefault := invocation.GetLogger() // uses log ri := invocation.GetRuntimeInfo() + staticNodeJsBinaryBool, parseErr := strconv.ParseBool(staticNodeJsBinary) + if parseErr != nil { + debugLogger.Print("Failed to parse staticNodeJsBinary:", parseErr) + } + + err = ValidateGlibcVersion(debugLogger, utils.DefaultGlibcVersion(), runtime.GOOS, runtime.GOARCH, staticNodeJsBinaryBool) + 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) @@ -156,10 +173,6 @@ func legacycliWorkflow( invocation.GetAnalytics().AddExtensionIntegerValue("exitcode", exitError.ExitCode()) } - staticNodeJsBinaryBool, parseErr := strconv.ParseBool(staticNodeJsBinary) - if parseErr != nil { - debugLogger.Print("Failed to parse staticNodeJsBinary:", parseErr) - } invocation.GetAnalytics().AddExtensionBoolValue("static-nodejs-binary", staticNodeJsBinaryBool) return output, err @@ -189,3 +202,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, staticNodeJsBinaryBool bool) error { + // Skip validation on linuxstatic, non-Linux, or if glibc not detected + if glibcVersion == "" || os != "linux" || staticNodeJsBinaryBool { + 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 +} diff --git a/cliv2/pkg/basic_workflows/legacycli_test.go b/cliv2/pkg/basic_workflows/legacycli_test.go index 7d708badad..de4252e87c 100644 --- a/cliv2/pkg/basic_workflows/legacycli_test.go +++ b/cliv2/pkg/basic_workflows/legacycli_test.go @@ -2,19 +2,25 @@ package basic_workflows import ( "context" + "errors" "fmt" + "net/http" + "net/http/httptest" + "net/url" + "runtime" + "testing" + "github.com/golang/mock/gomock" "github.com/rs/zerolog" - "github.com/snyk/cli/cliv2/internal/proxy" + "github.com/snyk/error-catalog-golang-public/snyk_errors" "github.com/snyk/go-application-framework/pkg/configuration" "github.com/snyk/go-application-framework/pkg/mocks" "github.com/snyk/go-application-framework/pkg/networking" - "net/http" - "net/http/httptest" - "net/url" - "testing" + + "github.com/snyk/cli/cliv2/internal/proxy" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_finalizeArguments(t *testing.T) { @@ -92,3 +98,154 @@ func Test_proxyWithErrorHandler(t *testing.T) { }) } } + +func Test_ValidateGlibcVersion_doesNotApplyOnNonLinux(t *testing.T) { + // skip for Linux + if runtime.GOOS == "linux" { + t.Skip("Test only applicable on non-Linux") + } + + logger := zerolog.Nop() + err := ValidateGlibcVersion(&logger, "", "darwin", "amd64", false) + assert.NoError(t, err) +} + +func Test_ValidateGlibcVersion_validation(t *testing.T) { + t.Parallel() + + logger := zerolog.Nop() + + type glibcTest struct { + name string + version string + os string + arch string + expectedSnykErrCode string + staticNodeJsBinary bool + } + + t.Run("validates successfully", func(t *testing.T) { + t.Parallel() + + tests := []glibcTest{ + { + name: "version exactly minimum on amd64", + version: MIN_GLIBC_VERSION_LINUX_AMD64, + os: "linux", + arch: "amd64", + staticNodeJsBinary: false, + }, + { + name: "version newer than minimum on amd64", + version: "2.35", + os: "linux", + arch: "amd64", + staticNodeJsBinary: false, + }, + { + name: "version exactly minimum on arm64", + version: MIN_GLIBC_VERSION_LINUX_ARM64, + os: "linux", + arch: "arm64", + staticNodeJsBinary: false, + }, + { + name: "version newer than minimum on arm64", + version: "2.35", + os: "linux", + arch: "arm64", + staticNodeJsBinary: false, + }, + { + name: "invalid version format", + version: "glibc version", // not a valid semver string + os: "linux", + arch: "amd64", + staticNodeJsBinary: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actualErr := ValidateGlibcVersion(&logger, tt.version, tt.os, tt.arch, tt.staticNodeJsBinary) + + assert.NoError(t, actualErr) + }) + } + }) + + t.Run("returns error", func(t *testing.T) { + t.Parallel() + + testCases := []glibcTest{ + { + name: "version too old on amd64", + version: "2.27", // below minimum of 2.28 + os: "linux", + arch: "amd64", + expectedSnykErrCode: "SNYK-0010", + staticNodeJsBinary: false, + }, + { + name: "version too old on arm64", + version: "2.30", // below minimum of 2.31 + os: "linux", + arch: "arm64", + expectedSnykErrCode: "SNYK-0010", + staticNodeJsBinary: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actualErr := ValidateGlibcVersion(&logger, tc.version, tc.os, tc.arch, tc.staticNodeJsBinary) + require.NotNil(t, actualErr, "Expected error but got nil") + var snykErr snyk_errors.Error + require.True(t, errors.As(actualErr, &snykErr), "Expected snyk_errors.Error but got: %v", actualErr) + assert.Equal(t, tc.expectedSnykErrCode, snykErr.ErrorCode) + }) + } + }) + + t.Run("returns nil", func(t *testing.T) { + t.Parallel() + + testCases := []glibcTest{ + { + name: "invalid os", + version: "1.0.0", + os: "windows", + arch: "amd64", + staticNodeJsBinary: false, + }, + { + name: "invalid arch", + version: "1.0.0", + os: "linux", + arch: "riscv", + staticNodeJsBinary: false, + }, + { + name: "musl/Alpine", + version: "", + os: "linux", + arch: "amd64", + staticNodeJsBinary: false, + }, + { + name: "linux static builds", + version: "1.0.0", + os: "linux", + arch: "amd64", + staticNodeJsBinary: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actualErr := ValidateGlibcVersion(&logger, tc.version, tc.os, tc.arch, tc.staticNodeJsBinary) + require.Nil(t, actualErr, "Expected no error but got: %v", actualErr) + }) + } + }) +} diff --git a/help/cli-commands/monitor.md b/help/cli-commands/monitor.md index b1a31079f4..61920f5af9 100644 --- a/help/cli-commands/monitor.md +++ b/help/cli-commands/monitor.md @@ -10,8 +10,6 @@ The `snyk monitor` command creates a project in your Snyk account to be continuo Use the `monitor` command before integrating a project into production, to take a snapshot of the code to be monitored in order to avoid pushing vulnerabilities into production. Choose a test frequency in your Settings if you want to change the frequency from the default, which is daily. -A PR check will also do a test. - After running the `snyk monitor` command, log in to the Snyk website and view your projects to see the monitor. If you make changes to your code, you must run the `monitor` command again. @@ -149,7 +147,7 @@ Example: `$ snyk monitor --file=req.txt --package-manager=pip` For more information, see [Options for Python projects](https://docs.snyk.io/snyk-cli/commands/monitor#options-for-python-projects) -### `--source-dir=` +### `--source-dir=` Specify a directory of source code to be analyzed. Use with `--reachability`. @@ -235,7 +233,7 @@ This is an alias for `--project-tags` Use `--maven-aggregate-project` instead of `--all-projects` when scanning Maven aggregate projects, that is, projects that use modules and inheritance. -Using `--maven-aggregate-project` instructs Snyk to perform a compilation step to ensure all modules within the project are resolvable by the Maven reactor. This ensures a comprehensive scan that includes dependencies of all sub-modules. +Use `--maven-aggregate-project` to instruct Snyk to perform a compilation step to ensure all modules within the project are resolvable by the Maven reactor. This ensures a comprehensive scan that includes dependencies of all sub-modules. Be sure to run the scan in the same directory as the root `pom.xml` file. @@ -261,6 +259,14 @@ Auto-detect Maven, JAR, WAR, and AAR files recursively from the current folder. **Note**: Custom-built JAR files, even with open-source dependencies, are not supported. +### Maven-specific options + +Add the `--` option for Maven-specific options, followed by the Maven option. + +The following examples are not all-inclusive. For more details, see [Maven CLI options](https://maven.apache.org/ref/3.9.11/maven-embedder/cli.html) + +Examples: `-- -Dpkg_version=1.4` ; `-- -Dprofile=my-profile` ; `-- -s path/to/settings.xml` + ## Options for Gradle projects ### `--sub-project=`, `--gradle-sub-project=` diff --git a/help/cli-commands/redteam.md b/help/cli-commands/redteam.md index 2495601b0d..3e849f78a8 100644 --- a/help/cli-commands/redteam.md +++ b/help/cli-commands/redteam.md @@ -11,15 +11,15 @@ Redteam is potentially disruptive. Before running this command, ensure you: ## Prerequisites -- Requires an [internet connection](../../snyk-ci-cd-integrations/azure-pipelines-integration/regional-api-endpoints.md). -- Requires Snyk CLI v1.1300.1 (or later). -- Create your YAML [configuration file](redteam.md#configuration-file). -- If your Snyk CLI isn’t authenticated yet, authenticate by running the `snyk auth` command ([see docs](auth.md)). -- A role with Edit Organization permission, granted using a custom rule or by an Organization or Group administrator. +- An [internet connection](../../snyk-ci-cd-integrations/azure-pipelines-integration/regional-api-endpoints.md). +- Snyk CLI v1.1300.1 (or later). +- YAML [configuration file](redteam.md#configuration-file). +- The Snyk CLI must be authenticated. Run `snyk auth` if it is not authenticated. For guidance on the `snyk auth` command, visit the [Auth](auth.md) page. +- A role with the Edit Organization permission, granted by an Organization or Group Administrator, or using a custom role. ## Usage -Run the following command: `snyk redteam --experimental [