Comparing version 1.6.0 to 1.6.1-alpha1
@@ -5,2 +5,5 @@ var snyk = require('../../lib'); | ||
module.exports = function (path, options) { | ||
if (!options) { | ||
options = {}; | ||
} | ||
return snyk.modules(path || process.cwd()).then(function (modules) { | ||
@@ -7,0 +10,0 @@ |
@@ -65,20 +65,7 @@ module.exports = { | ||
} | ||
// if no upgrade, then hopefully a patch | ||
res = sort('publicationTime')(b, a); | ||
} | ||
// // 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; | ||
@@ -126,2 +113,3 @@ } | ||
var from = curr.from[1]; | ||
if (!acc[from]) { | ||
@@ -134,15 +122,31 @@ // only copy the biggest change | ||
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; | ||
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]) { | ||
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 = { | ||
// 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, | ||
@@ -152,18 +156,2 @@ 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); | ||
@@ -181,37 +169,40 @@ if (p.name !== acc[from].grouped.affected.name && | ||
// console.log(commonPatch); | ||
// 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; | ||
} | ||
} | ||
// var dropped = []; | ||
// res = res.filter(function (vuln) { | ||
// if (vuln.grouped) { | ||
// if (vuln.grouped.main) { | ||
// debug('ok!!'); | ||
// 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; | ||
} | ||
} | ||
// // 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; | ||
}); | ||
// return true; | ||
// }); | ||
// resort after we made changes | ||
res.sort(function (a) { | ||
if (a.grouped) { | ||
return -1; | ||
} | ||
// resort after we made changes putting upgrades first | ||
// res.sort(function (a) { | ||
// if (a.grouped) {//} && a.upgradePath[1]) { // RS changed to add in the upgradePath | ||
// return -1; | ||
// } | ||
if (a.upgradePath[1]) { | ||
return -1; | ||
} | ||
// if (a.upgradePath[1]) { | ||
// return -1; | ||
// } | ||
return 1; | ||
}); | ||
// return 1; | ||
// }); | ||
@@ -233,3 +224,3 @@ debug(res.map(function (v) { | ||
// do stuff | ||
// console.log(prompts); | ||
@@ -299,2 +290,3 @@ return prompts; | ||
var messageIntro; | ||
var fromText = false; | ||
var group = vuln.grouped && vuln.grouped.main ? vuln.grouped : false; | ||
@@ -310,6 +302,12 @@ | ||
messageIntro = severity + ' severity vulnerability found in ' + vulnIn + | ||
', introduced via ' + from + (from !== vuln.from.slice(1).join(' > ') ? | ||
'\n- from: ' + vuln.from.slice(1).join(' > ') : ''); | ||
', introduced via ' + from; | ||
fromText = (from !== vuln.from.slice(1).join(' > ') ? | ||
'- from: ' + vuln.from.slice(1).join(' > ') : ''); | ||
} | ||
var note = false; | ||
if (vuln.note) { | ||
note = '- note: ' + vuln.note; | ||
} | ||
var res = { | ||
@@ -337,5 +335,16 @@ when: function (answers) { | ||
// if we've upgraded, then stop asking | ||
var updatedTo = null; | ||
res = groupAnswer.filter(function (answer) { | ||
return answer.choice === 'update'; | ||
if (answer.choice === 'update') { | ||
updatedTo = answer; | ||
return true; | ||
} | ||
}).length === 0; | ||
if (!res) { | ||
// echo out what would be upgraded | ||
var via = 'Fixed through previous upgrade instruction to ' + updatedTo.vuln.upgradePath[1]; | ||
console.log(['', messageIntro, infoLink, via].join('\n')); | ||
// console.log(updatedTo); | ||
} | ||
} | ||
@@ -350,3 +359,4 @@ | ||
type: 'list', | ||
message: [messageIntro, infoLink, ' Remediation options'].join('\n') | ||
message: [messageIntro, fromText, infoLink, note, ' Remediation options'] | ||
.filter(Boolean).join('\n') | ||
}; | ||
@@ -390,9 +400,9 @@ | ||
// No upgrade available (as per no patch) | ||
choices.push({ | ||
value: 'skip', | ||
key: 'u', | ||
short: 'Upgrade (none available)', | ||
name: 'Upgrade (no sufficient upgrade available for ' + | ||
from.split('@')[0] + ', we\'ll notify you when there is one)', | ||
}); | ||
// choices.push({ | ||
// value: 'skip', | ||
// key: 'u', | ||
// short: 'Upgrade (none available)', | ||
// name: 'Upgrade (no sufficient upgrade available for ' + | ||
// from.split('@')[0] + ', we\'ll notify you when there is one)', | ||
// }); | ||
} | ||
@@ -402,7 +412,4 @@ | ||
if (group && group.upgrades.length) { | ||
review.meta = { | ||
groupId: group.id, | ||
}; | ||
choices.push(review); | ||
if (upgradeAvailable && group) { | ||
} else { | ||
@@ -421,6 +428,14 @@ if (vuln.patches && vuln.patches.length) { | ||
res.patches = patches; | ||
if (group) { | ||
patch.name = 'Patch the ' + group.count + ' vulnerabilities'; | ||
} | ||
choices.push(patch); | ||
} | ||
} | ||
} | ||
// only show patch option if this is NOT a grouped upgrade | ||
if (upgradeAvailable === false || !group) { | ||
if (patches === null) { | ||
@@ -440,2 +455,9 @@ // add a disabled option saying that patch isn't available | ||
if (group) { | ||
review.meta = { | ||
groupId: group.id, | ||
}; | ||
choices.push(review); | ||
} | ||
if (patches === null && !upgradeAvailable) { | ||
@@ -442,0 +464,0 @@ ignore.default = true; |
@@ -123,2 +123,6 @@ module.exports = test; | ||
if (vuln.note) { | ||
res += vuln.note + '\n'; | ||
} | ||
var upgradeSteps = (vuln.upgradePath || []).filter(Boolean); | ||
@@ -125,0 +129,0 @@ |
{ | ||
"API": "https://app.snyk.io/api/v1", | ||
"API": "https://snyk.io/api/v1", | ||
"devDeps": false | ||
} |
@@ -11,2 +11,3 @@ module.exports = loadModules; | ||
var fs = require('then-fs'); | ||
var _ = require('lodash'); | ||
var Promise = require('es6-promise').Promise; // jshint ignore:line | ||
@@ -54,5 +55,5 @@ var path = require('path'); | ||
var modules = {}; | ||
var promise = Promise.resolve().then(function () { | ||
// 1. read package.json for written deps | ||
var pkg = tryRequire(path.resolve(root, 'package.json')); | ||
var dir = path.resolve(root, 'package.json'); | ||
// 1. read package.json for written deps | ||
var promise = tryRequire(dir).then(function (pkg) { | ||
@@ -77,62 +78,84 @@ // if there's a package found, collect this information too | ||
var res = dirs.map(function (dir) { | ||
// completely ignore `.bin` npm helper dir | ||
if (dir === '.bin') { | ||
return null; | ||
} | ||
// this is a scoped namespace, and we'd expect to find directories | ||
// inside *this* `dir`, so treat differently | ||
if (dir.indexOf('@') === 0) { | ||
dir = path.resolve(root, 'node_modules', dir); | ||
return fs.readdir(dir).then(function (dirs) { | ||
return Promise.all(dirs.map(function (scopedDir) { | ||
dir = path.resolve(dir, scopedDir, 'package.json'); | ||
return tryRequire(dir); | ||
})); | ||
}); | ||
} | ||
// otherwise try to load a package.json from this node_module dir | ||
dir = path.resolve(root, 'node_modules', dir, 'package.json'); | ||
return tryRequire(dir); | ||
}) | ||
.filter(Boolean); | ||
}); | ||
if (res.length === 0) { | ||
// effectively not a node module | ||
var e = new Error('missing node_modules'); | ||
e.code = 'MISSING_NODE_MODULES'; | ||
throw e; | ||
} | ||
return Promise.all(res).then(function (res) { | ||
res = _.flatten(res).filter(Boolean); | ||
res.reduce(function (acc, curr) { | ||
var license; | ||
var licenses = curr.license || curr.licenses; | ||
if (res.length === 0) { | ||
// effectively not a node module | ||
var e = new Error('missing node_modules'); | ||
e.code = 'MISSING_NODE_MODULES'; | ||
throw e; | ||
} | ||
if (Array.isArray(licenses)) { | ||
license = licenses.reduce(function (acc, curr) { | ||
acc.push((curr || {}).type || curr); | ||
res.reduce(function (acc, curr) { | ||
var license; | ||
var licenses = curr.license || curr.licenses; | ||
if (Array.isArray(licenses)) { | ||
license = licenses.reduce(function (acc, curr) { | ||
acc.push((curr || {}).type || curr); | ||
return acc; | ||
}, []).join('/'); | ||
} else { | ||
license = (licenses || {}).type || licenses; | ||
} | ||
var depType = rootDepType; | ||
if (depType !== DEP_TYPE_DEV) { | ||
if (pkg.dependencies && pkg.dependencies[curr.name]) { | ||
depType = DEP_TYPE_PROD; | ||
} else if (pkg.devDependencies && pkg.devDependencies[curr.name]) { | ||
depType = DEP_TYPE_DEV; | ||
} | ||
} | ||
// By default include all modules, but optionally skip devDeps | ||
if (depType === DEP_TYPE_DEV && !options.dev) { | ||
return acc; | ||
}, []).join('/'); | ||
} else { | ||
license = (licenses || {}).type || licenses; | ||
} | ||
} | ||
var depType = rootDepType; | ||
if (depType !== DEP_TYPE_DEV) { | ||
if (pkg.dependencies && pkg.dependencies[curr.name]) { | ||
depType = DEP_TYPE_PROD; | ||
} else if (pkg.devDependencies && pkg.devDependencies[curr.name]) { | ||
depType = DEP_TYPE_DEV; | ||
var valid = false; | ||
if (pkg.dependencies) { | ||
valid = semver.satisfies(curr.version, pkg.dependencies[curr.name]); | ||
} | ||
} | ||
// By default include all modules, but optionally skip devDeps | ||
if (depType === DEP_TYPE_DEV && !options.dev) { | ||
acc[curr.name] = { | ||
name: curr.name, | ||
version: curr.version || null, | ||
full: curr.name + '@' + (curr.version || '0.0.0'), | ||
valid: valid, | ||
devDependencies: curr.devDependencies, | ||
depType: depType, | ||
snyk: curr.snyk, | ||
license: license || 'none', | ||
dep: pkg.dependencies ? pkg.dependencies[curr.name] || null : null, | ||
}; | ||
return acc; | ||
} | ||
}, modules.dependencies); | ||
var valid = false; | ||
if (pkg.dependencies) { | ||
valid = semver.satisfies(curr.version, pkg.dependencies[curr.name]); | ||
} | ||
acc[curr.name] = { | ||
name: curr.name, | ||
version: curr.version || null, | ||
full: curr.name + '@' + (curr.version || '0.0.0'), | ||
valid: valid, | ||
devDependencies: curr.devDependencies, | ||
depType: depType, | ||
license: license || 'none', | ||
dep: pkg.dependencies ? pkg.dependencies[curr.name] || null : null, | ||
}; | ||
return acc; | ||
}, modules.dependencies); | ||
return modules; | ||
return modules; | ||
}); | ||
}).then(function (modules) { | ||
var deps = Object.keys(modules.dependencies); | ||
@@ -139,0 +162,0 @@ |
@@ -8,2 +8,3 @@ var yaml = require('js-yaml'); | ||
var parse = require('./parser'); | ||
var tryRequire = require('../try-require'); | ||
@@ -24,3 +25,3 @@ module.exports = { | ||
function load(root, options) { | ||
if (typeof root === 'object') { | ||
if (!Array.isArray(root) && typeof root !== 'string') { | ||
options = root; | ||
@@ -38,2 +39,6 @@ root = null; | ||
if (Array.isArray(root)) { | ||
return mergePolicies(root, options); | ||
} | ||
var filename = root ? path.resolve(root, '.snyk') : defaultFilename(); | ||
@@ -57,2 +62,54 @@ | ||
function mergePolicies(policyDirs, options) { | ||
return Promise.all(policyDirs.map(function (dir) { | ||
return load(dir, options); | ||
})).then(function (policies) { | ||
// firstly extend the paths in the ignore and patch | ||
var rootPolicy = policies[0]; | ||
var others = policies.slice(1); | ||
return Promise.all(others.map(function (policy) { | ||
var filename = path.dirname(policy.__filename) + '/package.json'; | ||
return tryRequire(filename).then(function (pkg) { | ||
var full = pkg.name + '@' + pkg.version; | ||
mergePath('ignore', 'suggest', full, rootPolicy, policy); | ||
mergePath('patch', 'patch', full, rootPolicy, policy); | ||
}); | ||
})).then(function () { | ||
return rootPolicy; | ||
}); | ||
// return policies[0]; | ||
}); | ||
} | ||
// note: mutates both objects, be warned! | ||
function mergePath(type, into, pathRoot, rootPolicy, policy) { | ||
if (!rootPolicy[into]) { | ||
rootPolicy[into] = {}; | ||
} | ||
Object.keys(policy[type]).forEach(function (id) { | ||
// convert the path from `module@version` to `parent > module@version` | ||
policy[type][id] = policy[type][id].map(function (path) { | ||
// this is because our policy file format favours "readable" yaml, | ||
// instead of easy to use object structures. | ||
var key = Object.keys(path).pop(); | ||
var newPath = {}; | ||
newPath[pathRoot + ' > ' + key] = path[key]; | ||
path[key].from = pathRoot; | ||
return newPath; | ||
}); | ||
// add the rule if we don't have it in our policy already | ||
if (!rootPolicy[into][id]) { | ||
rootPolicy[into][id] = policy[type][id]; | ||
return; | ||
} | ||
// otherwise we need to merge up manually | ||
rootPolicy[into][id] = rootPolicy[type][id].concat(policy[type][id]); | ||
}); | ||
} | ||
function save(object, root) { | ||
@@ -59,0 +116,0 @@ var filename = root ? |
// eventually we'll have v2 which will point to latestParser, and v1 will | ||
// need to process the old form of data and upgrade it to v2 structure | ||
module.exports = function (d) { return d; }; | ||
module.exports = function (policy) { | ||
if (!policy.ignore) { | ||
policy.ignore = {}; | ||
} | ||
if (!policy.patch) { | ||
policy.patch = {}; | ||
} | ||
return policy; | ||
}; |
@@ -9,2 +9,3 @@ var protect = module.exports = { | ||
filterPatched: filterPatched, | ||
attachNotes: attachNotes, | ||
applyPatch: applyPatch, | ||
@@ -223,2 +224,48 @@ }; | ||
function attachNotes(notes, vuln) { | ||
if (!notes) { | ||
return vuln; | ||
} | ||
debug('attaching notes'); | ||
var now = Date.now(); | ||
return vuln.map(function (vuln) { | ||
if (!notes[vuln.id]) { | ||
return vuln; | ||
} | ||
debug('%s has rules', vuln.id); | ||
// if rules.some, then add note to the vuln | ||
notes[vuln.id].forEach(function (rule) { | ||
var path = Object.keys(rule)[0]; // this is a string | ||
var expires = rule[path].expires; | ||
// first check if the path is a match on the rule | ||
var pathMatch = snyk.policy.matchToRule(vuln, rule); | ||
if (pathMatch && expires < now) { | ||
debug('%s vuln rule has expired (%s)', vuln.id, expires); | ||
return false; | ||
} | ||
if (pathMatch) { | ||
// strip any control characters in the 3rd party reason file | ||
var reason = rule[path].reason.replace('/[\x00-\x1F\x7F-\x9F]/u', ''); | ||
debug('adding note based on path match: %s ~= %s', path, | ||
vuln.from.slice(1).join(' > ')); | ||
vuln.note = 'Snyk policy in ' + rule[path].from + | ||
' suggests ignoring this issue, with reason: ' + reason; | ||
} | ||
return false; | ||
}); | ||
return vuln; | ||
}); | ||
} | ||
// cwd is used for testing | ||
@@ -468,9 +515,10 @@ function filterPatched(patched, vuln, cwd) { | ||
function generatePolicy(policy, tasks, live) { | ||
var promises = [ | ||
protect.ignore(tasks.ignore, live), | ||
protect.update(tasks.update, live), | ||
protect.patch(tasks.patch, live), | ||
log(tasks, live), | ||
]; | ||
var promises = ['ignore', 'update', 'patch'].filter(function (task) { | ||
return tasks[task].length; | ||
}).map(function (task) { | ||
return protect[task](tasks[task], live); | ||
}); | ||
promises.push(log(tasks, live)); | ||
return Promise.all(promises).then(function (res) { | ||
@@ -477,0 +525,0 @@ // we're squashing the arrays of arrays into a flat structure |
@@ -11,2 +11,3 @@ module.exports = test; | ||
var isCI = require('./is-ci'); | ||
var _ = require('lodash'); | ||
@@ -42,4 +43,7 @@ // important: this is different from ./config (which is the *user's* config) | ||
var p = Promise.resolve(); | ||
var policyLocations = [root]; | ||
if (exists) { | ||
p = snyk.modules(root, options).then(function (pkg) { | ||
policyLocations = policyLocations.concat(pluckPolicies(pkg)); | ||
debug('policies found', policyLocations); | ||
hasDevDependencies = pkg.hasDevDependencies; | ||
@@ -89,4 +93,4 @@ payload.method = 'POST'; | ||
}).then(function (res) { | ||
return snyk.policy.load(root, options).then(function (config) { | ||
if (!config.ignore || res.ok) { | ||
return snyk.policy.load(policyLocations, options).then(function (config) { | ||
if (res.ok) { | ||
return res; | ||
@@ -98,3 +102,4 @@ } | ||
config.ignore, | ||
res.vulnerabilities | ||
res.vulnerabilities, | ||
root | ||
); | ||
@@ -104,5 +109,14 @@ | ||
config.patch, | ||
res.vulnerabilities | ||
res.vulnerabilities, | ||
root | ||
); | ||
if (config.suggest) { | ||
res.vulnerabilities = protect.attachNotes( | ||
config.suggest, | ||
res.vulnerabilities, | ||
root | ||
); | ||
} | ||
// if there's no vulns after the ignore process, let's reset the `ok` | ||
@@ -134,1 +148,20 @@ // state and remove the vulns entirely. | ||
} | ||
function pluckPolicies(pkg) { | ||
if (!pkg) { | ||
return null; | ||
} | ||
if (pkg.snyk) { | ||
return pkg.snyk; | ||
} | ||
if (!pkg.dependencies) { | ||
return null; | ||
} | ||
return _.flatten(Object.keys(pkg.dependencies).map(function (name) { | ||
return pluckPolicies(pkg.dependencies[name]); | ||
}).filter(Boolean)); | ||
} |
module.exports = tryRequire; | ||
var fs = require('fs'); | ||
var fs = require('then-fs'); | ||
var path = require('path'); | ||
var debug = require('debug')('snyk'); | ||
var Promise = require('es6-promise').Promise; // jshint ignore:line | ||
function tryRequire(path) { | ||
try { | ||
return JSON.parse(fs.readFileSync(path, 'utf8')); | ||
} catch (e) { | ||
return null; | ||
} | ||
function tryRequire(filename) { | ||
return fs.readFile(filename, 'utf8') | ||
.then(JSON.parse) | ||
.catch(function (e) { | ||
debug('tryRequire silently failing on %s', e.message); | ||
return null; | ||
}) | ||
.then(function (pkg) { | ||
if (!pkg) { | ||
return pkg; | ||
} | ||
// also try to find a .snyk policy file whilst we're at it | ||
var dir = path.dirname(filename); | ||
if (!pkg.snyk) { | ||
pkg.snyk = fs.existsSync(path.resolve(dir, '.snyk')); | ||
} | ||
if (pkg.snyk) { | ||
pkg.snyk = dir; | ||
} | ||
return pkg; | ||
}); | ||
} |
@@ -5,2 +5,3 @@ { | ||
"main": "lib/index.js", | ||
"version": "1.6.1-alpha1", | ||
"directories": { | ||
@@ -61,4 +62,3 @@ "test": "test" | ||
"url": "https://github.com/Snyk/snyk.git" | ||
}, | ||
"version": "1.6.0" | ||
} | ||
} | ||
} |
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
94
3809
10
614918
2