UNPKG

ncm-cli

Version:

Command-line tool for NodeSource Certified Modules 2.0

232 lines (195 loc) 6.71 kB
'use strict' // In order to work around windows shell issues... const spawn = require('cross-spawn') const { getValue, setValue } = require('../lib/config') const clientRequest = require('../lib/client-request') const { queryReadline, reversedSplit } = require('../lib/util') const details = require('./details') const { COLORS, line, failure } = require('../lib/ncm-style') const { helpHeader } = require('../lib/help') const path = require('path') const rc = require('rc') const chalk = require('chalk') const L = console.log const E = console.error module.exports = install module.exports.optionsList = optionsList const publicRegistryUrl = 'https://registry.npmjs.org/' const cachedPublicPackages = getValue('cachedPublicPackages') const registryUrl = scope => { const result = rc('npm', { registry: publicRegistryUrl }) const url = result[`${scope}:registry`] || result.config_registry || result.registry return url.slice(-1) === '/' ? url : `${url}/` } const scannerLog = () => L(chalk`[NCM::SECURITY] Scanning npm dependency substitution vulnerabilities...`) const concurrentTaskQueue = [] const verificationTask = async ({ name, privateRegistryUrl }) => { try { L(`[NCM::SECURITY] Verifying the package "${name}"`) // skip cached or scoped packages if (cachedPublicPackages.includes(name) || name.includes('/')) return const { body: { versions = {} } } = await clientRequest({ method: 'GET', uri: `${privateRegistryUrl}${name}`, json: true, opted: true }) let verLen = Object.keys(versions).length if (!verLen) return const { integrity: privateIntegrity } = versions[(Object.keys(versions)[verLen - 1])].dist if (!privateIntegrity) return const { body: { versions: pubVers = {} } } = await clientRequest({ method: 'GET', uri: `${publicRegistryUrl}${name}`, json: true, opted: true }) verLen = Object.keys(pubVers).length if (!verLen) return const { integrity: publicIntegrity } = pubVers[(Object.keys(pubVers)[verLen - 1])].dist if (!publicIntegrity) return if (privateIntegrity !== publicIntegrity) { E(chalk.red(`[NCM::SECURITY ISSUE DETECTED] "${name}" is vulnerable to npm substitution attacks. Use scopes for the internal packages to fix this`)) process.exit(1) } else { cachedPublicPackages.push(name) setValue('cachedPublicPackages', [...new Set(cachedPublicPackages)]) } } catch (err) { E(err) } } async function install (argv, arg1, arg2, arg3) { const childArgv = Array.from(process.argv.slice(3)) const regUrl = registryUrl() const privateRegistryUrl = regUrl.endsWith('/') ? regUrl : `${regUrl}/` let name let version if (!arg1) { try { scannerLog() let deps try { deps = require(path.join(process.cwd(), 'package.json')) } catch (_) {} // skipped by below if there's no package.json if (deps) { const pkgTree = [...new Set(Object.keys({ ...(deps.dependencies || {}), ...(deps.devDependencies || {}) }))] pkgTree.forEach(async name => concurrentTaskQueue.push(verificationTask({ name, privateRegistryUrl })) ) try { await Promise.all(concurrentTaskQueue) } catch (pErr) { E(pErr) } } } catch (err) { E(err) } L(chalk`[NCM::SECURITY] Passed. Now installing...`) L() const args = [getValue('installCmd'), '', ...childArgv] const bin = getValue('installBin') const cp = spawn(bin, args, { stdio: 'inherit' }) cp.on('error', error => { E() E(chalk`{COLORS.red ${error}}`) E() }) cp.on('exit', code => { process.exitCode = code }) return } else if (arg1.lastIndexOf('@') > 0 && !arg2 && !arg3) { childArgv.splice(childArgv.indexOf(arg1), 1) ;[name, version] = reversedSplit(arg1, /@(?!$)/) } else if (arg2 === '@' && arg3) { name = arg1 version = arg3 childArgv.splice(childArgv.indexOf(arg1), 1) childArgv.splice(childArgv.indexOf(arg2), 1) childArgv.splice(childArgv.indexOf(arg3), 1) } else if (arg1) { name = arg1 version = arg2 || 'latest' childArgv.splice(childArgv.indexOf(arg1), 1) if (arg2) childArgv.splice(childArgv.indexOf(arg2), 1) } else { printHelp() process.exitCode = 1 return } try { scannerLog() concurrentTaskQueue.push(verificationTask({ name, privateRegistryUrl })) try { await Promise.all(concurrentTaskQueue) } catch (pErr) { E(pErr) } } catch (err) { E(err) } await details(argv, arg1, arg2, arg3) const good = process.exitCode === 0 || process.exitCode === undefined const confirm = good ? `${COLORS.green} (Y/n)` : `${COLORS.red} (y/N)` L(line('|➔', chalk`Install this module? {${confirm}}`, COLORS.yellow)) L() let choice if (argv.force) { choice = 'y' } else if (process.stdin.isTTY) { choice = (await queryReadline(chalk`{${COLORS.light1} > }`)).trim().toLowerCase() } else { choice = good ? 'y' : 'n' } if ((good && choice === '') || choice === 'y') { const args = [getValue('installCmd'), `${name}@${version}`, ...childArgv] const bin = getValue('installBin') L() L(chalk`Running: {${COLORS.teal} ${bin} ${args.join(' ')}}`) L() const cp = spawn(bin, args, { stdio: 'inherit' }) cp.on('error', error => { E() E(chalk`{COLORS.red ${error}}`) E() }) cp.on('exit', code => { process.exitCode = code }) } else { L() L(failure(chalk`Did {bold not} install ${name}@${version}`)) L() } } function printHelp () { helpHeader( 'install', chalk`{${COLORS.light1} ncm} {${COLORS.yellow} install} {${COLORS.teal} <module\{@version\}> [options] [npm options]}`, 'ncm install <module{@version}> [options] [npm options]', chalk` Runs and displays {${COLORS.light1} "ncm details <module\{@version\}>"} with an interactive confirmation prompt. If confirmed, attempts to run {${COLORS.light1} "npm install <module\{@version\}>"} with any additional options provided. {italic The config keys {${COLORS.yellow} installBin} and {${COLORS.yellow} installCmd} can adjust this to work with other package installers if necessary.} ` ) L(optionsList()) L() } function optionsList () { return chalk` {${COLORS.light1} ncm} {${COLORS.yellow} install} {${COLORS.teal} <module\{@version\}> [npm options]} {${COLORS.light1} ncm} {${COLORS.yellow} i} {${COLORS.teal} <module\{@version\}> [npm options]} {${COLORS.teal} -d, --dir} {white Directory to check for dependency path} `.trim() }