From 0ddc51731acb3e5090fd3b0a69e95bd9567ab000 Mon Sep 17 00:00:00 2001 From: Martin Guibert Date: Thu, 3 Nov 2022 11:32:17 +0100 Subject: [PATCH] feat: improve errors for cloud context Using cloud context without integrated iac flag on your org will now display a message explaining why it doesn't work. When an error happen with cloud context we now display the error content instead of juste asking the user to re run with -d. --- .../assert-iac-options-flag.ts | 46 ++++++++++++++++- src/cli/commands/test/iac/scan.ts | 6 ++- src/lib/iac/test/v2/errors.ts | 16 +++--- .../unit/iac/assert-iac-options-flag.spec.ts | 51 +++++++++++++++++++ test/jest/unit/lib/iac/test/v2/errors.spec.ts | 10 ++-- 5 files changed, 117 insertions(+), 12 deletions(-) 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 17fcc631c07..3fbdaf92b2f 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 @@ -1,7 +1,12 @@ import { CustomError } from '../../../../../lib/errors'; import { args } from '../../../../args'; import { getErrorStringCode } from './error-utils'; -import { IaCErrorCodes, IaCTestFlags, TerraformPlanScanMode } from './types'; +import { + IaCErrorCodes, + IacOrgSettings, + IaCTestFlags, + TerraformPlanScanMode, +} from './types'; import { Options, TestOptions } from '../../../../../lib/types'; const keys: (keyof IaCTestFlags)[] = [ @@ -38,7 +43,13 @@ const keys: (keyof IaCTestFlags)[] = [ 'remote-repo-url', 'target-name', ]; +const integratedKeys: (keyof IaCTestFlags)[] = [ + 'snyk-cloud-environment', + 'cloud-context', +]; + const allowed = new Set(keys); +const integratedOnlyFlags = new Set(integratedKeys); function camelcaseToDash(key: string) { return key.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()); @@ -61,6 +72,17 @@ export class FlagError extends CustomError { } } +export class IntegratedFlagError extends CustomError { + constructor(key: string, org: string) { + const flag = getFlagName(key); + const msg = `Flag "${flag}" is only supported when using Integrated IaC. To enable it for your organisation "${org}", please contact Snyk support.`; + super(msg); + this.code = IaCErrorCodes.FlagError; + this.strCode = getErrorStringCode(this.code); + this.userMessage = msg; + } +} + export class FeatureFlagError extends CustomError { constructor(key: string, featureFlag: string, hasSnykPreview?: boolean) { const flag = getFlagName(key); @@ -139,6 +161,28 @@ export function assertIaCOptionsFlags(argv: string[]): void { } } +/** + * Check that the flags used for the v1 flow do not contain any flag that are + * only usable with the new integrated iac (v2) flow + * @param settings organisation settings, used to get the org name + * @param argv command line args + */ +export function assertIntegratedIaCOnlyOptions( + settings: IacOrgSettings, + argv: string[], +): void { + // We process the process.argv so we don't get default values. + const parsed = args(argv); + for (const key of Object.keys(parsed.options)) { + // The _ property is a special case that contains non + // flag strings passed to the command line (usually files) + // and `iac` is the command provided. + if (key !== '_' && key !== 'iac' && integratedOnlyFlags.has(key)) { + throw new IntegratedFlagError(key, settings.meta.org); + } + } +} + const SUPPORTED_TF_PLAN_SCAN_MODES = [ TerraformPlanScanMode.DeltaScan, TerraformPlanScanMode.FullScan, diff --git a/src/cli/commands/test/iac/scan.ts b/src/cli/commands/test/iac/scan.ts index 72715025278..e652eb560ad 100644 --- a/src/cli/commands/test/iac/scan.ts +++ b/src/cli/commands/test/iac/scan.ts @@ -13,7 +13,10 @@ import * as utils from '../utils'; import { spinnerMessage } from '../../../../lib/formatters/iac-output/text'; import { test as iacTest } from './local-execution'; -import { assertIaCOptionsFlags } from './local-execution/assert-iac-options-flag'; +import { + assertIaCOptionsFlags, + assertIntegratedIaCOnlyOptions, +} from './local-execution/assert-iac-options-flag'; import { initRules } from './local-execution/rules/rules'; import { cleanLocalCache } from './local-execution/measurable-methods'; import * as ora from 'ora'; @@ -79,6 +82,7 @@ export async function scan( let res: (TestResult | TestResult[]) | Error; try { + assertIntegratedIaCOnlyOptions(iacOrgSettings, process.argv); assertIaCOptionsFlags(process.argv); if (pathLib.relative(projectRoot, path).includes('..')) { diff --git a/src/lib/iac/test/v2/errors.ts b/src/lib/iac/test/v2/errors.ts index 665360c7969..2bb5d24462d 100644 --- a/src/lib/iac/test/v2/errors.ts +++ b/src/lib/iac/test/v2/errors.ts @@ -30,15 +30,11 @@ const snykIacTestErrorsUserMessages = { UnableToReadPath: 'Unable to read path', NoLoadableInput: "The Snyk CLI couldn't find any valid IaC configuration files to scan", - FailedToMakeResourcesResolvers: - 'An error occurred preparing the requested cloud context. Please run the command again with the `-d` flag for more information.', - ResourcesResolverError: - 'An error occurred scanning cloud resources. Please run the command again with the `-d` flag for more information.', FailedToProcessResults: 'An error occurred while processing results. Please run the command again with the `-d` flag for more information.', }; -export function getErrorUserMessage(code: number): string { +export function getErrorUserMessage(code: number, error: string): string { if (code < 2000 || code >= 3000) { return 'INVALID_SNYK_IAC_TEST_ERROR'; } @@ -46,6 +42,14 @@ export function getErrorUserMessage(code: number): string { if (!errorName) { return 'INVALID_IAC_ERROR'; } + + if ( + code == IaCErrorCodes.FailedToMakeResourcesResolvers || + code == IaCErrorCodes.ResourcesResolverError + ) { + return `${error}. Please run the command again with the \`-d\` flag for more information.`; + } + return snykIacTestErrorsUserMessages[errorName]; } @@ -56,7 +60,7 @@ export class SnykIacTestError extends CustomError { super(scanError.message); this.code = scanError.code; this.strCode = getErrorStringCode(this.code); - this.userMessage = getErrorUserMessage(this.code); + this.userMessage = getErrorUserMessage(this.code, scanError.message); this.fields = Object.assign( { path: '', diff --git a/test/jest/unit/iac/assert-iac-options-flag.spec.ts b/test/jest/unit/iac/assert-iac-options-flag.spec.ts index 0805c20d9ec..71da3975dc5 100644 --- a/test/jest/unit/iac/assert-iac-options-flag.spec.ts +++ b/test/jest/unit/iac/assert-iac-options-flag.spec.ts @@ -1,7 +1,58 @@ import { assertIaCOptionsFlags, + assertIntegratedIaCOnlyOptions, FlagValueError, + IntegratedFlagError, } from '../../../../src/cli/commands/test/iac/local-execution/assert-iac-options-flag'; +import { IacOrgSettings } from '../../../../src/cli/commands/test/iac/local-execution/types'; + +describe('assertIntegratedIaCOnlyOptions()', () => { + const command = ['node', 'cli', 'iac', 'test']; + const files = ['input.tf']; + const org: IacOrgSettings = { + customPolicies: {}, + meta: { + org: 'orgname', + orgPublicId: 'orgpublicid', + }, + }; + + it('accepts all command line flags accepted by the iac command', () => { + const options = [ + '--debug', + '--insecure', + '--detection-depth', + '--severity-threshold', + '--json', + '--sarif', + '--json-file-output', + '--sarif-file-output', + '-v', + '--version', + '-h', + '--help', + '-q', + '--quiet', + ]; + expect(() => + assertIntegratedIaCOnlyOptions(org, [...command, ...options, ...files]), + ).not.toThrow(); + }); + + it('Refuses cloud-context flag', () => { + const options = ['--cloud-context', 'aws']; + expect(() => + assertIntegratedIaCOnlyOptions(org, [...command, ...options, ...files]), + ).toThrow(new IntegratedFlagError('cloud-context', org.meta.org)); + }); + + it('Refuses snyk-cloud-environment flag', () => { + const options = ['--snyk-cloud-environment', 'envid']; + expect(() => + assertIntegratedIaCOnlyOptions(org, [...command, ...options, ...files]), + ).toThrow(new IntegratedFlagError('snyk-cloud-environment', org.meta.org)); + }); +}); describe('assertIaCOptionsFlags()', () => { const command = ['node', 'cli', 'iac', 'test']; diff --git a/test/jest/unit/lib/iac/test/v2/errors.spec.ts b/test/jest/unit/lib/iac/test/v2/errors.spec.ts index 86805e2879d..7c03bdbf67f 100644 --- a/test/jest/unit/lib/iac/test/v2/errors.spec.ts +++ b/test/jest/unit/lib/iac/test/v2/errors.spec.ts @@ -2,12 +2,14 @@ import { getErrorUserMessage } from '../../../../../../../src/lib/iac/test/v2/er describe('getErrorUserMessage', () => { it('returns INVALID_SNYK_IAC_TEST_ERROR for an invalid snyk-iac-test error code', () => { - expect(getErrorUserMessage(0)).toEqual('INVALID_SNYK_IAC_TEST_ERROR'); - expect(getErrorUserMessage(3000)).toEqual('INVALID_SNYK_IAC_TEST_ERROR'); + expect(getErrorUserMessage(0, '')).toEqual('INVALID_SNYK_IAC_TEST_ERROR'); + expect(getErrorUserMessage(3000, '')).toEqual( + 'INVALID_SNYK_IAC_TEST_ERROR', + ); }); it('returns INVALID_IAC_ERROR for an invalid error code', () => { - expect(getErrorUserMessage(2999)).toEqual('INVALID_IAC_ERROR'); + expect(getErrorUserMessage(2999, '')).toEqual('INVALID_IAC_ERROR'); }); it.each` @@ -34,7 +36,7 @@ describe('getErrorUserMessage', () => { `( 'returns a user message for a valid snyk-iac-test error code - $expectedErrorCode', ({ expectedErrorCode }) => { - expect(typeof getErrorUserMessage(expectedErrorCode)).toBe('string'); + expect(typeof getErrorUserMessage(expectedErrorCode, '')).toBe('string'); }, ); });