diff --git a/README.md b/README.md index b2a9791..8203c17 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ socket --help socket info webtorrent@1.9.1 socket report create package.json --view socket report view QXU8PmK7LfH608RAwfIKdbcHgwEd_ZeWJ9QEGv05FJUQ +socket wrapper --enable ``` ## Commands @@ -35,6 +36,10 @@ socket report view QXU8PmK7LfH608RAwfIKdbcHgwEd_ZeWJ9QEGv05FJUQ * `socket report view ` - looks up issues and scores from a report +* `socket wrapper --enable` and `socket wrapper --disable` - Enables and disables the Socket 'safe-npm' wrapper. + +* `socket raw-npm` and `socket raw-npx` - Temporarily disables the Socket 'safe-npm' wrapper. + ## Aliases All aliases supports flags and arguments of the commands they alias. diff --git a/cli.js b/cli.js index 771cb50..55c3728 100755 --- a/cli.js +++ b/cli.js @@ -15,8 +15,17 @@ import { initUpdateNotifier } from './lib/utils/update-notifier.js' initUpdateNotifier() try { + const formattedCliCommands = Object.fromEntries(Object.entries(cliCommands).map((entry) => { + if (entry[0] === 'rawNpm') { + entry[0] = 'raw-npm' + } else if (entry[0] === 'rawNpx') { + entry[0] = 'raw-npx' + } + return entry + })) + await meowWithSubcommands( - cliCommands, + formattedCliCommands, { aliases: { ci: { diff --git a/lib/commands/index.js b/lib/commands/index.js index 8552736..aae0d33 100644 --- a/lib/commands/index.js +++ b/lib/commands/index.js @@ -4,3 +4,6 @@ export * from './npm/index.js' export * from './npx/index.js' export * from './login/index.js' export * from './logout/index.js' +export * from './wrapper/index.js' +export * from './raw-npm/index.js' +export * from './raw-npx/index.js' diff --git a/lib/commands/raw-npm/index.js b/lib/commands/raw-npm/index.js new file mode 100644 index 0000000..ffaadd5 --- /dev/null +++ b/lib/commands/raw-npm/index.js @@ -0,0 +1,59 @@ +import { spawn } from 'child_process' + +import meow from 'meow' + +import { validationFlags } from '../../flags/index.js' +import { printFlagList } from '../../utils/formatting.js' + +/** @type {import('../../utils/meow-with-subcommands.js').CliSubcommand} */ +export const rawNpm = { + description: 'Temporarily disable the Socket npm wrapper', + async run (argv, importMeta, { parentName }) { + const name = parentName + ' raw-npm' + + setupCommand(name, rawNpm.description, argv, importMeta) + } +} + +/** + * @param {string} name + * @param {string} description + * @param {readonly string[]} argv + * @param {ImportMeta} importMeta + * @returns {void} + */ +function setupCommand (name, description, argv, importMeta) { + const flags = validationFlags + + const cli = meow(` + Usage + $ ${name} + + Options + ${printFlagList(flags, 6)} + + Examples + $ ${name} install + `, { + argv, + description, + importMeta, + flags + }) + + if (!argv[0]) { + cli.showHelp() + return + } + + spawn('npm', [argv.join(' ')], { + stdio: 'inherit', + shell: true + }).on('exit', (code, signal) => { + if (signal) { + process.kill(process.pid, signal) + } else if (code !== null) { + process.exit(code) + } + }) +} diff --git a/lib/commands/raw-npx/index.js b/lib/commands/raw-npx/index.js new file mode 100644 index 0000000..5611457 --- /dev/null +++ b/lib/commands/raw-npx/index.js @@ -0,0 +1,59 @@ +import { spawn } from 'child_process' + +import meow from 'meow' + +import { validationFlags } from '../../flags/index.js' +import { printFlagList } from '../../utils/formatting.js' + +/** @type {import('../../utils/meow-with-subcommands.js').CliSubcommand} */ +export const rawNpx = { + description: 'Temporarily disable the Socket npm/npx wrapper', + async run (argv, importMeta, { parentName }) { + const name = parentName + ' raw-npx' + + setupCommand(name, rawNpx.description, argv, importMeta) + } +} + +/** + * @param {string} name + * @param {string} description + * @param {readonly string[]} argv + * @param {ImportMeta} importMeta + * @returns {void} + */ +function setupCommand (name, description, argv, importMeta) { + const flags = validationFlags + + const cli = meow(` + Usage + $ ${name} + + Options + ${printFlagList(flags, 6)} + + Examples + $ ${name} install + `, { + argv, + description, + importMeta, + flags + }) + + if (!argv[0]) { + cli.showHelp() + return + } + + spawn('npx', [argv.join(' ')], { + stdio: 'inherit', + shell: true + }).on('exit', (code, signal) => { + if (signal) { + process.kill(process.pid, signal) + } else if (code !== null) { + process.exit(code) + } + }) +} diff --git a/lib/commands/wrapper/index.js b/lib/commands/wrapper/index.js new file mode 100644 index 0000000..63d5105 --- /dev/null +++ b/lib/commands/wrapper/index.js @@ -0,0 +1,199 @@ +/* eslint-disable no-console */ +import fs from 'fs' +import homedir from 'os' +import readline from 'readline' + +import meow from 'meow' + +import { commandFlags } from '../../flags/index.js' +import { printFlagList } from '../../utils/formatting.js' + +const BASH_FILE = `${homedir.homedir()}/.bashrc` +const ZSH_BASH_FILE = `${homedir.homedir()}/.zshrc` + +/** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */ +export const wrapper = { + description: 'Enable or disable the Socket npm/npx wrapper', + async run (argv, importMeta, { parentName }) { + const name = parentName + ' wrapper' + + setupCommand(name, wrapper.description, argv, importMeta) + } +} + +/** + * @param {string} name + * @param {string} description + * @param {readonly string[]} argv + * @param {ImportMeta} importMeta + * @returns {void} + */ +function setupCommand (name, description, argv, importMeta) { + const flags = commandFlags + + const cli = meow(` + Usage + $ ${name} + + Options + ${printFlagList(flags, 6)} + + Examples + $ ${name} --enable + $ ${name} --disable + `, { + argv, + description, + importMeta, + flags + }) + + const { enable, disable } = cli.flags + + if (argv[0] === '--postinstall') { + // Check if the wrapper is already enabled before showing the postinstall prompt + const socketWrapperEnabled = (fs.existsSync(BASH_FILE) && checkSocketWrapperAlreadySetup(BASH_FILE)) || (fs.existsSync(ZSH_BASH_FILE) && checkSocketWrapperAlreadySetup(BASH_FILE)) + + if (!socketWrapperEnabled) { + installSafeNpm(`The Socket CLI is now successfully installed! 🎉 + + To better protect yourself against supply-chain attacks, our "safe npm" wrapper can warn you about malicious packages whenever you run 'npm install'. + + Do you want to install "safe npm" (this will create an alias to the socket-npm command)? (y/n)`) + } + + return + } + + if (!enable && !disable) { + cli.showHelp() + return + } + + if (enable) { + if (fs.existsSync(BASH_FILE)) { + const socketWrapperEnabled = checkSocketWrapperAlreadySetup(BASH_FILE) + !socketWrapperEnabled && addAlias(BASH_FILE) + } + if (fs.existsSync(ZSH_BASH_FILE)) { + const socketWrapperEnabled = checkSocketWrapperAlreadySetup(ZSH_BASH_FILE) + !socketWrapperEnabled && addAlias(ZSH_BASH_FILE) + } + } else if (disable) { + if (fs.existsSync(BASH_FILE)) { + removeAlias(BASH_FILE) + } + if (fs.existsSync(ZSH_BASH_FILE)) { + removeAlias(ZSH_BASH_FILE) + } + } + if (!fs.existsSync(BASH_FILE) && !fs.existsSync(ZSH_BASH_FILE)) { + console.error('There was an issue setting up the alias in your bash profile') + } + return +} + +/** + * @param {string} query + * @returns {void} + */ +const installSafeNpm = (query) => { + console.log(` + _____ _ _ +| __|___ ___| |_ ___| |_ +|__ | . | _| '_| -_| _| +|_____|___|___|_,_|___|_| + +`) + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }) + return askQuestion(rl, query) +} + +/** + * @param {any} rl + * @param {string} query + * @returns {void} + */ +const askQuestion = (rl, query) => { + rl.question(query, (/** @type {string} */ ans) => { + if (ans.toLowerCase() === 'y') { + try { + if (fs.existsSync(BASH_FILE)) { + addAlias(BASH_FILE) + } + if (fs.existsSync(ZSH_BASH_FILE)) { + addAlias(ZSH_BASH_FILE) + } + } catch (e) { + throw new Error(`There was an issue setting up the alias: ${e}`) + } + rl.close() + } else if (ans.toLowerCase() !== 'n') { + askQuestion(rl, 'Incorrect input: please enter either y (yes) or n (no): ') + } else { + rl.close() + } + }) +} + +/** + * @param {string} file + * @returns {void} + */ +const addAlias = (file) => { + return fs.appendFile(file, 'alias npm="socket npm"\nalias npx="socket npx"\n', (err) => { + if (err) { + return new Error(`There was an error setting up the alias: ${err}`) + } + console.log(` +The alias was added to ${file}. Running 'npm install' will now be wrapped in Socket's "safe npm" 🎉 +If you want to disable it at any time, run \`socket wrapper --disable\` +`) + }) +} + +/** + * @param {string} file + * @returns {void} + */ +const removeAlias = (file) => { + return fs.readFile(file, 'utf8', function (err, data) { + if (err) { + console.error(`There was an error removing the alias: ${err}`) + return + } + const linesWithoutSocketAlias = data.split('\n').filter(l => l !== 'alias npm="socket npm"' && l !== 'alias npx="socket npx"') + + const updatedFileContent = linesWithoutSocketAlias.join('\n') + + fs.writeFile(file, updatedFileContent, function (err) { + if (err) { + console.log(err) + return + } else { + console.log(` +The alias was removed from ${file}. Running 'npm install' will now run the standard npm command. +`) + } + }) + }) +} + +/** + * @param {string} file + * @returns {boolean} + */ +const checkSocketWrapperAlreadySetup = (file) => { + const fileContent = fs.readFileSync(file, 'utf-8') + const linesWithSocketAlias = fileContent.split('\n').filter(l => l === 'alias npm="socket npm"' || l === 'alias npx="socket npx"') + + if (linesWithSocketAlias.length) { + console.log(`The Socket npm/npx wrapper is set up in your bash profile (${file}).`) + return true + } + return false +} diff --git a/lib/flags/command.js b/lib/flags/command.js new file mode 100644 index 0000000..59a569a --- /dev/null +++ b/lib/flags/command.js @@ -0,0 +1,14 @@ +import { prepareFlags } from '../utils/flags.js' + +export const commandFlags = prepareFlags({ + enable: { + type: 'boolean', + default: false, + description: 'Enables the Socket npm/npx wrapper', + }, + disable: { + type: 'boolean', + default: false, + description: 'Disables the Socket npm/npx wrapper', + } +}) diff --git a/lib/flags/index.js b/lib/flags/index.js index f09dcdb..60ea717 100644 --- a/lib/flags/index.js +++ b/lib/flags/index.js @@ -1,2 +1,3 @@ export { outputFlags } from './output.js' export { validationFlags } from './validation.js' +export { commandFlags } from './command.js' diff --git a/package-lock.json b/package-lock.json index e807f94..25f9f67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,13 @@ { "name": "@socketsecurity/cli", - "version": "0.8.2", + "version": "0.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@socketsecurity/cli", - "version": "0.8.2", + "version": "0.9.0", + "hasInstallScript": true, "license": "MIT", "dependencies": { "@apideck/better-ajv-errors": "^0.3.6", diff --git a/package.json b/package.json index b91463a..4226411 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "prepare": "husky install", "test:unit": "c8 --reporter=lcov --reporter text node --test", "test-ci": "run-s test:*", - "test": "run-s check test:*" + "test": "run-s check test:*", + "postinstall": "node ./cli.js wrapper --postinstall" }, "devDependencies": { "@socketsecurity/eslint-config": "^3.0.1",