diff --git a/cli/args.js b/cli/args.js index 224b9bdf99..b0260a45a0 100644 --- a/cli/args.js +++ b/cli/args.js @@ -100,7 +100,8 @@ function args(processargv) { command === 'modules' || command === 'scenario' || command === 'monitor' || - command === 'wizard') { + command === 'wizard' || + command === 'ignore') { // copy all the options across to argv._ as an object argv._.push(argv); } diff --git a/cli/commands/ignore.js b/cli/commands/ignore.js new file mode 100644 index 0000000000..d845ef2df9 --- /dev/null +++ b/cli/commands/ignore.js @@ -0,0 +1,46 @@ +module.exports = ignore; + +var debug = require('debug')('snyk'); +var policy = require('snyk-policy'); +var Promise = require('es6-promise').Promise; // jshint ignore:line + +function ignore(options) { + debug('snyk ignore called with options: %O', options); + if (!options.id) { + return Promise.reject(Error('idRequired')); + } + options.expiry = new Date(options.expiry); + if (options.expiry.getTime() !== options.expiry.getTime()) { + debug('No/invalid expiry given, using the default 30 days'); + options.expiry = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); + } + if (!options.reason) { + options.reason = 'None Given'; + } + if (!options.path) { + debug('using cwd() as path'); + options.path = process.cwd(); + } + + debug('changing policy: ignore "%s", for all paths, reason: "%s", until: %o', + options.id, options.reason, options.expiry); + return policy.load(options.path) + .catch(function (error) { + if (error.code === 'ENOENT') { // file does not exist - create it + return policy.create(); + } + throw Error('policyFile'); + }) + .then(function ignoreIssue(pol) { + pol.ignore[options.id] = [ + { + '*': + { + reason: options.reason, + expires: options.expiry, + }, + }, + ]; + policy.save(pol, options.path); + }); +} diff --git a/cli/commands/index.js b/cli/commands/index.js index 8212986055..eb34e5a403 100644 --- a/cli/commands/index.js +++ b/cli/commands/index.js @@ -6,18 +6,19 @@ require('../../lib/spinner').isRequired = false; // time as low as possible var commands = { - help: hotload('./help'), auth: hotload('./auth'), - version: hotload('./version'), config: hotload('./config'), + help: hotload('./help'), + ignore: hotload('./ignore'), + modules: hotload('./modules'), monitor: hotload('./monitor'), - test: hotload('./test'), policy: hotload('./policy'), protect: hotload('./protect'), - wizard: hotload('./protect/wizard'), - modules: hotload('./modules'), scenario: hotload('./scenario'), + test: hotload('./test'), 'test-unpublished': hotload('./unpublished'), + version: hotload('./version'), + wizard: hotload('./protect/wizard'), }; commands.aliases = abbrev(Object.keys(commands)); commands.aliases.t = 'test'; diff --git a/help/help.txt b/help/help.txt index e228bbc475..6b71c63bea 100644 --- a/help/help.txt +++ b/help/help.txt @@ -19,6 +19,7 @@ Commands: monitor ............ Record the state of dependencies and any vulnerabilities on snyk.io. policy ............. Display the Snyk policy for a package. + ignore ............. Ignore an issue. For more help run `snyk help ignore`. Options: diff --git a/help/ignore.txt b/help/ignore.txt new file mode 100644 index 0000000000..d5c64d1af0 --- /dev/null +++ b/help/ignore.txt @@ -0,0 +1,16 @@ + +Ignore: + + $ snyk ignore --id=IssueID [--expiry=expiry] [--reason='reason for ignoring'] + +Ignore a certain issue, according to its snyk ID for all occurrences. + +Options: + + id ................. snyk ID for the issue to ignore. Required. + expiry ............. expiry date string, according to RFC2822 (https://tools.ietf.org/html/rfc2822#page-14) + reason ............. the reason to ignore this issue + +Examples: + + $ snyk ignore --id='npm:qs:20170213' --expiry='2017-03-30' --reason='testing' diff --git a/lib/error.js b/lib/error.js index 8a15a20793..1beafb7b4e 100644 --- a/lib/error.js +++ b/lib/error.js @@ -28,6 +28,9 @@ var errors = { 'and try again.', timeout: 'The request has timed out on the server side.\nPlease re-run ' + 'this command with the `-d` flag and send the output to support@snyk.io.', + policyFile: 'Bad policy file, please use --path=PATH to specify a ' + + 'directory with a .snyk file', + idRequired: 'id is a required field for `snyk ignore`', }; // a key/value pair of error.code (or error.message) as the key, and our nice diff --git a/test/cli.test.js b/test/cli.test.js index cbd78b972c..cf19ac193c 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -8,6 +8,7 @@ var port = process.env.PORT = process.env.SNYK_PORT = 12345; var sinon = require('sinon'); var proxyquire = require('proxyquire'); var parse = require('url').parse; +var policy = require('snyk-policy'); process.env.SNYK_API = 'http://localhost:' + port + '/api/v1'; process.env.SNYK_HOST = 'http://localhost:' + port; @@ -144,6 +145,58 @@ test('auth via github', function (t) { }); }); +test('snyk ignore - all options', function (t) { + t.plan(1); + var fullPolicy = {ID: [ + {'*': { + reason: 'REASON', + expires: new Date('2017-10-07T00:00:00.000Z'), }, + }, + ], + }; + var dir = testUtils.tmpdir(); + cli.ignore({ + id: 'ID', reason: 'REASON', expiry: new Date('2017-10-07'), path: dir, + }).catch(() => t.fail('ignore should succeed')) + .then(() => policy.load(dir)) + .then(pol => { + t.deepEquals(pol.ignore, fullPolicy, 'policy written correctly'); + }); +}); + +test('snyk ignore - no ID', function (t) { + t.plan(1); + var dir = testUtils.tmpdir(); + cli.ignore({ + reason: 'REASON', expiry: new Date('2017-10-07'), path: dir, + }).then(function (res) { + t.fail('should not succeed with missing ID'); + }).catch(function (e) { + var errors = require('../lib/error'); + var message = chalk.stripColor(errors.message(e)); + t.equal(message.toLowerCase().indexOf('id is a required field'), 0, + 'captured failed ignore (no --id given)'); + }); +}); + +test('snyk ignore - default options', function (t) { + t.plan(3); + var dir = testUtils.tmpdir(); + cli.ignore({id: 'ID3', path: dir, + }).catch(() => t.fail('ignore should succeed')) + .then(() => policy.load(dir)) + .then(pol => { + t.true(pol.ignore.ID3, 'policy ID written correctly'); + t.is(pol.ignore.ID3[0]['*'].reason, 'None Given', + 'policy (default) reason written correctly'); + var expiryFromNow = pol.ignore.ID3[0]['*'].expires - Date.now(); + // not more than 30 days ahead, not less than (30 days - 1 minute) + t.true(expiryFromNow <= 30 * 24 * 60 * 60 * 1000 && + expiryFromNow >= 30 * 24 * 59 * 60 * 1000, + 'policy (default) expiry wirtten correctly'); + }); +}); + after('teardown', function (t) { t.plan(4); diff --git a/test/utils.js b/test/utils.js index fbd5292b70..9f8c4d89d1 100644 --- a/test/utils.js +++ b/test/utils.js @@ -1,8 +1,13 @@ module.exports = { silenceLog: silenceLog, extendExpiries: extendExpiries, + tmpdir: tmpdir, }; +var osTmpdir = require('os').tmpdir; +var join = require('path').join; +var mkdirSync = require('fs').mkdirSync; + function silenceLog() { var old = console.log; @@ -21,4 +26,11 @@ function extendExpiries(policy) { rule[path].expires = d; }); }); -} \ No newline at end of file +} + +function tmpdir() { + var dirname = join(osTmpdir(), 'TMP' + Math.random().toString(36) + .replace(/[^a-z0-9]+/g, '').substr(2, 12)); + mkdirSync(dirname); + return dirname; +}