@socketsecurity/cli 0.1.2 → 0.2.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/README.md +13 -5
- package/lib/commands/info/index.js +56 -56
- package/lib/commands/report/create.js +86 -28
- package/lib/commands/report/index.js +2 -0
- package/lib/commands/report/view.js +146 -0
- package/lib/utils/api-helpers.js +43 -0
- package/lib/utils/format-issues.js +44 -0
- package/package.json +13 -13
package/README.md
CHANGED
|
@@ -15,19 +15,21 @@ npm install -g @socketsecurity/cli
|
|
|
15
15
|
```bash
|
|
16
16
|
socket --help
|
|
17
17
|
socket info webtorrent@1.9.1
|
|
18
|
-
socket report create package.json
|
|
18
|
+
socket report create package.json --view
|
|
19
|
+
socket report view QXU8PmK7LfH608RAwfIKdbcHgwEd_ZeWJ9QEGv05FJUQ
|
|
19
20
|
```
|
|
20
21
|
|
|
21
22
|
## Commands
|
|
22
23
|
|
|
23
24
|
* `socket info <package@version>` - looks up issues for a package
|
|
24
|
-
* `socket report create
|
|
25
|
+
* `socket report create <path(s)-to-folder-or-file>` - uploads the specified `package.json` and/or `package-lock.json` to create a report on [socket.dev](https://socket.dev/). If only one of a `package.json`/`package-lock.json` has been specified, the other will be automatically found and uploaded if it exists
|
|
26
|
+
* `socket report view <report-id>` - looks up issues and scores from a report
|
|
25
27
|
|
|
26
28
|
## Flags
|
|
27
29
|
|
|
28
|
-
###
|
|
30
|
+
### Command specific flags
|
|
29
31
|
|
|
30
|
-
* `--
|
|
32
|
+
* `--view` - when set on `socket report create` the command will immediately do a `socket report view` style view of the created report, waiting for the server to complete it
|
|
31
33
|
|
|
32
34
|
### Output flags
|
|
33
35
|
|
|
@@ -36,6 +38,7 @@ socket report create package.json
|
|
|
36
38
|
|
|
37
39
|
### Other flags
|
|
38
40
|
|
|
41
|
+
* `--dry-run` - like all CLI tools that perform an action should have, we have a dry run flag. Eg. `socket report create` supports running the command without actually uploading anything
|
|
39
42
|
* `--debug` - outputs additional debug output. Great for debugging, geeks and us who develop. Hopefully you will never _need_ it, but it can still be fun, right?
|
|
40
43
|
* `--help` - prints the help for the current command. All CLI tools should have this flag
|
|
41
44
|
* `--version` - prints the version of the tool. All CLI tools should have this flag
|
|
@@ -45,13 +48,18 @@ socket report create package.json
|
|
|
45
48
|
* `SOCKET_SECURITY_API_KEY` - if set, this will be used as the API-key
|
|
46
49
|
|
|
47
50
|
## Contributing
|
|
51
|
+
|
|
48
52
|
### Environment variables for development
|
|
49
53
|
|
|
50
54
|
* `SOCKET_SECURITY_API_BASE_URL` - if set, this will be the base for all API-calls. Defaults to `https://api.socket.dev/v0/`
|
|
51
55
|
* `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
|
|
52
56
|
|
|
57
|
+
## Similar projects
|
|
58
|
+
|
|
59
|
+
* [`@socketsecurity/sdk`](https://github.com/SocketDev/socket-sdk-js) - the SDK used in this CLI
|
|
60
|
+
|
|
53
61
|
## See also
|
|
54
62
|
|
|
55
|
-
* [
|
|
63
|
+
* [Announcement blog post](https://socket.dev/blog/announcing-socket-cli-preview)
|
|
56
64
|
* [Socket API Reference](https://docs.socket.dev/reference) - the API used in this CLI
|
|
57
65
|
* [Socket GitHub App](https://github.com/apps/socket-security) - the plug-and-play GitHub App
|
|
@@ -3,27 +3,45 @@
|
|
|
3
3
|
import chalk from 'chalk'
|
|
4
4
|
import meow from 'meow'
|
|
5
5
|
import ora from 'ora'
|
|
6
|
-
import { ErrorWithCause } from 'pony-cause'
|
|
7
6
|
|
|
7
|
+
import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js'
|
|
8
8
|
import { ChalkOrMarkdown } from '../../utils/chalk-markdown.js'
|
|
9
|
-
import {
|
|
9
|
+
import { InputError } from '../../utils/errors.js'
|
|
10
|
+
import { getSeveritySummary } from '../../utils/format-issues.js'
|
|
10
11
|
import { printFlagList } from '../../utils/formatting.js'
|
|
11
|
-
import { stringJoinWithSeparateFinalSeparator } from '../../utils/misc.js'
|
|
12
12
|
import { setupSdk } from '../../utils/sdk.js'
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
/** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */
|
|
15
|
+
export const info = {
|
|
16
|
+
description: 'Look up info regarding a package',
|
|
17
|
+
async run (argv, importMeta, { parentName }) {
|
|
18
|
+
const name = parentName + ' info'
|
|
19
|
+
|
|
20
|
+
const input = setupCommand(name, info.description, argv, importMeta)
|
|
21
|
+
const result = input && await fetchPackageData(input.pkgName, input.pkgVersion)
|
|
22
|
+
|
|
23
|
+
if (result) {
|
|
24
|
+
formatPackageDataOutput(result.data, { name, ...input })
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
15
28
|
|
|
16
|
-
|
|
17
|
-
const run = async (argv, importMeta, { parentName }) => {
|
|
18
|
-
const name = parentName + ' info'
|
|
29
|
+
// Internal functions
|
|
19
30
|
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} name
|
|
33
|
+
* @param {string} description
|
|
34
|
+
* @param {readonly string[]} argv
|
|
35
|
+
* @param {ImportMeta} importMeta
|
|
36
|
+
* @returns {void|{ outputJson: boolean, outputMarkdown: boolean, pkgName: string, pkgVersion: string }}
|
|
37
|
+
*/
|
|
38
|
+
function setupCommand (name, description, argv, importMeta) {
|
|
20
39
|
const cli = meow(`
|
|
21
40
|
Usage
|
|
22
41
|
$ ${name} <name>
|
|
23
42
|
|
|
24
43
|
Options
|
|
25
44
|
${printFlagList({
|
|
26
|
-
'--debug': 'Output debug information',
|
|
27
45
|
'--json': 'Output result as json',
|
|
28
46
|
'--markdown': 'Output result as markdown',
|
|
29
47
|
}, 6)}
|
|
@@ -36,11 +54,6 @@ const run = async (argv, importMeta, { parentName }) => {
|
|
|
36
54
|
description,
|
|
37
55
|
importMeta,
|
|
38
56
|
flags: {
|
|
39
|
-
debug: {
|
|
40
|
-
type: 'boolean',
|
|
41
|
-
alias: 'd',
|
|
42
|
-
default: false,
|
|
43
|
-
},
|
|
44
57
|
json: {
|
|
45
58
|
type: 'boolean',
|
|
46
59
|
alias: 'j',
|
|
@@ -83,69 +96,56 @@ const run = async (argv, importMeta, { parentName }) => {
|
|
|
83
96
|
throw new InputError('Need to specify a version, like eg: webtorrent@1.0.0')
|
|
84
97
|
}
|
|
85
98
|
|
|
86
|
-
|
|
99
|
+
return {
|
|
100
|
+
outputJson,
|
|
101
|
+
outputMarkdown,
|
|
102
|
+
pkgName,
|
|
103
|
+
pkgVersion
|
|
104
|
+
}
|
|
105
|
+
}
|
|
87
106
|
|
|
107
|
+
/**
|
|
108
|
+
* @param {string} pkgName
|
|
109
|
+
* @param {string} pkgVersion
|
|
110
|
+
* @returns {Promise<void|import('@socketsecurity/sdk').SocketSdkReturnType<'getIssuesByNPMPackage'>>}
|
|
111
|
+
*/
|
|
112
|
+
async function fetchPackageData (pkgName, pkgVersion) {
|
|
113
|
+
const socketSdk = await setupSdk()
|
|
88
114
|
const spinner = ora(`Looking up data for version ${pkgVersion} of ${pkgName}`).start()
|
|
89
|
-
|
|
90
|
-
/** @type {Awaited<ReturnType<import('@socketsecurity/sdk').SocketSdk["getIssuesByNPMPackage"]>>} */
|
|
91
|
-
let result
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
result = await socketSdk.getIssuesByNPMPackage(pkgName, pkgVersion)
|
|
95
|
-
} catch (cause) {
|
|
96
|
-
spinner.fail()
|
|
97
|
-
throw new ErrorWithCause('Failed to look up package', { cause })
|
|
98
|
-
}
|
|
115
|
+
const result = await handleApiCall(socketSdk.getIssuesByNPMPackage(pkgName, pkgVersion), spinner, 'looking up package')
|
|
99
116
|
|
|
100
117
|
if (result.success === false) {
|
|
101
|
-
|
|
102
|
-
spinner.stop()
|
|
103
|
-
throw new AuthError(result.error.message)
|
|
104
|
-
}
|
|
105
|
-
spinner.fail(chalk.white.bgRed('API returned an error:') + ' ' + result.error.message)
|
|
106
|
-
process.exit(1)
|
|
118
|
+
return handleUnsuccessfulApiResponse(result, spinner)
|
|
107
119
|
}
|
|
108
120
|
|
|
109
|
-
|
|
121
|
+
// Conclude the status of the API call
|
|
110
122
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const severityCount = { low: 0, middle: 0, high: 0, critical: 0 }
|
|
114
|
-
for (const issue of data) {
|
|
115
|
-
const value = issue.value
|
|
116
|
-
|
|
117
|
-
if (!value) {
|
|
118
|
-
continue
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (severityCount[value.severity] !== undefined) {
|
|
122
|
-
severityCount[value.severity] += 1
|
|
123
|
-
}
|
|
124
|
-
}
|
|
123
|
+
const issueSummary = getSeveritySummary(result.data)
|
|
124
|
+
spinner.succeed(`Found ${issueSummary || 'no'} issues for version ${pkgVersion} of ${pkgName}`)
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
severityCount.high ? severityCount.high + ' high' : undefined,
|
|
129
|
-
severityCount.middle ? severityCount.middle + ' middle' : undefined,
|
|
130
|
-
severityCount.low ? severityCount.low + ' low' : undefined,
|
|
131
|
-
])
|
|
126
|
+
return result
|
|
127
|
+
}
|
|
132
128
|
|
|
133
|
-
|
|
129
|
+
/**
|
|
130
|
+
* @param {import('@socketsecurity/sdk').SocketSdkReturnType<'getIssuesByNPMPackage'>["data"]} data
|
|
131
|
+
* @param {{ name: string, outputJson: boolean, outputMarkdown: boolean, pkgName: string, pkgVersion: string }} context
|
|
132
|
+
* @returns {void}
|
|
133
|
+
*/
|
|
134
|
+
function formatPackageDataOutput (data, { name, outputJson, outputMarkdown, pkgName, pkgVersion }) {
|
|
135
|
+
// If JSON, output and return...
|
|
134
136
|
|
|
135
137
|
if (outputJson) {
|
|
136
138
|
console.log(JSON.stringify(data, undefined, 2))
|
|
137
139
|
return
|
|
138
140
|
}
|
|
139
141
|
|
|
142
|
+
// ...else do the CLI / Markdown output dance
|
|
143
|
+
|
|
140
144
|
const format = new ChalkOrMarkdown(!!outputMarkdown)
|
|
141
145
|
const url = `https://socket.dev/npm/package/${pkgName}/overview/${pkgVersion}`
|
|
142
146
|
|
|
143
147
|
console.log('\nDetailed info on socket.dev: ' + format.hyperlink(`${pkgName} v${pkgVersion}`, url, { fallbackToUrl: true }))
|
|
144
|
-
|
|
145
148
|
if (!outputMarkdown) {
|
|
146
149
|
console.log(chalk.dim('\nOr rerun', chalk.italic(name), 'using the', chalk.italic('--json'), 'flag to get full JSON output'))
|
|
147
150
|
}
|
|
148
151
|
}
|
|
149
|
-
|
|
150
|
-
/** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */
|
|
151
|
-
export const info = { description, run }
|
|
@@ -3,24 +3,64 @@
|
|
|
3
3
|
import { stat } from 'node:fs/promises'
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
|
|
6
|
-
import chalk from 'chalk'
|
|
7
6
|
import meow from 'meow'
|
|
8
7
|
import ora from 'ora'
|
|
9
8
|
import { ErrorWithCause } from 'pony-cause'
|
|
10
9
|
|
|
10
|
+
import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js'
|
|
11
11
|
import { ChalkOrMarkdown, logSymbols } from '../../utils/chalk-markdown.js'
|
|
12
|
-
import {
|
|
12
|
+
import { InputError } from '../../utils/errors.js'
|
|
13
13
|
import { printFlagList } from '../../utils/formatting.js'
|
|
14
14
|
import { createDebugLogger } from '../../utils/misc.js'
|
|
15
15
|
import { setupSdk } from '../../utils/sdk.js'
|
|
16
16
|
import { isErrnoException } from '../../utils/type-helpers.js'
|
|
17
|
+
import { fetchReportData, formatReportDataOutput } from './view.js'
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
/** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */
|
|
20
|
+
export const create = {
|
|
21
|
+
description: 'Create a project report',
|
|
22
|
+
async run (argv, importMeta, { parentName }) {
|
|
23
|
+
const name = parentName + ' create'
|
|
24
|
+
|
|
25
|
+
const input = await setupCommand(name, create.description, argv, importMeta)
|
|
26
|
+
|
|
27
|
+
if (input) {
|
|
28
|
+
const {
|
|
29
|
+
cwd,
|
|
30
|
+
debugLog,
|
|
31
|
+
dryRun,
|
|
32
|
+
outputJson,
|
|
33
|
+
outputMarkdown,
|
|
34
|
+
packagePaths,
|
|
35
|
+
view,
|
|
36
|
+
} = input
|
|
37
|
+
|
|
38
|
+
const result = input && await createReport(packagePaths, { cwd, debugLog, dryRun })
|
|
39
|
+
|
|
40
|
+
if (result && view) {
|
|
41
|
+
const reportId = result.data.id
|
|
42
|
+
const reportResult = input && await fetchReportData(reportId)
|
|
43
|
+
|
|
44
|
+
if (reportResult) {
|
|
45
|
+
formatReportDataOutput(reportResult.data, { name, outputJson, outputMarkdown, reportId })
|
|
46
|
+
}
|
|
47
|
+
} else if (result) {
|
|
48
|
+
formatReportCreationOutput(result.data, { outputJson, outputMarkdown })
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
19
53
|
|
|
20
|
-
|
|
21
|
-
const run = async (argv, importMeta, { parentName }) => {
|
|
22
|
-
const name = parentName + ' create'
|
|
54
|
+
// Internal functions
|
|
23
55
|
|
|
56
|
+
/**
|
|
57
|
+
* @param {string} name
|
|
58
|
+
* @param {string} description
|
|
59
|
+
* @param {readonly string[]} argv
|
|
60
|
+
* @param {ImportMeta} importMeta
|
|
61
|
+
* @returns {Promise<void|{ cwd: string, debugLog: typeof console.error, dryRun: boolean, outputJson: boolean, outputMarkdown: boolean, packagePaths: string[], view: boolean }>}
|
|
62
|
+
*/
|
|
63
|
+
async function setupCommand (name, description, argv, importMeta) {
|
|
24
64
|
const cli = meow(`
|
|
25
65
|
Usage
|
|
26
66
|
$ ${name} <paths-to-package-folders-and-files>
|
|
@@ -31,12 +71,14 @@ const run = async (argv, importMeta, { parentName }) => {
|
|
|
31
71
|
'--dry-run': 'Only output what will be done without actually doing it',
|
|
32
72
|
'--json': 'Output result as json',
|
|
33
73
|
'--markdown': 'Output result as markdown',
|
|
74
|
+
'--view': 'Will wait for and return the created report'
|
|
34
75
|
}, 6)}
|
|
35
76
|
|
|
36
77
|
Examples
|
|
37
78
|
$ ${name} .
|
|
38
79
|
$ ${name} ../package-lock.json
|
|
39
80
|
$ ${name} /path/to/a/package.json /path/to/another/package.json
|
|
81
|
+
$ ${name} . --view --json
|
|
40
82
|
`, {
|
|
41
83
|
argv,
|
|
42
84
|
description,
|
|
@@ -61,6 +103,11 @@ const run = async (argv, importMeta, { parentName }) => {
|
|
|
61
103
|
alias: 'm',
|
|
62
104
|
default: false,
|
|
63
105
|
},
|
|
106
|
+
view: {
|
|
107
|
+
type: 'boolean',
|
|
108
|
+
alias: 'v',
|
|
109
|
+
default: false,
|
|
110
|
+
},
|
|
64
111
|
}
|
|
65
112
|
})
|
|
66
113
|
|
|
@@ -68,6 +115,7 @@ const run = async (argv, importMeta, { parentName }) => {
|
|
|
68
115
|
dryRun,
|
|
69
116
|
json: outputJson,
|
|
70
117
|
markdown: outputMarkdown,
|
|
118
|
+
view,
|
|
71
119
|
} = cli.flags
|
|
72
120
|
|
|
73
121
|
if (!cli.input[0]) {
|
|
@@ -80,6 +128,23 @@ const run = async (argv, importMeta, { parentName }) => {
|
|
|
80
128
|
const cwd = process.cwd()
|
|
81
129
|
const packagePaths = await resolvePackagePaths(cwd, cli.input)
|
|
82
130
|
|
|
131
|
+
return {
|
|
132
|
+
cwd,
|
|
133
|
+
debugLog,
|
|
134
|
+
dryRun,
|
|
135
|
+
outputJson,
|
|
136
|
+
outputMarkdown,
|
|
137
|
+
packagePaths,
|
|
138
|
+
view,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {string[]} packagePaths
|
|
144
|
+
* @param {{ cwd: string, debugLog: typeof console.error, dryRun: boolean }} context
|
|
145
|
+
* @returns {Promise<void|import('@socketsecurity/sdk').SocketSdkReturnType<'createReport'>>}
|
|
146
|
+
*/
|
|
147
|
+
async function createReport (packagePaths, { cwd, debugLog, dryRun }) {
|
|
83
148
|
debugLog(`${logSymbols.info} Uploading:`, packagePaths.join(`\n${logSymbols.info} Uploading:`))
|
|
84
149
|
|
|
85
150
|
if (dryRun) {
|
|
@@ -87,43 +152,36 @@ const run = async (argv, importMeta, { parentName }) => {
|
|
|
87
152
|
}
|
|
88
153
|
|
|
89
154
|
const socketSdk = await setupSdk()
|
|
90
|
-
|
|
91
155
|
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
|
-
}
|
|
156
|
+
const result = await handleApiCall(socketSdk.createReportFromFilePaths(packagePaths, cwd), spinner, 'creating report')
|
|
102
157
|
|
|
103
158
|
if (result.success === false) {
|
|
104
|
-
|
|
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)
|
|
159
|
+
return handleUnsuccessfulApiResponse(result, spinner)
|
|
110
160
|
}
|
|
111
161
|
|
|
162
|
+
// Conclude the status of the API call
|
|
163
|
+
|
|
112
164
|
spinner.succeed()
|
|
113
165
|
|
|
166
|
+
return result
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @param {import('@socketsecurity/sdk').SocketSdkReturnType<'createReport'>["data"]} data
|
|
171
|
+
* @param {{ outputJson: boolean, outputMarkdown: boolean }} context
|
|
172
|
+
* @returns {void}
|
|
173
|
+
*/
|
|
174
|
+
function formatReportCreationOutput (data, { outputJson, outputMarkdown }) {
|
|
114
175
|
if (outputJson) {
|
|
115
|
-
console.log(JSON.stringify(
|
|
176
|
+
console.log(JSON.stringify(data, undefined, 2))
|
|
116
177
|
return
|
|
117
178
|
}
|
|
118
179
|
|
|
119
180
|
const format = new ChalkOrMarkdown(!!outputMarkdown)
|
|
120
181
|
|
|
121
|
-
console.log('\nNew report: ' + format.hyperlink(
|
|
182
|
+
console.log('\nNew report: ' + format.hyperlink(data.id, data.url, { fallbackToUrl: true }))
|
|
122
183
|
}
|
|
123
184
|
|
|
124
|
-
/** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */
|
|
125
|
-
export const create = { description, run }
|
|
126
|
-
|
|
127
185
|
// TODO: Add globbing support with support for ignoring, as a "./**/package.json" in a project also traverses eg. node_modules
|
|
128
186
|
/**
|
|
129
187
|
* Takes paths to folders and/or package.json / package-lock.json files and resolves to package.json + package-lock.json pairs (where feasible)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { meowWithSubcommands } from '../../utils/meow-with-subcommands.js'
|
|
2
2
|
import { create } from './create.js'
|
|
3
|
+
import { view } from './view.js'
|
|
3
4
|
|
|
4
5
|
const description = 'Project report related commands'
|
|
5
6
|
|
|
@@ -10,6 +11,7 @@ export const report = {
|
|
|
10
11
|
await meowWithSubcommands(
|
|
11
12
|
{
|
|
12
13
|
create,
|
|
14
|
+
view,
|
|
13
15
|
},
|
|
14
16
|
{
|
|
15
17
|
argv,
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import meow from 'meow'
|
|
5
|
+
import ora from 'ora'
|
|
6
|
+
|
|
7
|
+
import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js'
|
|
8
|
+
import { ChalkOrMarkdown } from '../../utils/chalk-markdown.js'
|
|
9
|
+
import { InputError } from '../../utils/errors.js'
|
|
10
|
+
import { getSeveritySummary } from '../../utils/format-issues.js'
|
|
11
|
+
import { printFlagList } from '../../utils/formatting.js'
|
|
12
|
+
import { setupSdk } from '../../utils/sdk.js'
|
|
13
|
+
|
|
14
|
+
/** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */
|
|
15
|
+
export const view = {
|
|
16
|
+
description: 'View a project report',
|
|
17
|
+
async run (argv, importMeta, { parentName }) {
|
|
18
|
+
const name = parentName + ' view'
|
|
19
|
+
|
|
20
|
+
const input = setupCommand(name, view.description, argv, importMeta)
|
|
21
|
+
const result = input && await fetchReportData(input.reportId)
|
|
22
|
+
|
|
23
|
+
if (result) {
|
|
24
|
+
formatReportDataOutput(result.data, { name, ...input })
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Internal functions
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} name
|
|
33
|
+
* @param {string} description
|
|
34
|
+
* @param {readonly string[]} argv
|
|
35
|
+
* @param {ImportMeta} importMeta
|
|
36
|
+
* @returns {void|{ outputJson: boolean, outputMarkdown: boolean, reportId: string }}
|
|
37
|
+
*/
|
|
38
|
+
function setupCommand (name, description, argv, importMeta) {
|
|
39
|
+
const cli = meow(`
|
|
40
|
+
Usage
|
|
41
|
+
$ ${name} <report-identifier>
|
|
42
|
+
|
|
43
|
+
Options
|
|
44
|
+
${printFlagList({
|
|
45
|
+
'--json': 'Output result as json',
|
|
46
|
+
'--markdown': 'Output result as markdown',
|
|
47
|
+
}, 6)}
|
|
48
|
+
|
|
49
|
+
Examples
|
|
50
|
+
$ ${name} QXU8PmK7LfH608RAwfIKdbcHgwEd_ZeWJ9QEGv05FJUQ
|
|
51
|
+
`, {
|
|
52
|
+
argv,
|
|
53
|
+
description,
|
|
54
|
+
importMeta,
|
|
55
|
+
flags: {
|
|
56
|
+
debug: {
|
|
57
|
+
type: 'boolean',
|
|
58
|
+
alias: 'd',
|
|
59
|
+
default: false,
|
|
60
|
+
},
|
|
61
|
+
json: {
|
|
62
|
+
type: 'boolean',
|
|
63
|
+
alias: 'j',
|
|
64
|
+
default: false,
|
|
65
|
+
},
|
|
66
|
+
markdown: {
|
|
67
|
+
type: 'boolean',
|
|
68
|
+
alias: 'm',
|
|
69
|
+
default: false,
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// Extract the input
|
|
75
|
+
|
|
76
|
+
const {
|
|
77
|
+
json: outputJson,
|
|
78
|
+
markdown: outputMarkdown,
|
|
79
|
+
} = cli.flags
|
|
80
|
+
|
|
81
|
+
const [reportId, ...extraInput] = cli.input
|
|
82
|
+
|
|
83
|
+
if (!reportId) {
|
|
84
|
+
cli.showHelp()
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Validate the input
|
|
89
|
+
|
|
90
|
+
if (extraInput.length) {
|
|
91
|
+
throw new InputError(`Can only handle a single report ID at a time, but got ${cli.input.length} report ID:s: ${cli.input.join(', ')}`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
outputJson,
|
|
96
|
+
outputMarkdown,
|
|
97
|
+
reportId,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {string} reportId
|
|
103
|
+
* @returns {Promise<void|import('@socketsecurity/sdk').SocketSdkReturnType<'getReport'>>}
|
|
104
|
+
*/
|
|
105
|
+
export async function fetchReportData (reportId) {
|
|
106
|
+
// Do the API call
|
|
107
|
+
|
|
108
|
+
const socketSdk = await setupSdk()
|
|
109
|
+
const spinner = ora(`Fetching report with ID ${reportId}`).start()
|
|
110
|
+
const result = await handleApiCall(socketSdk.getReport(reportId), spinner, 'fetching report')
|
|
111
|
+
|
|
112
|
+
if (result.success === false) {
|
|
113
|
+
return handleUnsuccessfulApiResponse(result, spinner)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Conclude the status of the API call
|
|
117
|
+
|
|
118
|
+
const issueSummary = getSeveritySummary(result.data.issues)
|
|
119
|
+
spinner.succeed(`Report contains ${issueSummary || 'no'} issues`)
|
|
120
|
+
|
|
121
|
+
return result
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {import('@socketsecurity/sdk').SocketSdkReturnType<'getReport'>["data"]} data
|
|
126
|
+
* @param {{ name: string, outputJson: boolean, outputMarkdown: boolean, reportId: string }} context
|
|
127
|
+
* @returns {void}
|
|
128
|
+
*/
|
|
129
|
+
export function formatReportDataOutput (data, { name, outputJson, outputMarkdown, reportId }) {
|
|
130
|
+
// If JSON, output and return...
|
|
131
|
+
|
|
132
|
+
if (outputJson) {
|
|
133
|
+
console.log(JSON.stringify(data, undefined, 2))
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ...else do the CLI / Markdown output dance
|
|
138
|
+
|
|
139
|
+
const format = new ChalkOrMarkdown(!!outputMarkdown)
|
|
140
|
+
const url = `https://socket.dev/npm/reports/${encodeURIComponent(reportId)}`
|
|
141
|
+
|
|
142
|
+
console.log('\nDetailed info on socket.dev: ' + format.hyperlink(reportId, url, { fallbackToUrl: true }))
|
|
143
|
+
if (!outputMarkdown) {
|
|
144
|
+
console.log(chalk.dim('\nOr rerun', chalk.italic(name), 'using the', chalk.italic('--json'), 'flag to get full JSON output'))
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { ErrorWithCause } from 'pony-cause'
|
|
3
|
+
|
|
4
|
+
import { AuthError } from './errors.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @template T
|
|
8
|
+
* @param {import('@socketsecurity/sdk').SocketSdkErrorType<T>} result
|
|
9
|
+
* @param {import('ora').Ora} spinner
|
|
10
|
+
* @returns {void}
|
|
11
|
+
*/
|
|
12
|
+
export function handleUnsuccessfulApiResponse (result, spinner) {
|
|
13
|
+
const resultError = 'error' in result && result.error && typeof result.error === 'object' ? result.error : {}
|
|
14
|
+
const message = 'message' in resultError && typeof resultError.message === 'string' ? resultError.message : 'No error message returned'
|
|
15
|
+
|
|
16
|
+
if (result.status === 401 || result.status === 403) {
|
|
17
|
+
spinner.stop()
|
|
18
|
+
throw new AuthError(message)
|
|
19
|
+
}
|
|
20
|
+
spinner.fail(chalk.white.bgRed('API returned an error:') + ' ' + message)
|
|
21
|
+
process.exit(1)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @template T
|
|
26
|
+
* @param {Promise<T>} value
|
|
27
|
+
* @param {import('ora').Ora} spinner
|
|
28
|
+
* @param {string} description
|
|
29
|
+
* @returns {Promise<T>}
|
|
30
|
+
*/
|
|
31
|
+
export async function handleApiCall (value, spinner, description) {
|
|
32
|
+
/** @type {T} */
|
|
33
|
+
let result
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
result = await value
|
|
37
|
+
} catch (cause) {
|
|
38
|
+
spinner.fail()
|
|
39
|
+
throw new ErrorWithCause(`Failed ${description}`, { cause })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return result
|
|
43
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** @typedef {import('@socketsecurity/sdk').SocketSdkReturnType<'getIssuesByNPMPackage'>['data']} SocketIssueList */
|
|
2
|
+
/** @typedef {SocketIssueList[number]['value'] extends infer U | undefined ? U : never} SocketIssue */
|
|
3
|
+
|
|
4
|
+
import { stringJoinWithSeparateFinalSeparator } from './misc.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {SocketIssueList} issues
|
|
8
|
+
* @returns {Record<SocketIssue['severity'], number>}
|
|
9
|
+
*/
|
|
10
|
+
function getSeverityCount (issues) {
|
|
11
|
+
/** @type {Record<SocketIssue['severity'], number>} */
|
|
12
|
+
const severityCount = { low: 0, middle: 0, high: 0, critical: 0 }
|
|
13
|
+
|
|
14
|
+
for (const issue of issues) {
|
|
15
|
+
const value = issue.value
|
|
16
|
+
|
|
17
|
+
if (!value) {
|
|
18
|
+
continue
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (severityCount[value.severity] !== undefined) {
|
|
22
|
+
severityCount[value.severity] += 1
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return severityCount
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {SocketIssueList} issues
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
export function getSeveritySummary (issues) {
|
|
34
|
+
const severityCount = getSeverityCount(issues)
|
|
35
|
+
|
|
36
|
+
const issueSummary = stringJoinWithSeparateFinalSeparator([
|
|
37
|
+
severityCount.critical ? severityCount.critical + ' critical' : undefined,
|
|
38
|
+
severityCount.high ? severityCount.high + ' high' : undefined,
|
|
39
|
+
severityCount.middle ? severityCount.middle + ' middle' : undefined,
|
|
40
|
+
severityCount.low ? severityCount.low + ' low' : undefined,
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
return issueSummary
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@socketsecurity/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "CLI tool for Socket.dev",
|
|
5
5
|
"homepage": "http://github.com/SocketDev/socket-cli-js",
|
|
6
6
|
"repository": {
|
|
@@ -45,37 +45,37 @@
|
|
|
45
45
|
"@types/mocha": "^10.0.0",
|
|
46
46
|
"@types/node": "^14.18.31",
|
|
47
47
|
"@types/prompts": "^2.4.1",
|
|
48
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
49
|
-
"@typescript-eslint/parser": "^5.
|
|
48
|
+
"@typescript-eslint/eslint-plugin": "^5.44.0",
|
|
49
|
+
"@typescript-eslint/parser": "^5.44.0",
|
|
50
50
|
"c8": "^7.12.0",
|
|
51
51
|
"chai": "^4.3.6",
|
|
52
52
|
"dependency-check": "^5.0.0-7",
|
|
53
|
-
"eslint": "^8.
|
|
53
|
+
"eslint": "^8.28.0",
|
|
54
54
|
"eslint-config-standard": "^17.0.0",
|
|
55
55
|
"eslint-config-standard-jsx": "^11.0.0",
|
|
56
|
-
"eslint-import-resolver-typescript": "^3.5.
|
|
56
|
+
"eslint-import-resolver-typescript": "^3.5.2",
|
|
57
57
|
"eslint-plugin-import": "^2.26.0",
|
|
58
58
|
"eslint-plugin-jsdoc": "^39.5.0",
|
|
59
|
-
"eslint-plugin-n": "^15.
|
|
60
|
-
"eslint-plugin-promise": "^6.
|
|
61
|
-
"eslint-plugin-react": "^7.31.
|
|
59
|
+
"eslint-plugin-n": "^15.5.1",
|
|
60
|
+
"eslint-plugin-promise": "^6.1.1",
|
|
61
|
+
"eslint-plugin-react": "^7.31.11",
|
|
62
62
|
"eslint-plugin-react-hooks": "^4.6.0",
|
|
63
63
|
"husky": "^8.0.1",
|
|
64
|
-
"installed-check": "^6.0.
|
|
64
|
+
"installed-check": "^6.0.5",
|
|
65
65
|
"mocha": "^10.0.0",
|
|
66
66
|
"npm-run-all2": "^6.0.2",
|
|
67
|
-
"type-coverage": "^2.
|
|
68
|
-
"typescript": "~4.
|
|
67
|
+
"type-coverage": "^2.24.1",
|
|
68
|
+
"typescript": "~4.9.3"
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
|
-
"@socketsecurity/sdk": "^0.
|
|
71
|
+
"@socketsecurity/sdk": "^0.4.0",
|
|
72
72
|
"chalk": "^5.1.2",
|
|
73
73
|
"hpagent": "^1.2.0",
|
|
74
74
|
"is-interactive": "^2.0.0",
|
|
75
75
|
"is-unicode-supported": "^1.3.0",
|
|
76
76
|
"meow": "^11.0.0",
|
|
77
77
|
"ora": "^6.1.2",
|
|
78
|
-
"pony-cause": "^2.1.
|
|
78
|
+
"pony-cause": "^2.1.8",
|
|
79
79
|
"prompts": "^2.4.2",
|
|
80
80
|
"terminal-link": "^3.0.0"
|
|
81
81
|
}
|