Comparing version 1.5.1 to 1.6.0-alpha1
@@ -88,2 +88,4 @@ module.exports = args; | ||
command === 'test' || | ||
command === 'modules' || | ||
command === 'scenario' || | ||
command === 'wizard') { | ||
@@ -90,0 +92,0 @@ // copy all the options across to argv._ as an object |
@@ -19,5 +19,6 @@ var abbrev = require('abbrev'); | ||
wizard: hotload('./protect/wizard'), | ||
// modules: hotload('./modules'), | ||
modules: hotload('./modules'), | ||
scenario: hotload('./scenario'), | ||
}; | ||
commands.aliases = abbrev(Object.keys(commands)); | ||
module.exports = commands; |
var snyk = require('../../lib'); | ||
var Promise = require('es6-promise').Promise; // jshint ignore:line | ||
module.exports = function (path) { | ||
module.exports = function (path, options) { | ||
return snyk.modules(path || process.cwd()).then(function (modules) { | ||
@@ -12,2 +12,6 @@ | ||
if (options.json) { | ||
return JSON.stringify(modules, '', 2); | ||
} | ||
return parent + Object.keys(modules.dependencies).map(function (key) { | ||
@@ -14,0 +18,0 @@ return modules.dependencies[key].full; |
@@ -8,4 +8,6 @@ module.exports = { | ||
var _ = require('lodash'); | ||
var semver = require('semver'); | ||
var debug = require('debug')('snyk'); | ||
var protect = require('../../../lib/protect'); | ||
var moduleToObject = require('snyk-module'); | ||
var undefsafe = require('undefsafe'); | ||
@@ -15,3 +17,220 @@ var config = require('../../../lib/config'); | ||
// via http://stackoverflow.com/a/4760279/22617 | ||
function sort(prop) { | ||
var sortOrder = 1; | ||
if (prop[0] === '-') { | ||
sortOrder = -1; | ||
prop = prop.substr(1); | ||
} | ||
return function (a, b) { | ||
var result = (a[prop] < b[prop]) ? -1 : (a[prop] > b[prop]) ? 1 : 0; | ||
return result * sortOrder; | ||
}; | ||
} | ||
function sortPrompts(a, b) { | ||
var res = 0; | ||
// first sort by module affected | ||
var pa = moduleToObject(a.from[1]); | ||
var pb = moduleToObject(b.from[1]); | ||
res = sort('name')(pa, pb); | ||
if (res !== 0) { | ||
return res; | ||
} | ||
// we should have the same module, so the depth should be the same | ||
debug('sorting by upgradePath', a.upgradePath[1], b.upgradePath[1]); | ||
if (a.upgradePath[1] && b.upgradePath[1]) { | ||
// put upgrades ahead of patches | ||
if (b.upgradePath[1] === false) { | ||
return 1; | ||
} | ||
var pua = moduleToObject(a.upgradePath[1]); | ||
var pub = moduleToObject(b.upgradePath[1]); | ||
debug('%s > %s', pua.version, pub.version); | ||
res = semver.compare(pua.version, pub.version) * -1; | ||
if (res !== 0) { | ||
return res; | ||
} | ||
} else { | ||
if (a.upgradePath[1]) { | ||
return -1; | ||
} | ||
if (b.upgradePath[1]) { | ||
return 1; | ||
} | ||
} | ||
// // sort by patch date | ||
// if (a.patches.length) { | ||
// // .slice because sort mutates | ||
// var pda = a.patches.slice(0).sort(function (a, b) { | ||
// return a.modificationTime - b.modificationTime; | ||
// }).pop(); | ||
// var pdb = (b.patches || []).slice(0).sort(function (a, b) { | ||
// return a.modificationTime - b.modificationTime; | ||
// }).pop(); | ||
// if (pda && pdb) { | ||
// return pda.modificationTime < pdb.modificationTime ? 1 : | ||
// pda.modificationTime > pdb.modificationTime ? -1 : 0; | ||
// } | ||
// } | ||
return res; | ||
} | ||
function getPrompts(vulns, policy) { | ||
// take a copy so as not to mess with the original data | ||
var res = _.cloneDeep(vulns); | ||
// strip the irrelevant patches from the vulns at the same time, collect | ||
// the unique package vulns | ||
res = res.map(function (vuln) { | ||
if (vuln.patches) { | ||
vuln.patches = vuln.patches.filter(function (patch) { | ||
return semver.satisfies(vuln.version, patch.version); | ||
}); | ||
// sort by patchModification, then pick the latest one | ||
vuln.patches = vuln.patches.sort(function (a, b) { | ||
return b.modificationTime < a.modificationTime ? -1 : 1; | ||
}).slice(0, 1); | ||
// FIXME hack to give all the patches IDs if they don't already | ||
if (vuln.patches[0] && !vuln.patches[0].id) { | ||
vuln.patches[0].id = vuln.patches[0].urls[0].split('/').slice(-1).pop(); | ||
} | ||
} | ||
return vuln; | ||
}); | ||
// sort by vulnerable package and the largest version | ||
res.sort(sortPrompts); | ||
var copy = null; | ||
var offset = 0; | ||
// mutate our objects so we can try to group them | ||
// note that I use slice first becuase the `res` array will change length | ||
// and `reduce` _really_ doesn't like when you change the array under | ||
// it's feet | ||
res.slice(0).reduce(function (acc, curr, i) { | ||
var from = curr.from[1]; | ||
if (!acc[from]) { | ||
// only copy the biggest change | ||
copy = _.cloneDeep(curr); | ||
acc[from] = curr; | ||
return acc; | ||
} | ||
if (!acc[from].grouped) { | ||
acc[from].grouped = { | ||
affected: moduleToObject(from), | ||
main: true, | ||
id: acc[from].id + '-' + i, | ||
count: 1, | ||
upgrades: [], | ||
}; | ||
acc[from].grouped.affected.full = from; | ||
// splice this vuln into the list again so if the user choses to review | ||
// they'll get this individual vuln and remediation | ||
copy.grouped = { | ||
main: false, | ||
requires: acc[from].grouped.id, | ||
}; | ||
res.splice(i + offset, 0, copy); | ||
offset++; | ||
} | ||
debug('vuln found on group'); | ||
acc[from].grouped.count++; | ||
curr.grouped = { | ||
main: false, | ||
requires: acc[from].grouped.id, | ||
}; | ||
var upgrades = curr.upgradePath.slice(-1).shift(); | ||
debug('upgrade available? %s', upgrades && curr.upgradePath[1]); | ||
// otherwise it's a patch and that's hidden for now | ||
if (upgrades && curr.upgradePath[1]) { | ||
var p = moduleToObject(upgrades); | ||
if (p.name !== acc[from].grouped.affected.name && | ||
(' ' + acc[from].grouped.upgrades.join(' ') + ' ') | ||
.indexOf(p.name + '@') === -1) { | ||
debug('+ adding %s to upgrades', upgrades); | ||
acc[from].grouped.upgrades.push(upgrades); | ||
} | ||
} | ||
return acc; | ||
}, {}); | ||
// now filter out any vulns that don't have an upgrade path and only patches | ||
// and have already been grouped | ||
var dropped = []; | ||
res = res.filter(function (vuln) { | ||
if (vuln.grouped) { | ||
if (vuln.grouped.main) { | ||
if (vuln.grouped.upgrades.length === 0) { | ||
debug('dropping %s', vuln.grouped.id); | ||
dropped.push(vuln.grouped.id); | ||
return false; | ||
} | ||
} | ||
// we have to remove the group property on the collective vulns if the | ||
// top grouping has been removed, because otherwise they won't be shown | ||
if (dropped.indexOf(vuln.grouped.requires) !== -1) { | ||
delete vuln.grouped; | ||
} | ||
} | ||
return true; | ||
}); | ||
// resort after we made changes | ||
res.sort(function (a) { | ||
if (a.grouped) { | ||
return -1; | ||
} | ||
if (a.upgradePath[1]) { | ||
return -1; | ||
} | ||
return 1; | ||
}); | ||
debug(res.map(function (v) { | ||
return v.upgradePath[1]; | ||
})); | ||
// console.log(JSON.stringify(res.map(function (vuln) { | ||
// return vuln; | ||
// return { | ||
// from: vuln.from.slice(1).filter(Boolean).shift(), | ||
// upgrade: (vuln.grouped || {}).upgrades, | ||
// group: vuln.grouped | ||
// }; | ||
// }), '', 2)); | ||
var prompts = generatePrompt(res, policy); | ||
// do stuff | ||
return prompts; | ||
} | ||
function generatePrompt(vulns, policy) { | ||
if (!vulns) { | ||
@@ -62,2 +281,7 @@ vulns = []; // being defensive, but maybe we should throw an error? | ||
var update = _.cloneDeep(updateAction); | ||
var review = { | ||
value: 'review', | ||
short: 'Review', | ||
name: 'Review vulnerabilities separately', | ||
}; | ||
@@ -69,12 +293,54 @@ var choices = []; | ||
var severity = vuln.severity[0].toUpperCase() + vuln.severity.slice(1); | ||
var infoLink = '- info: ' + config.ROOT; | ||
var messageIntro; | ||
var group = vuln.grouped && vuln.grouped.main ? vuln.grouped : false; | ||
if (group) { | ||
infoLink += '/package/npm/' + group.affected.name + '/' + | ||
group.affected.version; | ||
messageIntro = group.count + ' vulnerabilities introduced via ' + | ||
group.affected.full; | ||
} else { | ||
infoLink += '/vuln/' + vuln.id; | ||
messageIntro = severity + ' severity vulnerability found in ' + vulnIn + | ||
', introduced via ' + from + (from !== vuln.from.slice(1).join(' > ') ? | ||
'\n- from: ' + vuln.from.slice(1).join(' > ') : ''); | ||
} | ||
var res = { | ||
when: function () { | ||
console.log(''); // blank line between prompts...kinda lame, sorry | ||
return true; | ||
when: function (answers) { | ||
var res = true; | ||
// console.log(answers); | ||
// only show this question if the user chose to review the details | ||
// of the vuln | ||
if (vuln.grouped && !vuln.grouped.main) { | ||
// find how they answered on the top level question | ||
var groupAnswer = Object.keys(answers).map(function (key) { | ||
if (answers[key].meta) { | ||
if (answers[key].meta.groupId === vuln.grouped.requires) { | ||
return answers[key]; | ||
} | ||
} | ||
return false; | ||
}).filter(Boolean); | ||
if (!groupAnswer.length) { | ||
return false; | ||
} | ||
// if we've upgraded, then stop asking | ||
res = groupAnswer.filter(function (answer) { | ||
return answer.choice === 'update'; | ||
}).length === 0; | ||
} | ||
if (res) { | ||
console.log(''); // blank line between prompts...kinda lame, sorry | ||
} | ||
return res; // true = show next | ||
}, | ||
name: id, | ||
type: 'list', | ||
message: severity + ' severity vulnerability found in ' + vulnIn + | ||
'\n - info: ' + config.ROOT + '/vuln/' + vuln.id + | ||
'\n - from: ' + vuln.from.join(' > ') | ||
message: [messageIntro, infoLink, ' Remediation options'].join('\n') | ||
}; | ||
@@ -104,3 +370,14 @@ | ||
choices.push(update); | ||
update.name = 'Upgrade to ' + vuln.upgradePath.filter(Boolean).shift(); | ||
var toPackage = vuln.upgradePath.filter(Boolean).shift(); | ||
update.short = 'Upgrade to ' + toPackage; | ||
var out = 'Upgrade to ' + toPackage; | ||
if (group) { | ||
out += ' (triggers upgrade to ' + group.upgrades.join(', ') + ')'; | ||
} else { | ||
var last = vuln.upgradePath.slice(-1).shift(); | ||
if (toPackage !== last) { | ||
out += ' (triggers upgrade to ' + last + ')'; | ||
} | ||
} | ||
update.name = out; | ||
} else { | ||
@@ -117,31 +394,38 @@ // No upgrade available (as per no patch) | ||
var patches = null; | ||
if (vuln.patches && vuln.patches.length) { | ||
// check that the version we have has a patch available | ||
patches = protect.patchesForPackage({ | ||
name: vuln.name, | ||
version: vuln.version, | ||
}, vuln); | ||
if (patches !== null) { | ||
debug('%s@%s', vuln.name, vuln.version, patches); | ||
if (!upgradeAvailable) { | ||
patch.default = true; | ||
if (group && group.upgrades.length) { | ||
review.meta = { | ||
groupId: group.id, | ||
}; | ||
choices.push(review); | ||
} else { | ||
if (vuln.patches && vuln.patches.length) { | ||
// check that the version we have has a patch available | ||
patches = protect.patchesForPackage({ | ||
name: vuln.name, | ||
version: vuln.version, | ||
}, vuln); | ||
if (patches !== null) { | ||
if (!upgradeAvailable) { | ||
patch.default = true; | ||
} | ||
res.patches = patches; | ||
choices.push(patch); | ||
} | ||
choices.push(patch); | ||
} | ||
} | ||
if (patches === null) { | ||
// add a disabled option saying that patch isn't available | ||
// note that adding `disabled: true` does nothing, so the user can | ||
// actually select this option. I'm not 100% it's the right thing, | ||
// but we'll keep a keen eye on user feedback. | ||
choices.push({ | ||
value: 'skip', | ||
key: 'p', | ||
short: 'Patch (none available)', | ||
name: 'Patch (no patch available, we\'ll notify you when there is one)', | ||
}); | ||
if (patches === null) { | ||
// add a disabled option saying that patch isn't available | ||
// note that adding `disabled: true` does nothing, so the user can | ||
// actually select this option. I'm not 100% it's the right thing, | ||
// but we'll keep a keen eye on user feedback. | ||
choices.push({ | ||
value: 'skip', | ||
key: 'p', | ||
short: 'Patch (none available)', | ||
name: 'Patch (no patch available, we\'ll notify you when there is one)', | ||
}); | ||
} | ||
} | ||
@@ -167,3 +451,2 @@ | ||
// kludge to make sure that we get the vuln in the user selection | ||
@@ -173,2 +456,8 @@ res.choices = choices.map(function (choice) { | ||
// this allows us to pass more data into the inquirer results | ||
if (vuln.grouped && !vuln.grouped.main) { | ||
if (!choice.meta) { | ||
choice.meta = {}; | ||
} | ||
choice.meta.groupId = vuln.grouped.requires; | ||
} | ||
choice.value = { | ||
@@ -182,2 +471,4 @@ meta: choice.meta, | ||
res.vuln = vuln; | ||
return res; | ||
@@ -202,2 +493,5 @@ }); | ||
when: function (answers) { | ||
if (!answers[curr.name]) { | ||
return false; | ||
} | ||
return answers[curr.name].choice === 'ignore'; | ||
@@ -222,3 +516,3 @@ }, | ||
function nextSteps(pkg) { | ||
function nextSteps(pkg, skipProtect) { | ||
var i; | ||
@@ -239,3 +533,3 @@ var prompts = []; | ||
i = (undefsafe(pkg, 'scripts.postinstall') || '').indexOf('snyk pro'); | ||
if (i === -1) { | ||
if (i === -1 && !skipProtect) { | ||
prompts.push({ | ||
@@ -242,0 +536,0 @@ name: 'misc-add-protect', |
@@ -8,3 +8,3 @@ module.exports = wizard; | ||
var debug = require('debug')('snyk'); | ||
var isAuthed = require('../auth').isAuthed; | ||
var auth = require('../auth'); | ||
var getVersion = require('../version'); | ||
@@ -47,3 +47,3 @@ var inquirer = require('inquirer'); | ||
}).then(function (policy) { | ||
return isAuthed().then(function (authed) { | ||
return auth.isAuthed().then(function (authed) { | ||
if (!authed) { | ||
@@ -83,13 +83,8 @@ throw new Error('Unauthorized'); | ||
} else { | ||
console.log(chalk.green('✓ Tested %s for known vulnerabilities, ' + | ||
'no vulnerabilities found.'), res.dependencyCount); | ||
console.log(chalk.green('✓ Tested %s dependencies for known ' + | ||
'vulnerabilities, no vulnerabilities found.'), | ||
res.dependencyCount); | ||
} | ||
if (prompts.length === 0) { | ||
return processAnswers({}, policy, options); | ||
} | ||
// otherwise we're fine, but we still want to ask the user | ||
// if they wanted to save snyk to their test process, etc. | ||
return fs.readFile(packageFile, 'utf8') | ||
@@ -99,3 +94,9 @@ .then(JSON.parse) | ||
prompts = prompts.concat(allPrompts.nextSteps(pkg)); | ||
// we're fine, but we still want to ask the user if they wanted to | ||
// save snyk to their test process, etc. | ||
prompts = prompts.concat(allPrompts.nextSteps(pkg, res.ok)); | ||
if (prompts.length === 0) { | ||
return processAnswers({}, policy, options); | ||
} | ||
return interactive(prompts, policy, options); | ||
@@ -149,2 +150,5 @@ | ||
var task = answer.choice; | ||
if (task === 'review') { | ||
task = 'skip'; | ||
} | ||
@@ -151,0 +155,0 @@ if (task === 'ignore') { |
{ | ||
"API": "https://app.snyk.io/api/v1", | ||
"API": "https://snyk.io/api/v1", | ||
"devDeps": false | ||
} |
module.exports = createSpinner; | ||
var isRequired = module.exports.isRequired = true; | ||
module.exports.isRequired = true; | ||
@@ -56,3 +56,3 @@ var debug = require('debug')('snyk:spinner'); | ||
function spinner(opt) { | ||
if (isRequired) { | ||
if (module.exports.isRequired) { | ||
return false; | ||
@@ -59,0 +59,0 @@ } |
@@ -5,2 +5,3 @@ { | ||
"main": "lib/index.js", | ||
"version": "1.6.0-alpha1", | ||
"directories": { | ||
@@ -61,4 +62,3 @@ "test": "test" | ||
"url": "https://github.com/Snyk/snyk.git" | ||
}, | ||
"version": "1.5.1" | ||
} | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
3359560
119
3633
2