From 87dcfc11a5d32b5aad5f49ce780c32a0128eeef3 Mon Sep 17 00:00:00 2001 From: CatalinSnyk Date: Tue, 4 Feb 2025 11:21:51 +0200 Subject: [PATCH 1/2] fix: ensure nested typescript cli invocations forward errors --- cliv2/cmd/cliv2/errorhandling.go | 69 ++++++++++++ cliv2/cmd/cliv2/errorhandling_test.go | 103 ++++++++++++++++++ cliv2/cmd/cliv2/main.go | 9 +- cliv2/internal/cliv2/cliv2.go | 23 +++- cliv2/internal/cliv2/cliv2_test.go | 50 +++++++++ src/cli/ipc.ts | 51 ++++++++- src/cli/main.ts | 45 ++++---- test/jest/acceptance/iac/helpers.ts | 1 + .../acceptance/snyk-sbom/npm-options.spec.ts | 2 +- test/jest/acceptance/snyk-sbom/sbom.spec.ts | 4 +- test/setup-jest.ts | 4 +- 11 files changed, 328 insertions(+), 33 deletions(-) diff --git a/cliv2/cmd/cliv2/errorhandling.go b/cliv2/cmd/cliv2/errorhandling.go index 43a5058e14..72542cc705 100644 --- a/cliv2/cmd/cliv2/errorhandling.go +++ b/cliv2/cmd/cliv2/errorhandling.go @@ -2,11 +2,13 @@ package main import ( "errors" + "iter" "os/exec" "github.com/snyk/error-catalog-golang-public/cli" "github.com/snyk/error-catalog-golang-public/snyk_errors" + "github.com/snyk/cli/cliv2/internal/cliv2" cli_errors "github.com/snyk/cli/cliv2/internal/errors" ) @@ -32,3 +34,70 @@ func decorateError(err error) error { } return err } + +// getErrorMessage returns the appropriate error message for the specified error. Defaults to the standard error message method, +// but if the error matches the Error Catalog model, the returned value will become the detail field. +func getErrorMessage(err error) string { + message := err.Error() + snykErr := snyk_errors.Error{} + if errors.As(err, &snykErr) { + message = snykErr.Detail + } + + return message +} + +// errorHasBeenShown return whether the error was already presented by the Typescript CLI or not. +// This will iterate through the error chain to find if any of the errors in the chain has been shown. +func errorHasBeenShown(err error) bool { + for err := range iterErrorChain(err) { + snykErr := snyk_errors.Error{} + if errors.As(err, &snykErr) { + wasDisplayed, ok := snykErr.Meta[cliv2.ERROR_HAS_BEEN_DISPLAYED].(bool) + if !ok { + continue + } + + // we stop checking the chain if we find an error that was presented already. + if wasDisplayed { + return true + } + } + } + + // none of the errors in the chain were presented. + return false +} + +// IterErrorChain returns an iterator with all the errors from the error parameter, including the initial error. +// Taken from this proposal: https://github.com/golang/go/issues/66455 +// Eg: errA -> errB -> errC will yield an iterator with the following errors: +// ["errA -> errB -> errC", "errA", "errB", "errC"] +func iterErrorChain(err error) iter.Seq[error] { + return func(yield func(error) bool) { + yieldAll(err, yield) + } +} + +// yieldAll is the generator function that walks the error chain. +func yieldAll(err error, yield func(error) bool) bool { + for err != nil { + if !yield(err) { + return false + } + switch x := err.(type) { + case interface{ Unwrap() error }: + err = x.Unwrap() + case interface{ Unwrap() []error }: + for _, err := range x.Unwrap() { + if !yieldAll(err, yield) { + return false + } + } + return true + default: + return true + } + } + return true +} diff --git a/cliv2/cmd/cliv2/errorhandling_test.go b/cliv2/cmd/cliv2/errorhandling_test.go index c5978e6573..41a74ecbfc 100644 --- a/cliv2/cmd/cliv2/errorhandling_test.go +++ b/cliv2/cmd/cliv2/errorhandling_test.go @@ -2,6 +2,7 @@ package main import ( "errors" + "fmt" "os" "os/exec" "testing" @@ -9,7 +10,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/snyk/error-catalog-golang-public/cli" + "github.com/snyk/error-catalog-golang-public/snyk_errors" + "github.com/snyk/cli/cliv2/internal/cliv2" cli_errors "github.com/snyk/cli/cliv2/internal/errors" ) @@ -45,3 +48,103 @@ func Test_decorateError(t *testing.T) { assert.ErrorAs(t, actualErrr, &expectedError) }) } + +func Test_iterErrorChain(t *testing.T) { + t.Run("wrapping errors.Join", func(t *testing.T) { + err1 := errors.New("first error") + err2 := fmt.Errorf("second error") + joined := errors.Join(err1, err2) + + errors := []error{} + for err := range iterErrorChain(joined) { + errors = append(errors, err) + } + + assert.Contains(t, errors, joined) + assert.Contains(t, errors, err1) + assert.Contains(t, errors, err2) + }) + + t.Run("wrapping using fmt.Errorf", func(t *testing.T) { + err1 := errors.New("first error") + err2 := fmt.Errorf("second error") + wrapped := fmt.Errorf("wrapped: %w %w", err1, err2) + + errors := []error{} + for err := range iterErrorChain(wrapped) { + errors = append(errors, err) + } + + assert.Contains(t, errors, wrapped) + assert.Contains(t, errors, err1) + assert.Contains(t, errors, err2) + }) + + t.Run("combined wrapping", func(t *testing.T) { + err1 := errors.New("first error") + err2 := fmt.Errorf("second error") + joined := errors.Join(err1, err2) + + cause := errors.New("cause") + snykErr := snyk_errors.Error{ + Title: "error struct", + Cause: cause, + } + + wrapped := fmt.Errorf("wrapped: %w %w", snykErr, joined) + + errors := []error{} + for err := range iterErrorChain(wrapped) { + errors = append(errors, err) + } + + assert.Contains(t, errors, wrapped) + assert.Contains(t, errors, snykErr) + assert.Contains(t, errors, cause) + assert.Contains(t, errors, joined) + assert.Contains(t, errors, err2) + assert.Contains(t, errors, err1) + }) +} + +func Test_errorHasBeenShown(t *testing.T) { + t.Run("has been displayed", func(t *testing.T) { + err := snyk_errors.Error{ + Meta: map[string]any{ + cliv2.ERROR_HAS_BEEN_DISPLAYED: true, + }, + } + + hasBeenShown := errorHasBeenShown(err) + assert.Equal(t, hasBeenShown, true) + }) + + t.Run("unset value", func(t *testing.T) { + err := snyk_errors.Error{ + Meta: map[string]any{}, + } + + hasBeenShown := errorHasBeenShown(err) + assert.Equal(t, hasBeenShown, false) + }) + + t.Run("multiple errors in chain", func(t *testing.T) { + first := snyk_errors.Error{ + Meta: map[string]any{ + cliv2.ERROR_HAS_BEEN_DISPLAYED: false, + }, + } + second := snyk_errors.Error{ + Meta: map[string]any{}, + } + third := snyk_errors.Error{ + Meta: map[string]any{ + cliv2.ERROR_HAS_BEEN_DISPLAYED: true, + }, + } + chain := fmt.Errorf("exit error: %w", errors.Join(first, second, third)) + + hasBeenShown := errorHasBeenShown(chain) + assert.Equal(t, hasBeenShown, true) + }) +} diff --git a/cliv2/cmd/cliv2/main.go b/cliv2/cmd/cliv2/main.go index ba3e3d0a23..ef0d74227c 100644 --- a/cliv2/cmd/cliv2/main.go +++ b/cliv2/cmd/cliv2/main.go @@ -457,14 +457,16 @@ func displayError(err error, userInterface ui.UserInterface, config configuratio if err != nil { _, isExitError := err.(*exec.ExitError) _, isErrorWithCode := err.(*cli_errors.ErrorWithExitCode) - if isExitError || isErrorWithCode { + if isExitError || isErrorWithCode || errorHasBeenShown(err) { return } if config.GetBool(output_workflow.OUTPUT_CONFIG_KEY_JSON) { + message := getErrorMessage(err) + jsonError := JsonErrorStruct{ Ok: false, - ErrorMsg: err.Error(), + ErrorMsg: message, Path: globalConfiguration.GetString(configuration.INPUT_DIRECTORY), } @@ -543,6 +545,9 @@ func MainWithErrorCode() (int, []error) { outputWorkflow, _ := globalEngine.GetWorkflow(localworkflows.WORKFLOWID_OUTPUT_WORKFLOW) outputFlags := workflow.FlagsetFromConfigurationOptions(outputWorkflow.GetConfigurationOptions()) rootCommand.PersistentFlags().AddFlagSet(outputFlags) + // add output flags as persistent flags + _ = rootCommand.ParseFlags(os.Args) + globalConfiguration.AddFlagSet(rootCommand.LocalFlags()) // add workflows as commands createCommandsForWorkflows(rootCommand, globalEngine) diff --git a/cliv2/internal/cliv2/cliv2.go b/cliv2/internal/cliv2/cliv2.go index 8b09357d46..ae132e6849 100644 --- a/cliv2/internal/cliv2/cliv2.go +++ b/cliv2/internal/cliv2/cliv2.go @@ -25,6 +25,7 @@ import ( "github.com/snyk/error-catalog-golang-public/snyk_errors" "github.com/snyk/go-application-framework/pkg/configuration" "github.com/snyk/go-application-framework/pkg/instrumentation" + "github.com/snyk/go-application-framework/pkg/local_workflows/output_workflow" "github.com/snyk/go-application-framework/pkg/runtimeinfo" "github.com/snyk/go-application-framework/pkg/utils" @@ -61,7 +62,10 @@ const ( V2_ABOUT Handler = iota ) -const configKeyErrFile = "INTERNAL_ERR_FILE_PATH" +const ( + configKeyErrFile = "INTERNAL_ERR_FILE_PATH" + ERROR_HAS_BEEN_DISPLAYED = "hasBeenDisplayed" +) func NewCLIv2(config configuration.Configuration, debugLogger *log.Logger, ri runtimeinfo.RuntimeInfo) (*CLI, error) { cacheDirectory := config.GetString(configuration.CACHE_PATH) @@ -482,9 +486,12 @@ func (c *CLI) getErrorFromFile(errFilePath string) (data error, err error) { } if len(jsonErrors) != 0 { + hasBeenDisplayed := GetErrorDisplayStatus(c.globalConfig) + errs := make([]error, len(jsonErrors)+1) for _, jerr := range jsonErrors { jerr.Meta["orign"] = "Typescript-CLI" + jerr.Meta[ERROR_HAS_BEEN_DISPLAYED] = hasBeenDisplayed errs = append(errs, jerr) } @@ -562,3 +569,17 @@ func DetermineInputDirectory(args []string) string { } return "" } + +// GetErrorDisplayStatus computes whether the IPC error was displayed by the TS CLI or not. It accounts for +// the usage of STDIO and the presence of the JSON flag when the Legacy CLI was invoked. +func GetErrorDisplayStatus(config configuration.Configuration) bool { + useSTDIO := config.GetBool(configuration.WORKFLOW_USE_STDIO) + jsonEnabled := config.GetBool(output_workflow.OUTPUT_CONFIG_KEY_JSON) + + hasBeenDisplayed := false + if useSTDIO && jsonEnabled { + hasBeenDisplayed = true + } + + return hasBeenDisplayed +} diff --git a/cliv2/internal/cliv2/cliv2_test.go b/cliv2/internal/cliv2/cliv2_test.go index 6420020510..cf608694e7 100644 --- a/cliv2/internal/cliv2/cliv2_test.go +++ b/cliv2/internal/cliv2/cliv2_test.go @@ -3,6 +3,7 @@ package cliv2_test import ( "context" "errors" + "fmt" "io" "log" "os" @@ -15,6 +16,7 @@ import ( "github.com/snyk/go-application-framework/pkg/app" "github.com/snyk/go-application-framework/pkg/configuration" + "github.com/snyk/go-application-framework/pkg/local_workflows/output_workflow" "github.com/snyk/go-application-framework/pkg/runtimeinfo" "github.com/snyk/go-application-framework/pkg/utils" @@ -537,3 +539,51 @@ func Test_determineInputDirectory(t *testing.T) { assert.Equal(t, expected, actual) }) } + +func Test_GetErrorDisplayStatus(t *testing.T) { + // Decision table: + // stdin | json | hasBeenDisplayed + // --------------------------------- + // true | true | true + // true | false | false + // false | * | false + + tests := []struct { + stdin bool + json bool + expected bool + }{ + { + stdin: true, + json: true, + expected: true, + }, + { + stdin: true, + json: false, + expected: false, + }, + { + stdin: false, + json: true, + expected: false, + }, + { + stdin: false, + json: false, + expected: false, + }, + } + + for _, tc := range tests { + testName := fmt.Sprintf("%t + %t = %t", tc.stdin, tc.json, tc.expected) + t.Run(testName, func(t *testing.T) { + config := configuration.NewWithOpts(configuration.WithAutomaticEnv()) + config.Set(configuration.WORKFLOW_USE_STDIO, tc.stdin) + config.Set(output_workflow.OUTPUT_CONFIG_KEY_JSON, tc.json) + + hasBeenDisplayed := cliv2.GetErrorDisplayStatus(config) + assert.Equal(t, hasBeenDisplayed, tc.expected) + }) + } +} diff --git a/src/cli/ipc.ts b/src/cli/ipc.ts index fecaeede8f..8bc5059183 100644 --- a/src/cli/ipc.ts +++ b/src/cli/ipc.ts @@ -11,9 +11,10 @@ const debug = Debug('snyk'); * Sends the specified error back at the Golang CLI, by writting it to the temporary error file. Errors that are not * inlcuded in the Error Catalog will be wraped in a generic model. * @param err {Error} The error to be sent to the Golang CLI + * @param isJson {boolean} If the prameter is set, the meta field "supressJsonOutput" will also be set, supressing the Golang CLI from displaying this error. * @returns {Promise} The result of the operation as a boolean value */ -export async function sendError(err: Error): Promise { +export async function sendError(err: Error, isJson: boolean): Promise { if (!ERROR_FILE_PATH) { debug('Error file path not set.'); return false; @@ -22,17 +23,22 @@ export async function sendError(err: Error): Promise { // @ts-expect-error Using this instead of 'instanceof' since the error might be caught from external CLI plugins. // See: https://github.com/snyk/error-catalog/blob/main/packages/error-catalog-nodejs/src/problem-error.ts#L17-L19 if (!err.isErrorCatalogError) { - const detail: string = stripAnsi(legacyErrors.message(err)); + let message = legacyErrors.message(err); + + if (isJson) { + // If the JSON flag is set, the error message field is already JSON formated, so we need to extract it from there. + message = extractMessageFromJson(err.message); + } + + const detail = stripAnsi(message); if (!detail || detail.trim().length === 0) return false; err = new CLI.GeneralCLIFailureError(detail); - // @ts-expect-error Overriding with specific err code from CustomErrors, or 0 for + // @ts-expect-error Overriding the HTTP status field. err.metadata.status = 0; } - const data = (err as ProblemError) - .toJsonApi('error-catalog-ipc-instance?') - .body(); + const data = (err as ProblemError).toJsonApi().body(); try { await writeFile(ERROR_FILE_PATH, JSON.stringify(data)); @@ -43,3 +49,36 @@ export async function sendError(err: Error): Promise { return true; } + +/** + * Extract the error message from the JSON formated errror message. + * @param message The Error.message field. + * @returns The extracted message. + * @example + * Error { message: "{\"ok\":false,\"error\":\"This is the actual error message.\",\"path\":\"./\"}" } + */ +function extractMessageFromJson(message: string): string { + try { + let msgObject = JSON.parse(message); + + /** IAC JSON mode can contain an array of error objects, we will use just the first one to send to the IPC. + * We are checking for the error field, since they can also combine results with the mentioned errors inside the same array. + * Example: + * Error { message: '[{"filesystemPolicy": false,"vulnerabilities": [],"targetFile": "rule_test.json","projectName": "test",...]}, + * {"ok": false,"code": 1010,"error": "Could not find any valid IaC files","path": "non-existing-dir"}]' + */ + if (Array.isArray(msgObject)) { + msgObject = msgObject.find((msgObj) => msgObj.error); + if (!msgObject) return message; + } + + if (msgObject.error) return msgObject.error; + if (msgObject.message) return msgObject.message; + + return message; + } catch (e) { + // Not actually JSON if it gets here. + debug('Failed to extract message from JSON: ', e); + return message; + } +} diff --git a/src/cli/main.ts b/src/cli/main.ts index 89485f2297..5fc9e70b30 100755 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -138,32 +138,37 @@ async function handleError(args, error) { } /** - * Exceptions from sending errors - * - json/sarif flags - this would just stringify the content as the error message; could look into outputing the Error Catalog JSON + * Exceptions from sending errors to IPC + * - sarif - no error message or details are present in the payload * - vulnsFound - issues are treated as errors (exit code 1), this should be some nice pretty formated output for users. */ const errorSent = - args.options.json || args.options.sarif || vulnsFound + vulnsFound || args.options.sarif ? false - : sendError(error); - if (!errorSent) { - if (args.options.debug && !args.options.json) { + : await sendError(error, args.options.json); + + // JSON output flow + if ( + args.options.json && + !(error instanceof UnsupportedOptionCombinationError) + ) { + const output = vulnsFound + ? error.message + : stripAnsi(error.json || error.stack); + if (error.jsonPayload) { + new JsonStreamStringify(error.jsonPayload, undefined, 2).pipe( + process.stdout, + ); + } else { + console.log(output); + } + // If the IPC communication failed, we default back to the original output flow + } else if (!errorSent) { + // Debug output flow + if (args.options.debug) { const output = vulnsFound ? error.message : error.stack; console.log(output); - } else if ( - args.options.json && - !(error instanceof UnsupportedOptionCombinationError) - ) { - const output = vulnsFound - ? error.message - : stripAnsi(error.json || error.stack); - if (error.jsonPayload) { - new JsonStreamStringify(error.jsonPayload, undefined, 2).pipe( - process.stdout, - ); - } else { - console.log(output); - } + // Human readable output/sarif } else { if (!args.options.quiet) { const result = errors.message(error); diff --git a/test/jest/acceptance/iac/helpers.ts b/test/jest/acceptance/iac/helpers.ts index f89652b8a3..20225e2411 100644 --- a/test/jest/acceptance/iac/helpers.ts +++ b/test/jest/acceptance/iac/helpers.ts @@ -1,5 +1,6 @@ import { exec } from 'child_process'; import { join } from 'path'; +import '../../util/runSnykCLI'; // Neede for TS errors regarding the .toContainText matcher import { fakeServer } from '../../../acceptance/fake-server'; /** diff --git a/test/jest/acceptance/snyk-sbom/npm-options.spec.ts b/test/jest/acceptance/snyk-sbom/npm-options.spec.ts index a8719509a8..6d34e99800 100644 --- a/test/jest/acceptance/snyk-sbom/npm-options.spec.ts +++ b/test/jest/acceptance/snyk-sbom/npm-options.spec.ts @@ -68,7 +68,7 @@ describe('snyk sbom: npm options (mocked server only)', () => { expect(code).toEqual(2); expect(stdout).toContainText( - 'An error occurred while running the underlying analysis needed to generate the SBOM.', + 'Dependency snyk was not found in package-lock.json.', ); expect(stderr).toContainText( 'OutOfSyncError: Dependency snyk was not found in package-lock.json.', diff --git a/test/jest/acceptance/snyk-sbom/sbom.spec.ts b/test/jest/acceptance/snyk-sbom/sbom.spec.ts index 055dd1c587..de88dd3cf5 100644 --- a/test/jest/acceptance/snyk-sbom/sbom.spec.ts +++ b/test/jest/acceptance/snyk-sbom/sbom.spec.ts @@ -1,5 +1,5 @@ import * as fs from 'fs'; - +import { NoSupportedManifestsFoundError } from '../../../../src/lib/errors/no-supported-manifests-found'; import { createProject, createProjectFromWorkspace, @@ -212,7 +212,7 @@ describe('snyk sbom (mocked server only)', () => { expect(code).toBe(3); expect(stdout).toContainText( - 'An error occurred while running the underlying analysis needed to generate the SBOM.', + NoSupportedManifestsFoundError([project.path()]).message, ); }); }); diff --git a/test/setup-jest.ts b/test/setup-jest.ts index 4b88d96eb9..6f094a8eb3 100644 --- a/test/setup-jest.ts +++ b/test/setup-jest.ts @@ -1,7 +1,9 @@ +const stripAnsi = require('strip-ansi'); + expect.extend({ toContainText(received: string, expected: string) { const [cleanReceived, cleanExpected] = [received, expected].map((t) => - t.replace(/\s/g, ''), + stripAnsi(t.replace(/\s/g, '')), ); const pass = cleanReceived.includes(cleanExpected); return { From 2ad1ac397248fd18097a7c84157f8d2d3156b8f7 Mon Sep 17 00:00:00 2001 From: Jason Luong Date: Mon, 3 Feb 2025 12:46:02 +0000 Subject: [PATCH 2/2] feat: compose error catalog errors in TS custom errors --- package-lock.json | 86 ++++++++++++--- package.json | 2 +- src/cli/commands/describe.ts | 2 + .../assert-iac-options-flag.ts | 14 ++- .../test/iac/local-execution/file-loader.ts | 3 + .../test/iac/local-execution/file-parser.ts | 2 + .../test/iac/local-execution/file-scanner.ts | 3 + .../test/iac/local-execution/index.ts | 2 + .../test/iac/local-execution/local-cache.ts | 6 + .../org-settings/get-iac-org-settings.ts | 2 + .../parsers/terraform-file-parser.ts | 2 + .../parsers/terraform-plan-parser.ts | 2 + .../process-results/results-formatter.ts | 2 + .../iac/local-execution/rules/oci-pull.ts | 9 +- .../test/iac/local-execution/rules/rules.ts | 8 +- .../iac/local-execution/usage-tracking.ts | 2 + .../test/iac/local-execution/yaml-parser.ts | 3 + src/cli/commands/test/iac/output.ts | 3 + src/cli/commands/test/iac/scan.ts | 4 +- src/cli/ipc.ts | 11 +- src/cli/main.ts | 4 +- src/lib/errors/authentication-failed-error.ts | 2 + src/lib/errors/bad-gateway-error.ts | 2 + src/lib/errors/connection-timeout-error.ts | 2 + src/lib/errors/custom-error.ts | 15 +++ .../errors/docker-image-not-found-error.ts | 2 + src/lib/errors/empty-sarif-output-error.ts | 2 + src/lib/errors/exclude-flag-bad-input.ts | 2 + src/lib/errors/exclude-flag-invalid-input.ts | 2 + src/lib/errors/fail-on-error.ts.ts | 2 + .../failed-to-get-vulnerabilities-error.ts | 2 + ...-to-get-vulns-from-unavailable-resource.ts | 4 +- src/lib/errors/failed-to-load-policy-error.ts | 2 + src/lib/errors/failed-to-run-test-error.ts | 10 +- src/lib/errors/file-flag-bad-input.ts | 2 + src/lib/errors/formatted-custom-error.ts | 11 ++ src/lib/errors/internal-server-error.ts | 2 + .../errors/invalid-detection-depth-value.ts | 2 + src/lib/errors/invalid-remote-url-error.ts | 2 + .../json-file-output-bad-input-error.ts | 2 + .../errors/misconfigured-auth-in-ci-error.ts | 2 + src/lib/errors/missing-api-token.ts | 2 + src/lib/errors/missing-arg-error.ts | 2 + src/lib/errors/missing-option-error.ts | 2 + src/lib/errors/missing-targetfile-error.ts | 2 + src/lib/errors/monitor-error.ts | 2 + .../errors/no-supported-manifests-found.ts | 2 + .../errors/no-supported-sast-files-found.ts | 4 + src/lib/errors/not-found-error.ts | 2 + src/lib/errors/not-supported-by-ecosystem.ts | 2 + src/lib/errors/policy-not-found-error.ts | 2 + src/lib/errors/service-unavailable-error.ts | 2 + src/lib/errors/token-expired-error.ts | 2 + src/lib/errors/too-many-vuln-paths.ts | 2 + .../errors/unsupported-entitlement-error.ts | 2 + .../unsupported-feature-for-org-error.ts | 3 +- .../unsupported-option-combination-error.ts | 2 + .../unsupported-package-manager-error.ts | 4 +- src/lib/errors/validation-error.ts | 2 + src/lib/iac/service-mappings.ts | 2 + src/lib/iac/test/v2/errors.ts | 2 + .../v2/local-cache/policy-engine/download.ts | 3 + .../local-cache/policy-engine/lookup-local.ts | 2 + src/lib/iac/test/v2/local-cache/utils.ts | 2 + src/lib/iac/test/v2/output.ts | 21 ++-- src/lib/iac/test/v2/scan/index.ts | 2 + src/lib/snyk-test/run-test.ts | 4 +- .../acceptance/iac/iac-entitlement.spec.ts | 9 +- .../acceptance/snyk-sbom/npm-options.spec.ts | 4 +- test/jest/acceptance/snyk-sbom/sbom.spec.ts | 11 +- test/jest/unit/cli/ipc.spec.ts | 104 ++++++++++++++++++ 71 files changed, 400 insertions(+), 51 deletions(-) create mode 100644 test/jest/unit/cli/ipc.spec.ts diff --git a/package-lock.json b/package-lock.json index 0bf96d4258..3e027b7b48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@snyk/code-client": "^4.23.5", "@snyk/dep-graph": "^2.7.4", "@snyk/docker-registry-v2-client": "^2.11.0", - "@snyk/error-catalog-nodejs-public": "^5.37.0", + "@snyk/error-catalog-nodejs-public": "^5.44.0", "@snyk/fix": "file:packages/snyk-fix", "@snyk/gemfile": "1.2.0", "@snyk/snyk-cocoapods-plugin": "2.5.3", @@ -144,6 +144,62 @@ "node": "^18" } }, + "../error-catalog": { + "name": "@snyk/error-catalog", + "version": "1.0.0", + "extraneous": true, + "license": "Apache-2.0", + "dependencies": { + "@octokit/rest": "^21.1.0", + "@snyk/log": "^5.3.0", + "express": "^4.21.2", + "handlebars": "^4.7.8", + "http-status-codes": "^2.3.0", + "ts-dedent": "^2.2.0", + "tslib": "^2.8.1", + "uuid": "^11.0.5" + }, + "devDependencies": { + "@nx/eslint-plugin": "^20.4.0", + "@nx/jest": "20.4.0", + "@nx/js": "20.4.0", + "@nx/linter": "19.8.4", + "@nx/workspace": "20.4.0", + "@rossmcewan/semantic-release-nx": "^0.2.0", + "@semantic-release/exec": "^6.0.3", + "@types/express": "^5.0.0", + "@types/jest": "29.5.14", + "@types/node": "^22.12.0", + "@types/uuid": "^10.0.0", + "@typescript-eslint/eslint-plugin": "^8.22.0", + "eslint": "9.19.0", + "fs-extra": "^11.3.0", + "jest": "29.7.0", + "jest-environment-jsdom": "^29.7.0", + "nx": "20.4.0", + "prettier": "^2.8.8", + "semantic-release": "^20.1.3", + "semver": "^7.7.0", + "simple-git": "^3.27.0", + "stream-buffers": "^3.0.3", + "ts-jest": "29.2.5", + "ts-node": "10.9.2", + "typescript": "5.7.3" + }, + "engines": { + "node": ">=16" + } + }, + "../error-catalog/packages/error-catalog-nodejs-public": { + "name": "@snyk/error-catalog-nodejs-public", + "version": "0.0.1", + "extraneous": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.1", + "uuid": "^11.0.5" + } + }, "node_modules/@alcalzone/ansi-tokenize": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz", @@ -3077,13 +3133,13 @@ } }, "node_modules/@snyk/error-catalog-nodejs-public": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/@snyk/error-catalog-nodejs-public/-/error-catalog-nodejs-public-5.37.0.tgz", - "integrity": "sha512-63SNBN5XC0fD2zmFYJtMtW8jg3kf774CCCOqMikyZW1rknkxnLiTFbGgA/Xwwjse3TobGxlltfBHt0yPpcRSPw==", + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/@snyk/error-catalog-nodejs-public/-/error-catalog-nodejs-public-5.44.0.tgz", + "integrity": "sha512-lCs8k4FeRbbsBwjPopZFisb9Yv9sIMYUArOUUw0G3lO82ih8MfLUoj8ilnw9aTl91/T5cjPHmP/nX5jsWYzz4Q==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.1", - "uuid": "^11.0.5" + "uuid": "^11.1.0" } }, "node_modules/@snyk/error-catalog-nodejs-public/node_modules/tslib": { @@ -3093,9 +3149,9 @@ "license": "0BSD" }, "node_modules/@snyk/error-catalog-nodejs-public/node_modules/uuid": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", - "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -26638,12 +26694,12 @@ } }, "@snyk/error-catalog-nodejs-public": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/@snyk/error-catalog-nodejs-public/-/error-catalog-nodejs-public-5.37.0.tgz", - "integrity": "sha512-63SNBN5XC0fD2zmFYJtMtW8jg3kf774CCCOqMikyZW1rknkxnLiTFbGgA/Xwwjse3TobGxlltfBHt0yPpcRSPw==", + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/@snyk/error-catalog-nodejs-public/-/error-catalog-nodejs-public-5.44.0.tgz", + "integrity": "sha512-lCs8k4FeRbbsBwjPopZFisb9Yv9sIMYUArOUUw0G3lO82ih8MfLUoj8ilnw9aTl91/T5cjPHmP/nX5jsWYzz4Q==", "requires": { "tslib": "^2.8.1", - "uuid": "^11.0.5" + "uuid": "^11.1.0" }, "dependencies": { "tslib": { @@ -26652,9 +26708,9 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "uuid": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", - "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==" + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==" } } }, diff --git a/package.json b/package.json index eb2d0774a3..86ef39f619 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "@snyk/code-client": "^4.23.5", "@snyk/dep-graph": "^2.7.4", "@snyk/docker-registry-v2-client": "^2.11.0", - "@snyk/error-catalog-nodejs-public": "^5.37.0", + "@snyk/error-catalog-nodejs-public": "^5.44.0", "@snyk/fix": "file:packages/snyk-fix", "@snyk/gemfile": "1.2.0", "@snyk/snyk-cocoapods-plugin": "2.5.3", diff --git a/src/cli/commands/describe.ts b/src/cli/commands/describe.ts index 5bd2183f24..63551ae5a0 100644 --- a/src/cli/commands/describe.ts +++ b/src/cli/commands/describe.ts @@ -17,6 +17,7 @@ import { DCTL_EXIT_CODES, runDriftCTL } from '../../lib/iac/drift/driftctl'; import { IaCErrorCodes } from './test/iac/local-execution/types'; import { getErrorStringCode } from './test/iac/local-execution/error-utils'; import { DescribeOptions } from '../../lib/iac/types'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; export class FlagError extends CustomError { constructor(flag: string) { @@ -25,6 +26,7 @@ export class FlagError extends CustomError { this.code = IaCErrorCodes.FlagError; this.strCode = getErrorStringCode(this.code); this.userMessage = msg; + this.errorCatalog = new CLI.InvalidFlagOptionError(''); } } export default async (...args: MethodArgs): Promise => { diff --git a/src/cli/commands/test/iac/local-execution/assert-iac-options-flag.ts b/src/cli/commands/test/iac/local-execution/assert-iac-options-flag.ts index 472f961483..c7e23d1b8a 100644 --- a/src/cli/commands/test/iac/local-execution/assert-iac-options-flag.ts +++ b/src/cli/commands/test/iac/local-execution/assert-iac-options-flag.ts @@ -9,6 +9,7 @@ import { } from './types'; import { Options, TestOptions } from '../../../../../lib/types'; import { IacV2Name } from '../../../../../lib/iac/constants'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; const keys: (keyof IaCTestFlags)[] = [ 'org', @@ -67,6 +68,7 @@ export class FlagError extends CustomError { this.code = IaCErrorCodes.FlagError; this.strCode = getErrorStringCode(this.code); this.userMessage = msg; + this.errorCatalog = new CLI.InvalidFlagOptionError(''); } } @@ -78,6 +80,7 @@ export class IntegratedFlagError extends CustomError { this.code = IaCErrorCodes.FlagError; this.strCode = getErrorStringCode(this.code); this.userMessage = msg; + this.errorCatalog = new CLI.InvalidFlagOptionError(''); } } @@ -94,6 +97,7 @@ export class FeatureFlagError extends CustomError { this.code = IaCErrorCodes.FeatureFlagError; this.strCode = getErrorStringCode(this.code); this.userMessage = msg; + this.errorCatalog = new CLI.InvalidFlagOptionError(''); } } @@ -105,29 +109,34 @@ export class FlagValueError extends CustomError { this.code = IaCErrorCodes.FlagValueError; this.strCode = getErrorStringCode(this.code); this.userMessage = msg; + this.errorCatalog = new CLI.InvalidFlagOptionError(''); } } export class UnsupportedEntitlementFlagError extends CustomError { constructor(key: string, entitlementName: string) { const flag = getFlagName(key); + const msg = `Flag "${flag}" is currently not supported for this org. To enable it, please contact snyk support.`; super( `Unsupported flag: ${flag} - Missing the ${entitlementName} entitlement`, ); this.code = IaCErrorCodes.UnsupportedEntitlementFlagError; this.strCode = getErrorStringCode(this.code); - this.userMessage = `Flag "${flag}" is currently not supported for this org. To enable it, please contact snyk support.`; + this.userMessage = msg; + this.errorCatalog = new CLI.InvalidFlagOptionError(''); } } export class UnsupportedEntitlementCommandError extends CustomError { constructor(key: string, entitlementName: string) { + const usrMsg = `Command "${key}" is currently not supported for this org. To enable it, please contact snyk support.`; super( `Unsupported command: ${key} - Missing the ${entitlementName} entitlement`, ); this.code = IaCErrorCodes.UnsupportedEntitlementFlagError; this.strCode = getErrorStringCode(this.code); - this.userMessage = `Command "${key}" is currently not supported for this org. To enable it, please contact snyk support.`; + this.userMessage = usrMsg; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } @@ -213,5 +222,6 @@ export class InvalidArgumentError extends CustomError { this.code = IaCErrorCodes.InvalidArgumentError; this.strCode = getErrorStringCode(this.code); this.userMessage = msg; + this.errorCatalog = new CLI.InvalidFlagOptionError(''); } } diff --git a/src/cli/commands/test/iac/local-execution/file-loader.ts b/src/cli/commands/test/iac/local-execution/file-loader.ts index d1f9955716..93eab54cb6 100644 --- a/src/cli/commands/test/iac/local-execution/file-loader.ts +++ b/src/cli/commands/test/iac/local-execution/file-loader.ts @@ -4,6 +4,7 @@ import { IacFileTypes } from '../../../../../lib/iac/constants'; import { CustomError } from '../../../../../lib/errors'; import { getErrorStringCode } from './error-utils'; import { getFileType } from './directory-loader'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; const DEFAULT_ENCODING = 'utf-8'; @@ -52,6 +53,7 @@ export class NoFilesToScanError extends CustomError { this.strCode = getErrorStringCode(this.code); this.userMessage = 'Could not find any valid infrastructure as code files. Supported file extensions are tf, yml, yaml & json.\nMore information can be found by running `snyk iac test --help` or through our documentation:\nhttps://support.snyk.io/hc/en-us/articles/360012429477-Test-your-Kubernetes-files-with-our-CLI-tool\nhttps://support.snyk.io/hc/en-us/articles/360013723877-Test-your-Terraform-files-with-our-CLI-tool'; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } @@ -63,5 +65,6 @@ export class FailedToLoadFileError extends CustomError { this.strCode = getErrorStringCode(this.code); this.filename = filename; this.userMessage = `We were unable to read file "${filename}" for scanning. Please ensure that it is readable.`; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } diff --git a/src/cli/commands/test/iac/local-execution/file-parser.ts b/src/cli/commands/test/iac/local-execution/file-parser.ts index 42e8dfbf74..84a832da09 100644 --- a/src/cli/commands/test/iac/local-execution/file-parser.ts +++ b/src/cli/commands/test/iac/local-execution/file-parser.ts @@ -24,6 +24,7 @@ import hclToJsonV2 from './parsers/hcl-to-json-v2'; import { IacProjectType } from '../../../../../lib/iac/constants'; import * as Debug from 'debug'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; const debug = Debug('snyk-test'); @@ -168,5 +169,6 @@ export class UnsupportedFileTypeError extends CustomError { this.code = IaCErrorCodes.UnsupportedFileTypeError; this.strCode = getErrorStringCode(this.code); this.userMessage = `Unable to process the file with extension ${fileType}. Supported file extensions are tf, yml, yaml & json.\nMore information can be found by running \`snyk iac test --help\` or through our documentation:\nhttps://support.snyk.io/hc/en-us/articles/360012429477-Test-your-Kubernetes-files-with-our-CLI-tool\nhttps://support.snyk.io/hc/en-us/articles/360013723877-Test-your-Terraform-files-with-our-CLI-tool`; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } diff --git a/src/cli/commands/test/iac/local-execution/file-scanner.ts b/src/cli/commands/test/iac/local-execution/file-scanner.ts index 399d129631..04c0743731 100644 --- a/src/cli/commands/test/iac/local-execution/file-scanner.ts +++ b/src/cli/commands/test/iac/local-execution/file-scanner.ts @@ -13,6 +13,7 @@ import { CustomError } from '../../../../../lib/errors'; import { getErrorStringCode } from './error-utils'; import { IacFileInDirectory } from '../../../../../lib/types'; import { SEVERITIES } from '../../../../../lib/snyk-test/common'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; export async function scanFiles(parsedFiles: Array): Promise<{ scannedFiles: IacFileScanResult[]; @@ -170,6 +171,7 @@ export class FailedToBuildPolicyEngine extends CustomError { this.strCode = getErrorStringCode(this.code); this.userMessage = 'We were unable to run the test. Please run the command again with the `-d` flag and contact support@snyk.io with the contents of the output'; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } export class FailedToExecutePolicyEngine extends CustomError { @@ -179,5 +181,6 @@ export class FailedToExecutePolicyEngine extends CustomError { this.strCode = getErrorStringCode(this.code); this.userMessage = 'We were unable to run the test. Please run the command again with the `-d` flag and contact support@snyk.io with the contents of the output'; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } diff --git a/src/cli/commands/test/iac/local-execution/index.ts b/src/cli/commands/test/iac/local-execution/index.ts index 97a4d081ab..d854f4603f 100644 --- a/src/cli/commands/test/iac/local-execution/index.ts +++ b/src/cli/commands/test/iac/local-execution/index.ts @@ -32,6 +32,7 @@ import { CustomError } from '../../../../../lib/errors'; import { getErrorStringCode } from './error-utils'; import { NoFilesToScanError } from './file-loader'; import { Tag } from '../../../../../lib/types'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; // this method executes the local processing engine and then formats the results to adapt with the CLI output. // this flow is the default GA flow for IAC scanning. @@ -184,5 +185,6 @@ export class InvalidVarFilePath extends CustomError { this.code = IaCErrorCodes.InvalidVarFilePath; this.strCode = getErrorStringCode(this.code); this.userMessage = `We were unable to locate a variable definitions file at: "${path}". The file at the provided path does not exist`; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } diff --git a/src/cli/commands/test/iac/local-execution/local-cache.ts b/src/cli/commands/test/iac/local-execution/local-cache.ts index 3962bda4d3..ead6e91c00 100644 --- a/src/cli/commands/test/iac/local-execution/local-cache.ts +++ b/src/cli/commands/test/iac/local-execution/local-cache.ts @@ -10,6 +10,7 @@ import { getErrorStringCode } from './error-utils'; import config from '../../../../../lib/config'; import { streamRequest } from '../../../../../lib/request/request'; import envPaths from 'env-paths'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; const debug = Debug('iac-local-cache'); @@ -176,6 +177,7 @@ export class FailedToInitLocalCacheError extends CustomError { this.strCode = getErrorStringCode(this.code); this.userMessage = 'We were unable to create a local directory to store the test assets, please ensure that the current working directory is writable'; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } @@ -186,6 +188,7 @@ export class FailedToDownloadRulesError extends CustomError { this.strCode = getErrorStringCode(this.code); this.userMessage = 'We were unable to download the security rules, please ensure the network can access https://downloads.snyk.io'; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } @@ -195,6 +198,7 @@ export class FailedToExtractCustomRulesError extends CustomError { this.code = IaCErrorCodes.FailedToExtractCustomRulesError; this.strCode = getErrorStringCode(this.code); this.userMessage = `We were unable to extract the rules provided at: ${path}. The provided bundle may be corrupted or invalid. Please ensure it was generated using the 'snyk-iac-rules' SDK`; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } @@ -204,6 +208,7 @@ export class InvalidCustomRules extends CustomError { this.code = IaCErrorCodes.InvalidCustomRules; this.strCode = getErrorStringCode(this.code); this.userMessage = `We were unable to extract the rules provided at: ${path}. The provided bundle does not match the required structure. Please ensure it was generated using the 'snyk-iac-rules' SDK`; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } @@ -213,6 +218,7 @@ export class InvalidCustomRulesPath extends CustomError { this.code = IaCErrorCodes.InvalidCustomRulesPath; this.strCode = getErrorStringCode(this.code); this.userMessage = `We were unable to extract the rules provided at: ${path}. The bundle at the provided path does not exist`; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } diff --git a/src/cli/commands/test/iac/local-execution/org-settings/get-iac-org-settings.ts b/src/cli/commands/test/iac/local-execution/org-settings/get-iac-org-settings.ts index 31c7fb29c3..eb0d90d6c9 100644 --- a/src/cli/commands/test/iac/local-execution/org-settings/get-iac-org-settings.ts +++ b/src/cli/commands/test/iac/local-execution/org-settings/get-iac-org-settings.ts @@ -6,6 +6,7 @@ import { getAuthHeader } from '../../../../../../lib/api-token'; import { makeRequest } from '../../../../../../lib/request'; import { CustomError } from '../../../../../../lib/errors'; import { getErrorStringCode } from '../error-utils'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; export function getIacOrgSettings( publicOrgId?: string, @@ -41,5 +42,6 @@ export class FailedToGetIacOrgSettingsError extends CustomError { this.strCode = getErrorStringCode(this.code); this.userMessage = 'We failed to fetch your organization settings, including custom severity overrides for infrastructure-as-code policies. Please run the command again with the `-d` flag and contact support@snyk.io with the contents of the output.'; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } diff --git a/src/cli/commands/test/iac/local-execution/parsers/terraform-file-parser.ts b/src/cli/commands/test/iac/local-execution/parsers/terraform-file-parser.ts index 62432ed51d..326fee9216 100644 --- a/src/cli/commands/test/iac/local-execution/parsers/terraform-file-parser.ts +++ b/src/cli/commands/test/iac/local-execution/parsers/terraform-file-parser.ts @@ -1,6 +1,7 @@ import { IaCErrorCodes } from '../types'; import { CustomError } from '../../../../../../lib/errors'; import { getErrorStringCode } from '../error-utils'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; export class FailedToParseTerraformFileError extends CustomError { public filename: string; @@ -10,5 +11,6 @@ export class FailedToParseTerraformFileError extends CustomError { this.strCode = getErrorStringCode(this.code); this.filename = filename; this.userMessage = `We were unable to parse the Terraform file "${filename}", please ensure it is valid HCL2. This can be done by running it through the 'terraform validate' command.`; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } diff --git a/src/cli/commands/test/iac/local-execution/parsers/terraform-plan-parser.ts b/src/cli/commands/test/iac/local-execution/parsers/terraform-plan-parser.ts index e1e3c82e1d..227331eb73 100644 --- a/src/cli/commands/test/iac/local-execution/parsers/terraform-plan-parser.ts +++ b/src/cli/commands/test/iac/local-execution/parsers/terraform-plan-parser.ts @@ -16,6 +16,7 @@ import { import { CustomError } from '../../../../../../lib/errors'; import { getErrorStringCode } from '../error-utils'; import { IacProjectType } from '../../../../../../lib/iac/constants'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; function terraformPlanReducer( scanInput: TerraformScanInput, @@ -190,5 +191,6 @@ export class FailedToExtractResourcesInTerraformPlanError extends CustomError { this.strCode = getErrorStringCode(this.code); this.userMessage = 'We failed to extract resource changes from the Terraform plan file, please contact support@snyk.io, if possible with a redacted version of the file'; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } diff --git a/src/cli/commands/test/iac/local-execution/process-results/results-formatter.ts b/src/cli/commands/test/iac/local-execution/process-results/results-formatter.ts index 70cc3709f0..ee0027e8eb 100644 --- a/src/cli/commands/test/iac/local-execution/process-results/results-formatter.ts +++ b/src/cli/commands/test/iac/local-execution/process-results/results-formatter.ts @@ -19,6 +19,7 @@ import { } from '@snyk/cloud-config-parser'; import * as path from 'path'; import { isLocalFolder } from '../../../../../../lib/detect'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; const severitiesArray = SEVERITIES.map((s) => s.verboseName); @@ -172,6 +173,7 @@ export class FailedToFormatResults extends CustomError { this.strCode = getErrorStringCode(this.code); this.userMessage = 'We failed printing the results, please contact support@snyk.io'; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } diff --git a/src/cli/commands/test/iac/local-execution/rules/oci-pull.ts b/src/cli/commands/test/iac/local-execution/rules/oci-pull.ts index 244fbd1636..f59332268f 100644 --- a/src/cli/commands/test/iac/local-execution/rules/oci-pull.ts +++ b/src/cli/commands/test/iac/local-execution/rules/oci-pull.ts @@ -7,6 +7,7 @@ import { LOCAL_POLICY_ENGINE_DIR } from '../local-cache'; import * as Debug from 'debug'; import { createIacDir } from '../file-utils'; import { OciRegistry } from './oci-registry'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; const debug = Debug('iac-oci-pull'); export const CUSTOM_RULES_TARBALL = 'custom-bundle.tar.gz'; @@ -80,6 +81,7 @@ export class FailedToBuildOCIArtifactError extends CustomError { this.strCode = getErrorStringCode(this.code); this.userMessage = 'We were unable to build the remote OCI Artifact locally, please ensure that the local directory is writeable.'; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } @@ -89,6 +91,7 @@ export class InvalidManifestSchemaVersionError extends CustomError { this.code = IaCErrorCodes.InvalidRemoteRegistryURLError; this.strCode = getErrorStringCode(this.code); this.userMessage = `Invalid manifest schema version: ${message}. We currently support Image Manifest Version 2, Schema 2`; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } @@ -97,9 +100,8 @@ export class InvalidRemoteRegistryURLError extends CustomError { super('Invalid URL for Remote Registry'); this.code = IaCErrorCodes.InvalidRemoteRegistryURLError; this.strCode = getErrorStringCode(this.code); - this.userMessage = `The provided remote registry URL${ - url ? `: "${url}"` : '' - } is invalid. Please check it again.`; + this.userMessage = `The provided remote registry URL${url ? `: "${url}"` : ''} is invalid. Please check it again.`; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } @@ -109,5 +111,6 @@ export class UnsupportedEntitlementPullError extends CustomError { this.code = IaCErrorCodes.UnsupportedEntitlementPullError; this.strCode = getErrorStringCode(this.code); this.userMessage = `The custom rules feature is currently not supported for this org. To enable it, please contact snyk support.`; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } diff --git a/src/cli/commands/test/iac/local-execution/rules/rules.ts b/src/cli/commands/test/iac/local-execution/rules/rules.ts index 424aee3913..e6b0b01fb7 100644 --- a/src/cli/commands/test/iac/local-execution/rules/rules.ts +++ b/src/cli/commands/test/iac/local-execution/rules/rules.ts @@ -25,6 +25,7 @@ import { import { OciRegistry, RemoteOciRegistry } from './oci-registry'; import { isValidUrl } from '../url-utils'; import { isFeatureFlagSupportedForOrg } from '../../../../../../lib/feature-flags'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; export async function initRules( buildOciRegistry: () => OciRegistry, @@ -201,6 +202,7 @@ export class FailedToPullCustomBundleError extends CustomError { `${message ? message + ' ' : ''}` + '\nWe were unable to download the custom bundle to the disk. Please ensure access to the remote Registry and validate you have provided all the right parameters.' + '\nSee documentation on troubleshooting: https://docs.snyk.io/products/snyk-infrastructure-as-code/custom-rules/use-IaC-custom-rules-with-CLI/using-a-remote-custom-rules-bundle#troubleshooting'; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } @@ -209,8 +211,8 @@ export class FailedToExecuteCustomRulesError extends CustomError { super(message || 'Could not execute custom rules mode'); this.code = IaCErrorCodes.FailedToExecuteCustomRulesError; this.strCode = getErrorStringCode(this.code); - this.userMessage = ` - Remote and local custom rules bundle can not be used at the same time. - Please provide a registry URL for the remote bundle, or specify local path location by using the --rules flag for the local bundle.`; + this.userMessage = `Remote and local custom rules bundle can not be used at the same time. + Please provide a registry URL for the remote bundle, or specify local path location by using the --rules flag for the local bundle.`; + this.errorCatalog = new CLI.InvalidFlagOptionError(''); } } diff --git a/src/cli/commands/test/iac/local-execution/usage-tracking.ts b/src/cli/commands/test/iac/local-execution/usage-tracking.ts index 044bf6cef3..1cf55b6cfd 100644 --- a/src/cli/commands/test/iac/local-execution/usage-tracking.ts +++ b/src/cli/commands/test/iac/local-execution/usage-tracking.ts @@ -2,6 +2,7 @@ import { makeRequest } from '../../../../../lib/request'; import config from '../../../../../lib/config'; import { getAuthHeader } from '../../../../../lib/api-token'; import { CustomError } from '../../../../../lib/errors'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; export async function trackUsage( formattedResults: TrackableResult[], @@ -42,6 +43,7 @@ export class TestLimitReachedError extends CustomError { super( 'Test limit reached! You have exceeded your infrastructure as code test allocation for this billing period.', ); + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } diff --git a/src/cli/commands/test/iac/local-execution/yaml-parser.ts b/src/cli/commands/test/iac/local-execution/yaml-parser.ts index 09ba0fffb7..4db2c2e33a 100644 --- a/src/cli/commands/test/iac/local-execution/yaml-parser.ts +++ b/src/cli/commands/test/iac/local-execution/yaml-parser.ts @@ -1,3 +1,4 @@ +import { CLI } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from '../../../../../lib/errors'; import { getErrorStringCode } from './error-utils'; import { IaCErrorCodes, IacFileData } from './types'; @@ -28,6 +29,7 @@ export class InvalidJsonFileError extends CustomError { this.strCode = getErrorStringCode(this.code); this.filename = filename; this.userMessage = `We were unable to parse the JSON file "${filename}". Please ensure that it contains properly structured JSON`; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } @@ -39,5 +41,6 @@ export class InvalidYamlFileError extends CustomError { this.strCode = getErrorStringCode(this.code); this.filename = filename; this.userMessage = `We were unable to parse the YAML file "${filename}". Please ensure that it contains properly structured YAML, without any template directives`; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } diff --git a/src/cli/commands/test/iac/output.ts b/src/cli/commands/test/iac/output.ts index bd79cb6158..e734d0427d 100644 --- a/src/cli/commands/test/iac/output.ts +++ b/src/cli/commands/test/iac/output.ts @@ -37,6 +37,7 @@ import { formatShareResultsOutputIacPlus, shareResultsError, } from '../../../../lib/formatters/iac-output/text/share-results'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; const SEPARATOR = '\n-------------------------------------------------------\n'; @@ -183,6 +184,8 @@ export function buildOutput({ ? new FormattedCustomError( errorResults[0].message, formatFailuresList(allTestFailures), + undefined, + new CLI.GeneralIACFailureError(formatFailuresList(allTestFailures)), ) : new CustomError(response); error.code = errorResults[0].code; diff --git a/src/cli/commands/test/iac/scan.ts b/src/cli/commands/test/iac/scan.ts index 1446b266a2..2b4a2c4697 100644 --- a/src/cli/commands/test/iac/scan.ts +++ b/src/cli/commands/test/iac/scan.ts @@ -31,6 +31,7 @@ import { getRepositoryRootForPath } from '../../../../lib/iac/git'; import { getInfo } from '../../../../lib/project-metadata/target-builders/git'; import { buildMeta, GitRepository, GitRepositoryFinder } from './meta'; import { MAX_STRING_LENGTH } from '../../../../lib/constants'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; const debug = debugLib('snyk-iac'); @@ -199,9 +200,10 @@ class CurrentWorkingDirectoryTraversalError extends CustomError { super('Path is outside the current working directory'); this.code = IaCErrorCodes.CurrentWorkingDirectoryTraversalError; this.strCode = getErrorStringCode(this.code); - this.userMessage = `Path is outside the current working directory`; + this.userMessage = 'Path is outside the current working directory'; this.filename = path; this.projectRoot = projectRoot; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } diff --git a/src/cli/ipc.ts b/src/cli/ipc.ts index 8bc5059183..631bac5057 100644 --- a/src/cli/ipc.ts +++ b/src/cli/ipc.ts @@ -3,9 +3,9 @@ import { CLI, ProblemError } from '@snyk/error-catalog-nodejs-public'; import { debug as Debug } from 'debug'; import * as legacyErrors from '../lib/errors/legacy-errors'; import stripAnsi = require('strip-ansi'); +import { CustomError } from '../lib/errors'; -const ERROR_FILE_PATH = process.env.SNYK_ERR_FILE; -const debug = Debug('snyk'); +const debug = Debug('snyk:ipc'); /** * Sends the specified error back at the Golang CLI, by writting it to the temporary error file. Errors that are not @@ -15,6 +15,7 @@ const debug = Debug('snyk'); * @returns {Promise} The result of the operation as a boolean value */ export async function sendError(err: Error, isJson: boolean): Promise { + const ERROR_FILE_PATH = process.env.SNYK_ERR_FILE; if (!ERROR_FILE_PATH) { debug('Error file path not set.'); return false; @@ -22,7 +23,7 @@ export async function sendError(err: Error, isJson: boolean): Promise { // @ts-expect-error Using this instead of 'instanceof' since the error might be caught from external CLI plugins. // See: https://github.com/snyk/error-catalog/blob/main/packages/error-catalog-nodejs/src/problem-error.ts#L17-L19 - if (!err.isErrorCatalogError) { + if (!err.isErrorCatalogError && !err.errorCatalog) { let message = legacyErrors.message(err); if (isJson) { @@ -38,6 +39,10 @@ export async function sendError(err: Error, isJson: boolean): Promise { err.metadata.status = 0; } + if (err instanceof CustomError && err.errorCatalog) { + err = err.errorCatalog; + } + const data = (err as ProblemError).toJsonApi().body(); try { diff --git a/src/cli/main.ts b/src/cli/main.ts index 5fc9e70b30..868210f580 100755 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -142,7 +142,7 @@ async function handleError(args, error) { * - sarif - no error message or details are present in the payload * - vulnsFound - issues are treated as errors (exit code 1), this should be some nice pretty formated output for users. */ - const errorSent = + const shouldOutputError = vulnsFound || args.options.sarif ? false : await sendError(error, args.options.json); @@ -163,7 +163,7 @@ async function handleError(args, error) { console.log(output); } // If the IPC communication failed, we default back to the original output flow - } else if (!errorSent) { + } else if (!shouldOutputError) { // Debug output flow if (args.options.debug) { const output = vulnsFound ? error.message : error.stack; diff --git a/src/lib/errors/authentication-failed-error.ts b/src/lib/errors/authentication-failed-error.ts index fc5024e600..284c19ee64 100644 --- a/src/lib/errors/authentication-failed-error.ts +++ b/src/lib/errors/authentication-failed-error.ts @@ -1,4 +1,5 @@ import { CustomError } from './custom-error'; +import { Snyk } from '@snyk/error-catalog-nodejs-public'; import config from '../config'; export function AuthFailedError( @@ -10,5 +11,6 @@ export function AuthFailedError( error.code = errorCode; error.strCode = 'authfail'; error.userMessage = errorMessage; + error.errorCatalog = new Snyk.UnauthorisedError(''); return error; } diff --git a/src/lib/errors/bad-gateway-error.ts b/src/lib/errors/bad-gateway-error.ts index 77d6c84938..7e3045a25f 100644 --- a/src/lib/errors/bad-gateway-error.ts +++ b/src/lib/errors/bad-gateway-error.ts @@ -1,4 +1,5 @@ import { CustomError } from './custom-error'; +import { Snyk } from '@snyk/error-catalog-nodejs-public'; export class BadGatewayError extends CustomError { private static ERROR_CODE = 502; @@ -10,5 +11,6 @@ export class BadGatewayError extends CustomError { this.code = BadGatewayError.ERROR_CODE; this.strCode = BadGatewayError.ERROR_STRING_CODE; this.userMessage = userMessage || BadGatewayError.ERROR_MESSAGE; + this.errorCatalog = new Snyk.BadGatewayError(''); } } diff --git a/src/lib/errors/connection-timeout-error.ts b/src/lib/errors/connection-timeout-error.ts index 3bfe08bb68..455ab47bdc 100644 --- a/src/lib/errors/connection-timeout-error.ts +++ b/src/lib/errors/connection-timeout-error.ts @@ -1,4 +1,5 @@ import { CustomError } from './custom-error'; +import { Snyk } from '@snyk/error-catalog-nodejs-public'; export class ConnectionTimeoutError extends CustomError { private static ERROR_MESSAGE = 'Connection timeout.'; @@ -7,5 +8,6 @@ export class ConnectionTimeoutError extends CustomError { super(ConnectionTimeoutError.ERROR_MESSAGE); this.code = 504; this.userMessage = ConnectionTimeoutError.ERROR_MESSAGE; + this.errorCatalog = new Snyk.TimeoutError(''); } } diff --git a/src/lib/errors/custom-error.ts b/src/lib/errors/custom-error.ts index c5fcfa96c9..3bea50b0c0 100644 --- a/src/lib/errors/custom-error.ts +++ b/src/lib/errors/custom-error.ts @@ -1,8 +1,11 @@ +import { ProblemError } from '@snyk/error-catalog-nodejs-public'; + export class CustomError extends Error { public innerError; public code: number | undefined; public userMessage: string | undefined; public strCode: string | undefined; + protected _errorCatalog: ProblemError | undefined; constructor(message: string) { super(message); @@ -12,5 +15,17 @@ export class CustomError extends Error { this.strCode = undefined; this.innerError = undefined; this.userMessage = undefined; + this._errorCatalog = undefined; + } + + set errorCatalog(ec: ProblemError | undefined) { + this._errorCatalog = ec; + } + + get errorCatalog(): ProblemError | undefined { + if (this._errorCatalog) { + this._errorCatalog.detail = this.userMessage ?? this.message; + } + return this._errorCatalog; } } diff --git a/src/lib/errors/docker-image-not-found-error.ts b/src/lib/errors/docker-image-not-found-error.ts index 94e8b570f5..db20601d1f 100644 --- a/src/lib/errors/docker-image-not-found-error.ts +++ b/src/lib/errors/docker-image-not-found-error.ts @@ -1,3 +1,4 @@ +import { CustomBaseImages } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; export class DockerImageNotFoundError extends CustomError { @@ -8,5 +9,6 @@ export class DockerImageNotFoundError extends CustomError { super(message); this.code = DockerImageNotFoundError.ERROR_CODE; this.userMessage = message; + this.errorCatalog = new CustomBaseImages.ImageNotFoundError(''); } } diff --git a/src/lib/errors/empty-sarif-output-error.ts b/src/lib/errors/empty-sarif-output-error.ts index a7e28ce4a1..9fb7f7798a 100644 --- a/src/lib/errors/empty-sarif-output-error.ts +++ b/src/lib/errors/empty-sarif-output-error.ts @@ -1,3 +1,4 @@ +import { CLI } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; export class SarifFileOutputEmptyError extends CustomError { @@ -9,5 +10,6 @@ export class SarifFileOutputEmptyError extends CustomError { super(SarifFileOutputEmptyError.ERROR_MESSAGE); this.code = SarifFileOutputEmptyError.ERROR_CODE; this.userMessage = SarifFileOutputEmptyError.ERROR_MESSAGE; + this.errorCatalog = new CLI.EmptyFlagOptionError(''); } } diff --git a/src/lib/errors/exclude-flag-bad-input.ts b/src/lib/errors/exclude-flag-bad-input.ts index 8e1f610a29..4007f76761 100644 --- a/src/lib/errors/exclude-flag-bad-input.ts +++ b/src/lib/errors/exclude-flag-bad-input.ts @@ -1,3 +1,4 @@ +import { CLI } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; export class ExcludeFlagBadInputError extends CustomError { @@ -9,5 +10,6 @@ export class ExcludeFlagBadInputError extends CustomError { super(ExcludeFlagBadInputError.ERROR_MESSAGE); this.code = ExcludeFlagBadInputError.ERROR_CODE; this.userMessage = ExcludeFlagBadInputError.ERROR_MESSAGE; + this.errorCatalog = new CLI.EmptyFlagOptionError(''); } } diff --git a/src/lib/errors/exclude-flag-invalid-input.ts b/src/lib/errors/exclude-flag-invalid-input.ts index 0b9ed5e807..7805080141 100644 --- a/src/lib/errors/exclude-flag-invalid-input.ts +++ b/src/lib/errors/exclude-flag-invalid-input.ts @@ -1,3 +1,4 @@ +import { CLI } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; export class ExcludeFlagInvalidInputError extends CustomError { @@ -9,5 +10,6 @@ export class ExcludeFlagInvalidInputError extends CustomError { super(ExcludeFlagInvalidInputError.ERROR_MESSAGE); this.code = ExcludeFlagInvalidInputError.ERROR_CODE; this.userMessage = ExcludeFlagInvalidInputError.ERROR_MESSAGE; + this.errorCatalog = new CLI.InvalidFlagOptionError(''); } } diff --git a/src/lib/errors/fail-on-error.ts.ts b/src/lib/errors/fail-on-error.ts.ts index 51a8e3f915..aa1ff2978e 100644 --- a/src/lib/errors/fail-on-error.ts.ts +++ b/src/lib/errors/fail-on-error.ts.ts @@ -1,5 +1,6 @@ import { CustomError } from './custom-error'; import { FAIL_ON } from '../snyk-test/common'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; export class FailOnError extends CustomError { private static ERROR_MESSAGE = @@ -8,5 +9,6 @@ export class FailOnError extends CustomError { constructor() { super(FailOnError.ERROR_MESSAGE); + this.errorCatalog = new CLI.InvalidFlagOptionError(''); } } diff --git a/src/lib/errors/failed-to-get-vulnerabilities-error.ts b/src/lib/errors/failed-to-get-vulnerabilities-error.ts index 25aab35908..c752498d1e 100644 --- a/src/lib/errors/failed-to-get-vulnerabilities-error.ts +++ b/src/lib/errors/failed-to-get-vulnerabilities-error.ts @@ -1,3 +1,4 @@ +import { Snyk } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; export class FailedToGetVulnerabilitiesError extends CustomError { @@ -11,5 +12,6 @@ export class FailedToGetVulnerabilitiesError extends CustomError { this.strCode = FailedToGetVulnerabilitiesError.ERROR_STRING_CODE; this.userMessage = userMessage || FailedToGetVulnerabilitiesError.ERROR_MESSAGE; + this.errorCatalog = new Snyk.ServerError(''); } } diff --git a/src/lib/errors/failed-to-get-vulns-from-unavailable-resource.ts b/src/lib/errors/failed-to-get-vulns-from-unavailable-resource.ts index 2a829edb6a..b41a81df61 100644 --- a/src/lib/errors/failed-to-get-vulns-from-unavailable-resource.ts +++ b/src/lib/errors/failed-to-get-vulns-from-unavailable-resource.ts @@ -1,3 +1,4 @@ +import { CLI } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; const errorNpmMessage = @@ -9,12 +10,13 @@ export function FailedToGetVulnsFromUnavailableResource( root: string, statusCode: number, ): CustomError { - const isRepository = root.startsWith('http' || 'https'); + const isRepository = root.startsWith('http'); const errorMsg = `We couldn't test ${root}. ${ isRepository ? errorRepositoryMessage : errorNpmMessage }`; const error = new CustomError(errorMsg); error.code = statusCode; error.userMessage = errorMsg; + error.errorCatalog = new CLI.GetVulnsFromResourceFailedError(''); return error; } diff --git a/src/lib/errors/failed-to-load-policy-error.ts b/src/lib/errors/failed-to-load-policy-error.ts index aff737e3cb..ad2a3250d8 100644 --- a/src/lib/errors/failed-to-load-policy-error.ts +++ b/src/lib/errors/failed-to-load-policy-error.ts @@ -1,3 +1,4 @@ +import { Policies } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; export class FailedToLoadPolicyError extends CustomError { @@ -10,5 +11,6 @@ export class FailedToLoadPolicyError extends CustomError { this.code = FailedToLoadPolicyError.ERROR_CODE; this.strCode = FailedToLoadPolicyError.ERROR_STRING_CODE; this.userMessage = FailedToLoadPolicyError.ERROR_MESSAGE; + this.errorCatalog = new Policies.InvalidPolicyApplyError(''); } } diff --git a/src/lib/errors/failed-to-run-test-error.ts b/src/lib/errors/failed-to-run-test-error.ts index 43be36d42a..164204767a 100644 --- a/src/lib/errors/failed-to-run-test-error.ts +++ b/src/lib/errors/failed-to-run-test-error.ts @@ -1,14 +1,22 @@ +import { ProblemError } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; export class FailedToRunTestError extends CustomError { private static ERROR_MESSAGE = 'Failed to run a test'; public innerError: any | undefined; - constructor(userMessage, errorCode?, innerError?: any) { + constructor( + userMessage, + errorCode?, + innerError?: any, + errorCatalog?: ProblemError, + ) { const code = errorCode || 500; super(userMessage || FailedToRunTestError.ERROR_MESSAGE); this.code = errorCode || code; this.userMessage = userMessage || FailedToRunTestError.ERROR_MESSAGE; this.innerError = innerError; + this.errorCatalog = errorCatalog ?? new CLI.GeneralCLIFailureError(''); } } diff --git a/src/lib/errors/file-flag-bad-input.ts b/src/lib/errors/file-flag-bad-input.ts index d1e580a594..1902b9978c 100644 --- a/src/lib/errors/file-flag-bad-input.ts +++ b/src/lib/errors/file-flag-bad-input.ts @@ -1,3 +1,4 @@ +import { CLI } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; export class FileFlagBadInputError extends CustomError { @@ -9,5 +10,6 @@ export class FileFlagBadInputError extends CustomError { super(FileFlagBadInputError.ERROR_MESSAGE); this.code = FileFlagBadInputError.ERROR_CODE; this.userMessage = FileFlagBadInputError.ERROR_MESSAGE; + this.errorCatalog = new CLI.EmptyFlagOptionError(''); } } diff --git a/src/lib/errors/formatted-custom-error.ts b/src/lib/errors/formatted-custom-error.ts index 23025355e5..5102d1b246 100644 --- a/src/lib/errors/formatted-custom-error.ts +++ b/src/lib/errors/formatted-custom-error.ts @@ -1,5 +1,6 @@ import chalk from 'chalk'; import { CustomError } from '.'; +import { CLI, ProblemError } from '@snyk/error-catalog-nodejs-public'; export class FormattedCustomError extends CustomError { public formattedUserMessage: string; @@ -8,9 +9,19 @@ export class FormattedCustomError extends CustomError { message: string, formattedUserMessage: string, userMessage?: string, + errorCatalog?: ProblemError, ) { super(message); this.userMessage = userMessage || chalk.reset(formattedUserMessage); this.formattedUserMessage = formattedUserMessage; + this.errorCatalog = errorCatalog ?? new CLI.GeneralCLIFailureError(''); + } + + set errorCatalog(ec: ProblemError | undefined) { + super.errorCatalog = ec; + } + + get errorCatalog(): ProblemError | undefined { + return this._errorCatalog; } } diff --git a/src/lib/errors/internal-server-error.ts b/src/lib/errors/internal-server-error.ts index 113a370a1a..ee334a3ecb 100644 --- a/src/lib/errors/internal-server-error.ts +++ b/src/lib/errors/internal-server-error.ts @@ -1,4 +1,5 @@ import { CustomError } from './custom-error'; +import { Snyk } from '@snyk/error-catalog-nodejs-public'; export class InternalServerError extends CustomError { private static ERROR_CODE = 500; @@ -10,5 +11,6 @@ export class InternalServerError extends CustomError { this.code = InternalServerError.ERROR_CODE; this.strCode = InternalServerError.ERROR_STRING_CODE; this.userMessage = userMessage || InternalServerError.ERROR_MESSAGE; + this.errorCatalog = new Snyk.ServerError(''); } } diff --git a/src/lib/errors/invalid-detection-depth-value.ts b/src/lib/errors/invalid-detection-depth-value.ts index 86cf00e118..5e70775ed6 100644 --- a/src/lib/errors/invalid-detection-depth-value.ts +++ b/src/lib/errors/invalid-detection-depth-value.ts @@ -1,3 +1,4 @@ +import { CLI } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; export class InvalidDetectionDepthValue extends CustomError { @@ -6,5 +7,6 @@ export class InvalidDetectionDepthValue extends CustomError { super(msg); this.code = 422; this.userMessage = msg; + this.errorCatalog = new CLI.InvalidFlagOptionError(''); } } diff --git a/src/lib/errors/invalid-remote-url-error.ts b/src/lib/errors/invalid-remote-url-error.ts index f68699a6cb..1a93303a94 100644 --- a/src/lib/errors/invalid-remote-url-error.ts +++ b/src/lib/errors/invalid-remote-url-error.ts @@ -1,3 +1,4 @@ +import { CLI } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; export class InvalidRemoteUrlError extends CustomError { @@ -6,5 +7,6 @@ export class InvalidRemoteUrlError extends CustomError { constructor() { super(InvalidRemoteUrlError.ERROR_MESSAGE); + this.errorCatalog = new CLI.InvalidFlagOptionError(''); } } diff --git a/src/lib/errors/json-file-output-bad-input-error.ts b/src/lib/errors/json-file-output-bad-input-error.ts index d5aeabe59f..fafa76769e 100644 --- a/src/lib/errors/json-file-output-bad-input-error.ts +++ b/src/lib/errors/json-file-output-bad-input-error.ts @@ -1,3 +1,4 @@ +import { CLI } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; export class JsonFileOutputBadInputError extends CustomError { @@ -9,5 +10,6 @@ export class JsonFileOutputBadInputError extends CustomError { super(JsonFileOutputBadInputError.ERROR_MESSAGE); this.code = JsonFileOutputBadInputError.ERROR_CODE; this.userMessage = JsonFileOutputBadInputError.ERROR_MESSAGE; + this.errorCatalog = new CLI.EmptyFlagOptionError(''); } } diff --git a/src/lib/errors/misconfigured-auth-in-ci-error.ts b/src/lib/errors/misconfigured-auth-in-ci-error.ts index 52c079fa61..066c87d2ff 100644 --- a/src/lib/errors/misconfigured-auth-in-ci-error.ts +++ b/src/lib/errors/misconfigured-auth-in-ci-error.ts @@ -1,3 +1,4 @@ +import { CLI } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; export function MisconfiguredAuthInCI() { @@ -9,5 +10,6 @@ export function MisconfiguredAuthInCI() { error.code = 401; error.strCode = 'noAuthInCI'; error.userMessage = errorMsg; + error.errorCatalog = new CLI.AuthConfigError(''); return error; } diff --git a/src/lib/errors/missing-api-token.ts b/src/lib/errors/missing-api-token.ts index 370d2fedff..824824e79a 100644 --- a/src/lib/errors/missing-api-token.ts +++ b/src/lib/errors/missing-api-token.ts @@ -1,3 +1,4 @@ +import { CLI } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; export class MissingApiTokenError extends CustomError { @@ -22,5 +23,6 @@ export class MissingApiTokenError extends CustomError { this.code = MissingApiTokenError.ERROR_CODE; this.strCode = MissingApiTokenError.ERROR_STRING_CODE; this.userMessage = MissingApiTokenError.ERROR_MESSAGE; + this.errorCatalog = new CLI.AuthConfigError(''); } } diff --git a/src/lib/errors/missing-arg-error.ts b/src/lib/errors/missing-arg-error.ts index c7ae8f13ad..a4c423be0d 100644 --- a/src/lib/errors/missing-arg-error.ts +++ b/src/lib/errors/missing-arg-error.ts @@ -1,3 +1,4 @@ +import { CLI } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; export class MissingArgError extends CustomError { @@ -7,5 +8,6 @@ export class MissingArgError extends CustomError { super(msg); this.code = 422; this.userMessage = msg; + this.errorCatalog = new CLI.CommandArgsError(''); } } diff --git a/src/lib/errors/missing-option-error.ts b/src/lib/errors/missing-option-error.ts index 4a1d34afcd..4e08a5a41d 100644 --- a/src/lib/errors/missing-option-error.ts +++ b/src/lib/errors/missing-option-error.ts @@ -1,3 +1,4 @@ +import { CLI } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; export class MissingOptionError extends CustomError { @@ -8,5 +9,6 @@ export class MissingOptionError extends CustomError { super(msg); this.code = 422; this.userMessage = msg; + this.errorCatalog = new CLI.InvalidFlagOptionError(''); } } diff --git a/src/lib/errors/missing-targetfile-error.ts b/src/lib/errors/missing-targetfile-error.ts index ad43f5d47a..545f986cb6 100644 --- a/src/lib/errors/missing-targetfile-error.ts +++ b/src/lib/errors/missing-targetfile-error.ts @@ -1,3 +1,4 @@ +import { CLI } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; export function MissingTargetFileError(path: string) { @@ -8,5 +9,6 @@ export function MissingTargetFileError(path: string) { const error = new CustomError(errorMsg); error.code = 422; error.userMessage = errorMsg; + error.errorCatalog = new CLI.CommandArgsError(''); return error; } diff --git a/src/lib/errors/monitor-error.ts b/src/lib/errors/monitor-error.ts index 598cb7eb23..5f411e5f01 100644 --- a/src/lib/errors/monitor-error.ts +++ b/src/lib/errors/monitor-error.ts @@ -1,3 +1,4 @@ +import { Snyk } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; export class MonitorError extends CustomError { @@ -10,5 +11,6 @@ export class MonitorError extends CustomError { super(MonitorError.ERROR_MESSAGE + `Status code: ${code}${errorMessage}`); this.code = errorCode; this.userMessage = message; + this.errorCatalog = new Snyk.ServerError(''); } } diff --git a/src/lib/errors/no-supported-manifests-found.ts b/src/lib/errors/no-supported-manifests-found.ts index 2318754ff0..a94aa0ff98 100644 --- a/src/lib/errors/no-supported-manifests-found.ts +++ b/src/lib/errors/no-supported-manifests-found.ts @@ -1,5 +1,6 @@ import chalk from 'chalk'; import { CustomError } from './custom-error'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; export function NoSupportedManifestsFoundError( atLocations: string[], @@ -16,5 +17,6 @@ export function NoSupportedManifestsFoundError( const error = new CustomError(errorMsg); error.code = 422; error.userMessage = errorMsg; + error.errorCatalog = new CLI.NoSupportedFilesFoundError(''); return error; } diff --git a/src/lib/errors/no-supported-sast-files-found.ts b/src/lib/errors/no-supported-sast-files-found.ts index 3b2b13b64a..e63624248f 100644 --- a/src/lib/errors/no-supported-sast-files-found.ts +++ b/src/lib/errors/no-supported-sast-files-found.ts @@ -1,5 +1,6 @@ import chalk from 'chalk'; import { CustomError } from './custom-error'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; export class NoSupportedSastFiles extends CustomError { private static ERROR_MESSAGE = @@ -13,5 +14,8 @@ export class NoSupportedSastFiles extends CustomError { super(NoSupportedSastFiles.ERROR_MESSAGE); this.code = 422; this.userMessage = NoSupportedSastFiles.ERROR_MESSAGE; + this.errorCatalog = new CLI.NoSupportedFilesFoundError( + NoSupportedSastFiles.ERROR_MESSAGE, + ); } } diff --git a/src/lib/errors/not-found-error.ts b/src/lib/errors/not-found-error.ts index 6b2d3c2ebb..3af0b7bde3 100644 --- a/src/lib/errors/not-found-error.ts +++ b/src/lib/errors/not-found-error.ts @@ -1,3 +1,4 @@ +import { OpenAPI } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; export class NotFoundError extends CustomError { @@ -8,5 +9,6 @@ export class NotFoundError extends CustomError { super(userMessage || NotFoundError.ERROR_MESSAGE); this.code = NotFoundError.ERROR_CODE; this.userMessage = userMessage || NotFoundError.ERROR_MESSAGE; + this.errorCatalog = new OpenAPI.NotFoundError(''); } } diff --git a/src/lib/errors/not-supported-by-ecosystem.ts b/src/lib/errors/not-supported-by-ecosystem.ts index 5ac9e2afa5..715d3338c5 100644 --- a/src/lib/errors/not-supported-by-ecosystem.ts +++ b/src/lib/errors/not-supported-by-ecosystem.ts @@ -1,6 +1,7 @@ import { CustomError } from './custom-error'; import { SupportedPackageManagers } from '../package-managers'; import { Ecosystem } from '../ecosystems/types'; +import { Fix } from '@snyk/error-catalog-nodejs-public'; export class FeatureNotSupportedByEcosystemError extends CustomError { public readonly feature: string; @@ -14,5 +15,6 @@ export class FeatureNotSupportedByEcosystemError extends CustomError { this.feature = feature; this.userMessage = `\`${feature}\` is not supported for ecosystem '${ecosystem}'`; + this.errorCatalog = new Fix.UnsupportedEcosystemError(''); } } diff --git a/src/lib/errors/policy-not-found-error.ts b/src/lib/errors/policy-not-found-error.ts index 05ac75249e..2c80bf736c 100644 --- a/src/lib/errors/policy-not-found-error.ts +++ b/src/lib/errors/policy-not-found-error.ts @@ -1,3 +1,4 @@ +import { Policies } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; export class PolicyNotFoundError extends CustomError { @@ -10,5 +11,6 @@ export class PolicyNotFoundError extends CustomError { this.code = PolicyNotFoundError.ERROR_CODE; this.strCode = PolicyNotFoundError.ERROR_STRING_CODE; this.userMessage = PolicyNotFoundError.ERROR_MESSAGE; + this.errorCatalog = new Policies.InvalidPolicyApplyError(''); } } diff --git a/src/lib/errors/service-unavailable-error.ts b/src/lib/errors/service-unavailable-error.ts index 60497ebefe..aa36a1242b 100644 --- a/src/lib/errors/service-unavailable-error.ts +++ b/src/lib/errors/service-unavailable-error.ts @@ -1,3 +1,4 @@ +import { Snyk } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; export class ServiceUnavailableError extends CustomError { @@ -10,5 +11,6 @@ export class ServiceUnavailableError extends CustomError { this.code = ServiceUnavailableError.ERROR_CODE; this.strCode = ServiceUnavailableError.ERROR_STRING_CODE; this.userMessage = userMessage || ServiceUnavailableError.ERROR_MESSAGE; + this.errorCatalog = new Snyk.ServiceUnavailableError(''); } } diff --git a/src/lib/errors/token-expired-error.ts b/src/lib/errors/token-expired-error.ts index e25d4b647a..f74594b87e 100644 --- a/src/lib/errors/token-expired-error.ts +++ b/src/lib/errors/token-expired-error.ts @@ -1,3 +1,4 @@ +import { CLI } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; export function TokenExpiredError() { @@ -9,5 +10,6 @@ export function TokenExpiredError() { error.code = 401; error.strCode = 'AUTH_TIMEOUT'; error.userMessage = errorMsg; + error.errorCatalog = new CLI.AuthConfigError(''); return error; } diff --git a/src/lib/errors/too-many-vuln-paths.ts b/src/lib/errors/too-many-vuln-paths.ts index ae37d34629..c68f6ec29f 100644 --- a/src/lib/errors/too-many-vuln-paths.ts +++ b/src/lib/errors/too-many-vuln-paths.ts @@ -1,3 +1,4 @@ +import { CLI } from '@snyk/error-catalog-nodejs-public/src'; import { CustomError } from './custom-error'; export class TooManyVulnPaths extends CustomError { @@ -11,5 +12,6 @@ export class TooManyVulnPaths extends CustomError { this.code = TooManyVulnPaths.ERROR_CODE; this.strCode = TooManyVulnPaths.ERROR_STRING_CODE; this.userMessage = TooManyVulnPaths.ERROR_MESSAGE; + this.errorCatalog = new CLI.TooManyVulnerablePathsError(''); } } diff --git a/src/lib/errors/unsupported-entitlement-error.ts b/src/lib/errors/unsupported-entitlement-error.ts index 13ebae9912..8b692f0ef4 100644 --- a/src/lib/errors/unsupported-entitlement-error.ts +++ b/src/lib/errors/unsupported-entitlement-error.ts @@ -1,3 +1,4 @@ +import { OpenAPI } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; export class UnsupportedEntitlementError extends CustomError { @@ -17,5 +18,6 @@ export class UnsupportedEntitlementError extends CustomError { this.entitlement = entitlement; this.code = UnsupportedEntitlementError.ERROR_CODE; this.userMessage = userMessage; + this.errorCatalog = new OpenAPI.ForbiddenError(''); } } diff --git a/src/lib/errors/unsupported-feature-for-org-error.ts b/src/lib/errors/unsupported-feature-for-org-error.ts index f5a3a40538..27425036f8 100644 --- a/src/lib/errors/unsupported-feature-for-org-error.ts +++ b/src/lib/errors/unsupported-feature-for-org-error.ts @@ -1,3 +1,4 @@ +import { OpenAPI } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; export class FeatureNotSupportedForOrgError extends CustomError { @@ -7,10 +8,10 @@ export class FeatureNotSupportedForOrgError extends CustomError { super(`Unsupported action for org ${org}.`); this.code = 422; this.org = org; - this.userMessage = `${feature} is not supported for org` + (org ? ` ${org}` : '') + (additionalUserHelp ? `: ${additionalUserHelp}` : '.'); + this.errorCatalog = new OpenAPI.ForbiddenError(''); } } diff --git a/src/lib/errors/unsupported-option-combination-error.ts b/src/lib/errors/unsupported-option-combination-error.ts index c1e2f361ce..0242b594d3 100644 --- a/src/lib/errors/unsupported-option-combination-error.ts +++ b/src/lib/errors/unsupported-option-combination-error.ts @@ -1,3 +1,4 @@ +import { CLI } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; export class UnsupportedOptionCombinationError extends CustomError { @@ -14,5 +15,6 @@ export class UnsupportedOptionCombinationError extends CustomError { this.code = 422; this.userMessage = UnsupportedOptionCombinationError.ERROR_MESSAGE + options.join(' + '); + this.errorCatalog = new CLI.InvalidFlagOptionError(''); } } diff --git a/src/lib/errors/unsupported-package-manager-error.ts b/src/lib/errors/unsupported-package-manager-error.ts index 4996761375..3a42974179 100644 --- a/src/lib/errors/unsupported-package-manager-error.ts +++ b/src/lib/errors/unsupported-package-manager-error.ts @@ -1,5 +1,6 @@ import { CustomError } from './custom-error'; import * as pms from '../package-managers'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; export class UnsupportedPackageManagerError extends CustomError { private static ERROR_MESSAGE: string = @@ -11,12 +12,13 @@ export class UnsupportedPackageManagerError extends CustomError { constructor(packageManager) { super( - `Unsupported package manager ${packageManager}.` + + `Unsupported package manager '${packageManager}''. ` + UnsupportedPackageManagerError.ERROR_MESSAGE, ); this.code = 422; this.userMessage = `Unsupported package manager '${packageManager}''. ` + UnsupportedPackageManagerError.ERROR_MESSAGE; + this.errorCatalog = new CLI.NoSupportedFilesFoundError(''); } } diff --git a/src/lib/errors/validation-error.ts b/src/lib/errors/validation-error.ts index f3beb1564f..51817022f8 100644 --- a/src/lib/errors/validation-error.ts +++ b/src/lib/errors/validation-error.ts @@ -1,8 +1,10 @@ +import { CLI } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from './custom-error'; export class ValidationError extends CustomError { constructor(message: string) { super(message); this.userMessage = message; + this.errorCatalog = new CLI.ValidationFailureError(''); } } diff --git a/src/lib/iac/service-mappings.ts b/src/lib/iac/service-mappings.ts index 408105f1e3..ad07a0c45c 100644 --- a/src/lib/iac/service-mappings.ts +++ b/src/lib/iac/service-mappings.ts @@ -1,6 +1,7 @@ import { CustomError } from '../errors'; import { IaCErrorCodes } from '../../cli/commands/test/iac/local-execution/types'; import { getErrorStringCode } from '../../cli/commands/test/iac/local-execution/error-utils'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; export const services2resources = new Map>([ // Amazon @@ -267,5 +268,6 @@ export class InvalidServiceError extends CustomError { this.code = IaCErrorCodes.InvalidServiceError; this.strCode = getErrorStringCode(this.code); this.userMessage = msg; + this.errorCatalog = new CLI.InvalidFlagOptionError(''); } } diff --git a/src/lib/iac/test/v2/errors.ts b/src/lib/iac/test/v2/errors.ts index 7e28292e48..4693c100aa 100644 --- a/src/lib/iac/test/v2/errors.ts +++ b/src/lib/iac/test/v2/errors.ts @@ -1,3 +1,4 @@ +import { CLI } from '@snyk/error-catalog-nodejs-public'; import { getErrorStringCode } from '../../../../cli/commands/test/iac/local-execution/error-utils'; import { IaCErrorCodes } from '../../../../cli/commands/test/iac/local-execution/types'; import { CustomError } from '../../../errors'; @@ -75,6 +76,7 @@ export class SnykIacTestError extends CustomError { }, scanError.fields, ); + this.errorCatalog = new CLI.GeneralIACFailureError(''); } public get path(): string { diff --git a/src/lib/iac/test/v2/local-cache/policy-engine/download.ts b/src/lib/iac/test/v2/local-cache/policy-engine/download.ts index 1b2092cd4b..4f1315c62a 100644 --- a/src/lib/iac/test/v2/local-cache/policy-engine/download.ts +++ b/src/lib/iac/test/v2/local-cache/policy-engine/download.ts @@ -14,6 +14,7 @@ import { policyEngineReleaseVersion, } from './constants'; import { saveFile } from '../../../../file-utils'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; const debugLog = createDebugLogger('snyk-iac'); @@ -68,6 +69,7 @@ export class FailedToDownloadPolicyEngineError extends CustomError { this.userMessage = `Could not fetch cache resource from: ${policyEngineUrl}` + '\nEnsure valid network connection.'; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } @@ -111,5 +113,6 @@ export class FailedToCachePolicyEngineError extends CustomError { this.userMessage = `Could not write the downloaded cache resource to: ${savePath}` + '\nEnsure the cache directory is writable.'; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } diff --git a/src/lib/iac/test/v2/local-cache/policy-engine/lookup-local.ts b/src/lib/iac/test/v2/local-cache/policy-engine/lookup-local.ts index e0ef28208f..f05235a290 100644 --- a/src/lib/iac/test/v2/local-cache/policy-engine/lookup-local.ts +++ b/src/lib/iac/test/v2/local-cache/policy-engine/lookup-local.ts @@ -5,6 +5,7 @@ import { getErrorStringCode } from '../../../../../../cli/commands/test/iac/loca import { TestConfig } from '../../types'; import { policyEngineFileName } from './constants'; import { InvalidUserPathError, lookupLocal } from '../utils'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; export class InvalidUserPolicyEnginePathError extends CustomError { constructor(path: string, message?: string, userMessage?: string) { @@ -18,6 +19,7 @@ export class InvalidUserPolicyEnginePathError extends CustomError { userMessage || `Could not find a valid Policy Engine executable in the configured path: ${path}` + '\nEnsure the configured path points to a valid Policy Engine executable.'; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } diff --git a/src/lib/iac/test/v2/local-cache/utils.ts b/src/lib/iac/test/v2/local-cache/utils.ts index 88bd0e33be..b1c63cfe14 100644 --- a/src/lib/iac/test/v2/local-cache/utils.ts +++ b/src/lib/iac/test/v2/local-cache/utils.ts @@ -4,6 +4,7 @@ import * as path from 'path'; import { CustomError } from '../../../../errors'; import { streamRequest } from '../../../../request/request'; import { ReadableStream } from 'needle'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; const debugLogger = createDebugLogger('snyk-iac'); @@ -38,6 +39,7 @@ export async function lookupLocal( export class InvalidUserPathError extends CustomError { constructor(message: string) { super(message); + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } diff --git a/src/lib/iac/test/v2/output.ts b/src/lib/iac/test/v2/output.ts index 658bf421f3..91c6ce0938 100644 --- a/src/lib/iac/test/v2/output.ts +++ b/src/lib/iac/test/v2/output.ts @@ -36,6 +36,7 @@ import { import * as wrapAnsi from 'wrap-ansi'; import { formatIacTestWarnings } from '../../../formatters/iac-output/text/failures/list'; import { IacV2Name, IacV2ShortLink } from '../../constants'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; export function buildOutput({ scanResult, @@ -273,16 +274,19 @@ export class NoSuccessfulScansError extends FormattedCustomError { : options.sarif ? responseData.sarif : firstErr.message; + const formattedMessage = isText + ? formatIacTestFailures( + errors.map((scanError) => ({ + failureReason: scanError.userMessage, + filePath: scanError.fields.path, + })), + ) + : stripAnsi(message); super( message, - isText - ? formatIacTestFailures( - errors.map((scanError) => ({ - failureReason: scanError.userMessage, - filePath: scanError.fields.path, - })), - ) - : stripAnsi(message), + formattedMessage, + undefined, + new CLI.GeneralIACFailureError(formattedMessage), ); this.code = firstErr.code; @@ -326,5 +330,6 @@ export class FoundIssuesError extends CustomError { this.userMessage = responseData.response; this.jsonStringifiedResults = responseData.json; this.sarifStringifiedResults = responseData.sarif; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } diff --git a/src/lib/iac/test/v2/scan/index.ts b/src/lib/iac/test/v2/scan/index.ts index e7c28d001a..4274bdfa37 100644 --- a/src/lib/iac/test/v2/scan/index.ts +++ b/src/lib/iac/test/v2/scan/index.ts @@ -13,6 +13,7 @@ import config from '../../../../config'; import { api, getOAuthToken } from '../../../../api-token'; import envPaths from 'env-paths'; import * as analytics from '../../../../analytics'; +import { CLI } from '@snyk/error-catalog-nodejs-public'; const debug = newDebug('snyk-iac'); const debugOutput = newDebug('snyk-iac:output'); @@ -343,5 +344,6 @@ class ScanError extends CustomError { this.code = IaCErrorCodes.PolicyEngineScanError; this.strCode = getErrorStringCode(this.code); this.userMessage = 'An error occurred when running the scan'; + this.errorCatalog = new CLI.GeneralIACFailureError(''); } } diff --git a/src/lib/snyk-test/run-test.ts b/src/lib/snyk-test/run-test.ts index 8e54764003..78fd649f66 100644 --- a/src/lib/snyk-test/run-test.ts +++ b/src/lib/snyk-test/run-test.ts @@ -23,6 +23,7 @@ import { import { AuthFailedError, BadGatewayError, + CustomError, DockerImageNotFoundError, errorMessageWithRetry, FailedToGetVulnerabilitiesError, @@ -412,6 +413,7 @@ export async function runTest( `Failed to test ${projectType} project`, error.code, error.innerError, + error.errorCatalog, ); } finally { spinner.clear(spinnerLbl)(); @@ -554,7 +556,7 @@ function sendTestPayload( function handleTestHttpErrorResponse(res, body) { const { statusCode } = res; - let err; + let err: CustomError; const userMessage = body && body.userMessage; switch (statusCode) { case 401: diff --git a/test/jest/acceptance/iac/iac-entitlement.spec.ts b/test/jest/acceptance/iac/iac-entitlement.spec.ts index ac30fa43ea..c64de2df64 100644 --- a/test/jest/acceptance/iac/iac-entitlement.spec.ts +++ b/test/jest/acceptance/iac/iac-entitlement.spec.ts @@ -15,12 +15,17 @@ describe('iac test with infrastructureAsCode entitlement', () => { afterAll(async () => teardown()); it('fails to scan because the user is not entitled to infrastructureAsCode', async () => { - const { stdout, exitCode } = await run( - `snyk iac test --org=no-iac-entitlements ./iac/terraform/sg_open_ssh.tf`, + const { stdout, stderr, exitCode } = await run( + `snyk iac test -d --org=no-iac-entitlements ./iac/terraform/sg_open_ssh.tf`, ); + expect(stdout).toContainText('SNYK-OPENAPI-0002'); expect(stdout).toContainText( 'This feature is currently not enabled for your org. To enable it, please contact snyk support.', ); + expect(stderr).toContainText('SNYK-OPENAPI-0002'); + expect(stderr).toContainText( + 'This feature is currently not enabled for your org. To enable it, please contact snyk support.', + ); expect(exitCode).toBe(2); }); }); diff --git a/test/jest/acceptance/snyk-sbom/npm-options.spec.ts b/test/jest/acceptance/snyk-sbom/npm-options.spec.ts index 6d34e99800..8fe2415628 100644 --- a/test/jest/acceptance/snyk-sbom/npm-options.spec.ts +++ b/test/jest/acceptance/snyk-sbom/npm-options.spec.ts @@ -67,11 +67,13 @@ describe('snyk sbom: npm options (mocked server only)', () => { ); expect(code).toEqual(2); + expect(stdout).toContainText('SNYK-CLI-0000'); expect(stdout).toContainText( 'Dependency snyk was not found in package-lock.json.', ); + expect(stderr).toContainText('SNYK-CLI-0000'); expect(stderr).toContainText( - 'OutOfSyncError: Dependency snyk was not found in package-lock.json.', + 'Dependency snyk was not found in package-lock.json.', ); }); }); diff --git a/test/jest/acceptance/snyk-sbom/sbom.spec.ts b/test/jest/acceptance/snyk-sbom/sbom.spec.ts index de88dd3cf5..ec1190899d 100644 --- a/test/jest/acceptance/snyk-sbom/sbom.spec.ts +++ b/test/jest/acceptance/snyk-sbom/sbom.spec.ts @@ -1,5 +1,5 @@ import * as fs from 'fs'; -import { NoSupportedManifestsFoundError } from '../../../../src/lib/errors/no-supported-manifests-found'; + import { createProject, createProjectFromWorkspace, @@ -202,7 +202,7 @@ describe('snyk sbom (mocked server only)', () => { test('`sbom` retains the exit error code of the underlying SCA process', async () => { const project = await createProject('empty'); - const { code, stdout } = await runSnykCLI( + const { code, stdout, stderr } = await runSnykCLI( `sbom --org aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee --format cyclonedx1.5+json --debug`, { cwd: project.path(), @@ -211,8 +211,9 @@ describe('snyk sbom (mocked server only)', () => { ); expect(code).toBe(3); - expect(stdout).toContainText( - NoSupportedManifestsFoundError([project.path()]).message, - ); + expect(stdout).toContainText('SNYK-CLI-0000'); + expect(stdout).toContainText('Could not detect supported target files'); + expect(stderr).toContainText('SNYK-CLI-0000'); + expect(stderr).toContainText('Could not detect supported target files'); }); }); diff --git a/test/jest/unit/cli/ipc.spec.ts b/test/jest/unit/cli/ipc.spec.ts new file mode 100644 index 0000000000..30506259b7 --- /dev/null +++ b/test/jest/unit/cli/ipc.spec.ts @@ -0,0 +1,104 @@ +import { CLI } from '@snyk/error-catalog-nodejs-public'; +import { sendError } from '../../../../src/cli/ipc'; +import { tmpdir } from 'os'; +import { ExcludeFlagBadInputError } from '../../../../src/lib/errors'; + +describe('sendError()', () => { + const backupEnv = { ...process.env }; + afterEach(() => { + process.env = { ...backupEnv }; + }); + + describe('returns true', () => { + beforeEach(() => { + process.env.SNYK_ERR_FILE = tmpdir() + '/tmp_err_file.txt'; + }); + + it('when given a simple error', async () => { + const error = new Error('something went wrong'); + expect(await sendError(error, false)).toBeTruthy(); + }); + + it('when given an Error Catalog error', async () => { + const error = new CLI.GeneralCLIFailureError('something went wrong'); + expect(await sendError(error, false)).toBeTruthy(); + }); + + it('when given an TS custom error', async () => { + const error = new ExcludeFlagBadInputError(); + expect(await sendError(error, false)).toBeTruthy(); + }); + + describe('JSON formatted errors', () => { + const JSONerr = { + ok: false, + code: 1234, + error: 'err in a list', + path: 'somewhere/file', + }; + + it('when given a single error', async () => { + const error = new Error(JSON.stringify(JSONerr)); + expect(await sendError(error, true)).toBeTruthy(); + }); + + it('when given an error in a list', async () => { + const errMsg = [ + { + meta: { + isPrivate: true, + isLicensesEnabled: false, + ignoreSettings: { + adminOnly: false, + reasonRequired: false, + disregardFilesystemIgnores: false, + autoApproveIgnores: false, + }, + org: 'ORG', + orgPublicId: 'ORGID', + policy: '', + }, + filesystemPolicy: false, + vulnerabilities: [], + dependencyCount: 0, + licensesPolicy: null, + ignoreSettings: null, + targetFile: 'var_deref/nested_var_deref/variables.tf', + projectName: 'badProject', + org: 'ORG', + policy: '', + isPrivate: true, + targetFilePath: + '/a/valid/path/fixtures/iac/terraform/var_deref/nested_var_deref/variables.tf', + packageManager: 'terraformconfig', + path: './path/fixtures/iac/terraform', + projectType: 'terraformconfig', + ok: true, + infrastructureAsCodeIssues: [], + }, + JSONerr, + ]; + const error = new Error(JSON.stringify(errMsg)); + expect(await sendError(error, true)).toBeTruthy(); + }); + }); + }); + + describe('returns false', () => { + it('when no error file path is specified', async () => { + const error = new Error('something went wrong'); + expect(await sendError(error, false)).toBeFalsy(); + }); + + it('when no error message is specified', async () => { + const error = new Error(''); + expect(await sendError(error, false)).toBeFalsy(); + }); + + it('when the file cannot be written', async () => { + process.env.SNYK_ERR_FILE = './does/not/exist'; + const error = new Error('something went wrong'); + expect(await sendError(error, false)).toBeFalsy(); + }); + }); +});