Skip to content

Commit 47047ab

Browse files
Merge pull request #98 from SocketDev/cg/postInstallSafeNpm
Add postinstall script to install the safe npm alias
2 parents ad7b908 + 8345060 commit 47047ab

File tree

10 files changed

+355
-4
lines changed

10 files changed

+355
-4
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ socket --help
1818
socket info [email protected]
1919
socket report create package.json --view
2020
socket report view QXU8PmK7LfH608RAwfIKdbcHgwEd_ZeWJ9QEGv05FJUQ
21+
socket wrapper --enable
2122
```
2223

2324
## Commands
@@ -35,6 +36,10 @@ socket report view QXU8PmK7LfH608RAwfIKdbcHgwEd_ZeWJ9QEGv05FJUQ
3536

3637
* `socket report view <report-id>` - looks up issues and scores from a report
3738

39+
* `socket wrapper --enable` and `socket wrapper --disable` - Enables and disables the Socket 'safe-npm' wrapper.
40+
41+
* `socket raw-npm` and `socket raw-npx` - Temporarily disables the Socket 'safe-npm' wrapper.
42+
3843
## Aliases
3944

4045
All aliases supports flags and arguments of the commands they alias.

cli.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,17 @@ import { initUpdateNotifier } from './lib/utils/update-notifier.js'
1515
initUpdateNotifier()
1616

1717
try {
18+
const formattedCliCommands = Object.fromEntries(Object.entries(cliCommands).map((entry) => {
19+
if (entry[0] === 'rawNpm') {
20+
entry[0] = 'raw-npm'
21+
} else if (entry[0] === 'rawNpx') {
22+
entry[0] = 'raw-npx'
23+
}
24+
return entry
25+
}))
26+
1827
await meowWithSubcommands(
19-
cliCommands,
28+
formattedCliCommands,
2029
{
2130
aliases: {
2231
ci: {

lib/commands/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@ export * from './npm/index.js'
44
export * from './npx/index.js'
55
export * from './login/index.js'
66
export * from './logout/index.js'
7+
export * from './wrapper/index.js'
8+
export * from './raw-npm/index.js'
9+
export * from './raw-npx/index.js'

lib/commands/raw-npm/index.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { spawn } from 'child_process'
2+
3+
import meow from 'meow'
4+
5+
import { validationFlags } from '../../flags/index.js'
6+
import { printFlagList } from '../../utils/formatting.js'
7+
8+
/** @type {import('../../utils/meow-with-subcommands.js').CliSubcommand} */
9+
export const rawNpm = {
10+
description: 'Temporarily disable the Socket npm wrapper',
11+
async run (argv, importMeta, { parentName }) {
12+
const name = parentName + ' raw-npm'
13+
14+
setupCommand(name, rawNpm.description, argv, importMeta)
15+
}
16+
}
17+
18+
/**
19+
* @param {string} name
20+
* @param {string} description
21+
* @param {readonly string[]} argv
22+
* @param {ImportMeta} importMeta
23+
* @returns {void}
24+
*/
25+
function setupCommand (name, description, argv, importMeta) {
26+
const flags = validationFlags
27+
28+
const cli = meow(`
29+
Usage
30+
$ ${name} <npm command>
31+
32+
Options
33+
${printFlagList(flags, 6)}
34+
35+
Examples
36+
$ ${name} install
37+
`, {
38+
argv,
39+
description,
40+
importMeta,
41+
flags
42+
})
43+
44+
if (!argv[0]) {
45+
cli.showHelp()
46+
return
47+
}
48+
49+
spawn('npm', [argv.join(' ')], {
50+
stdio: 'inherit',
51+
shell: true
52+
}).on('exit', (code, signal) => {
53+
if (signal) {
54+
process.kill(process.pid, signal)
55+
} else if (code !== null) {
56+
process.exit(code)
57+
}
58+
})
59+
}

lib/commands/raw-npx/index.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { spawn } from 'child_process'
2+
3+
import meow from 'meow'
4+
5+
import { validationFlags } from '../../flags/index.js'
6+
import { printFlagList } from '../../utils/formatting.js'
7+
8+
/** @type {import('../../utils/meow-with-subcommands.js').CliSubcommand} */
9+
export const rawNpx = {
10+
description: 'Temporarily disable the Socket npm/npx wrapper',
11+
async run (argv, importMeta, { parentName }) {
12+
const name = parentName + ' raw-npx'
13+
14+
setupCommand(name, rawNpx.description, argv, importMeta)
15+
}
16+
}
17+
18+
/**
19+
* @param {string} name
20+
* @param {string} description
21+
* @param {readonly string[]} argv
22+
* @param {ImportMeta} importMeta
23+
* @returns {void}
24+
*/
25+
function setupCommand (name, description, argv, importMeta) {
26+
const flags = validationFlags
27+
28+
const cli = meow(`
29+
Usage
30+
$ ${name} <npx command>
31+
32+
Options
33+
${printFlagList(flags, 6)}
34+
35+
Examples
36+
$ ${name} install
37+
`, {
38+
argv,
39+
description,
40+
importMeta,
41+
flags
42+
})
43+
44+
if (!argv[0]) {
45+
cli.showHelp()
46+
return
47+
}
48+
49+
spawn('npx', [argv.join(' ')], {
50+
stdio: 'inherit',
51+
shell: true
52+
}).on('exit', (code, signal) => {
53+
if (signal) {
54+
process.kill(process.pid, signal)
55+
} else if (code !== null) {
56+
process.exit(code)
57+
}
58+
})
59+
}

lib/commands/wrapper/index.js

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/* eslint-disable no-console */
2+
import fs from 'fs'
3+
import homedir from 'os'
4+
import readline from 'readline'
5+
6+
import meow from 'meow'
7+
8+
import { commandFlags } from '../../flags/index.js'
9+
import { printFlagList } from '../../utils/formatting.js'
10+
11+
const BASH_FILE = `${homedir.homedir()}/.bashrc`
12+
const ZSH_BASH_FILE = `${homedir.homedir()}/.zshrc`
13+
14+
/** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */
15+
export const wrapper = {
16+
description: 'Enable or disable the Socket npm/npx wrapper',
17+
async run (argv, importMeta, { parentName }) {
18+
const name = parentName + ' wrapper'
19+
20+
setupCommand(name, wrapper.description, argv, importMeta)
21+
}
22+
}
23+
24+
/**
25+
* @param {string} name
26+
* @param {string} description
27+
* @param {readonly string[]} argv
28+
* @param {ImportMeta} importMeta
29+
* @returns {void}
30+
*/
31+
function setupCommand (name, description, argv, importMeta) {
32+
const flags = commandFlags
33+
34+
const cli = meow(`
35+
Usage
36+
$ ${name} <flag>
37+
38+
Options
39+
${printFlagList(flags, 6)}
40+
41+
Examples
42+
$ ${name} --enable
43+
$ ${name} --disable
44+
`, {
45+
argv,
46+
description,
47+
importMeta,
48+
flags
49+
})
50+
51+
const { enable, disable } = cli.flags
52+
53+
if (argv[0] === '--postinstall') {
54+
// Check if the wrapper is already enabled before showing the postinstall prompt
55+
const socketWrapperEnabled = (fs.existsSync(BASH_FILE) && checkSocketWrapperAlreadySetup(BASH_FILE)) || (fs.existsSync(ZSH_BASH_FILE) && checkSocketWrapperAlreadySetup(BASH_FILE))
56+
57+
if (!socketWrapperEnabled) {
58+
installSafeNpm(`The Socket CLI is now successfully installed! 🎉
59+
60+
To better protect yourself against supply-chain attacks, our "safe npm" wrapper can warn you about malicious packages whenever you run 'npm install'.
61+
62+
Do you want to install "safe npm" (this will create an alias to the socket-npm command)? (y/n)`)
63+
}
64+
65+
return
66+
}
67+
68+
if (!enable && !disable) {
69+
cli.showHelp()
70+
return
71+
}
72+
73+
if (enable) {
74+
if (fs.existsSync(BASH_FILE)) {
75+
const socketWrapperEnabled = checkSocketWrapperAlreadySetup(BASH_FILE)
76+
!socketWrapperEnabled && addAlias(BASH_FILE)
77+
}
78+
if (fs.existsSync(ZSH_BASH_FILE)) {
79+
const socketWrapperEnabled = checkSocketWrapperAlreadySetup(ZSH_BASH_FILE)
80+
!socketWrapperEnabled && addAlias(ZSH_BASH_FILE)
81+
}
82+
} else if (disable) {
83+
if (fs.existsSync(BASH_FILE)) {
84+
removeAlias(BASH_FILE)
85+
}
86+
if (fs.existsSync(ZSH_BASH_FILE)) {
87+
removeAlias(ZSH_BASH_FILE)
88+
}
89+
}
90+
if (!fs.existsSync(BASH_FILE) && !fs.existsSync(ZSH_BASH_FILE)) {
91+
console.error('There was an issue setting up the alias in your bash profile')
92+
}
93+
return
94+
}
95+
96+
/**
97+
* @param {string} query
98+
* @returns {void}
99+
*/
100+
const installSafeNpm = (query) => {
101+
console.log(`
102+
_____ _ _
103+
| __|___ ___| |_ ___| |_
104+
|__ | . | _| '_| -_| _|
105+
|_____|___|___|_,_|___|_|
106+
107+
`)
108+
109+
const rl = readline.createInterface({
110+
input: process.stdin,
111+
output: process.stdout,
112+
})
113+
return askQuestion(rl, query)
114+
}
115+
116+
/**
117+
* @param {any} rl
118+
* @param {string} query
119+
* @returns {void}
120+
*/
121+
const askQuestion = (rl, query) => {
122+
rl.question(query, (/** @type {string} */ ans) => {
123+
if (ans.toLowerCase() === 'y') {
124+
try {
125+
if (fs.existsSync(BASH_FILE)) {
126+
addAlias(BASH_FILE)
127+
}
128+
if (fs.existsSync(ZSH_BASH_FILE)) {
129+
addAlias(ZSH_BASH_FILE)
130+
}
131+
} catch (e) {
132+
throw new Error(`There was an issue setting up the alias: ${e}`)
133+
}
134+
rl.close()
135+
} else if (ans.toLowerCase() !== 'n') {
136+
askQuestion(rl, 'Incorrect input: please enter either y (yes) or n (no): ')
137+
} else {
138+
rl.close()
139+
}
140+
})
141+
}
142+
143+
/**
144+
* @param {string} file
145+
* @returns {void}
146+
*/
147+
const addAlias = (file) => {
148+
return fs.appendFile(file, 'alias npm="socket npm"\nalias npx="socket npx"\n', (err) => {
149+
if (err) {
150+
return new Error(`There was an error setting up the alias: ${err}`)
151+
}
152+
console.log(`
153+
The alias was added to ${file}. Running 'npm install' will now be wrapped in Socket's "safe npm" 🎉
154+
If you want to disable it at any time, run \`socket wrapper --disable\`
155+
`)
156+
})
157+
}
158+
159+
/**
160+
* @param {string} file
161+
* @returns {void}
162+
*/
163+
const removeAlias = (file) => {
164+
return fs.readFile(file, 'utf8', function (err, data) {
165+
if (err) {
166+
console.error(`There was an error removing the alias: ${err}`)
167+
return
168+
}
169+
const linesWithoutSocketAlias = data.split('\n').filter(l => l !== 'alias npm="socket npm"' && l !== 'alias npx="socket npx"')
170+
171+
const updatedFileContent = linesWithoutSocketAlias.join('\n')
172+
173+
fs.writeFile(file, updatedFileContent, function (err) {
174+
if (err) {
175+
console.log(err)
176+
return
177+
} else {
178+
console.log(`
179+
The alias was removed from ${file}. Running 'npm install' will now run the standard npm command.
180+
`)
181+
}
182+
})
183+
})
184+
}
185+
186+
/**
187+
* @param {string} file
188+
* @returns {boolean}
189+
*/
190+
const checkSocketWrapperAlreadySetup = (file) => {
191+
const fileContent = fs.readFileSync(file, 'utf-8')
192+
const linesWithSocketAlias = fileContent.split('\n').filter(l => l === 'alias npm="socket npm"' || l === 'alias npx="socket npx"')
193+
194+
if (linesWithSocketAlias.length) {
195+
console.log(`The Socket npm/npx wrapper is set up in your bash profile (${file}).`)
196+
return true
197+
}
198+
return false
199+
}

lib/flags/command.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { prepareFlags } from '../utils/flags.js'
2+
3+
export const commandFlags = prepareFlags({
4+
enable: {
5+
type: 'boolean',
6+
default: false,
7+
description: 'Enables the Socket npm/npx wrapper',
8+
},
9+
disable: {
10+
type: 'boolean',
11+
default: false,
12+
description: 'Disables the Socket npm/npx wrapper',
13+
}
14+
})

lib/flags/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { outputFlags } from './output.js'
22
export { validationFlags } from './validation.js'
3+
export { commandFlags } from './command.js'

package-lock.json

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)