snyk
Version:
snyk library and cli utility
1,302 lines (1,283 loc) • 58.8 kB
JavaScript
"use strict";
exports.id = 959;
exports.ids = [959,875];
exports.modules = {
/***/ 71771:
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.verifyAPI = exports.isAuthed = void 0;
const snyk = __webpack_require__(9146);
const config_1 = __webpack_require__(22541);
const request_1 = __webpack_require__(52050);
function isAuthed() {
const token = snyk.config.get('api');
return verifyAPI(token).then((res) => {
return res.body.ok;
});
}
exports.isAuthed = isAuthed;
function verifyAPI(api) {
const payload = {
body: {
api,
},
method: 'POST',
url: config_1.default.API + '/verify/token',
json: true,
};
return new Promise((resolve, reject) => {
request_1.makeRequest(payload, (error, res, body) => {
if (error) {
return reject(error);
}
resolve({
res,
body,
});
});
});
}
exports.verifyAPI = verifyAPI;
/***/ }),
/***/ 47272:
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.startOver = exports.nextSteps = exports.getPrompts = exports.getIgnorePrompts = exports.getPatchPrompts = exports.getUpdatePrompts = void 0;
const constants_1 = __webpack_require__(65623);
const cloneDeep = __webpack_require__(83465);
const get = __webpack_require__(29208);
const semver = __webpack_require__(36625);
const util_1 = __webpack_require__(31669);
const debugModule = __webpack_require__(15158);
const protect = __webpack_require__(5409);
const snyk_module_1 = __webpack_require__(60390);
const config_1 = __webpack_require__(22541);
const snykPolicy = __webpack_require__(70535);
const chalk_1 = __webpack_require__(32589);
const theme_1 = __webpack_require__(86988);
const common_1 = __webpack_require__(53110);
const legacy_format_issue_1 = __webpack_require__(63540);
const debug = debugModule('snyk');
const ignoreDisabledReasons = {
notAdmin: 'Set to ignore (only administrators can ignore issues)',
disregardFilesystemIgnores: 'Set to ignore (ignoring via the CLI is not enabled for this organization)',
};
// via http://stackoverflow.com/a/4760279/22617
function sort(prop) {
let sortOrder = 1;
if (prop[0] === '-') {
sortOrder = -1;
prop = prop.substr(1);
}
return (a, b) => {
const result = a[prop] < b[prop] ? -1 : a[prop] > b[prop] ? 1 : 0;
return result * sortOrder;
};
}
function createSeverityBasedIssueHeading(msg, severity) {
// Example: ✗ Medium severity vulnerability found in xmldom
return common_1.colorTextBySeverity(severity, msg);
}
function sortUpgradePrompts(a, b) {
let res = 0;
// first sort by module affected
if (!a.from[1]) {
return -1;
}
if (!b.from[1]) {
return 1;
}
const pa = snyk_module_1.parsePackageString(a.from[1]);
const pb = snyk_module_1.parsePackageString(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
if (a.upgradePath[1] && b.upgradePath[1]) {
// put upgrades ahead of patches
if (b.upgradePath[1] === false) {
return 1;
}
const pua = snyk_module_1.parsePackageString(a.upgradePath[1]);
const pub = snyk_module_1.parsePackageString(b.upgradePath[1]);
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;
}
// if no upgrade, then hopefully a patch
res = sort('publicationTime')(b, a);
}
return res;
}
function sortPatchPrompts(a, b) {
let res = 0;
// first sort by module affected
const afrom = a.from.slice(1).pop();
const bfrom = b.from.slice(1).pop();
if (!afrom) {
return -1;
}
if (!bfrom[1]) {
return 1;
}
const pa = snyk_module_1.parsePackageString(afrom);
const pb = snyk_module_1.parsePackageString(bfrom);
res = sort('name')(pa, pb);
if (res !== 0) {
return res;
}
// if no upgrade, then hopefully a patch
res = sort('publicationTime')(b, a);
return res;
}
function stripInvalidPatches(vulns) {
// strip the irrelevant patches from the vulns at the same time, collect
// the unique package vulns
return vulns.map((vuln) => {
// strip verbose meta
delete vuln.description;
delete vuln.credit;
if (vuln.patches) {
vuln.patches = vuln.patches.filter((patch) => {
return semver.satisfies(vuln.version, patch.version);
});
// sort by patchModification, then pick the latest one
vuln.patches = vuln.patches
.sort((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;
});
}
function getPrompts(vulns, policy) {
return getUpdatePrompts(vulns, policy)
.concat(getPatchPrompts(vulns, policy))
.concat(getIgnorePrompts(vulns, policy));
}
exports.getPrompts = getPrompts;
function getPatchPrompts(vulns, policy, options) {
debug('getPatchPrompts');
if (!vulns || vulns.length === 0) {
return [];
}
let res = stripInvalidPatches(cloneDeep(vulns)).filter((vuln) => {
// if there's any upgrade available, then remove it
return canBeUpgraded(vuln) || vuln.type === 'license' ? false : true;
});
// sort by vulnerable package and the largest version
res.sort(sortPatchPrompts);
const copy = {};
let 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
// TODO(kyegupov): convert this reduce to a loop, make grouping logic more clear
res.slice(0).reduce((acc, curr, i, all) => {
// var upgrades = curr.upgradePath[1];
// otherwise it's a patch and that's hidden for now
if (curr.patches && curr.patches.length) {
// TODO allow for cross over patches on modules (i.e. patch can work
// on A-1 and A-2)
let last;
if (acc[curr.id]) {
last = curr.id;
}
else {
// try to find the right vuln id based on the publication times
last = (all
.filter((vuln) => {
const patch = vuln.patches[0];
// don't select the one we're looking at
if (curr.id === vuln.id) {
return false;
}
// only look at packages with the same name
if (curr.name !== vuln.name || !patch) {
return false;
}
// and ensure the patch can be applied to *our* module version
if (semver.satisfies(curr.version, patch.version)) {
// finally make sure the publicationTime is newer than the curr
// vulnerability
if (curr.publicationTime < vuln.publicationTime) {
debug('found alternative location for %s@%s (%s by %s) in %s', curr.name, curr.version, patch.version, curr.id, vuln.id);
return true;
}
}
})
.shift() || curr).id;
}
if (!acc[last]) {
// only copy the biggest change
copy[last] = cloneDeep(curr);
acc[last] = curr;
return acc;
}
// only happens on the 2nd time around
if (!acc[last].grouped) {
acc[last].grouped = {
affected: snyk_module_1.parsePackageString(acc[last].name + '@' + acc[last].version),
main: true,
id: acc[last].id + '-' + i,
count: 1,
upgrades: [
{
// all this information is used when the user selects group patch
// specifically: in ./tasks.js~42
from: acc[last].from,
filename: acc[last].__filename,
patches: acc[last].patches,
version: acc[last].version,
},
],
patch: true,
};
acc[last].grouped.affected.full = acc[last].name;
// splice this vuln into the list again so if the user choses to review
// they'll get this individual vuln and remediation
copy[last].grouped = {
main: false,
requires: acc[last].grouped.id,
};
res.splice(i + offset, 0, copy[last]);
offset++;
}
acc[last].grouped.count++;
curr.grouped = {
main: false,
requires: acc[last].grouped.id,
};
// add the from path to our group upgrades if we don't have it already
const have = !!acc[last].grouped.upgrades.filter((upgrade) => {
return upgrade.from.join(' ') === curr.from.join(' ');
}).length;
if (!have) {
acc[last].grouped.upgrades.push({
from: curr.from,
filename: curr.__filename,
patches: curr.patches,
version: curr.version,
});
}
else {
if (!acc[last].grouped.includes) {
acc[last].grouped.includes = [];
}
acc[last].grouped.includes.push(curr.id);
}
}
return acc;
}, {});
// FIXME this should not just strip those that have an upgrade path, but
// take into account the previous answers, and if the package has been
// upgraded, it should be left *out* of our list.
res = res.filter((curr) => {
// if (curr.upgradePath[1]) {
// return false;
// }
if (!curr.patches || curr.patches.length === 0) {
return false;
}
return true;
});
const prompts = generatePrompt(res, policy, 'p', options);
return prompts;
}
exports.getPatchPrompts = getPatchPrompts;
function getIgnorePrompts(vulns, policy, options) {
debug('getIgnorePrompts');
if (!vulns || vulns.length === 0) {
return [];
}
const res = stripInvalidPatches(cloneDeep(vulns)).filter((vuln) => {
// remove all patches and updates
// if there's any upgrade available
if (canBeUpgraded(vuln)) {
return false;
}
if (vuln.patches && vuln.patches.length) {
return false;
}
return true;
});
const prompts = generatePrompt(res, policy, 'i', options);
return prompts;
}
exports.getIgnorePrompts = getIgnorePrompts;
function getUpdatePrompts(vulns, policy, options) {
debug('getUpdatePrompts');
if (!vulns || vulns.length === 0) {
return [];
}
let res = stripInvalidPatches(cloneDeep(vulns)).filter((vuln) => {
// only keep upgradeable
return canBeUpgraded(vuln);
});
// sort by vulnerable package and the largest version
res.sort(sortUpgradePrompts);
let copy = null;
let 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
// TODO(kyegupov): rewrite this reduce into more readable loop, avoid mutating original list,
// understand and document the grouping logic
res.slice(0).reduce((acc, curr, i) => {
const from = curr.from[1];
if (!acc[from]) {
// only copy the biggest change
copy = cloneDeep(curr);
acc[from] = curr;
return acc;
}
const upgrades = curr.upgradePath.slice(-1).shift();
// otherwise it's a patch and that's hidden for now
if (upgrades && curr.upgradePath[1]) {
if (!acc[from].grouped) {
acc[from].grouped = {
affected: snyk_module_1.parsePackageString(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++;
}
acc[from].grouped.count++;
curr.grouped = {
main: false,
requires: acc[from].grouped.id,
};
const p = snyk_module_1.parsePackageString(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 strip anything that doesn't have an upgrade path
res = res.filter((curr) => {
return !!curr.upgradePath[1];
});
const prompts = generatePrompt(res, policy, 'u', options);
return prompts;
}
exports.getUpdatePrompts = getUpdatePrompts;
function canBeUpgraded(vuln) {
if (vuln.parentDepType === 'extraneous') {
return false;
}
if (vuln.bundled) {
return false;
}
if (vuln.shrinkwrap) {
return false;
}
return vuln.upgradePath.some((pkg, i) => {
// if the upgade path is to upgrade the module to the same range the
// user already asked for, then it means we need to just blow that
// module away and re-install
if (vuln.from.length > i && pkg === vuln.from[i]) {
return true;
}
// if the upgradePath contains the first two elements, that is
// the project itself (i.e. jsbin) then the direct dependency can be
// upgraded. Note that if the first two elements
if (vuln.upgradePath.slice(0, 2).filter(Boolean).length) {
return true;
}
});
}
function generatePrompt(vulns, policy, prefix, options) {
if (!prefix) {
prefix = '';
}
if (!vulns) {
vulns = []; // being defensive, but maybe we should throw an error?
}
const skipAction = {
value: 'skip',
key: 's',
name: 'Skip',
};
const ignoreAction = {
value: options && options.ignoreDisabled ? 'skip' : 'ignore',
key: 'i',
meta: {
// arbitrary data that we'll merged into the `value` later on
days: 30,
},
short: 'Ignore',
name: options && options.ignoreDisabled
? ignoreDisabledReasons[options.ignoreDisabled.reasonCode]
: 'Set to ignore for 30 days (updates policy)',
};
const patchAction = {
value: 'patch',
key: 'p',
short: 'Patch',
name: 'Patch (modifies files locally, updates policy for `snyk protect` ' +
'runs)',
};
const updateAction = {
value: 'update',
key: 'u',
short: 'Upgrade',
name: '',
};
let prompts = vulns.map((vuln, i) => {
let id = vuln.id || 'node-' + vuln.name + '@' + vuln.below;
id += '-' + prefix + i;
// make complete copies of the actions, otherwise we'll mutate the object
const ignore = cloneDeep(ignoreAction);
const skip = cloneDeep(skipAction);
const patch = cloneDeep(patchAction);
const update = cloneDeep(updateAction);
const review = {
value: 'review',
short: 'Review',
name: 'Review issues separately',
};
const choices = [];
const from = vuln.from
.slice(1)
.filter(Boolean)
.shift();
// FIXME this should be handled a little more gracefully
if (vuln.from.length === 1) {
debug('Skipping issues in core package with no upgrade path: ' + id);
}
const vulnIn = vuln.from.slice(-1).pop();
const severity = legacy_format_issue_1.titleCaseText(vuln.severity);
let infoLink = ' Info: ' + chalk_1.default.underline(config_1.default.ROOT);
let messageIntro;
let fromText = false;
const group = vuln.grouped && vuln.grouped.main ? vuln.grouped : false;
let originalSeverityStr = '';
if (vuln.originalSeverity && vuln.originalSeverity !== vuln.severity) {
originalSeverityStr = ` (originally ${legacy_format_issue_1.titleCaseText(vuln.originalSeverity)})`;
}
if (group) {
infoLink += chalk_1.default.underline('/package/npm/' + group.affected.name + '/' + group.affected.version);
const joiningText = group.patch ? 'in' : 'via';
const issues = vuln.type === 'license' ? 'issues' : 'vulnerabilities';
messageIntro = util_1.format(`${theme_1.icon.ISSUE} %s %s %s introduced %s %s`, group.count, `${severity}${originalSeverityStr}`, issues, joiningText, group.affected.full);
messageIntro = createSeverityBasedIssueHeading(messageIntro, vuln.severity);
}
else {
infoLink += chalk_1.default.underline('/vuln/' + vuln.id);
messageIntro = util_1.format(`${theme_1.icon.ISSUE} %s severity %s found in %s, introduced via`, `${severity}${originalSeverityStr}`, vuln.type === 'license' ? 'issue' : 'vuln', vulnIn, from);
messageIntro = createSeverityBasedIssueHeading(messageIntro, vuln.severity);
messageIntro += '\n Description: ' + vuln.title;
fromText =
from !== vuln.from.slice(1).join(constants_1.PATH_SEPARATOR)
? ' From: ' + vuln.from.slice(1).join(constants_1.PATH_SEPARATOR)
: '';
}
let note = false;
if (vuln.note) {
if (group && group.patch) {
// no-op
}
else {
note = ' Note: ' + vuln.note;
}
}
const res = {
when(answers) {
let haventUpgraded = true;
// only show this question if the user choose to review the details
// of the vuln
if (vuln.grouped && !vuln.grouped.main) {
// find how they answered on the top level question
const groupAnswer = Object.keys(answers)
.map((key) => {
if (answers[key].meta) {
// this meta.groupId only appears on a "review" choice, and thus
// this map will pick out those vulns that are specifically
// associated with this review group.
if (answers[key].meta.groupId === vuln.grouped.requires) {
if (answers[key].choice === 'ignore' &&
answers[key].meta.review) {
answers[key].meta.vulnsInGroup.push({
id: vuln.id,
from: vuln.from,
});
return false;
}
return answers[key];
}
}
return false;
})
.filter(Boolean);
if (!groupAnswer.length) {
debug('no group answer: show %s when %s', vuln.id, false);
return false;
}
// if we've upgraded, then stop asking
let updatedTo = null;
haventUpgraded =
groupAnswer.filter((answer) => {
if (answer.choice === 'update') {
updatedTo = answer;
return true;
}
}).length === 0;
if (!haventUpgraded) {
// echo out what would be upgraded
const via = 'Fixed through previous upgrade instruction to ' +
updatedTo.vuln.upgradePath[1];
console.log(['', messageIntro, infoLink, via].join('\n'));
}
}
if (haventUpgraded) {
console.log(''); // blank line between prompts...kinda lame, sorry
}
debug('final show %s when %s', vuln.id, res);
return res; // true = show next
},
name: id,
type: 'list',
message: [
messageIntro,
infoLink,
fromText,
note,
chalk_1.default.green('\n Remediation options'),
]
.filter(Boolean)
.join('\n'),
};
const upgradeAvailable = canBeUpgraded(vuln);
const toPackage = vuln.upgradePath.filter(Boolean).shift();
const isReinstall = toPackage === from;
const isYarn = !!(options && options.packageManager === 'yarn');
// note: the language presented the user is "upgrade" rather than "update"
// this change came long after all this code was written. I've decided
// *not* to update all the variables referring to `update`, but just
// to warn my dear code-reader that this is intentional.
// note: Yarn reinstallation does not currently work because the
// remediation advice is actually for npm
if (upgradeAvailable && (!isYarn || !isReinstall)) {
choices.push(update);
const word = isReinstall ? 'Re-install ' : 'Upgrade to ';
update.short = word + toPackage;
let out = word + toPackage;
const toPackageVersion = snyk_module_1.parsePackageString(toPackage).version;
const diff = semver.diff(snyk_module_1.parsePackageString(from).version, toPackageVersion);
let lead = '';
const breaking = theme_1.color.status.error('potentially breaking change');
if (diff === 'major') {
lead = ' (' + breaking + ', ';
}
else {
lead = ' (';
}
lead += 'triggers upgrade to ';
if (group && group.upgrades.length) {
out += lead + group.upgrades.join(', ') + ')';
}
else {
const last = vuln.upgradePath.slice(-1).shift();
if (toPackage !== last) {
out += lead + last + ')';
}
else if (diff === 'major') {
out += ' (' + breaking + ')';
}
}
update.name = out;
}
else {
// No upgrade available (as per no patch)
let reason = '';
if (vuln.parentDepType === 'extraneous') {
reason = util_1.format('extraneous package %s cannot be upgraded', vuln.from[1]);
}
else if (vuln.shrinkwrap) {
reason = util_1.format('upgrade unavailable as %s@%s is shrinkwrapped by %s', vuln.name, vuln.version, vuln.shrinkwrap);
}
else if (vuln.bundled) {
reason = util_1.format('upgrade unavailable as %s is bundled in vulnerable %s', vuln.bundled.slice(-1).pop(), vuln.name);
}
else {
reason =
"no sufficient upgrade available we'll notify you when " +
'there is one';
}
choices.push({
value: 'skip',
key: 'u',
short: 'Upgrade (none available)',
name: 'Upgrade (' + reason + ')',
});
}
let patches = null;
if (upgradeAvailable && group) {
// no-op
}
else {
if (vuln.patches && vuln.patches.length) {
// check that the version we have has a patch available
patches = protect.patchesForPackage(vuln);
if (patches !== null) {
if (!upgradeAvailable) {
patch.default = true;
}
res.patches = patches;
if (group) {
patch.name = util_1.format('Patch the %s vulnerabilities', group.count);
}
choices.push(patch);
}
}
}
// only show patch option if this is NOT a grouped upgrade
if (upgradeAvailable === false || !group) {
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 (group) {
review.meta = {
groupId: group.id,
review: true,
};
choices.push(review);
ignore.meta.review = true;
ignore.meta.groupId = group.id;
ignore.meta.vulnsInGroup = [];
}
if (patches === null && !upgradeAvailable) {
ignore.default = true;
}
choices.push(ignore);
choices.push(skip);
// look for a default - the `res.default` needs to be the index
// of the choice, so we remap the choices to include the index, value of
// choice and whether it was supposed to be a default. If the user is
// updating their policy options, then we select the choice they had
// before, otherwise we select the default
res.default = (choices
.map((choice, cIndex) => {
return { i: cIndex, default: choice.default };
})
.filter((choice) => {
return choice.default;
})
.shift() || { i: 0 }).i;
// kludge to make sure that we get the vuln in the user selection
res.choices = choices.map((choice) => {
const value = choice.value;
// 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 = {
meta: choice.meta,
vuln,
choice: value,
};
return choice;
});
res.vuln = vuln;
return res;
});
// zip together every prompt and a prompt asking "why", note that the `when`
// callback controls whether not to prompt the user with this question,
// in this case, we always show if the user choses to ignore.
prompts = prompts.reduce((acc, curr) => {
acc.push(curr);
const rule = snykPolicy.getByVuln(policy, curr.choices[0].value.vuln);
let defaultAnswer = 'None given';
if (rule && rule.type === 'ignore') {
defaultAnswer = rule.reason;
}
const issue = curr.choices[0].value.vuln &&
curr.choices[0].value.vuln.type === 'license'
? 'issue'
: 'vulnerability';
acc.push({
name: curr.name + '-reason',
message: '[audit] Reason for ignoring ' + issue + '?',
default: defaultAnswer,
when(answers) {
if (!answers[curr.name]) {
return false;
}
return answers[curr.name].choice === 'ignore';
},
});
return acc;
}, []);
return prompts;
}
function startOver() {
return {
name: 'misc-start-over',
message: 'Existing .snyk policy found. Ignore it and start from scratch [y] or update it [N]?',
type: 'confirm',
default: false,
};
}
exports.startOver = startOver;
function nextSteps(pkg, prevAnswers) {
let skipProtect = false;
const prompts = [];
let i;
i = get(pkg, 'scripts.test', '').indexOf('snyk test');
if (i === -1) {
prompts.push({
name: 'misc-add-test',
message: 'Add `snyk test` to package.json file to fail test on newly ' +
'disclosed vulnerabilities?\n' +
'This will require authentication via `snyk auth` when running tests.',
type: 'confirm',
default: false,
});
}
// early exit if prevAnswers is false (when snyk test.ok === true)
if (prevAnswers === false) {
return prompts;
}
i = get(pkg, 'scripts.prepublish', '').indexOf('snyk-pro');
// if `snyk protect` doesn't already appear, then check if we need to add it
if (i === -1) {
skipProtect = Object.keys(prevAnswers).every((key) => {
return prevAnswers[key].choice !== 'patch';
});
}
else {
skipProtect = true;
}
if (!skipProtect) {
prompts.push({
name: 'misc-add-protect',
message: 'Add `snyk protect` as a package.json installation hook to ' +
'apply chosen patches on install?',
type: 'confirm',
default: true,
});
}
return prompts;
}
exports.nextSteps = nextSteps;
/***/ }),
/***/ 19414:
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
Object.defineProperty(exports, "__esModule", ({ value: true }));
const debugModule = __webpack_require__(15158);
const debug = debugModule('snyk');
const cloneDeep = __webpack_require__(83465);
function answersToTasks(answers) {
const tasks = {
ignore: [],
update: [],
patch: [],
skip: [],
};
Object.keys(answers).forEach((key) => {
// if we're looking at a reason, skip it
if (key.indexOf('-reason') !== -1) {
return;
}
// ignore misc questions, like "add snyk test to package?"
if (key.indexOf('misc-') === 0) {
return;
}
const answer = answers[key];
const task = answer.choice;
if (task === 'review' || task === 'skip') {
// task = 'skip';
return;
}
const vuln = answer.vuln;
if (task === 'patch' && vuln.grouped && vuln.grouped.upgrades) {
// ignore the first as it's the same one as this particular answer
debug('additional answers required: %s', vuln.grouped.count - 1, vuln.grouped);
const additional = vuln.grouped.upgrades.slice(1);
additional.forEach((upgrade) => {
const copy = cloneDeep(vuln);
copy.from = upgrade.from;
copy.__filename = upgrade.filename;
copy.patches = upgrade.patches;
copy.version = upgrade.version;
tasks[task].push(copy);
});
}
if (task === 'ignore') {
answer.meta.reason = answers[key + '-reason'];
if (answer.meta.vulnsInGroup) {
// also ignore any in the group
answer.meta.vulnsInGroup.forEach((vulnInGroup) => {
tasks[task].push({
meta: answer.meta,
vuln: vulnInGroup,
});
});
}
else {
tasks[task].push(answer);
}
}
else {
tasks[task].push(vuln);
}
});
return tasks;
}
exports.default = answersToTasks;
/***/ }),
/***/ 55959:
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.processAnswers = exports.inquire = exports.interactive = void 0;
const debugModule = __webpack_require__(15158);
const debug = debugModule('snyk');
const path = __webpack_require__(85622);
const inquirer = __webpack_require__(32139);
const fs = __webpack_require__(35747);
const tryRequire = __webpack_require__(14402);
const chalk_1 = __webpack_require__(32589);
const url = __webpack_require__(78835);
const cloneDeep = __webpack_require__(83465);
const get = __webpack_require__(29208);
const child_process_1 = __webpack_require__(63129);
const api_token_1 = __webpack_require__(95181);
const auth = __webpack_require__(71771);
const version_1 = __webpack_require__(74970);
const allPrompts = __webpack_require__(47272);
const tasks_1 = __webpack_require__(19414);
const snyk = __webpack_require__(9146);
const monitor_1 = __webpack_require__(3959);
const is_ci_1 = __webpack_require__(10090);
const protect = __webpack_require__(5409);
const authorization = __webpack_require__(69943);
const config_1 = __webpack_require__(22541);
const spinner_1 = __webpack_require__(86766);
const analytics = __webpack_require__(82744);
const alerts = __webpack_require__(21696);
const npm_1 = __webpack_require__(46103);
const detect = __webpack_require__(45318);
const plugins = __webpack_require__(45632);
const module_info_1 = __webpack_require__(80777);
const misconfigured_auth_in_ci_error_1 = __webpack_require__(27747);
const missing_targetfile_error_1 = __webpack_require__(56775);
const pm = __webpack_require__(53847);
const theme_1 = __webpack_require__(86988);
function wizard(options) {
options = options || {};
options.org = options.org || config_1.default.org || null;
return processPackageManager(options)
.then(processWizardFlow)
.catch((error) => Promise.reject(error));
}
exports.default = wizard;
async function processPackageManager(options) {
const packageManager = detect.detectPackageManager(process.cwd(), options);
const supportsWizard = pm.WIZARD_SUPPORTED_PACKAGE_MANAGERS.includes(packageManager);
if (!supportsWizard) {
return Promise.reject(`Snyk wizard for ${pm.SUPPORTED_PACKAGE_MANAGER_NAME[packageManager]} projects is not currently supported`);
}
const nodeModulesExist = fs.existsSync(path.join('.', 'node_modules'));
if (!nodeModulesExist) {
// throw a custom error
throw new Error("Missing node_modules folder: we can't patch without having installed packages." +
`\nPlease run '${packageManager} install' first.`);
}
return Promise.resolve(options);
}
async function loadOrCreatePolicyFile(options) {
let policyFile;
try {
policyFile = await snyk.policy.load(options['policy-path'], options);
return policyFile;
}
catch (error) {
// if we land in the catch, but we're in interactive mode, then it means
// the file hasn't been created yet, and that's fine, so we'll resolve
// with an empty object
if (error.code === 'ENOENT') {
options.newPolicy = true;
policyFile = snyk.policy.create();
return policyFile;
}
throw error;
}
}
async function processWizardFlow(options) {
spinner_1.spinner.sticky();
const message = options['dry-run']
? '*** dry run ****'
: '~~~~ LIVE RUN ~~~~';
debug(message);
const policyFile = await loadOrCreatePolicyFile(options);
const authed = await auth.isAuthed();
analytics.add('inline-auth', !authed);
if (!authed && is_ci_1.isCI()) {
throw misconfigured_auth_in_ci_error_1.MisconfiguredAuthInCI();
}
api_token_1.apiTokenExists();
const cliIgnoreAuthorization = await authorization.actionAllowed('cliIgnore', options);
options.ignoreDisabled = cliIgnoreAuthorization.allowed
? false
: cliIgnoreAuthorization;
if (options.ignoreDisabled) {
debug('ignore disabled');
}
const intro = `Snyk's wizard will:
* Enumerate your local dependencies and query Snyk's servers for vulnerabilities
* Guide you through fixing found vulnerabilities
* Create a .snyk policy file to guide snyk commands such as \`test\` and \`protect\`
* Remember your dependencies to alert you when new vulnerabilities are disclosed
`;
return Promise.resolve(intro)
.then((str) => {
if (!is_ci_1.isCI()) {
console.log(str);
}
})
.then(() => {
return new Promise((resolve) => {
if (options.newPolicy) {
return resolve(); // don't prompt to start over
}
inquirer.prompt(allPrompts.startOver()).then((answers) => {
analytics.add('start-over', answers['misc-start-over']);
if (answers['misc-start-over']) {
options['ignore-policy'] = true;
}
resolve();
});
});
})
.then(() => {
// We need to have modules information for remediation. See Payload.modules
options.traverseNodeModules = true;
return snyk.test(process.cwd(), options).then((oneOrManyRes) => {
if (oneOrManyRes[0]) {
throw new Error('Multiple subprojects are not yet supported by snyk wizard');
}
const res = oneOrManyRes;
if (alerts.hasAlert('tests-reached') && res.isPrivate) {
return;
}
const packageFile = path.resolve(process.cwd(), 'package.json');
if (!res.ok) {
const vulns = res.vulnerabilities;
const paths = vulns.length === 1 ? 'path' : 'paths';
const ies = vulns.length === 1 ? 'y' : 'ies';
// echo out the deps + vulns found
console.log('Tested %s dependencies for known vulnerabilities, %s', res.dependencyCount, chalk_1.default.bold.red('found ' +
res.uniqueCount +
' vulnerabilit' +
ies +
', ' +
vulns.length +
' vulnerable ' +
paths +
'.'));
}
else {
console.log(theme_1.color.status.success(`${theme_1.icon.VALID} Tested %s dependencies for known vulnerabilities, no vulnerable paths found.`), res.dependencyCount);
}
return snyk.policy.loadFromText(res.policy).then((combinedPolicy) => {
return tryRequire(packageFile).then((pkg) => {
options.packageLeading = pkg.leading;
options.packageTrailing = pkg.trailing;
return interactive(res, pkg, combinedPolicy, options).then((answers) => processAnswers(answers, policyFile, options));
});
});
});
});
}
function interactive(test, pkg, policy, options) {
const vulns = test.vulnerabilities;
if (!policy) {
policy = {};
}
if (!pkg) {
// only really happening in tests
pkg = {};
}
return new Promise((resolve) => {
debug('starting questions');
const prompts = allPrompts.getUpdatePrompts(vulns, policy, options);
resolve(inquire(prompts, {}));
})
.then((answers) => {
const prompts = allPrompts.getPatchPrompts(vulns, policy, options);
return inquire(prompts, answers);
})
.then((answers) => {
const prompts = allPrompts.getIgnorePrompts(vulns, policy, options);
return inquire(prompts, answers);
})
.then((answers) => {
const prompts = allPrompts.nextSteps(pkg, test.ok ? false : answers);
return inquire(prompts, answers);
})
.then((answers) => {
if (pkg.shrinkwrap) {
answers['misc-build-shrinkwrap'] = true;
}
return answers;
});
}
exports.interactive = interactive;
function inquire(prompts, answers) {
if (prompts.length === 0) {
return Promise.resolve(answers);
}
// inquirer will handle dots in name as path in hash (CSUP-272)
prompts.forEach((prompt) => {
prompt.name = prompt.name.replace(/\./g, '--DOT--');
});
return new Promise((resolve) => {
inquirer.prompt(prompts).then((theseAnswers) => {
answers = { ...answers, ...theseAnswers };
Object.keys(answers).forEach((answerName) => {
if (answerName.indexOf('--DOT--') > -1) {
const newName = answerName.replace(/--DOT--/g, '.');
answers[newName] = answers[answerName];
delete answers[answerName];
}
});
resolve(answers);
});
});
}
exports.inquire = inquire;
function getNewScriptContent(scriptContent, cmd) {
if (scriptContent) {
// only add the command if it's not already in the script
if (scriptContent.indexOf(cmd) === -1) {
return cmd + ' && ' + scriptContent;
}
return scriptContent;
}
return cmd;
}
function addProtectScripts(existingScripts, npmVersion, options) {
const scripts = existingScripts ? cloneDeep(existingScripts) : {};
scripts['snyk-protect'] = 'snyk protect';
let cmd = 'npm run snyk-protect';
// legacy check for `postinstall`, if `npm run snyk-protect` is in there
// we'll replace it with `true` so it can be cleanly swapped out
const postinstall = scripts.postinstall;
if (postinstall && postinstall.indexOf(cmd) !== -1) {
scripts.postinstall = postinstall.replace(cmd, 'true');
}
if (options.packageManager === 'yarn') {
cmd = 'yarn run snyk-protect';
scripts.prepare = getNewScriptContent(scripts.prepare, cmd);
return scripts;
}
const npmVersionMajor = parseInt(npmVersion.split('.')[0], 10);
if (npmVersionMajor >= 5) {
scripts.prepare = getNewScriptContent(scripts.prepare, cmd);
return scripts;
}
scripts.prepublish = getNewScriptContent(scripts.prepublish, cmd);
return scripts;
}
function calculatePkgFileIndentation(packageFile) {
let pkgIndentation = 2;
const whitespaceMatch = packageFile.match(/{\n(\s+)"/);
if (whitespaceMatch && whitespaceMatch[1]) {
pkgIndentation = whitespaceMatch[1].length;
}
return pkgIndentation;
}
function processAnswers(answers, policy, options) {
if (!options) {
options = {};
}
options.packageLeading = options.packageLeading || '';
options.packageTrailing = options.packageTrailing || '';
// allow us to capture the answers the users gave so we can combine this
// the scenario running
if (options.json) {
return Promise.resolve(JSON.stringify(answers, null, 2));
}
const cwd = process.cwd();
const packageFile = path.resolve(cwd, 'package.json');
const packageManager = detect.detectPackageManager(cwd, options);
const targetFile = options.file || detect.detectPackageFile(cwd);
if (!targetFile) {
throw missing_targetfile_error_1.MissingTargetFileError(cwd);
}
const isLockFileBased = targetFile.endsWith('package-lock.json') ||
targetFile.endsWith('yarn.lock');
let pkg = {};
let pkgIndentation = 2;
sendWizardAnalyticsData(answers);
const tasks = tasks_1.default(answers);
debug(tasks);
const live = !options['dry-run'];
let snykVersion = '*';
const res = protect
.generatePolicy(policy, tasks, live, options.packageManager)
.then(async (policy2) => {
if (!live) {
// if this was a dry run, we'll throw an error to bail out of the
// promise chain, then in the catch, check the error.code and if
// it matches `DRYRUN` we'll return the text and not an error
// (which avoids the exit code 1).
const e = new Error('This was a dry run: nothing changed');
e.code = 'DRYRUN';
throw e;
}
await policy2.save(cwd, spinner_1.spinner);
// don't do this during testing
if (is_ci_1.isCI() || process.env.TAP) {
return Promise.resolve();
}
return new Promise((resolve) => {
child_process_1.exec('git add .snyk', {
cwd,
}, (error, stdout, stderr) => {
if (error) {
debug('error adding .snyk to git', error);
}
if (stderr) {
debug('stderr adding .snyk to git', stderr.trim());
}
// resolve either way
resolve();
});
});
})
.then(() => {
// re-read the package.json - because the generatePolicy can apply
// an `npm install` which will change the deps
return Promise.resolve(fs.readFileSync(packageFile, 'utf8'))
.then((packageFileString) => {
pkgIndentation = calculatePkgFileIndentation(packageFileString);
return packageFileString;
})
.then(JSON.parse)
.then((updatedPkg) => {
pkg = updatedPkg;
});
})
.then(version_1.default)
.then((v) => {
debug('snyk version: %s', v);
// little hack to circumvent local testing where the version will
// be the git branch + commit
if (v.match(/^\d+\./) === null) {
v = '*';
}
else {
v = '^' + v;
}
snykVersion = v;
})
.then(() => {
analytics.add('add-snyk-test', answers['misc-add-test']);
if (!answers['misc-add-test']) {
return;
}
debug('adding `snyk test` to package');
if (!pkg.scripts) {
pkg.scripts = {};
}
const test = pkg.scripts.test;
const cmd = 'snyk test';
if (test && test !== 'echo "Error: no test specified" && exit 1') {
// only add the test if it's not already in the test
if (test.indexOf(cmd) === -1) {
pkg.scripts.test = cmd + ' && ' + test;
}
}
else {
pkg.scripts.test = cmd;
}
})
.then(async () => {
const npmVersion = await npm_1.getVersion();
analytics.add('add-snyk-protect', answers['misc-add-protect']);
if (!answers['misc-add-protect']) {
return;
}
debug('adding `snyk protect` to package');
if (!pkg.scripts) {
pkg.scripts = {};
}
pkg.scripts = addProtectScripts(pkg.scripts, npmVersion, options);
pkg.snyk = true;
})
.then(() => {
let lbl = 'Updating package.json...';
const addSnykToDependencies = answers['misc-add-test'] || answers['misc-add-protect'];
let updateSnykFunc = () => {
return;
}; // noop
if (addSnykToDependencies) {
updateSnykFunc = () => protect.install(packageManager, ['snyk'], live);
}
if (addSnykToDependencies) {
debug('updating %s', packageFile);
if (get(pkg, 'dependencies.snyk') ||
get(pkg, 'peerDependencies.snyk') ||
get(pkg, 'optionalDependencies.snyk')) {
// nothing to do as the user already has Snyk
// TODO decide whether we should update the version being used
// and how do we reconcile if the global install is older
// than the local version?
}
else {
const addSnykToProdDeps = answers['misc-add-protect'];
const snykIsInDevDeps = get(pkg, 'devDependencies.snyk');
if (addSnykToProdDeps) {
if (!pkg.dependencies) {
pkg.dependencies = {};
}
pkg.dependencies.snyk = snykVersion;
lbl =
'Adding Snyk t