diff --git a/src/cli/args.ts b/src/cli/args.ts index bd1fa9b7f9..583880c566 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -1,6 +1,7 @@ import * as abbrev from 'abbrev'; import debugModule = require('debug'); +import { parseMode } from './modes'; export declare interface Global extends NodeJS.Global { ignoreUnknownCA: boolean; @@ -111,6 +112,9 @@ export function args(rawArgv: string[]): Args { // an argument to our command, like `snyk help protect` let command = argv._.shift() as string; // can actually be undefined + // snyk [mode?] [command] [paths?] [options-double-dash] + command = parseMode(command, argv); + // alias switcheroo - allows us to have if (cli.aliases[command]) { command = cli.aliases[command]; diff --git a/src/cli/index.ts b/src/cli/index.ts index 4d339255c7..eef8dbc2bb 100755 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -25,6 +25,7 @@ import { } from '../lib/errors'; import stripAnsi from 'strip-ansi'; import { ExcludeFlagInvalidInputError } from '../lib/errors/exclude-flag-invalid-input'; +import { modeValidation } from './modes'; const debug = Debug('snyk'); const EXIT_CODES = { @@ -158,6 +159,8 @@ async function main() { let failed = false; let exitCode = EXIT_CODES.ERROR; try { + modeValidation(args); + if (args.options.scanAllUnmanaged && args.options.file) { throw new UnsupportedOptionCombinationError([ 'file', diff --git a/src/cli/modes.ts b/src/cli/modes.ts new file mode 100644 index 0000000000..02c3bfc833 --- /dev/null +++ b/src/cli/modes.ts @@ -0,0 +1,66 @@ +import * as abbrev from 'abbrev'; +import { UnsupportedOptionCombinationError, CustomError } from '../lib/errors'; + +interface ModeData { + allowedCommands: Array; + config: (args) => []; +} + +const modes: Record = { + container: { + allowedCommands: ['test', 'monitor'], + config: (args): [] => { + args['docker'] = true; + + return args; + }, + }, +}; + +export function parseMode(mode: string, args): string { + if (isValidMode(mode)) { + const command: string = args._[0]; + + if (isValidCommand(mode, command)) { + configArgs(mode, args); + mode = args._.shift(); + } + } + + return mode; +} + +export function modeValidation(args: object) { + const mode = args['command']; + const commands: Array = args['options']._; + + if (isValidMode(mode) && commands.length <= 1) { + const allowed = modes[mode].allowedCommands + .join(', ') + .replace(/, ([^,]*)$/, ' or $1'); + const message = `use snyk ${mode} with ${allowed}`; + + throw new CustomError(message); + } + + const command = commands[0]; + if (isValidMode(mode) && !isValidCommand(mode, command)) { + const notSupported = [mode, command]; + + throw new UnsupportedOptionCombinationError(notSupported); + } +} + +function isValidMode(mode: string): boolean { + return Object.keys(modes).includes(mode); +} + +function isValidCommand(mode: string, command: string): boolean { + const aliases = abbrev(modes[mode].allowedCommands); + + return Object.keys(aliases).includes(command); +} + +function configArgs(mode: string, args): [] { + return modes[mode].config(args); +} diff --git a/test/args.test.ts b/test/args.test.ts index f7f470ebae..1f35fe1e42 100644 --- a/test/args.test.ts +++ b/test/args.test.ts @@ -150,3 +150,39 @@ test('test command line test --container', (t) => { t.notOk(result.options.container); t.end(); }); + +test('test command line "container test"', (t) => { + const cliArgs = [ + '/Users/dror/.nvm/versions/node/v6.9.2/bin/node', + '/Users/dror/work/snyk/snyk-internal/cli', + 'container', + 'test', + ]; + const result = args(cliArgs); + t.ok(result.options.docker); + t.end(); +}); + +test('test command line "container monitor"', (t) => { + const cliArgs = [ + '/Users/dror/.nvm/versions/node/v6.9.2/bin/node', + '/Users/dror/work/snyk/snyk-internal/cli', + 'container', + 'monitor', + ]; + const result = args(cliArgs); + t.ok(result.options.docker); + t.end(); +}); + +test('test command line "container protect"', (t) => { + const cliArgs = [ + '/Users/dror/.nvm/versions/node/v6.9.2/bin/node', + '/Users/dror/work/snyk/snyk-internal/cli', + 'container', + 'protect', + ]; + const result = args(cliArgs); + t.notOk(result.options.docker); + t.end(); +}); diff --git a/test/modes.test.ts b/test/modes.test.ts new file mode 100644 index 0000000000..8f70ad1cec --- /dev/null +++ b/test/modes.test.ts @@ -0,0 +1,212 @@ +import { test } from 'tap'; +import { + UnsupportedOptionCombinationError, + CustomError, +} from '../src/lib/errors'; +import { parseMode, modeValidation } from '../src/cli/modes'; + +test('when is missing command', (c) => { + c.test('should do nothing', (t) => { + const cliCommand = 'container'; + const cliArgs = { + _: [], + 'package-manager': 'pip', + }; + + const command = parseMode(cliCommand, cliArgs); + + t.equal(command, cliCommand); + t.equal(cliArgs, cliArgs); + t.notOk(cliArgs['docker']); + t.end(); + }); + c.end(); +}); + +test('when is not a valid mode', (c) => { + c.test('should do nothing', (t) => { + const cliCommand = 'test'; + const cliArgs = { + _: [], + 'package-manager': 'pip', + }; + + const command = parseMode(cliCommand, cliArgs); + + t.equal(command, cliCommand); + t.equal(cliArgs, cliArgs); + t.notOk(cliArgs['docker']); + t.end(); + }); + c.end(); +}); + +test('when is a valid mode', (c) => { + c.test('when is allowed command', (d) => { + d.test( + '"container test" should set docker option and test command', + (t) => { + const expectedCommand = 'test'; + const expectedArgs = { + _: [], + docker: true, + 'package-manager': 'pip', + }; + const cliCommand = 'container'; + const cliArgs = { + _: ['test'], + 'package-manager': 'pip', + }; + + const command = parseMode(cliCommand, cliArgs); + + t.equal(command, expectedCommand); + t.same(cliArgs, expectedArgs); + t.ok(cliArgs['docker']); + t.end(); + }, + ); + + d.test('when there is a command alias', (t) => { + t.test('"container t" should set docker option and test command', (t) => { + const expectedCommand = 't'; + const expectedArgs = { + _: [], + docker: true, + 'package-manager': 'pip', + }; + const cliCommand = 'container'; + const cliArgs = { + _: ['t'], + 'package-manager': 'pip', + }; + + const command = parseMode(cliCommand, cliArgs); + + t.equal(command, expectedCommand); + t.same(cliArgs, expectedArgs); + t.ok(cliArgs['docker']); + t.end(); + }); + t.end(); + }); + d.end(); + }); + + c.test('when is not allowed command', (d) => { + d.test( + '"container protect" should not set docker option and return same command', + (t) => { + const expectedCommand = 'container'; + const expectedArgs = { + _: ['protect'], + 'package-manager': 'pip', + }; + const cliCommand = 'container'; + const cliArgs = { + _: ['protect'], + 'package-manager': 'pip', + }; + + const command = parseMode(cliCommand, cliArgs); + + t.equal(command, expectedCommand); + t.same(cliArgs, expectedArgs); + t.notOk(cliArgs['docker']); + t.end(); + }, + ); + d.end(); + }); + + c.test('mode validation', (d) => { + d.test('when there is no command, throw error', (t) => { + const args = { + command: 'container', + options: { + _: ['container'], + }, + }; + + try { + modeValidation(args); + } catch (err) { + t.ok(err instanceof CustomError, 'should throw CustomError'); + t.equal( + err.message, + 'use snyk container with test or monitor', + 'should have error message', + ); + t.end(); + } + }); + + d.test('when command is not valid, throw error', (t) => { + const args = { + command: 'container', + options: { + _: ['protect', 'container'], + }, + }; + + try { + modeValidation(args); + } catch (err) { + t.ok( + err instanceof UnsupportedOptionCombinationError, + 'should throw UnsupportedOptionCombinationError', + ); + t.equal( + err.message, + 'The following option combination is not currently supported: container + protect', + 'should have error message', + ); + t.end(); + } + }); + + d.test('when command is valid, do nothing', (t) => { + const args = { + command: 'container', + options: { + _: ['test', 'container'], + }, + }; + + modeValidation(args); + + t.ok('should not throw error'); + t.end(); + }); + + d.test('when there is no valid mode, do nothing', (t) => { + const args = { + command: 'test', + options: { + _: ['test'], + }, + }; + + modeValidation(args); + + t.ok('should not throw error'); + t.end(); + }); + + d.test('when there is no mode, do nothing', (t) => { + const args = { + command: '', + options: { + _: [], + }, + }; + + modeValidation(args); + + t.ok('should not throw error'); + t.end(); + }); + d.end(); + }); + c.end(); +});