@socketsecurity/cli 0.2.1 → 0.4.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 +19 -1
- package/cli.js +1 -0
- package/lib/commands/info/index.js +66 -26
- package/lib/commands/report/create.js +83 -125
- package/lib/commands/report/index.js +1 -1
- package/lib/commands/report/view.js +59 -25
- package/lib/utils/errors.js +13 -1
- package/lib/utils/format-issues.js +42 -14
- package/lib/utils/misc.js +38 -4
- package/lib/utils/path-resolve.js +152 -0
- package/lib/utils/sdk.js +8 -1
- package/package.json +21 -11
package/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# Socket CLI
|
|
2
2
|
|
|
3
|
+
[](https://socket.dev/npm/package/@socketsecurity/cli)
|
|
3
4
|
[](https://www.npmjs.com/package/@socketsecurity/cli)
|
|
4
5
|
[](https://github.com/SocketDev/eslint-config)
|
|
5
6
|
[](https://twitter.com/SocketSecurity)
|
|
@@ -22,7 +23,15 @@ socket report view QXU8PmK7LfH608RAwfIKdbcHgwEd_ZeWJ9QEGv05FJUQ
|
|
|
22
23
|
## Commands
|
|
23
24
|
|
|
24
25
|
* `socket info <package@version>` - looks up issues for a package
|
|
25
|
-
|
|
26
|
+
|
|
27
|
+
* `socket report create <path(s)-to-folder-or-file>` - creates a report on [socket.dev](https://socket.dev/)
|
|
28
|
+
|
|
29
|
+
Uploads the specified `package.json` and lock files and, if any folder is specified, the ones found in there. Also includes the complementary `package.json` and lock file to any specified. Currently `package-lock.json` and `yarn.lock` are supported.
|
|
30
|
+
|
|
31
|
+
Supports globbing such as `**/package.json`.
|
|
32
|
+
|
|
33
|
+
Ignores any file specified in your project's `.gitignore`, the `projectIgnorePaths` in your project's [`socket.yml`](https://docs.socket.dev/docs/socket-yml) and on top of that has a sensible set of [default ignores](https://www.npmjs.com/package/ignore-by-default)
|
|
34
|
+
|
|
26
35
|
* `socket report view <report-id>` - looks up issues and scores from a report
|
|
27
36
|
|
|
28
37
|
## Flags
|
|
@@ -36,6 +45,11 @@ socket report view QXU8PmK7LfH608RAwfIKdbcHgwEd_ZeWJ9QEGv05FJUQ
|
|
|
36
45
|
* `--json` - outputs result as json which you can then pipe into [`jq`](https://stedolan.github.io/jq/) and other tools
|
|
37
46
|
* `--markdown` - outputs result as markdown which you can then copy into an issue, PR or even chat
|
|
38
47
|
|
|
48
|
+
## Strictness flags
|
|
49
|
+
|
|
50
|
+
* `--all` - by default only `high` and `critical` issues are included, by setting this flag all issues will be included
|
|
51
|
+
* `--strict` - when set, exits with an error code if report result is deemed unhealthy
|
|
52
|
+
|
|
39
53
|
### Other flags
|
|
40
54
|
|
|
41
55
|
* `--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
|
|
@@ -43,6 +57,10 @@ socket report view QXU8PmK7LfH608RAwfIKdbcHgwEd_ZeWJ9QEGv05FJUQ
|
|
|
43
57
|
* `--help` - prints the help for the current command. All CLI tools should have this flag
|
|
44
58
|
* `--version` - prints the version of the tool. All CLI tools should have this flag
|
|
45
59
|
|
|
60
|
+
## Configuration files
|
|
61
|
+
|
|
62
|
+
The CLI reads and uses data from a [`socket.yml` file](https://docs.socket.dev/docs/socket-yml) in the folder you run it in. It supports the version 2 of the `socket.yml` file format and makes use of the `projectIgnorePaths` to excludes files when creating a report.
|
|
63
|
+
|
|
46
64
|
## Environment variables
|
|
47
65
|
|
|
48
66
|
* `SOCKET_SECURITY_API_KEY` - if set, this will be used as the API-key
|
package/cli.js
CHANGED
|
@@ -7,8 +7,9 @@ import ora from 'ora'
|
|
|
7
7
|
import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js'
|
|
8
8
|
import { ChalkOrMarkdown } from '../../utils/chalk-markdown.js'
|
|
9
9
|
import { InputError } from '../../utils/errors.js'
|
|
10
|
-
import {
|
|
10
|
+
import { getSeverityCount, formatSeverityCount } from '../../utils/format-issues.js'
|
|
11
11
|
import { printFlagList } from '../../utils/formatting.js'
|
|
12
|
+
import { objectSome } from '../../utils/misc.js'
|
|
12
13
|
import { setupSdk } from '../../utils/sdk.js'
|
|
13
14
|
|
|
14
15
|
/** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */
|
|
@@ -18,32 +19,44 @@ export const info = {
|
|
|
18
19
|
const name = parentName + ' info'
|
|
19
20
|
|
|
20
21
|
const input = setupCommand(name, info.description, argv, importMeta)
|
|
21
|
-
const
|
|
22
|
+
const packageData = input && await fetchPackageData(input.pkgName, input.pkgVersion, input)
|
|
22
23
|
|
|
23
|
-
if (
|
|
24
|
-
formatPackageDataOutput(
|
|
24
|
+
if (packageData) {
|
|
25
|
+
formatPackageDataOutput(packageData, { name, ...input })
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
// Internal functions
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* @typedef CommandContext
|
|
34
|
+
* @property {boolean} includeAllIssues
|
|
35
|
+
* @property {boolean} outputJson
|
|
36
|
+
* @property {boolean} outputMarkdown
|
|
37
|
+
* @property {string} pkgName
|
|
38
|
+
* @property {string} pkgVersion
|
|
39
|
+
* @property {boolean} strict
|
|
40
|
+
*/
|
|
41
|
+
|
|
31
42
|
/**
|
|
32
43
|
* @param {string} name
|
|
33
44
|
* @param {string} description
|
|
34
45
|
* @param {readonly string[]} argv
|
|
35
46
|
* @param {ImportMeta} importMeta
|
|
36
|
-
* @returns {void|
|
|
47
|
+
* @returns {void|CommandContext}
|
|
37
48
|
*/
|
|
38
|
-
|
|
49
|
+
function setupCommand (name, description, argv, importMeta) {
|
|
39
50
|
const cli = meow(`
|
|
40
51
|
Usage
|
|
41
52
|
$ ${name} <name>
|
|
42
53
|
|
|
43
54
|
Options
|
|
44
55
|
${printFlagList({
|
|
56
|
+
'--all': 'Include all issues',
|
|
45
57
|
'--json': 'Output result as json',
|
|
46
58
|
'--markdown': 'Output result as markdown',
|
|
59
|
+
'--strict': 'Exits with an error code if any matching issues are found',
|
|
47
60
|
}, 6)}
|
|
48
61
|
|
|
49
62
|
Examples
|
|
@@ -54,6 +67,10 @@ export const info = {
|
|
|
54
67
|
description,
|
|
55
68
|
importMeta,
|
|
56
69
|
flags: {
|
|
70
|
+
all: {
|
|
71
|
+
type: 'boolean',
|
|
72
|
+
default: false,
|
|
73
|
+
},
|
|
57
74
|
json: {
|
|
58
75
|
type: 'boolean',
|
|
59
76
|
alias: 'j',
|
|
@@ -64,12 +81,18 @@ export const info = {
|
|
|
64
81
|
alias: 'm',
|
|
65
82
|
default: false,
|
|
66
83
|
},
|
|
84
|
+
strict: {
|
|
85
|
+
type: 'boolean',
|
|
86
|
+
default: false,
|
|
87
|
+
},
|
|
67
88
|
}
|
|
68
89
|
})
|
|
69
90
|
|
|
70
91
|
const {
|
|
92
|
+
all: includeAllIssues,
|
|
71
93
|
json: outputJson,
|
|
72
94
|
markdown: outputMarkdown,
|
|
95
|
+
strict,
|
|
73
96
|
} = cli.flags
|
|
74
97
|
|
|
75
98
|
if (cli.input.length > 1) {
|
|
@@ -97,19 +120,28 @@ export const info = {
|
|
|
97
120
|
}
|
|
98
121
|
|
|
99
122
|
return {
|
|
123
|
+
includeAllIssues,
|
|
100
124
|
outputJson,
|
|
101
125
|
outputMarkdown,
|
|
102
126
|
pkgName,
|
|
103
|
-
pkgVersion
|
|
127
|
+
pkgVersion,
|
|
128
|
+
strict,
|
|
104
129
|
}
|
|
105
130
|
}
|
|
106
131
|
|
|
132
|
+
/**
|
|
133
|
+
* @typedef PackageData
|
|
134
|
+
* @property {import('@socketsecurity/sdk').SocketSdkReturnType<'getIssuesByNPMPackage'>["data"]} data
|
|
135
|
+
* @property {Record<import('../../utils/format-issues').SocketIssue['severity'], number>} severityCount
|
|
136
|
+
*/
|
|
137
|
+
|
|
107
138
|
/**
|
|
108
139
|
* @param {string} pkgName
|
|
109
140
|
* @param {string} pkgVersion
|
|
110
|
-
* @
|
|
141
|
+
* @param {Pick<CommandContext, 'includeAllIssues' | 'strict'>} context
|
|
142
|
+
* @returns {Promise<void|PackageData>}
|
|
111
143
|
*/
|
|
112
|
-
async function fetchPackageData (pkgName, pkgVersion) {
|
|
144
|
+
async function fetchPackageData (pkgName, pkgVersion, { includeAllIssues, strict }) {
|
|
113
145
|
const socketSdk = await setupSdk()
|
|
114
146
|
const spinner = ora(`Looking up data for version ${pkgVersion} of ${pkgName}`).start()
|
|
115
147
|
const result = await handleApiCall(socketSdk.getIssuesByNPMPackage(pkgName, pkgVersion), spinner, 'looking up package')
|
|
@@ -120,32 +152,40 @@ async function fetchPackageData (pkgName, pkgVersion) {
|
|
|
120
152
|
|
|
121
153
|
// Conclude the status of the API call
|
|
122
154
|
|
|
123
|
-
const
|
|
124
|
-
|
|
155
|
+
const severityCount = getSeverityCount(result.data, includeAllIssues ? undefined : 'high')
|
|
156
|
+
|
|
157
|
+
if (objectSome(severityCount)) {
|
|
158
|
+
const issueSummary = formatSeverityCount(severityCount)
|
|
159
|
+
spinner[strict ? 'fail' : 'succeed'](`Package has these issues: ${issueSummary}`)
|
|
160
|
+
} else {
|
|
161
|
+
spinner.succeed('Package has no issues')
|
|
162
|
+
}
|
|
125
163
|
|
|
126
|
-
return
|
|
164
|
+
return {
|
|
165
|
+
data: result.data,
|
|
166
|
+
severityCount,
|
|
167
|
+
}
|
|
127
168
|
}
|
|
128
169
|
|
|
129
170
|
/**
|
|
130
|
-
* @param {
|
|
131
|
-
* @param {{ name: string
|
|
171
|
+
* @param {PackageData} packageData
|
|
172
|
+
* @param {{ name: string } & CommandContext} context
|
|
132
173
|
* @returns {void}
|
|
133
174
|
*/
|
|
134
|
-
function formatPackageDataOutput (data, { name, outputJson, outputMarkdown, pkgName, pkgVersion }) {
|
|
135
|
-
// If JSON, output and return...
|
|
136
|
-
|
|
175
|
+
function formatPackageDataOutput ({ data, severityCount }, { name, outputJson, outputMarkdown, pkgName, pkgVersion, strict }) {
|
|
137
176
|
if (outputJson) {
|
|
138
177
|
console.log(JSON.stringify(data, undefined, 2))
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// ...else do the CLI / Markdown output dance
|
|
178
|
+
} else {
|
|
179
|
+
const format = new ChalkOrMarkdown(!!outputMarkdown)
|
|
180
|
+
const url = `https://socket.dev/npm/package/${pkgName}/overview/${pkgVersion}`
|
|
143
181
|
|
|
144
|
-
|
|
145
|
-
|
|
182
|
+
console.log('\nDetailed info on socket.dev: ' + format.hyperlink(`${pkgName} v${pkgVersion}`, url, { fallbackToUrl: true }))
|
|
183
|
+
if (!outputMarkdown) {
|
|
184
|
+
console.log(chalk.dim('\nOr rerun', chalk.italic(name), 'using the', chalk.italic('--json'), 'flag to get full JSON output'))
|
|
185
|
+
}
|
|
186
|
+
}
|
|
146
187
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
console.log(chalk.dim('\nOr rerun', chalk.italic(name), 'using the', chalk.italic('--json'), 'flag to get full JSON output'))
|
|
188
|
+
if (strict && objectSome(severityCount)) {
|
|
189
|
+
process.exit(1)
|
|
150
190
|
}
|
|
151
191
|
}
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
2
|
|
|
3
|
-
import { stat } from 'node:fs/promises'
|
|
4
3
|
import path from 'node:path'
|
|
5
4
|
|
|
5
|
+
import { betterAjvErrors } from '@apideck/better-ajv-errors'
|
|
6
|
+
import { readSocketConfig, SocketValidationError } from '@socketsecurity/config'
|
|
6
7
|
import meow from 'meow'
|
|
7
8
|
import ora from 'ora'
|
|
8
9
|
import { ErrorWithCause } from 'pony-cause'
|
|
9
10
|
|
|
11
|
+
import { fetchReportData, formatReportDataOutput } from './view.js'
|
|
10
12
|
import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js'
|
|
11
13
|
import { ChalkOrMarkdown, logSymbols } from '../../utils/chalk-markdown.js'
|
|
12
14
|
import { InputError } from '../../utils/errors.js'
|
|
13
15
|
import { printFlagList } from '../../utils/formatting.js'
|
|
14
16
|
import { createDebugLogger } from '../../utils/misc.js'
|
|
17
|
+
import { getPackageFiles } from '../../utils/path-resolve.js'
|
|
15
18
|
import { setupSdk } from '../../utils/sdk.js'
|
|
16
|
-
import { isErrnoException } from '../../utils/type-helpers.js'
|
|
17
|
-
import { fetchReportData, formatReportDataOutput } from './view.js'
|
|
18
19
|
|
|
19
20
|
/** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */
|
|
20
21
|
export const create = {
|
|
@@ -26,23 +27,26 @@ export const create = {
|
|
|
26
27
|
|
|
27
28
|
if (input) {
|
|
28
29
|
const {
|
|
30
|
+
config,
|
|
29
31
|
cwd,
|
|
30
32
|
debugLog,
|
|
31
33
|
dryRun,
|
|
34
|
+
includeAllIssues,
|
|
32
35
|
outputJson,
|
|
33
36
|
outputMarkdown,
|
|
34
37
|
packagePaths,
|
|
38
|
+
strict,
|
|
35
39
|
view,
|
|
36
40
|
} = input
|
|
37
41
|
|
|
38
|
-
const result = input && await createReport(packagePaths, { cwd, debugLog, dryRun })
|
|
42
|
+
const result = input && await createReport(packagePaths, { config, cwd, debugLog, dryRun })
|
|
39
43
|
|
|
40
44
|
if (result && view) {
|
|
41
45
|
const reportId = result.data.id
|
|
42
|
-
const
|
|
46
|
+
const reportData = input && await fetchReportData(reportId, { includeAllIssues, strict })
|
|
43
47
|
|
|
44
|
-
if (
|
|
45
|
-
formatReportDataOutput(
|
|
48
|
+
if (reportData) {
|
|
49
|
+
formatReportDataOutput(reportData, { includeAllIssues, name, outputJson, outputMarkdown, reportId, strict })
|
|
46
50
|
}
|
|
47
51
|
} else if (result) {
|
|
48
52
|
formatReportCreationOutput(result.data, { outputJson, outputMarkdown })
|
|
@@ -53,30 +57,57 @@ export const create = {
|
|
|
53
57
|
|
|
54
58
|
// Internal functions
|
|
55
59
|
|
|
60
|
+
/**
|
|
61
|
+
* @typedef CommandContext
|
|
62
|
+
* @property {import('@socketsecurity/config').SocketYml|undefined} config
|
|
63
|
+
* @property {string} cwd
|
|
64
|
+
* @property {typeof console.error} debugLog
|
|
65
|
+
* @property {boolean} dryRun
|
|
66
|
+
* @property {boolean} includeAllIssues
|
|
67
|
+
* @property {boolean} outputJson
|
|
68
|
+
* @property {boolean} outputMarkdown
|
|
69
|
+
* @property {string[]} packagePaths
|
|
70
|
+
* @property {boolean} strict
|
|
71
|
+
* @property {boolean} view
|
|
72
|
+
*/
|
|
73
|
+
|
|
56
74
|
/**
|
|
57
75
|
* @param {string} name
|
|
58
76
|
* @param {string} description
|
|
59
77
|
* @param {readonly string[]} argv
|
|
60
78
|
* @param {ImportMeta} importMeta
|
|
61
|
-
* @returns {Promise<void|
|
|
79
|
+
* @returns {Promise<void|CommandContext>}
|
|
62
80
|
*/
|
|
63
81
|
async function setupCommand (name, description, argv, importMeta) {
|
|
64
82
|
const cli = meow(`
|
|
65
83
|
Usage
|
|
66
84
|
$ ${name} <paths-to-package-folders-and-files>
|
|
67
85
|
|
|
86
|
+
Uploads the specified "package.json" and lock files and, if any folder is
|
|
87
|
+
specified, the ones found in there. Also includes the complementary
|
|
88
|
+
"package.json" and lock file to any specified. Currently "package-lock.json"
|
|
89
|
+
and "yarn.lock" are supported.
|
|
90
|
+
|
|
91
|
+
Supports globbing such as "**/package.json".
|
|
92
|
+
|
|
93
|
+
Ignores any file specified in your project's ".gitignore", your project's
|
|
94
|
+
"socket.yml" file's "projectIgnorePaths" and also has a sensible set of
|
|
95
|
+
default ignores from the "ignore-by-default" module.
|
|
96
|
+
|
|
68
97
|
Options
|
|
69
98
|
${printFlagList({
|
|
99
|
+
'--all': 'Include all issues',
|
|
70
100
|
'--debug': 'Output debug information',
|
|
71
101
|
'--dry-run': 'Only output what will be done without actually doing it',
|
|
72
102
|
'--json': 'Output result as json',
|
|
73
103
|
'--markdown': 'Output result as markdown',
|
|
104
|
+
'--strict': 'Exits with an error code if any matching issues are found',
|
|
74
105
|
'--view': 'Will wait for and return the created report'
|
|
75
106
|
}, 6)}
|
|
76
107
|
|
|
77
108
|
Examples
|
|
78
109
|
$ ${name} .
|
|
79
|
-
$ ${name}
|
|
110
|
+
$ ${name} '**/package.json'
|
|
80
111
|
$ ${name} /path/to/a/package.json /path/to/another/package.json
|
|
81
112
|
$ ${name} . --view --json
|
|
82
113
|
`, {
|
|
@@ -84,6 +115,10 @@ async function setupCommand (name, description, argv, importMeta) {
|
|
|
84
115
|
description,
|
|
85
116
|
importMeta,
|
|
86
117
|
flags: {
|
|
118
|
+
all: {
|
|
119
|
+
type: 'boolean',
|
|
120
|
+
default: false,
|
|
121
|
+
},
|
|
87
122
|
debug: {
|
|
88
123
|
type: 'boolean',
|
|
89
124
|
alias: 'd',
|
|
@@ -103,6 +138,10 @@ async function setupCommand (name, description, argv, importMeta) {
|
|
|
103
138
|
alias: 'm',
|
|
104
139
|
default: false,
|
|
105
140
|
},
|
|
141
|
+
strict: {
|
|
142
|
+
type: 'boolean',
|
|
143
|
+
default: false,
|
|
144
|
+
},
|
|
106
145
|
view: {
|
|
107
146
|
type: 'boolean',
|
|
108
147
|
alias: 'v',
|
|
@@ -112,9 +151,11 @@ async function setupCommand (name, description, argv, importMeta) {
|
|
|
112
151
|
})
|
|
113
152
|
|
|
114
153
|
const {
|
|
154
|
+
all: includeAllIssues,
|
|
115
155
|
dryRun,
|
|
116
156
|
json: outputJson,
|
|
117
157
|
markdown: outputMarkdown,
|
|
158
|
+
strict,
|
|
118
159
|
view,
|
|
119
160
|
} = cli.flags
|
|
120
161
|
|
|
@@ -125,27 +166,53 @@ async function setupCommand (name, description, argv, importMeta) {
|
|
|
125
166
|
|
|
126
167
|
const debugLog = createDebugLogger(dryRun || cli.flags.debug)
|
|
127
168
|
|
|
169
|
+
// TODO: Allow setting a custom cwd and/or configFile path?
|
|
128
170
|
const cwd = process.cwd()
|
|
129
|
-
const
|
|
171
|
+
const absoluteConfigPath = path.join(cwd, 'socket.yml')
|
|
172
|
+
|
|
173
|
+
const config = await readSocketConfig(absoluteConfigPath)
|
|
174
|
+
.catch(/** @param {unknown} cause */ cause => {
|
|
175
|
+
if (cause && typeof cause === 'object' && cause instanceof SocketValidationError) {
|
|
176
|
+
// Inspired by workbox-build: https://github.com/GoogleChrome/workbox/blob/95f97a207fd51efb3f8a653f6e3e58224183a778/packages/workbox-build/src/lib/validate-options.ts#L68-L71
|
|
177
|
+
const betterErrors = betterAjvErrors({
|
|
178
|
+
basePath: 'config',
|
|
179
|
+
data: cause.data,
|
|
180
|
+
errors: cause.validationErrors,
|
|
181
|
+
// @ts-ignore
|
|
182
|
+
schema: cause.schema,
|
|
183
|
+
})
|
|
184
|
+
throw new InputError(
|
|
185
|
+
'The socket.yml config is not valid',
|
|
186
|
+
betterErrors.map((err) => `[${err.path}] ${err.message}.${err.suggestion ? err.suggestion : ''}`).join('\n')
|
|
187
|
+
)
|
|
188
|
+
} else {
|
|
189
|
+
throw new ErrorWithCause('Failed to read socket.yml config', { cause })
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
const packagePaths = await getPackageFiles(cwd, cli.input, config, debugLog)
|
|
130
194
|
|
|
131
195
|
return {
|
|
196
|
+
config,
|
|
132
197
|
cwd,
|
|
133
198
|
debugLog,
|
|
134
199
|
dryRun,
|
|
200
|
+
includeAllIssues,
|
|
135
201
|
outputJson,
|
|
136
202
|
outputMarkdown,
|
|
137
203
|
packagePaths,
|
|
204
|
+
strict,
|
|
138
205
|
view,
|
|
139
206
|
}
|
|
140
207
|
}
|
|
141
208
|
|
|
142
209
|
/**
|
|
143
210
|
* @param {string[]} packagePaths
|
|
144
|
-
* @param {
|
|
211
|
+
* @param {Pick<CommandContext, 'config' | 'cwd' | 'debugLog' | 'dryRun'>} context
|
|
145
212
|
* @returns {Promise<void|import('@socketsecurity/sdk').SocketSdkReturnType<'createReport'>>}
|
|
146
213
|
*/
|
|
147
|
-
async function createReport (packagePaths, { cwd, debugLog, dryRun }) {
|
|
148
|
-
debugLog(
|
|
214
|
+
async function createReport (packagePaths, { config, cwd, debugLog, dryRun }) {
|
|
215
|
+
debugLog('Uploading:', packagePaths.join(`\n${logSymbols.info} Uploading: `))
|
|
149
216
|
|
|
150
217
|
if (dryRun) {
|
|
151
218
|
return
|
|
@@ -153,7 +220,8 @@ async function createReport (packagePaths, { cwd, debugLog, dryRun }) {
|
|
|
153
220
|
|
|
154
221
|
const socketSdk = await setupSdk()
|
|
155
222
|
const spinner = ora(`Creating report with ${packagePaths.length} package files`).start()
|
|
156
|
-
const
|
|
223
|
+
const apiCall = socketSdk.createReportFromFilePaths(packagePaths, cwd, config?.issueRules)
|
|
224
|
+
const result = await handleApiCall(apiCall, spinner, 'creating report')
|
|
157
225
|
|
|
158
226
|
if (result.success === false) {
|
|
159
227
|
return handleUnsuccessfulApiResponse(result, spinner)
|
|
@@ -168,7 +236,7 @@ async function createReport (packagePaths, { cwd, debugLog, dryRun }) {
|
|
|
168
236
|
|
|
169
237
|
/**
|
|
170
238
|
* @param {import('@socketsecurity/sdk').SocketSdkReturnType<'createReport'>["data"]} data
|
|
171
|
-
* @param {
|
|
239
|
+
* @param {Pick<CommandContext, 'outputJson' | 'outputMarkdown'>} context
|
|
172
240
|
* @returns {void}
|
|
173
241
|
*/
|
|
174
242
|
function formatReportCreationOutput (data, { outputJson, outputMarkdown }) {
|
|
@@ -181,113 +249,3 @@ function formatReportCreationOutput (data, { outputJson, outputMarkdown }) {
|
|
|
181
249
|
|
|
182
250
|
console.log('\nNew report: ' + format.hyperlink(data.id, data.url, { fallbackToUrl: true }))
|
|
183
251
|
}
|
|
184
|
-
|
|
185
|
-
// TODO: Add globbing support with support for ignoring, as a "./**/package.json" in a project also traverses eg. node_modules
|
|
186
|
-
/**
|
|
187
|
-
* Takes paths to folders and/or package.json / package-lock.json files and resolves to package.json + package-lock.json pairs (where feasible)
|
|
188
|
-
*
|
|
189
|
-
* @param {string} cwd
|
|
190
|
-
* @param {string[]} inputPaths
|
|
191
|
-
* @returns {Promise<string[]>}
|
|
192
|
-
* @throws {InputError}
|
|
193
|
-
*/
|
|
194
|
-
async function resolvePackagePaths (cwd, inputPaths) {
|
|
195
|
-
const packagePathLookups = inputPaths.map(async (filePath) => {
|
|
196
|
-
const packagePath = await resolvePackagePath(cwd, filePath)
|
|
197
|
-
return findComplementaryPackageFile(packagePath)
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
const packagePaths = await Promise.all(packagePathLookups)
|
|
201
|
-
|
|
202
|
-
const uniquePackagePaths = new Set(packagePaths.flat())
|
|
203
|
-
|
|
204
|
-
return [...uniquePackagePaths]
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Resolves a package.json / package-lock.json path from a relative folder / file path
|
|
209
|
-
*
|
|
210
|
-
* @param {string} cwd
|
|
211
|
-
* @param {string} inputPath
|
|
212
|
-
* @returns {Promise<string>}
|
|
213
|
-
* @throws {InputError}
|
|
214
|
-
*/
|
|
215
|
-
async function resolvePackagePath (cwd, inputPath) {
|
|
216
|
-
const filePath = path.resolve(cwd, inputPath)
|
|
217
|
-
/** @type {string|undefined} */
|
|
218
|
-
let filePathAppended
|
|
219
|
-
|
|
220
|
-
try {
|
|
221
|
-
const fileStat = await stat(filePath)
|
|
222
|
-
|
|
223
|
-
if (fileStat.isDirectory()) {
|
|
224
|
-
filePathAppended = path.resolve(filePath, 'package.json')
|
|
225
|
-
}
|
|
226
|
-
} catch (err) {
|
|
227
|
-
if (isErrnoException(err) && err.code === 'ENOENT') {
|
|
228
|
-
throw new InputError(`Expected '${inputPath}' to point to an existing file or directory`)
|
|
229
|
-
}
|
|
230
|
-
throw new ErrorWithCause('Failed to resolve path to package.json', { cause: err })
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (filePathAppended) {
|
|
234
|
-
/** @type {import('node:fs').Stats} */
|
|
235
|
-
let filePathAppendedStat
|
|
236
|
-
|
|
237
|
-
try {
|
|
238
|
-
filePathAppendedStat = await stat(filePathAppended)
|
|
239
|
-
} catch (err) {
|
|
240
|
-
if (isErrnoException(err) && err.code === 'ENOENT') {
|
|
241
|
-
throw new InputError(`Expected directory '${inputPath}' to contain a package.json file`)
|
|
242
|
-
}
|
|
243
|
-
throw new ErrorWithCause('Failed to resolve package.json in directory', { cause: err })
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (!filePathAppendedStat.isFile()) {
|
|
247
|
-
throw new InputError(`Expected '${filePathAppended}' to be a file`)
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return filePathAppended
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return filePath
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Finds any complementary file to a package.json or package-lock.json
|
|
258
|
-
*
|
|
259
|
-
* @param {string} packagePath
|
|
260
|
-
* @returns {Promise<string[]>}
|
|
261
|
-
* @throws {InputError}
|
|
262
|
-
*/
|
|
263
|
-
async function findComplementaryPackageFile (packagePath) {
|
|
264
|
-
const basename = path.basename(packagePath)
|
|
265
|
-
const dirname = path.dirname(packagePath)
|
|
266
|
-
|
|
267
|
-
if (basename === 'package-lock.json') {
|
|
268
|
-
// We need the package file as well
|
|
269
|
-
return [
|
|
270
|
-
packagePath,
|
|
271
|
-
path.resolve(dirname, 'package.json')
|
|
272
|
-
]
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (basename === 'package.json') {
|
|
276
|
-
const lockfilePath = path.resolve(dirname, 'package-lock.json')
|
|
277
|
-
try {
|
|
278
|
-
const lockfileStat = await stat(lockfilePath)
|
|
279
|
-
if (lockfileStat.isFile()) {
|
|
280
|
-
return [packagePath, lockfilePath]
|
|
281
|
-
}
|
|
282
|
-
} catch (err) {
|
|
283
|
-
if (isErrnoException(err) && err.code === 'ENOENT') {
|
|
284
|
-
return [packagePath]
|
|
285
|
-
}
|
|
286
|
-
throw new ErrorWithCause(`Unexpected error when finding a lockfile for '${packagePath}'`, { cause: err })
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
throw new InputError(`Encountered a non-file at lockfile path '${lockfilePath}'`)
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
throw new InputError(`Expected '${packagePath}' to point to a package.json or package-lock.json or to a folder containing a package.json`)
|
|
293
|
-
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { meowWithSubcommands } from '../../utils/meow-with-subcommands.js'
|
|
2
1
|
import { create } from './create.js'
|
|
3
2
|
import { view } from './view.js'
|
|
3
|
+
import { meowWithSubcommands } from '../../utils/meow-with-subcommands.js'
|
|
4
4
|
|
|
5
5
|
const description = 'Project report related commands'
|
|
6
6
|
|
|
@@ -7,7 +7,7 @@ import ora from 'ora'
|
|
|
7
7
|
import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js'
|
|
8
8
|
import { ChalkOrMarkdown } from '../../utils/chalk-markdown.js'
|
|
9
9
|
import { InputError } from '../../utils/errors.js'
|
|
10
|
-
import {
|
|
10
|
+
import { getSeverityCount, formatSeverityCount } from '../../utils/format-issues.js'
|
|
11
11
|
import { printFlagList } from '../../utils/formatting.js'
|
|
12
12
|
import { setupSdk } from '../../utils/sdk.js'
|
|
13
13
|
|
|
@@ -18,22 +18,32 @@ export const view = {
|
|
|
18
18
|
const name = parentName + ' view'
|
|
19
19
|
|
|
20
20
|
const input = setupCommand(name, view.description, argv, importMeta)
|
|
21
|
-
const result = input && await fetchReportData(input.reportId)
|
|
21
|
+
const result = input && await fetchReportData(input.reportId, input)
|
|
22
22
|
|
|
23
23
|
if (result) {
|
|
24
|
-
formatReportDataOutput(result
|
|
24
|
+
formatReportDataOutput(result, { name, ...input })
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
// Internal functions
|
|
30
30
|
|
|
31
|
+
// TODO: Share more of the flag setup inbetween the commands
|
|
32
|
+
/**
|
|
33
|
+
* @typedef CommandContext
|
|
34
|
+
* @property {boolean} includeAllIssues
|
|
35
|
+
* @property {boolean} outputJson
|
|
36
|
+
* @property {boolean} outputMarkdown
|
|
37
|
+
* @property {string} reportId
|
|
38
|
+
* @property {boolean} strict
|
|
39
|
+
*/
|
|
40
|
+
|
|
31
41
|
/**
|
|
32
42
|
* @param {string} name
|
|
33
43
|
* @param {string} description
|
|
34
44
|
* @param {readonly string[]} argv
|
|
35
45
|
* @param {ImportMeta} importMeta
|
|
36
|
-
* @returns {void|
|
|
46
|
+
* @returns {void|CommandContext}
|
|
37
47
|
*/
|
|
38
48
|
function setupCommand (name, description, argv, importMeta) {
|
|
39
49
|
const cli = meow(`
|
|
@@ -42,8 +52,10 @@ function setupCommand (name, description, argv, importMeta) {
|
|
|
42
52
|
|
|
43
53
|
Options
|
|
44
54
|
${printFlagList({
|
|
55
|
+
'--all': 'Include all issues',
|
|
45
56
|
'--json': 'Output result as json',
|
|
46
57
|
'--markdown': 'Output result as markdown',
|
|
58
|
+
'--strict': 'Exits with an error code if report result is deemed unhealthy',
|
|
47
59
|
}, 6)}
|
|
48
60
|
|
|
49
61
|
Examples
|
|
@@ -53,9 +65,8 @@ function setupCommand (name, description, argv, importMeta) {
|
|
|
53
65
|
description,
|
|
54
66
|
importMeta,
|
|
55
67
|
flags: {
|
|
56
|
-
|
|
68
|
+
all: {
|
|
57
69
|
type: 'boolean',
|
|
58
|
-
alias: 'd',
|
|
59
70
|
default: false,
|
|
60
71
|
},
|
|
61
72
|
json: {
|
|
@@ -68,14 +79,20 @@ function setupCommand (name, description, argv, importMeta) {
|
|
|
68
79
|
alias: 'm',
|
|
69
80
|
default: false,
|
|
70
81
|
},
|
|
82
|
+
strict: {
|
|
83
|
+
type: 'boolean',
|
|
84
|
+
default: false,
|
|
85
|
+
},
|
|
71
86
|
}
|
|
72
87
|
})
|
|
73
88
|
|
|
74
89
|
// Extract the input
|
|
75
90
|
|
|
76
91
|
const {
|
|
92
|
+
all: includeAllIssues,
|
|
77
93
|
json: outputJson,
|
|
78
94
|
markdown: outputMarkdown,
|
|
95
|
+
strict,
|
|
79
96
|
} = cli.flags
|
|
80
97
|
|
|
81
98
|
const [reportId, ...extraInput] = cli.input
|
|
@@ -92,17 +109,24 @@ function setupCommand (name, description, argv, importMeta) {
|
|
|
92
109
|
}
|
|
93
110
|
|
|
94
111
|
return {
|
|
112
|
+
includeAllIssues,
|
|
95
113
|
outputJson,
|
|
96
114
|
outputMarkdown,
|
|
97
115
|
reportId,
|
|
116
|
+
strict,
|
|
98
117
|
}
|
|
99
118
|
}
|
|
100
119
|
|
|
120
|
+
/**
|
|
121
|
+
* @typedef {import('@socketsecurity/sdk').SocketSdkReturnType<'getReport'>["data"]} ReportData
|
|
122
|
+
*/
|
|
123
|
+
|
|
101
124
|
/**
|
|
102
125
|
* @param {string} reportId
|
|
103
|
-
* @
|
|
126
|
+
* @param {Pick<CommandContext, 'includeAllIssues' | 'strict'>} context
|
|
127
|
+
* @returns {Promise<void|ReportData>}
|
|
104
128
|
*/
|
|
105
|
-
export async function fetchReportData (reportId) {
|
|
129
|
+
export async function fetchReportData (reportId, { includeAllIssues, strict }) {
|
|
106
130
|
// Do the API call
|
|
107
131
|
|
|
108
132
|
const socketSdk = await setupSdk()
|
|
@@ -115,32 +139,42 @@ export async function fetchReportData (reportId) {
|
|
|
115
139
|
|
|
116
140
|
// Conclude the status of the API call
|
|
117
141
|
|
|
118
|
-
|
|
119
|
-
|
|
142
|
+
if (strict) {
|
|
143
|
+
if (result.data.healthy) {
|
|
144
|
+
spinner.succeed('Report result is healthy and great!')
|
|
145
|
+
} else {
|
|
146
|
+
spinner.fail('Report result deemed unhealthy for project')
|
|
147
|
+
}
|
|
148
|
+
} else if (result.data.healthy === false) {
|
|
149
|
+
const severityCount = getSeverityCount(result.data.issues, includeAllIssues ? undefined : 'high')
|
|
150
|
+
const issueSummary = formatSeverityCount(severityCount)
|
|
151
|
+
spinner.succeed(`Report has these issues: ${issueSummary}`)
|
|
152
|
+
} else {
|
|
153
|
+
spinner.succeed('Report has no issues')
|
|
154
|
+
}
|
|
120
155
|
|
|
121
|
-
return result
|
|
156
|
+
return result.data
|
|
122
157
|
}
|
|
123
158
|
|
|
124
159
|
/**
|
|
125
|
-
* @param {
|
|
126
|
-
* @param {{ name: string
|
|
160
|
+
* @param {ReportData} data
|
|
161
|
+
* @param {{ name: string } & CommandContext} context
|
|
127
162
|
* @returns {void}
|
|
128
163
|
*/
|
|
129
|
-
export function formatReportDataOutput (data, { name, outputJson, outputMarkdown, reportId }) {
|
|
130
|
-
// If JSON, output and return...
|
|
131
|
-
|
|
164
|
+
export function formatReportDataOutput (data, { name, outputJson, outputMarkdown, reportId, strict }) {
|
|
132
165
|
if (outputJson) {
|
|
133
166
|
console.log(JSON.stringify(data, undefined, 2))
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
// ...else do the CLI / Markdown output dance
|
|
167
|
+
} else {
|
|
168
|
+
const format = new ChalkOrMarkdown(!!outputMarkdown)
|
|
169
|
+
const url = `https://socket.dev/npm/reports/${encodeURIComponent(reportId)}`
|
|
138
170
|
|
|
139
|
-
|
|
140
|
-
|
|
171
|
+
console.log('\nDetailed info on socket.dev: ' + format.hyperlink(reportId, url, { fallbackToUrl: true }))
|
|
172
|
+
if (!outputMarkdown) {
|
|
173
|
+
console.log(chalk.dim('\nOr rerun', chalk.italic(name), 'using the', chalk.italic('--json'), 'flag to get full JSON output'))
|
|
174
|
+
}
|
|
175
|
+
}
|
|
141
176
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
console.log(chalk.dim('\nOr rerun', chalk.italic(name), 'using the', chalk.italic('--json'), 'flag to get full JSON output'))
|
|
177
|
+
if (strict && data.healthy === false) {
|
|
178
|
+
process.exit(1)
|
|
145
179
|
}
|
|
146
180
|
}
|
package/lib/utils/errors.js
CHANGED
|
@@ -1,2 +1,14 @@
|
|
|
1
1
|
export class AuthError extends Error {}
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
export class InputError extends Error {
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} message
|
|
6
|
+
* @param {string} [body]
|
|
7
|
+
*/
|
|
8
|
+
constructor (message, body) {
|
|
9
|
+
super(message)
|
|
10
|
+
|
|
11
|
+
/** @type {string|undefined} */
|
|
12
|
+
this.body = body
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -1,15 +1,43 @@
|
|
|
1
1
|
/** @typedef {import('@socketsecurity/sdk').SocketSdkReturnType<'getIssuesByNPMPackage'>['data']} SocketIssueList */
|
|
2
2
|
/** @typedef {SocketIssueList[number]['value'] extends infer U | undefined ? U : never} SocketIssue */
|
|
3
3
|
|
|
4
|
-
import { stringJoinWithSeparateFinalSeparator } from './misc.js'
|
|
4
|
+
import { pick, stringJoinWithSeparateFinalSeparator } from './misc.js'
|
|
5
|
+
|
|
6
|
+
const SEVERITIES_BY_ORDER = /** @type {const} */ ([
|
|
7
|
+
'critical',
|
|
8
|
+
'high',
|
|
9
|
+
'middle',
|
|
10
|
+
'low',
|
|
11
|
+
])
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {SocketIssue['severity']|undefined} lowestToInclude
|
|
15
|
+
* @returns {Array<SocketIssue['severity']>}
|
|
16
|
+
*/
|
|
17
|
+
function getDesiredSeverities (lowestToInclude) {
|
|
18
|
+
/** @type {Array<SocketIssue['severity']>} */
|
|
19
|
+
const result = []
|
|
20
|
+
|
|
21
|
+
for (const severity of SEVERITIES_BY_ORDER) {
|
|
22
|
+
result.push(severity)
|
|
23
|
+
if (severity === lowestToInclude) {
|
|
24
|
+
break
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return result
|
|
29
|
+
}
|
|
5
30
|
|
|
6
31
|
/**
|
|
7
32
|
* @param {SocketIssueList} issues
|
|
33
|
+
* @param {SocketIssue['severity']} [lowestToInclude]
|
|
8
34
|
* @returns {Record<SocketIssue['severity'], number>}
|
|
9
35
|
*/
|
|
10
|
-
function getSeverityCount (issues) {
|
|
11
|
-
|
|
12
|
-
|
|
36
|
+
export function getSeverityCount (issues, lowestToInclude) {
|
|
37
|
+
const severityCount = pick(
|
|
38
|
+
{ low: 0, middle: 0, high: 0, critical: 0 },
|
|
39
|
+
getDesiredSeverities(lowestToInclude)
|
|
40
|
+
)
|
|
13
41
|
|
|
14
42
|
for (const issue of issues) {
|
|
15
43
|
const value = issue.value
|
|
@@ -27,18 +55,18 @@ function getSeverityCount (issues) {
|
|
|
27
55
|
}
|
|
28
56
|
|
|
29
57
|
/**
|
|
30
|
-
* @param {
|
|
58
|
+
* @param {Record<SocketIssue['severity'], number>} severityCount
|
|
31
59
|
* @returns {string}
|
|
32
60
|
*/
|
|
33
|
-
export function
|
|
34
|
-
|
|
61
|
+
export function formatSeverityCount (severityCount) {
|
|
62
|
+
/** @type {string[]} */
|
|
63
|
+
const summary = []
|
|
35
64
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
])
|
|
65
|
+
for (const severity of SEVERITIES_BY_ORDER) {
|
|
66
|
+
if (severityCount[severity]) {
|
|
67
|
+
summary.push(`${severityCount[severity]} ${severity}`)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
42
70
|
|
|
43
|
-
return
|
|
71
|
+
return stringJoinWithSeparateFinalSeparator(summary)
|
|
44
72
|
}
|
package/lib/utils/misc.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import { logSymbols } from './chalk-markdown.js'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* @param {boolean|undefined} printDebugLogs
|
|
3
5
|
* @returns {typeof console.error}
|
|
4
6
|
*/
|
|
5
7
|
export function createDebugLogger (printDebugLogs) {
|
|
6
|
-
|
|
8
|
+
return printDebugLogs
|
|
7
9
|
// eslint-disable-next-line no-console
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return () => {}
|
|
10
|
+
? (...params) => console.error(logSymbols.info, ...params)
|
|
11
|
+
: () => {}
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -26,3 +27,36 @@ export function stringJoinWithSeparateFinalSeparator (list, separator = ' and ')
|
|
|
26
27
|
|
|
27
28
|
return values.join(', ') + separator + finalValue
|
|
28
29
|
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Returns a new object with only the specified keys from the input object
|
|
33
|
+
*
|
|
34
|
+
* @template {Record<string,any>} T
|
|
35
|
+
* @template {keyof T} K
|
|
36
|
+
* @param {T} input
|
|
37
|
+
* @param {K[]|ReadonlyArray<K>} keys
|
|
38
|
+
* @returns {Pick<T, K>}
|
|
39
|
+
*/
|
|
40
|
+
export function pick (input, keys) {
|
|
41
|
+
/** @type {Partial<Pick<T, K>>} */
|
|
42
|
+
const result = {}
|
|
43
|
+
|
|
44
|
+
for (const key of keys) {
|
|
45
|
+
result[key] = input[key]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return /** @type {Pick<T, K>} */ (result)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {Record<string,any>} obj
|
|
53
|
+
* @returns {boolean}
|
|
54
|
+
*/
|
|
55
|
+
export function objectSome (obj) {
|
|
56
|
+
for (const key in obj) {
|
|
57
|
+
if (obj[key]) {
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { globby } from 'globby'
|
|
5
|
+
import ignore from 'ignore'
|
|
6
|
+
// @ts-ignore This package provides no types
|
|
7
|
+
import { directories } from 'ignore-by-default'
|
|
8
|
+
import { ErrorWithCause } from 'pony-cause'
|
|
9
|
+
|
|
10
|
+
import { InputError } from './errors.js'
|
|
11
|
+
import { isErrnoException } from './type-helpers.js'
|
|
12
|
+
|
|
13
|
+
/** @type {readonly string[]} */
|
|
14
|
+
const SUPPORTED_LOCKFILES = [
|
|
15
|
+
'package-lock.json',
|
|
16
|
+
'yarn.lock',
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* There are a lot of possible folders that we should not be looking in and "ignore-by-default" helps us with defining those
|
|
21
|
+
*
|
|
22
|
+
* @type {readonly string[]}
|
|
23
|
+
*/
|
|
24
|
+
const ignoreByDefault = directories()
|
|
25
|
+
|
|
26
|
+
/** @type {readonly string[]} */
|
|
27
|
+
const GLOB_IGNORE = [
|
|
28
|
+
...ignoreByDefault.map(item => '**/' + item)
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolves package.json and lockfiles from (globbed) input paths, applying relevant ignores
|
|
33
|
+
*
|
|
34
|
+
* @param {string} cwd The working directory to use when resolving paths
|
|
35
|
+
* @param {string[]} inputPaths A list of paths to folders, package.json files and/or recognized lockfiles. Supports globs.
|
|
36
|
+
* @param {import('@socketsecurity/config').SocketYml|undefined} config
|
|
37
|
+
* @param {typeof console.error} debugLog
|
|
38
|
+
* @returns {Promise<string[]>}
|
|
39
|
+
* @throws {InputError}
|
|
40
|
+
*/
|
|
41
|
+
export async function getPackageFiles (cwd, inputPaths, config, debugLog) {
|
|
42
|
+
const entries = await globby(inputPaths, {
|
|
43
|
+
absolute: true,
|
|
44
|
+
cwd,
|
|
45
|
+
expandDirectories: false,
|
|
46
|
+
gitignore: true,
|
|
47
|
+
ignore: [...GLOB_IGNORE],
|
|
48
|
+
markDirectories: true,
|
|
49
|
+
onlyFiles: false,
|
|
50
|
+
unique: true,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
debugLog(`Globbed resolved ${inputPaths.length} paths to ${entries.length} paths:`, entries)
|
|
54
|
+
|
|
55
|
+
const packageFiles = await mapGlobResultToFiles(entries)
|
|
56
|
+
|
|
57
|
+
debugLog(`Mapped ${entries.length} entries to ${packageFiles.length} files:`, packageFiles)
|
|
58
|
+
|
|
59
|
+
const includedPackageFiles = config?.projectIgnorePaths?.length
|
|
60
|
+
? ignore()
|
|
61
|
+
.add(config.projectIgnorePaths)
|
|
62
|
+
.filter(packageFiles.map(item => path.relative(cwd, item)))
|
|
63
|
+
.map(item => path.resolve(cwd, item))
|
|
64
|
+
: packageFiles
|
|
65
|
+
|
|
66
|
+
return includedPackageFiles
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Takes paths to folders, package.json and/or recognized lock files and resolves them to package.json + lockfile pairs (where possible)
|
|
71
|
+
*
|
|
72
|
+
* @param {string[]} entries
|
|
73
|
+
* @returns {Promise<string[]>}
|
|
74
|
+
* @throws {InputError}
|
|
75
|
+
*/
|
|
76
|
+
export async function mapGlobResultToFiles (entries) {
|
|
77
|
+
const packageFiles = await Promise.all(entries.map(mapGlobEntryToFiles))
|
|
78
|
+
|
|
79
|
+
const uniquePackageFiles = [...new Set(packageFiles.flat())]
|
|
80
|
+
|
|
81
|
+
return uniquePackageFiles
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Takes a single path to a folder, package.json or a recognized lock file and resolves to a package.json + lockfile pair (where possible)
|
|
86
|
+
*
|
|
87
|
+
* @param {string} entry
|
|
88
|
+
* @returns {Promise<string[]>}
|
|
89
|
+
* @throws {InputError}
|
|
90
|
+
*/
|
|
91
|
+
export async function mapGlobEntryToFiles (entry) {
|
|
92
|
+
/** @type {string|undefined} */
|
|
93
|
+
let pkgFile
|
|
94
|
+
/** @type {string|undefined} */
|
|
95
|
+
let lockFile
|
|
96
|
+
|
|
97
|
+
if (entry.endsWith('/')) {
|
|
98
|
+
// If the match is a folder and that folder contains a package.json file, then include it
|
|
99
|
+
const filePath = path.resolve(entry, 'package.json')
|
|
100
|
+
pkgFile = await fileExists(filePath) ? filePath : undefined
|
|
101
|
+
} else if (path.basename(entry) === 'package.json') {
|
|
102
|
+
// If the match is a package.json file, then include it
|
|
103
|
+
pkgFile = entry
|
|
104
|
+
} else if (SUPPORTED_LOCKFILES.includes(path.basename(entry))) {
|
|
105
|
+
// If the match is a lock file, include both it and the corresponding package.json file
|
|
106
|
+
lockFile = entry
|
|
107
|
+
pkgFile = path.resolve(path.dirname(entry), 'package.json')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// If we will include a package.json file but don't already have a corresponding lockfile, then look for one
|
|
111
|
+
if (!lockFile && pkgFile) {
|
|
112
|
+
const pkgDir = path.dirname(pkgFile)
|
|
113
|
+
|
|
114
|
+
for (const name of SUPPORTED_LOCKFILES) {
|
|
115
|
+
const lockFileAlternative = path.resolve(pkgDir, name)
|
|
116
|
+
if (await fileExists(lockFileAlternative)) {
|
|
117
|
+
lockFile = lockFileAlternative
|
|
118
|
+
break
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (pkgFile && lockFile) {
|
|
124
|
+
return [pkgFile, lockFile]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return pkgFile ? [pkgFile] : []
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param {string} filePath
|
|
132
|
+
* @returns {Promise<boolean>}
|
|
133
|
+
*/
|
|
134
|
+
export async function fileExists (filePath) {
|
|
135
|
+
/** @type {import('node:fs').Stats} */
|
|
136
|
+
let pathStat
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
pathStat = await stat(filePath)
|
|
140
|
+
} catch (err) {
|
|
141
|
+
if (isErrnoException(err) && err.code === 'ENOENT') {
|
|
142
|
+
return false
|
|
143
|
+
}
|
|
144
|
+
throw new ErrorWithCause('Error while checking if file exists', { cause: err })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!pathStat.isFile()) {
|
|
148
|
+
throw new InputError(`Expected '${filePath}' to be a file`)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return true
|
|
152
|
+
}
|
package/lib/utils/sdk.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
import { SocketSdk, createUserAgentFromPkgJson } from '@socketsecurity/sdk'
|
|
2
6
|
import isInteractive from 'is-interactive'
|
|
3
7
|
import prompts from 'prompts'
|
|
4
8
|
|
|
@@ -34,11 +38,14 @@ export async function setupSdk () {
|
|
|
34
38
|
https: new HttpsProxyAgent({ proxy: process.env['SOCKET_SECURITY_API_PROXY'] }),
|
|
35
39
|
}
|
|
36
40
|
}
|
|
41
|
+
const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), '../../package.json')
|
|
42
|
+
const packageJson = await readFile(packageJsonPath, 'utf8')
|
|
37
43
|
|
|
38
44
|
/** @type {import('@socketsecurity/sdk').SocketSdkOptions} */
|
|
39
45
|
const sdkOptions = {
|
|
40
46
|
agent,
|
|
41
47
|
baseUrl: process.env['SOCKET_SECURITY_API_BASE_URL'],
|
|
48
|
+
userAgent: createUserAgentFromPkgJson(JSON.parse(packageJson))
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
return new SocketSdk(apiKey || '', sdkOptions)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@socketsecurity/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "CLI tool for Socket.dev",
|
|
5
5
|
"homepage": "http://github.com/SocketDev/socket-cli-js",
|
|
6
6
|
"repository": {
|
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
"check:tsc": "tsc",
|
|
33
33
|
"check:type-coverage": "type-coverage --detail --strict --at-least 95 --ignore-files 'test/*'",
|
|
34
34
|
"check": "run-p -c --aggregate-output check:*",
|
|
35
|
-
"generate-types": "node lib/utils/generate-types.js > lib/types/api.d.ts",
|
|
36
35
|
"prepare": "husky install",
|
|
37
36
|
"test:mocha": "c8 --reporter=lcov --reporter text mocha 'test/**/*.spec.js'",
|
|
38
37
|
"test-ci": "run-s test:*",
|
|
@@ -42,36 +41,47 @@
|
|
|
42
41
|
"@socketsecurity/eslint-config": "^1.0.0",
|
|
43
42
|
"@tsconfig/node14": "^1.0.3",
|
|
44
43
|
"@types/chai": "^4.3.3",
|
|
44
|
+
"@types/chai-as-promised": "^7.1.5",
|
|
45
45
|
"@types/mocha": "^10.0.0",
|
|
46
|
+
"@types/mock-fs": "^4.13.1",
|
|
46
47
|
"@types/node": "^14.18.31",
|
|
47
48
|
"@types/prompts": "^2.4.1",
|
|
48
49
|
"@types/update-notifier": "^6.0.1",
|
|
49
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
50
|
-
"@typescript-eslint/parser": "^5.
|
|
50
|
+
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
|
51
|
+
"@typescript-eslint/parser": "^5.48.2",
|
|
51
52
|
"c8": "^7.12.0",
|
|
52
53
|
"chai": "^4.3.6",
|
|
54
|
+
"chai-as-promised": "^7.1.1",
|
|
53
55
|
"dependency-check": "^5.0.0-7",
|
|
54
|
-
"eslint": "^8.
|
|
56
|
+
"eslint": "^8.32.0",
|
|
55
57
|
"eslint-config-standard": "^17.0.0",
|
|
56
58
|
"eslint-config-standard-jsx": "^11.0.0",
|
|
57
|
-
"eslint-import-resolver-typescript": "^3.5.
|
|
58
|
-
"eslint-plugin-import": "^2.
|
|
59
|
+
"eslint-import-resolver-typescript": "^3.5.3",
|
|
60
|
+
"eslint-plugin-import": "^2.27.5",
|
|
59
61
|
"eslint-plugin-jsdoc": "^39.5.0",
|
|
60
|
-
"eslint-plugin-n": "^15.
|
|
62
|
+
"eslint-plugin-n": "^15.6.1",
|
|
61
63
|
"eslint-plugin-promise": "^6.1.1",
|
|
62
|
-
"eslint-plugin-react": "^7.
|
|
64
|
+
"eslint-plugin-react": "^7.32.1",
|
|
63
65
|
"eslint-plugin-react-hooks": "^4.6.0",
|
|
66
|
+
"eslint-plugin-unicorn": "^45.0.2",
|
|
64
67
|
"husky": "^8.0.1",
|
|
65
68
|
"installed-check": "^6.0.5",
|
|
66
69
|
"mocha": "^10.0.0",
|
|
70
|
+
"mock-fs": "^5.2.0",
|
|
71
|
+
"nock": "^13.3.0",
|
|
67
72
|
"npm-run-all2": "^6.0.2",
|
|
68
73
|
"type-coverage": "^2.24.1",
|
|
69
|
-
"typescript": "~4.9.
|
|
74
|
+
"typescript": "~4.9.4"
|
|
70
75
|
},
|
|
71
76
|
"dependencies": {
|
|
72
|
-
"@
|
|
77
|
+
"@apideck/better-ajv-errors": "^0.3.6",
|
|
78
|
+
"@socketsecurity/config": "^2.0.0",
|
|
79
|
+
"@socketsecurity/sdk": "^0.5.2",
|
|
73
80
|
"chalk": "^5.1.2",
|
|
81
|
+
"globby": "^13.1.3",
|
|
74
82
|
"hpagent": "^1.2.0",
|
|
83
|
+
"ignore": "^5.2.1",
|
|
84
|
+
"ignore-by-default": "^2.1.0",
|
|
75
85
|
"is-interactive": "^2.0.0",
|
|
76
86
|
"is-unicode-supported": "^1.3.0",
|
|
77
87
|
"meow": "^11.0.0",
|