@socketsecurity/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Pelle Wessman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,14 @@
1
+ # Socket CLI
2
+
3
+ ## Commands
4
+
5
+ * `report create` - creates a report
6
+
7
+ ## Environment variables
8
+
9
+ * `SOCKET_SECURITY_API_KEY` - if set, this will be used as the API-key
10
+
11
+ ### Environment variables for development
12
+
13
+ * `SOCKET_SECURITY_API_BASE_URL` - if set, this will be the base for all API-calls. Defaults to `https://api.socket.dev/v0/`
14
+ * `SOCKET_SECURITY_API_PROXY` - if set to something like [`http://127.0.0.1:9090`](https://docs.proxyman.io/troubleshooting/couldnt-see-any-requests-from-3rd-party-network-libraries), then all request will be proxied through that proxy
package/cli.js ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+
4
+ import chalk from 'chalk'
5
+ import { messageWithCauses, stackWithCauses } from 'pony-cause'
6
+
7
+ import * as cliCommands from './lib/commands/index.js'
8
+ import { logSymbols } from './lib/utils/chalk-markdown.js'
9
+ import { AuthError, InputError } from './lib/utils/errors.js'
10
+ import { meowWithSubcommands } from './lib/utils/meow-with-subcommands.js'
11
+
12
+ // TODO: Add autocompletion using https://www.npmjs.com/package/omelette
13
+
14
+ try {
15
+ await meowWithSubcommands(
16
+ cliCommands,
17
+ {
18
+ argv: process.argv.slice(2),
19
+ name: 'socket',
20
+ importMeta: import.meta
21
+ }
22
+ )
23
+ } catch (err) {
24
+ /** @type {string} */
25
+ let errorTitle
26
+ /** @type {string} */
27
+ let errorMessage = ''
28
+ /** @type {string|undefined} */
29
+ let errorBody
30
+
31
+ if (err instanceof AuthError) {
32
+ errorTitle = 'Authentication error'
33
+ errorMessage = err.message
34
+ } else if (err instanceof InputError) {
35
+ errorTitle = 'Invalid input'
36
+ errorMessage = err.message
37
+ } else if (err instanceof Error) {
38
+ errorTitle = 'Unexpected error'
39
+ errorMessage = messageWithCauses(err)
40
+ errorBody = stackWithCauses(err)
41
+ } else {
42
+ errorTitle = 'Unexpected error with no details'
43
+ }
44
+
45
+ console.error(`${logSymbols.error} ${chalk.white.bgRed(errorTitle + ':')} ${errorMessage}`)
46
+ if (errorBody) {
47
+ console.error('\n' + errorBody)
48
+ }
49
+
50
+ process.exit(1)
51
+ }
@@ -0,0 +1,2 @@
1
+ export * from './info/index.js'
2
+ export * from './report/index.js'
@@ -0,0 +1,152 @@
1
+ /* eslint-disable no-console */
2
+
3
+ import chalk from 'chalk'
4
+ import meow from 'meow'
5
+ import ora from 'ora'
6
+ import { ErrorWithCause } from 'pony-cause'
7
+
8
+ import { ChalkOrMarkdown } from '../../utils/chalk-markdown.js'
9
+ import { AuthError, InputError } from '../../utils/errors.js'
10
+ import { printFlagList } from '../../utils/formatting.js'
11
+ import { stringJoinWithSeparateFinalSeparator } from '../../utils/misc.js'
12
+ import { setupSdk } from '../../utils/sdk.js'
13
+
14
+ const description = 'Look up info regarding a package'
15
+
16
+ /** @type {import('../../utils/meow-with-subcommands').CliSubcommandRun} */
17
+ const run = async (argv, importMeta, { parentName }) => {
18
+ const name = parentName + ' info'
19
+
20
+ const cli = meow(`
21
+ Usage
22
+ $ ${name} <name>
23
+
24
+ Options
25
+ ${printFlagList({
26
+ '--debug': 'Output debug information',
27
+ '--json': 'Output result as json',
28
+ '--markdown': 'Output result as markdown',
29
+ }, 6)}
30
+
31
+ Examples
32
+ $ ${name} webtorrent
33
+ $ ${name} webtorrent@1.9.1
34
+ `, {
35
+ argv,
36
+ description,
37
+ importMeta,
38
+ flags: {
39
+ debug: {
40
+ type: 'boolean',
41
+ alias: 'd',
42
+ default: false,
43
+ },
44
+ json: {
45
+ type: 'boolean',
46
+ alias: 'j',
47
+ default: false,
48
+ },
49
+ markdown: {
50
+ type: 'boolean',
51
+ alias: 'm',
52
+ default: false,
53
+ },
54
+ }
55
+ })
56
+
57
+ const {
58
+ json: outputJson,
59
+ markdown: outputMarkdown,
60
+ } = cli.flags
61
+
62
+ if (cli.input.length > 1) {
63
+ throw new InputError('Only one package lookup supported at once')
64
+ }
65
+
66
+ const [rawPkgName = ''] = cli.input
67
+
68
+ if (!rawPkgName) {
69
+ cli.showHelp()
70
+ return
71
+ }
72
+
73
+ const versionSeparator = rawPkgName.lastIndexOf('@')
74
+
75
+ if (versionSeparator < 1) {
76
+ throw new InputError('Need to specify a full package identifier, like eg: webtorrent@1.0.0')
77
+ }
78
+
79
+ const pkgName = rawPkgName.slice(0, versionSeparator)
80
+ const pkgVersion = rawPkgName.slice(versionSeparator + 1)
81
+ console.log('sdfd', pkgName, pkgVersion)
82
+
83
+ if (!pkgVersion) {
84
+ throw new InputError('Need to specify a version, like eg: webtorrent@1.0.0')
85
+ }
86
+
87
+ const socketSdk = await setupSdk()
88
+
89
+ const spinner = ora(`Looking up data for version ${pkgVersion} of ${pkgName}`).start()
90
+
91
+ /** @type {Awaited<ReturnType<import('@socketsecurity/sdk').SocketSdk["getIssuesByNPMPackage"]>>} */
92
+ let result
93
+
94
+ try {
95
+ result = await socketSdk.getIssuesByNPMPackage(pkgName, pkgVersion)
96
+ } catch (cause) {
97
+ spinner.fail()
98
+ throw new ErrorWithCause('Failed to look up package', { cause })
99
+ }
100
+
101
+ if (result.success === false) {
102
+ if (result.status === 401 || result.status === 403) {
103
+ spinner.stop()
104
+ throw new AuthError(result.error.message)
105
+ }
106
+ spinner.fail(chalk.white.bgRed('API returned an error:') + ' ' + result.error.message)
107
+ process.exit(1)
108
+ }
109
+
110
+ const data = result.data
111
+
112
+ /** @typedef {(typeof data)[number]["value"] extends infer U | undefined ? U : never} SocketSdkIssue */
113
+ /** @type {Record<SocketSdkIssue["severity"], number>} */
114
+ const severityCount = { low: 0, middle: 0, high: 0, critical: 0 }
115
+ for (const issue of data) {
116
+ const value = issue.value
117
+
118
+ if (!value) {
119
+ continue
120
+ }
121
+
122
+ if (severityCount[value.severity] !== undefined) {
123
+ severityCount[value.severity] += 1
124
+ }
125
+ }
126
+
127
+ const issueSummary = stringJoinWithSeparateFinalSeparator([
128
+ severityCount.critical ? severityCount.critical + ' critical' : undefined,
129
+ severityCount.high ? severityCount.high + ' high' : undefined,
130
+ severityCount.middle ? severityCount.middle + ' middle' : undefined,
131
+ severityCount.low ? severityCount.low + ' low' : undefined,
132
+ ])
133
+
134
+ spinner.succeed(`Found ${issueSummary || 'no'} issues for version ${pkgVersion} of ${pkgName}`)
135
+
136
+ if (outputJson) {
137
+ console.log(JSON.stringify(data, undefined, 2))
138
+ return
139
+ }
140
+
141
+ const format = new ChalkOrMarkdown(!!outputMarkdown)
142
+ const url = `https://socket.dev/npm/package/${pkgName}/overview/${pkgVersion}`
143
+
144
+ console.log('\nDetailed info on socket.dev: ' + format.hyperlink(`${pkgName} v${pkgVersion}`, url, { fallbackToUrl: true }))
145
+
146
+ if (!outputMarkdown) {
147
+ console.log(chalk.dim('\nOr rerun', chalk.italic(name), 'using the', chalk.italic('--json'), 'flag to get full JSON output'))
148
+ }
149
+ }
150
+
151
+ /** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */
152
+ export const info = { description, run }
@@ -0,0 +1,235 @@
1
+ /* eslint-disable no-console */
2
+
3
+ import { stat } from 'node:fs/promises'
4
+ import path from 'node:path'
5
+
6
+ import chalk from 'chalk'
7
+ import meow from 'meow'
8
+ import ora from 'ora'
9
+ import { ErrorWithCause } from 'pony-cause'
10
+
11
+ import { ChalkOrMarkdown, logSymbols } from '../../utils/chalk-markdown.js'
12
+ import { AuthError, InputError } from '../../utils/errors.js'
13
+ import { printFlagList } from '../../utils/formatting.js'
14
+ import { createDebugLogger } from '../../utils/misc.js'
15
+ import { setupSdk } from '../../utils/sdk.js'
16
+ import { isErrnoException } from '../../utils/type-helpers.js'
17
+
18
+ const description = 'Create a project report'
19
+
20
+ /** @type {import('../../utils/meow-with-subcommands').CliSubcommandRun} */
21
+ const run = async (argv, importMeta, { parentName }) => {
22
+ const name = parentName + ' create'
23
+
24
+ const cli = meow(`
25
+ Usage
26
+ $ ${name} <paths-to-package-folders-and-files>
27
+
28
+ Options
29
+ ${printFlagList({
30
+ '--debug': 'Output debug information',
31
+ '--dry-run': 'Only output what will be done without actually doing it',
32
+ '--json': 'Output result as json',
33
+ '--markdown': 'Output result as markdown',
34
+ }, 6)}
35
+
36
+ Examples
37
+ $ ${name} .
38
+ $ ${name} ../package-lock.json
39
+ $ ${name} /path/to/a/package.json /path/to/another/package.json
40
+ `, {
41
+ argv,
42
+ description,
43
+ importMeta,
44
+ flags: {
45
+ debug: {
46
+ type: 'boolean',
47
+ alias: 'd',
48
+ default: false,
49
+ },
50
+ dryRun: {
51
+ type: 'boolean',
52
+ default: false,
53
+ },
54
+ json: {
55
+ type: 'boolean',
56
+ alias: 'j',
57
+ default: false,
58
+ },
59
+ markdown: {
60
+ type: 'boolean',
61
+ alias: 'm',
62
+ default: false,
63
+ },
64
+ }
65
+ })
66
+
67
+ const {
68
+ dryRun,
69
+ json: outputJson,
70
+ markdown: outputMarkdown,
71
+ } = cli.flags
72
+
73
+ if (!cli.input[0]) {
74
+ cli.showHelp()
75
+ return
76
+ }
77
+
78
+ const debugLog = createDebugLogger(dryRun || cli.flags.debug)
79
+
80
+ const cwd = process.cwd()
81
+ const packagePaths = await resolvePackagePaths(cwd, cli.input)
82
+
83
+ debugLog(`${logSymbols.info} Uploading:`, packagePaths.join(`\n${logSymbols.info} Uploading:`))
84
+
85
+ if (dryRun) {
86
+ return
87
+ }
88
+
89
+ const socketSdk = await setupSdk()
90
+
91
+ const spinner = ora(`Creating report with ${packagePaths.length} package files`).start()
92
+
93
+ /** @type {Awaited<ReturnType<typeof socketSdk.createReportFromFilePaths>>} */
94
+ let result
95
+
96
+ try {
97
+ result = await socketSdk.createReportFromFilePaths(packagePaths, cwd)
98
+ } catch (cause) {
99
+ spinner.fail()
100
+ throw new ErrorWithCause('Failed creating report', { cause })
101
+ }
102
+
103
+ if (result.success === false) {
104
+ if (result.status === 401 || result.status === 403) {
105
+ spinner.stop()
106
+ throw new AuthError(result.error.message)
107
+ }
108
+ spinner.fail(chalk.white.bgRed('API returned an error:') + ' ' + result.error.message)
109
+ process.exit(1)
110
+ }
111
+
112
+ spinner.succeed()
113
+
114
+ if (outputJson) {
115
+ console.log(JSON.stringify(result.data, undefined, 2))
116
+ return
117
+ }
118
+
119
+ const format = new ChalkOrMarkdown(!!outputMarkdown)
120
+
121
+ console.log('\nNew report: ' + format.hyperlink(result.data.id, result.data.url, { fallbackToUrl: true }))
122
+ }
123
+
124
+ /** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */
125
+ export const create = { description, run }
126
+
127
+ // TODO: Add globbing support with support for ignoring, as a "./**/package.json" in a project also traverses eg. node_modules
128
+ /**
129
+ * Takes paths to folders and/or package.json / package-lock.json files and resolves to package.json + package-lock.json pairs (where feasible)
130
+ *
131
+ * @param {string} cwd
132
+ * @param {string[]} inputPaths
133
+ * @returns {Promise<string[]>}
134
+ * @throws {InputError}
135
+ */
136
+ async function resolvePackagePaths (cwd, inputPaths) {
137
+ const packagePathLookups = inputPaths.map(async (filePath) => {
138
+ const packagePath = await resolvePackagePath(cwd, filePath)
139
+ return findComplementaryPackageFile(packagePath)
140
+ })
141
+
142
+ const packagePaths = await Promise.all(packagePathLookups)
143
+
144
+ const uniquePackagePaths = new Set(packagePaths.flat())
145
+
146
+ return [...uniquePackagePaths]
147
+ }
148
+
149
+ /**
150
+ * Resolves a package.json / package-lock.json path from a relative folder / file path
151
+ *
152
+ * @param {string} cwd
153
+ * @param {string} inputPath
154
+ * @returns {Promise<string>}
155
+ * @throws {InputError}
156
+ */
157
+ async function resolvePackagePath (cwd, inputPath) {
158
+ const filePath = path.resolve(cwd, inputPath)
159
+ /** @type {string|undefined} */
160
+ let filePathAppended
161
+
162
+ try {
163
+ const fileStat = await stat(filePath)
164
+
165
+ if (fileStat.isDirectory()) {
166
+ filePathAppended = path.resolve(filePath, 'package.json')
167
+ }
168
+ } catch (err) {
169
+ if (isErrnoException(err) && err.code === 'ENOENT') {
170
+ throw new InputError(`Expected '${inputPath}' to point to an existing file or directory`)
171
+ }
172
+ throw new ErrorWithCause('Failed to resolve path to package.json', { cause: err })
173
+ }
174
+
175
+ if (filePathAppended) {
176
+ /** @type {import('node:fs').Stats} */
177
+ let filePathAppendedStat
178
+
179
+ try {
180
+ filePathAppendedStat = await stat(filePathAppended)
181
+ } catch (err) {
182
+ if (isErrnoException(err) && err.code === 'ENOENT') {
183
+ throw new InputError(`Expected directory '${inputPath}' to contain a package.json file`)
184
+ }
185
+ throw new ErrorWithCause('Failed to resolve package.json in directory', { cause: err })
186
+ }
187
+
188
+ if (!filePathAppendedStat.isFile()) {
189
+ throw new InputError(`Expected '${filePathAppended}' to be a file`)
190
+ }
191
+
192
+ return filePathAppended
193
+ }
194
+
195
+ return filePath
196
+ }
197
+
198
+ /**
199
+ * Finds any complementary file to a package.json or package-lock.json
200
+ *
201
+ * @param {string} packagePath
202
+ * @returns {Promise<string[]>}
203
+ * @throws {InputError}
204
+ */
205
+ async function findComplementaryPackageFile (packagePath) {
206
+ const basename = path.basename(packagePath)
207
+ const dirname = path.dirname(packagePath)
208
+
209
+ if (basename === 'package-lock.json') {
210
+ // We need the package file as well
211
+ return [
212
+ packagePath,
213
+ path.resolve(dirname, 'package.json')
214
+ ]
215
+ }
216
+
217
+ if (basename === 'package.json') {
218
+ const lockfilePath = path.resolve(dirname, 'package-lock.json')
219
+ try {
220
+ const lockfileStat = await stat(lockfilePath)
221
+ if (lockfileStat.isFile()) {
222
+ return [packagePath, lockfilePath]
223
+ }
224
+ } catch (err) {
225
+ if (isErrnoException(err) && err.code === 'ENOENT') {
226
+ return [packagePath]
227
+ }
228
+ throw new ErrorWithCause(`Unexpected error when finding a lockfile for '${packagePath}'`, { cause: err })
229
+ }
230
+
231
+ throw new InputError(`Encountered a non-file at lockfile path '${lockfilePath}'`)
232
+ }
233
+
234
+ throw new InputError(`Expected '${packagePath}' to point to a package.json or package-lock.json or to a folder containing a package.json`)
235
+ }
@@ -0,0 +1,22 @@
1
+ import { meowWithSubcommands } from '../../utils/meow-with-subcommands.js'
2
+ import { create } from './create.js'
3
+
4
+ const description = 'Project report related commands'
5
+
6
+ /** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */
7
+ export const report = {
8
+ description,
9
+ run: async (argv, importMeta, { parentName }) => {
10
+ await meowWithSubcommands(
11
+ {
12
+ create,
13
+ },
14
+ {
15
+ argv,
16
+ description,
17
+ importMeta,
18
+ name: parentName + ' report',
19
+ }
20
+ )
21
+ }
22
+ }
@@ -0,0 +1,125 @@
1
+ import chalk from 'chalk'
2
+ import isUnicodeSupported from 'is-unicode-supported'
3
+ import terminalLink from 'terminal-link'
4
+
5
+ // From the 'log-symbols' module
6
+ const unicodeLogSymbols = {
7
+ info: chalk.blue('ℹ'),
8
+ success: chalk.green('✔'),
9
+ warning: chalk.yellow('⚠'),
10
+ error: chalk.red('✖'),
11
+ }
12
+
13
+ // From the 'log-symbols' module
14
+ const fallbackLogSymbols = {
15
+ info: chalk.blue('i'),
16
+ success: chalk.green('√'),
17
+ warning: chalk.yellow('‼'),
18
+ error: chalk.red('×'),
19
+ }
20
+
21
+ // From the 'log-symbols' module
22
+ export const logSymbols = isUnicodeSupported() ? unicodeLogSymbols : fallbackLogSymbols
23
+
24
+ const markdownLogSymbols = {
25
+ info: ':information_source:',
26
+ error: ':stop_sign:',
27
+ success: ':white_check_mark:',
28
+ warning: ':warning:',
29
+ }
30
+
31
+ export class ChalkOrMarkdown {
32
+ /** @type {boolean} */
33
+ useMarkdown
34
+
35
+ /**
36
+ * @param {boolean} useMarkdown
37
+ */
38
+ constructor (useMarkdown) {
39
+ this.useMarkdown = !!useMarkdown
40
+ }
41
+
42
+ /**
43
+ * @param {string} text
44
+ * @param {number} [level]
45
+ * @returns {string}
46
+ */
47
+ header (text, level = 1) {
48
+ return this.useMarkdown
49
+ ? `\n${''.padStart(level, '#')} ${text}\n`
50
+ : chalk.underline(`\n${level === 1 ? chalk.bold(text) : text}\n`)
51
+ }
52
+
53
+ /**
54
+ * @param {string} text
55
+ * @returns {string}
56
+ */
57
+ bold (text) {
58
+ return this.useMarkdown
59
+ ? `**${text}**`
60
+ : chalk.bold(`${text}`)
61
+ }
62
+
63
+ /**
64
+ * @param {string} text
65
+ * @returns {string}
66
+ */
67
+ italic (text) {
68
+ return this.useMarkdown
69
+ ? `_${text}_`
70
+ : chalk.italic(`${text}`)
71
+ }
72
+
73
+ /**
74
+ * @param {string} text
75
+ * @param {string|undefined} url
76
+ * @param {{ fallback?: boolean, fallbackToUrl?: boolean }} options
77
+ * @returns {string}
78
+ */
79
+ hyperlink (text, url, { fallback = true, fallbackToUrl } = {}) {
80
+ if (!url) return text
81
+ return this.useMarkdown
82
+ ? `[${text}](${url})`
83
+ : terminalLink(text, url, {
84
+ fallback: fallbackToUrl ? (_text, url) => url : fallback
85
+ })
86
+ }
87
+
88
+ /**
89
+ * @param {string[]} items
90
+ * @returns {string}
91
+ */
92
+ list (items) {
93
+ const indentedContent = items.map(item => this.indent(item).trimStart())
94
+ return this.useMarkdown
95
+ ? '* ' + indentedContent.join('\n* ') + '\n'
96
+ : indentedContent.join('\n') + '\n'
97
+ }
98
+
99
+ /**
100
+ * @returns {typeof logSymbols}
101
+ */
102
+ get logSymbols () {
103
+ return this.useMarkdown ? markdownLogSymbols : logSymbols
104
+ }
105
+
106
+ /**
107
+ * @param {string} text
108
+ * @param {number} [level]
109
+ * @returns {string}
110
+ */
111
+ indent (text, level = 1) {
112
+ const indent = ''.padStart(level * 2, ' ')
113
+ return indent + text.split('\n').join('\n' + indent)
114
+ }
115
+
116
+ /**
117
+ * @param {unknown} value
118
+ * @returns {string}
119
+ */
120
+ json (value) {
121
+ return this.useMarkdown
122
+ ? '```json\n' + JSON.stringify(value) + '\n```'
123
+ : JSON.stringify(value)
124
+ }
125
+ }
@@ -0,0 +1,2 @@
1
+ export class AuthError extends Error {}
2
+ export class InputError extends Error {}
@@ -0,0 +1,36 @@
1
+ /** @typedef {string|{ description: string }} ListDescription */
2
+
3
+ /**
4
+ * @param {Record<string,ListDescription>} list
5
+ * @param {number} indent
6
+ * @param {number} padName
7
+ * @returns {string}
8
+ */
9
+ export function printHelpList (list, indent, padName = 18) {
10
+ const names = Object.keys(list).sort()
11
+
12
+ let result = ''
13
+
14
+ for (const name of names) {
15
+ const rawDescription = list[name]
16
+ const description = (typeof rawDescription === 'object' ? rawDescription.description : rawDescription) || ''
17
+
18
+ result += ''.padEnd(indent) + name.padEnd(padName) + description + '\n'
19
+ }
20
+
21
+ return result.trim()
22
+ }
23
+
24
+ /**
25
+ * @param {Record<string,ListDescription>} list
26
+ * @param {number} indent
27
+ * @param {number} padName
28
+ * @returns {string}
29
+ */
30
+ export function printFlagList (list, indent, padName = 18) {
31
+ return printHelpList({
32
+ '--help': 'Print this help and exits.',
33
+ '--version': 'Prints current version and exits.',
34
+ ...list,
35
+ }, indent, padName)
36
+ }
@@ -0,0 +1,69 @@
1
+ import meow from 'meow'
2
+
3
+ import { printFlagList, printHelpList } from './formatting.js'
4
+ import { ensureIsKeyOf } from './type-helpers.js'
5
+
6
+ /**
7
+ * @callback CliSubcommandRun
8
+ * @param {readonly string[]} argv
9
+ * @param {ImportMeta} importMeta
10
+ * @param {{ parentName: string }} context
11
+ * @returns {Promise<void>|void}
12
+ */
13
+
14
+ /**
15
+ * @typedef CliSubcommand
16
+ * @property {string} description
17
+ * @property {CliSubcommandRun} run
18
+ */
19
+
20
+ /**
21
+ * @template {import('meow').AnyFlags} Flags
22
+ * @param {Record<string, CliSubcommand>} subcommands
23
+ * @param {import('meow').Options<Flags> & { argv: readonly string[], name: string }} options
24
+ * @returns {Promise<void>}
25
+ */
26
+ export async function meowWithSubcommands (subcommands, options) {
27
+ const {
28
+ argv,
29
+ name,
30
+ importMeta,
31
+ ...additionalOptions
32
+ } = options
33
+ const [rawCommandName, ...commandArgv] = argv
34
+
35
+ const commandName = ensureIsKeyOf(subcommands, rawCommandName)
36
+ const command = commandName ? subcommands[commandName] : undefined
37
+
38
+ // If a valid command has been specified, run it...
39
+ if (command) {
40
+ return await command.run(
41
+ commandArgv,
42
+ importMeta,
43
+ {
44
+ parentName: name
45
+ }
46
+ )
47
+ }
48
+
49
+ // ...else provide basic instructions and help
50
+ const cli = meow(`
51
+ Usage
52
+ $ ${name} <command>
53
+
54
+ Commands
55
+ ${printHelpList(subcommands, 6)}
56
+
57
+ Options
58
+ ${printFlagList({}, 6)}
59
+
60
+ Examples
61
+ $ ${name} --help
62
+ `, {
63
+ argv,
64
+ importMeta,
65
+ ...additionalOptions,
66
+ })
67
+
68
+ cli.showHelp()
69
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @param {boolean|undefined} printDebugLogs
3
+ * @returns {typeof console.error}
4
+ */
5
+ export function createDebugLogger (printDebugLogs) {
6
+ if (printDebugLogs) {
7
+ // eslint-disable-next-line no-console
8
+ return console.error.bind(console)
9
+ }
10
+ return () => {}
11
+ }
12
+
13
+ /**
14
+ * @param {(string|undefined)[]} list
15
+ * @param {string} separator
16
+ * @returns {string}
17
+ */
18
+ export function stringJoinWithSeparateFinalSeparator (list, separator = ' and ') {
19
+ const values = list.filter(value => !!value)
20
+
21
+ if (values.length < 2) {
22
+ return values[0] || ''
23
+ }
24
+
25
+ const finalValue = values.pop()
26
+
27
+ return values.join(', ') + separator + finalValue
28
+ }
@@ -0,0 +1,45 @@
1
+ import { SocketSdk } from '@socketsecurity/sdk'
2
+ import isInteractive from 'is-interactive'
3
+ import prompts from 'prompts'
4
+
5
+ import { AuthError } from './errors.js'
6
+
7
+ /**
8
+ * @returns {Promise<import('@socketsecurity/sdk').SocketSdk>}
9
+ */
10
+ export async function setupSdk () {
11
+ let apiKey = process.env['SOCKET_SECURITY_API_KEY']
12
+
13
+ if (!apiKey && isInteractive()) {
14
+ const input = await prompts({
15
+ type: 'password',
16
+ name: 'apiKey',
17
+ message: 'Enter your Socket.dev API key',
18
+ })
19
+
20
+ apiKey = input.apiKey
21
+ }
22
+
23
+ if (!apiKey) {
24
+ throw new AuthError('You need to provide an API key')
25
+ }
26
+
27
+ /** @type {import('@socketsecurity/sdk').SocketSdkOptions["agent"]} */
28
+ let agent
29
+
30
+ if (process.env['SOCKET_SECURITY_API_PROXY']) {
31
+ const { HttpProxyAgent, HttpsProxyAgent } = await import('hpagent')
32
+ agent = {
33
+ http: new HttpProxyAgent({ proxy: process.env['SOCKET_SECURITY_API_PROXY'] }),
34
+ https: new HttpsProxyAgent({ proxy: process.env['SOCKET_SECURITY_API_PROXY'] }),
35
+ }
36
+ }
37
+
38
+ /** @type {import('@socketsecurity/sdk').SocketSdkOptions} */
39
+ const sdkOptions = {
40
+ agent,
41
+ baseUrl: process.env['SOCKET_SECURITY_API_BASE_URL'],
42
+ }
43
+
44
+ return new SocketSdk(apiKey || '', sdkOptions)
45
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @template T
3
+ * @param {T} obj
4
+ * @param {string|undefined} key
5
+ * @returns {(keyof T) | undefined}
6
+ */
7
+ export function ensureIsKeyOf (obj, key) {
8
+ return /** @type {keyof T} */ (key && Object.prototype.hasOwnProperty.call(obj, key) ? key : undefined)
9
+ }
10
+
11
+ /**
12
+ * @param {unknown} value
13
+ * @returns {value is NodeJS.ErrnoException}
14
+ */
15
+ export function isErrnoException (value) {
16
+ if (!(value instanceof Error)) {
17
+ return false
18
+ }
19
+
20
+ const errnoException = /** @type NodeJS.ErrnoException} */ (value)
21
+
22
+ return errnoException.code !== undefined
23
+ }
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@socketsecurity/cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool for Socket.dev",
5
+ "homepage": "http://github.com/SocketDev/socket-commando",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git://github.com/SocketDev/socket-commando.git"
9
+ },
10
+ "keywords": [],
11
+ "author": {
12
+ "name": "Socket Inc",
13
+ "email": "eng@socket.dev",
14
+ "url": "https://socket.dev"
15
+ },
16
+ "license": "MIT",
17
+ "engines": {
18
+ "node": "^14.18.0 || >=16.0.0"
19
+ },
20
+ "type": "module",
21
+ "bin": {
22
+ "socket": "cli.js"
23
+ },
24
+ "files": [
25
+ "cli.js",
26
+ "lib/**/*.js"
27
+ ],
28
+ "scripts": {
29
+ "check:dependency-check": "dependency-check '*.js' 'test/**/*.js' --no-dev",
30
+ "check:installed-check": "installed-check -i eslint-plugin-jsdoc",
31
+ "check:lint": "eslint --report-unused-disable-directives .",
32
+ "check:tsc": "tsc",
33
+ "check:type-coverage": "type-coverage --detail --strict --at-least 95 --ignore-files 'test/*'",
34
+ "check": "run-p -c --aggregate-output check:*",
35
+ "generate-types": "node lib/utils/generate-types.js > lib/types/api.d.ts",
36
+ "prepare": "husky install",
37
+ "test:mocha": "c8 --reporter=lcov --reporter text mocha 'test/**/*.spec.js'",
38
+ "test-ci": "run-s test:*",
39
+ "test": "run-s check test:*"
40
+ },
41
+ "devDependencies": {
42
+ "@socketsecurity/eslint-config": "^1.0.0",
43
+ "@tsconfig/node14": "^1.0.3",
44
+ "@types/chai": "^4.3.3",
45
+ "@types/mocha": "^10.0.0",
46
+ "@types/node": "^14.18.31",
47
+ "@types/prompts": "^2.4.1",
48
+ "@typescript-eslint/eslint-plugin": "^5.36.2",
49
+ "@typescript-eslint/parser": "^5.36.2",
50
+ "c8": "^7.12.0",
51
+ "chai": "^4.3.6",
52
+ "dependency-check": "^5.0.0-7",
53
+ "eslint": "^8.23.0",
54
+ "eslint-config-standard": "^17.0.0",
55
+ "eslint-config-standard-jsx": "^11.0.0",
56
+ "eslint-import-resolver-typescript": "^3.5.1",
57
+ "eslint-plugin-import": "^2.26.0",
58
+ "eslint-plugin-jsdoc": "^39.5.0",
59
+ "eslint-plugin-n": "^15.3.0",
60
+ "eslint-plugin-promise": "^6.0.1",
61
+ "eslint-plugin-react": "^7.31.9",
62
+ "eslint-plugin-react-hooks": "^4.6.0",
63
+ "husky": "^8.0.1",
64
+ "installed-check": "^6.0.4",
65
+ "mocha": "^10.0.0",
66
+ "npm-run-all2": "^6.0.2",
67
+ "type-coverage": "^2.21.2",
68
+ "typescript": "~4.8.4"
69
+ },
70
+ "dependencies": {
71
+ "@socketsecurity/sdk": "^0.3.1",
72
+ "chalk": "^5.1.2",
73
+ "hpagent": "^1.2.0",
74
+ "is-interactive": "^2.0.0",
75
+ "is-unicode-supported": "^1.3.0",
76
+ "meow": "^11.0.0",
77
+ "ora": "^6.1.2",
78
+ "pony-cause": "^2.1.4",
79
+ "prompts": "^2.4.2",
80
+ "terminal-link": "^3.0.0"
81
+ }
82
+ }