UNPKG

snyk

Version:

snyk library and cli utility

1,302 lines (1,283 loc) 58.8 kB
"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