From 4f16863db8dc90462e3f3be0b9e89b4af179e54b Mon Sep 17 00:00:00 2001 From: rotem Date: Sun, 23 Aug 2020 16:07:40 +0300 Subject: [PATCH] feat: adding cta for docker users --- src/cli/commands/test/index.ts | 69 ++++++++++++----- src/lib/errors/index.ts | 1 + src/lib/errors/test-limit-reached-error.ts | 12 +++ src/lib/types.ts | 1 + test/acceptance/docker-token.test.ts | 88 ++++++++++++++++++++++ 5 files changed, 152 insertions(+), 19 deletions(-) create mode 100644 src/lib/errors/test-limit-reached-error.ts diff --git a/src/cli/commands/test/index.ts b/src/cli/commands/test/index.ts index 533af71d97b..695d0ba6713 100644 --- a/src/cli/commands/test/index.ts +++ b/src/cli/commands/test/index.ts @@ -49,6 +49,7 @@ import { import * as utils from './utils'; import { getIacDisplayedOutput } from './iac-output'; import { getEcosystem, testEcosystem } from '../../../lib/ecosystems'; +import { TestLimitReachedError } from '../../../lib/errors'; const debug = Debug('snyk-test'); const SEPARATOR = '\n-------------------------------------------------------\n'; @@ -100,6 +101,7 @@ async function test(...args: MethodArgs): Promise { } catch (err) { if (options.docker && getDockerToken()) { options.testDepGraphDockerEndpoint = '/docker-jwt/test-dep-graph'; + options.isDockerUser = true; } else { throw err; } @@ -274,6 +276,13 @@ async function test(...args: MethodArgs): Promise { // first one error.code = errorResults[0].code; error.userMessage = errorResults[0].userMessage; + if ( + error.userMessage === TestLimitReachedError().userMessage && + options.isDockerUser + ) { + error.userMessage = + 'You have reached your scan limit. Sign up to Snyk for additional free scans https://dockr.ly/3ePqVcp'; + } throw error; } @@ -472,13 +481,17 @@ function displayResult( ) ? '\n\nTip: Snyk only tests production dependencies by default. You can try re-running with the `--dev` flag.' : ''; + + const dockerCTA = dockerUserCTA(options); return ( prefix + meta + '\n\n' + summaryOKText + multiProjAdvice + - (isCI() ? '' : dockerAdvice + nextStepsText + snykPackageTestTip) + (isCI() + ? '' + : dockerAdvice + nextStepsText + snykPackageTestTip + dockerCTA) ); } @@ -562,23 +575,7 @@ function getDisplayedOutput( '\n\nRun `snyk wizard` to address these issues.', ); } - let dockerSuggestion = ''; - if (options.docker && config.disableSuggestions !== 'true') { - const optOutSuggestions = - '\n\nTo remove this message in the future, please run `snyk config set disableSuggestions=true`'; - if (!options.file) { - dockerSuggestion += - chalk.bold.white( - '\n\nPro tip: use `--file` option to get base image remediation advice.' + - `\nExample: $ snyk test --docker ${options.path} --file=path/to/Dockerfile`, - ) + optOutSuggestions; - } else if (!options['exclude-base-image-vulns']) { - dockerSuggestion += - chalk.bold.white( - '\n\nPro tip: use `--exclude-base-image-vulns` to exclude from display Docker base image vulnerabilities.', - ) + optOutSuggestions; - } - } + const dockerSuggestion = getDockerSuggestionText(options, config); const vulns = res.vulnerabilities || []; const groupedVulns: GroupedVuln[] = groupVulnerabilities(vulns); @@ -632,13 +629,15 @@ function getDisplayedOutput( } const ignoredIssues = ''; + const dockerCTA = dockerUserCTA(options); return ( prefix + body + multiProjAdvice + ignoredIssues + dockerAdvice + - dockerSuggestion + dockerSuggestion + + dockerCTA ); } @@ -708,3 +707,35 @@ function metadataForVuln(vuln): VulnMetaData { packageManager: vuln.packageManager, }; } + +function getDockerSuggestionText(options, config): string { + if (!options.docker || options.isDockerUser) { + return ''; + } + + let dockerSuggestion = ''; + if (config && config.disableSuggestions !== 'true') { + const optOutSuggestions = + '\n\nTo remove this message in the future, please run `snyk config set disableSuggestions=true`'; + if (!options.file) { + dockerSuggestion += + chalk.bold.white( + '\n\nPro tip: use `--file` option to get base image remediation advice.' + + `\nExample: $ snyk test --docker ${options.path} --file=path/to/Dockerfile`, + ) + optOutSuggestions; + } else if (!options['exclude-base-image-vulns']) { + dockerSuggestion += + chalk.bold.white( + '\n\nPro tip: use `--exclude-base-image-vulns` to exclude from display Docker base image vulnerabilities.', + ) + optOutSuggestions; + } + } + return dockerSuggestion; +} + +function dockerUserCTA(options) { + if (options.isDockerUser) { + return '\n\nFor more free scans that keep your images secure, sign up to Snyk at https://dockr.ly/3ePqVcp'; + } + return ''; +} diff --git a/src/lib/errors/index.ts b/src/lib/errors/index.ts index c5a8479b68f..e59ba2471e0 100644 --- a/src/lib/errors/index.ts +++ b/src/lib/errors/index.ts @@ -23,3 +23,4 @@ export { NotSupportedIacFileError, IllegalIacFileError, } from './invalid-iac-file'; +export { TestLimitReachedError } from './test-limit-reached-error'; diff --git a/src/lib/errors/test-limit-reached-error.ts b/src/lib/errors/test-limit-reached-error.ts new file mode 100644 index 00000000000..f560c220a33 --- /dev/null +++ b/src/lib/errors/test-limit-reached-error.ts @@ -0,0 +1,12 @@ +import { CustomError } from './custom-error'; + +export function TestLimitReachedError( + errorMessage = 'Test limit reached!', + errorCode = 429, +) { + const error = new CustomError(errorMessage); + error.code = errorCode; + error.strCode = 'TEST_LIMIT_REACHED'; + error.userMessage = errorMessage; + return error; +} diff --git a/src/lib/types.ts b/src/lib/types.ts index 9e182c49065..70147067014 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -22,6 +22,7 @@ export interface TestOptions { reachableVulnsTimeout?: number; yarnWorkspaces?: boolean; testDepGraphDockerEndpoint?: string | null; + isDockerUser?: boolean; } export interface WizardOptions { diff --git a/test/acceptance/docker-token.test.ts b/test/acceptance/docker-token.test.ts index af054ef0967..4eca128c07f 100644 --- a/test/acceptance/docker-token.test.ts +++ b/test/acceptance/docker-token.test.ts @@ -120,6 +120,94 @@ test('`snyk test` without docker flag - docker token and no api key', async (t) } }); +test('`snyk test` with docker flag - displays CTA', async (t) => { + stubDockerPluginResponse( + plugins, + { + plugin: { + packageManager: 'deb', + }, + package: { + name: 'docker-image', + dependencies: { + 'apt/libapt-pkg5.0': { + version: '1.6.3ubuntu0.1', + dependencies: { + 'bzip2/libbz2-1.0': { + version: '1.0.6-8.1', + }, + }, + }, + 'bzip2/libbz2-1.0': { + version: '1.0.6-8.1', + }, + }, + }, + }, + t, + ); + const vulns = require('./fixtures/docker/find-result.json'); + server.setNextResponse(vulns); + + try { + await cli.test('foo:latest', { + docker: true, + }); + } catch (err) { + const msg = err.message; + t.match( + msg, + 'For more free scans that keep your images secure, sign up to Snyk at https://dockr.ly/3ePqVcp', + 'displays docker CTA for scan with vulns', + ); + } +}); + +test('`snyk test` with docker flag - does not display CTA', async (t) => { + stubDockerPluginResponse( + plugins, + { + plugin: { + packageManager: 'deb', + }, + package: { + name: 'docker-image', + dependencies: { + 'apt/libapt-pkg5.0': { + version: '1.6.3ubuntu0.1', + dependencies: { + 'bzip2/libbz2-1.0': { + version: '1.0.6-8.1', + }, + }, + }, + 'bzip2/libbz2-1.0': { + version: '1.0.6-8.1', + }, + }, + }, + }, + t, + ); + const vulns = require('./fixtures/docker/find-result.json'); + server.setNextResponse(vulns); + await cli.config('set', 'api=' + apiKey); + try { + await cli.test('foo:latest', { + docker: true, + }); + } catch (err) { + const msg = err.message; + t.notMatch( + msg, + 'For more free scans that keep your images secure, sign up to Snyk at https://dockr.ly/3ePqVcp', + 'does not display docker CTA if API key was used', + ); + } + await cli.config('unset', 'api'); + t.end(); +}); + test('teardown', async (t) => { t.plan(4);