@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 +21 -0
- package/README.md +14 -0
- package/cli.js +51 -0
- package/lib/commands/index.js +2 -0
- package/lib/commands/info/index.js +152 -0
- package/lib/commands/report/create.js +235 -0
- package/lib/commands/report/index.js +22 -0
- package/lib/utils/chalk-markdown.js +125 -0
- package/lib/utils/errors.js +2 -0
- package/lib/utils/formatting.js +36 -0
- package/lib/utils/meow-with-subcommands.js +69 -0
- package/lib/utils/misc.js +28 -0
- package/lib/utils/sdk.js +45 -0
- package/lib/utils/type-helpers.js +23 -0
- package/package.json +82 -0
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,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,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
|
+
}
|
package/lib/utils/sdk.js
ADDED
|
@@ -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
|
+
}
|