@socketsecurity/cli 0.2.1 → 0.3.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 +18 -1
- package/cli.js +1 -0
- package/lib/commands/info/index.js +66 -26
- package/lib/commands/report/create.js +75 -121
- package/lib/commands/report/view.js +60 -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/package.json +12 -2
package/README.md
CHANGED
|
@@ -22,7 +22,15 @@ socket report view QXU8PmK7LfH608RAwfIKdbcHgwEd_ZeWJ9QEGv05FJUQ
|
|
|
22
22
|
## Commands
|
|
23
23
|
|
|
24
24
|
* `socket info <package@version>` - looks up issues for a package
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
* `socket report create <path(s)-to-folder-or-file>` - creates a report on [socket.dev](https://socket.dev/)
|
|
27
|
+
|
|
28
|
+
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.
|
|
29
|
+
|
|
30
|
+
Supports globbing such as `**/package.json`.
|
|
31
|
+
|
|
32
|
+
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)
|
|
33
|
+
|
|
26
34
|
* `socket report view <report-id>` - looks up issues and scores from a report
|
|
27
35
|
|
|
28
36
|
## Flags
|
|
@@ -36,6 +44,11 @@ socket report view QXU8PmK7LfH608RAwfIKdbcHgwEd_ZeWJ9QEGv05FJUQ
|
|
|
36
44
|
* `--json` - outputs result as json which you can then pipe into [`jq`](https://stedolan.github.io/jq/) and other tools
|
|
37
45
|
* `--markdown` - outputs result as markdown which you can then copy into an issue, PR or even chat
|
|
38
46
|
|
|
47
|
+
## Strictness flags
|
|
48
|
+
|
|
49
|
+
* `--all` - by default only `high` and `critical` issues are included, by setting this flag all issues will be included
|
|
50
|
+
* `--strict` - when set, exits with an error code if any issues were found
|
|
51
|
+
|
|
39
52
|
### Other flags
|
|
40
53
|
|
|
41
54
|
* `--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 +56,10 @@ socket report view QXU8PmK7LfH608RAwfIKdbcHgwEd_ZeWJ9QEGv05FJUQ
|
|
|
43
56
|
* `--help` - prints the help for the current command. All CLI tools should have this flag
|
|
44
57
|
* `--version` - prints the version of the tool. All CLI tools should have this flag
|
|
45
58
|
|
|
59
|
+
## Configuration files
|
|
60
|
+
|
|
61
|
+
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.
|
|
62
|
+
|
|
46
63
|
## Environment variables
|
|
47
64
|
|
|
48
65
|
* `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,8 +1,9 @@
|
|
|
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'
|
|
@@ -12,8 +13,8 @@ import { ChalkOrMarkdown, logSymbols } from '../../utils/chalk-markdown.js'
|
|
|
12
13
|
import { InputError } from '../../utils/errors.js'
|
|
13
14
|
import { printFlagList } from '../../utils/formatting.js'
|
|
14
15
|
import { createDebugLogger } from '../../utils/misc.js'
|
|
16
|
+
import { getPackageFiles } from '../../utils/path-resolve.js'
|
|
15
17
|
import { setupSdk } from '../../utils/sdk.js'
|
|
16
|
-
import { isErrnoException } from '../../utils/type-helpers.js'
|
|
17
18
|
import { fetchReportData, formatReportDataOutput } from './view.js'
|
|
18
19
|
|
|
19
20
|
/** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */
|
|
@@ -29,9 +30,11 @@ export const create = {
|
|
|
29
30
|
cwd,
|
|
30
31
|
debugLog,
|
|
31
32
|
dryRun,
|
|
33
|
+
includeAllIssues,
|
|
32
34
|
outputJson,
|
|
33
35
|
outputMarkdown,
|
|
34
36
|
packagePaths,
|
|
37
|
+
strict,
|
|
35
38
|
view,
|
|
36
39
|
} = input
|
|
37
40
|
|
|
@@ -39,10 +42,10 @@ export const create = {
|
|
|
39
42
|
|
|
40
43
|
if (result && view) {
|
|
41
44
|
const reportId = result.data.id
|
|
42
|
-
const
|
|
45
|
+
const reportData = input && await fetchReportData(reportId, { includeAllIssues, strict })
|
|
43
46
|
|
|
44
|
-
if (
|
|
45
|
-
formatReportDataOutput(
|
|
47
|
+
if (reportData) {
|
|
48
|
+
formatReportDataOutput(reportData, { includeAllIssues, name, outputJson, outputMarkdown, reportId, strict })
|
|
46
49
|
}
|
|
47
50
|
} else if (result) {
|
|
48
51
|
formatReportCreationOutput(result.data, { outputJson, outputMarkdown })
|
|
@@ -53,30 +56,56 @@ export const create = {
|
|
|
53
56
|
|
|
54
57
|
// Internal functions
|
|
55
58
|
|
|
59
|
+
/**
|
|
60
|
+
* @typedef CommandContext
|
|
61
|
+
* @property {string} cwd
|
|
62
|
+
* @property {typeof console.error} debugLog
|
|
63
|
+
* @property {boolean} dryRun
|
|
64
|
+
* @property {boolean} includeAllIssues
|
|
65
|
+
* @property {boolean} outputJson
|
|
66
|
+
* @property {boolean} outputMarkdown
|
|
67
|
+
* @property {string[]} packagePaths
|
|
68
|
+
* @property {boolean} strict
|
|
69
|
+
* @property {boolean} view
|
|
70
|
+
*/
|
|
71
|
+
|
|
56
72
|
/**
|
|
57
73
|
* @param {string} name
|
|
58
74
|
* @param {string} description
|
|
59
75
|
* @param {readonly string[]} argv
|
|
60
76
|
* @param {ImportMeta} importMeta
|
|
61
|
-
* @returns {Promise<void|
|
|
77
|
+
* @returns {Promise<void|CommandContext>}
|
|
62
78
|
*/
|
|
63
79
|
async function setupCommand (name, description, argv, importMeta) {
|
|
64
80
|
const cli = meow(`
|
|
65
81
|
Usage
|
|
66
82
|
$ ${name} <paths-to-package-folders-and-files>
|
|
67
83
|
|
|
84
|
+
Uploads the specified "package.json" and lock files and, if any folder is
|
|
85
|
+
specified, the ones found in there. Also includes the complementary
|
|
86
|
+
"package.json" and lock file to any specified. Currently "package-lock.json"
|
|
87
|
+
and "yarn.lock" are supported.
|
|
88
|
+
|
|
89
|
+
Supports globbing such as "**/package.json".
|
|
90
|
+
|
|
91
|
+
Ignores any file specified in your project's ".gitignore", your project's
|
|
92
|
+
"socket.yml" file's "projectIgnorePaths" and also has a sensible set of
|
|
93
|
+
default ignores from the "ignore-by-default" module.
|
|
94
|
+
|
|
68
95
|
Options
|
|
69
96
|
${printFlagList({
|
|
97
|
+
'--all': 'Include all issues',
|
|
70
98
|
'--debug': 'Output debug information',
|
|
71
99
|
'--dry-run': 'Only output what will be done without actually doing it',
|
|
72
100
|
'--json': 'Output result as json',
|
|
73
101
|
'--markdown': 'Output result as markdown',
|
|
102
|
+
'--strict': 'Exits with an error code if any matching issues are found',
|
|
74
103
|
'--view': 'Will wait for and return the created report'
|
|
75
104
|
}, 6)}
|
|
76
105
|
|
|
77
106
|
Examples
|
|
78
107
|
$ ${name} .
|
|
79
|
-
$ ${name}
|
|
108
|
+
$ ${name} '**/package.json'
|
|
80
109
|
$ ${name} /path/to/a/package.json /path/to/another/package.json
|
|
81
110
|
$ ${name} . --view --json
|
|
82
111
|
`, {
|
|
@@ -84,6 +113,10 @@ async function setupCommand (name, description, argv, importMeta) {
|
|
|
84
113
|
description,
|
|
85
114
|
importMeta,
|
|
86
115
|
flags: {
|
|
116
|
+
all: {
|
|
117
|
+
type: 'boolean',
|
|
118
|
+
default: false,
|
|
119
|
+
},
|
|
87
120
|
debug: {
|
|
88
121
|
type: 'boolean',
|
|
89
122
|
alias: 'd',
|
|
@@ -103,6 +136,10 @@ async function setupCommand (name, description, argv, importMeta) {
|
|
|
103
136
|
alias: 'm',
|
|
104
137
|
default: false,
|
|
105
138
|
},
|
|
139
|
+
strict: {
|
|
140
|
+
type: 'boolean',
|
|
141
|
+
default: false,
|
|
142
|
+
},
|
|
106
143
|
view: {
|
|
107
144
|
type: 'boolean',
|
|
108
145
|
alias: 'v',
|
|
@@ -112,9 +149,11 @@ async function setupCommand (name, description, argv, importMeta) {
|
|
|
112
149
|
})
|
|
113
150
|
|
|
114
151
|
const {
|
|
152
|
+
all: includeAllIssues,
|
|
115
153
|
dryRun,
|
|
116
154
|
json: outputJson,
|
|
117
155
|
markdown: outputMarkdown,
|
|
156
|
+
strict,
|
|
118
157
|
view,
|
|
119
158
|
} = cli.flags
|
|
120
159
|
|
|
@@ -125,27 +164,52 @@ async function setupCommand (name, description, argv, importMeta) {
|
|
|
125
164
|
|
|
126
165
|
const debugLog = createDebugLogger(dryRun || cli.flags.debug)
|
|
127
166
|
|
|
167
|
+
// TODO: Allow setting a custom cwd and/or configFile path?
|
|
128
168
|
const cwd = process.cwd()
|
|
129
|
-
const
|
|
169
|
+
const absoluteConfigPath = path.join(cwd, 'socket.yml')
|
|
170
|
+
|
|
171
|
+
const config = await readSocketConfig(absoluteConfigPath)
|
|
172
|
+
.catch(/** @param {unknown} cause */ cause => {
|
|
173
|
+
if (cause && typeof cause === 'object' && cause instanceof SocketValidationError) {
|
|
174
|
+
// Inspired by workbox-build: https://github.com/GoogleChrome/workbox/blob/95f97a207fd51efb3f8a653f6e3e58224183a778/packages/workbox-build/src/lib/validate-options.ts#L68-L71
|
|
175
|
+
const betterErrors = betterAjvErrors({
|
|
176
|
+
basePath: 'config',
|
|
177
|
+
data: cause.data,
|
|
178
|
+
errors: cause.validationErrors,
|
|
179
|
+
// @ts-ignore
|
|
180
|
+
schema: cause.schema,
|
|
181
|
+
})
|
|
182
|
+
throw new InputError(
|
|
183
|
+
'The socket.yml config is not valid',
|
|
184
|
+
betterErrors.map((err) => `[${err.path}] ${err.message}.${err.suggestion ? err.suggestion : ''}`).join('\n')
|
|
185
|
+
)
|
|
186
|
+
} else {
|
|
187
|
+
throw new ErrorWithCause('Failed to read socket.yml config', { cause })
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
const packagePaths = await getPackageFiles(cwd, cli.input, config, debugLog)
|
|
130
192
|
|
|
131
193
|
return {
|
|
132
194
|
cwd,
|
|
133
195
|
debugLog,
|
|
134
196
|
dryRun,
|
|
197
|
+
includeAllIssues,
|
|
135
198
|
outputJson,
|
|
136
199
|
outputMarkdown,
|
|
137
200
|
packagePaths,
|
|
201
|
+
strict,
|
|
138
202
|
view,
|
|
139
203
|
}
|
|
140
204
|
}
|
|
141
205
|
|
|
142
206
|
/**
|
|
143
207
|
* @param {string[]} packagePaths
|
|
144
|
-
* @param {
|
|
208
|
+
* @param {Pick<CommandContext, 'cwd' | 'debugLog' | 'dryRun'>} context
|
|
145
209
|
* @returns {Promise<void|import('@socketsecurity/sdk').SocketSdkReturnType<'createReport'>>}
|
|
146
210
|
*/
|
|
147
211
|
async function createReport (packagePaths, { cwd, debugLog, dryRun }) {
|
|
148
|
-
debugLog(
|
|
212
|
+
debugLog('Uploading:', packagePaths.join(`\n${logSymbols.info} Uploading: `))
|
|
149
213
|
|
|
150
214
|
if (dryRun) {
|
|
151
215
|
return
|
|
@@ -168,7 +232,7 @@ async function createReport (packagePaths, { cwd, debugLog, dryRun }) {
|
|
|
168
232
|
|
|
169
233
|
/**
|
|
170
234
|
* @param {import('@socketsecurity/sdk').SocketSdkReturnType<'createReport'>["data"]} data
|
|
171
|
-
* @param {
|
|
235
|
+
* @param {Pick<CommandContext, 'outputJson' | 'outputMarkdown'>} context
|
|
172
236
|
* @returns {void}
|
|
173
237
|
*/
|
|
174
238
|
function formatReportCreationOutput (data, { outputJson, outputMarkdown }) {
|
|
@@ -181,113 +245,3 @@ function formatReportCreationOutput (data, { outputJson, outputMarkdown }) {
|
|
|
181
245
|
|
|
182
246
|
console.log('\nNew report: ' + format.hyperlink(data.id, data.url, { fallbackToUrl: true }))
|
|
183
247
|
}
|
|
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
|
-
}
|
|
@@ -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,22 +19,32 @@ export const view = {
|
|
|
18
19
|
const name = parentName + ' view'
|
|
19
20
|
|
|
20
21
|
const input = setupCommand(name, view.description, argv, importMeta)
|
|
21
|
-
const result = input && await fetchReportData(input.reportId)
|
|
22
|
+
const result = input && await fetchReportData(input.reportId, input)
|
|
22
23
|
|
|
23
24
|
if (result) {
|
|
24
|
-
formatReportDataOutput(result
|
|
25
|
+
formatReportDataOutput(result, { name, ...input })
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
// Internal functions
|
|
30
31
|
|
|
32
|
+
// TODO: Share more of the flag setup inbetween the commands
|
|
33
|
+
/**
|
|
34
|
+
* @typedef CommandContext
|
|
35
|
+
* @property {boolean} includeAllIssues
|
|
36
|
+
* @property {boolean} outputJson
|
|
37
|
+
* @property {boolean} outputMarkdown
|
|
38
|
+
* @property {string} reportId
|
|
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(`
|
|
@@ -42,8 +53,10 @@ function setupCommand (name, description, argv, importMeta) {
|
|
|
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
|
|
@@ -53,9 +66,8 @@ function setupCommand (name, description, argv, importMeta) {
|
|
|
53
66
|
description,
|
|
54
67
|
importMeta,
|
|
55
68
|
flags: {
|
|
56
|
-
|
|
69
|
+
all: {
|
|
57
70
|
type: 'boolean',
|
|
58
|
-
alias: 'd',
|
|
59
71
|
default: false,
|
|
60
72
|
},
|
|
61
73
|
json: {
|
|
@@ -68,14 +80,20 @@ function setupCommand (name, description, argv, importMeta) {
|
|
|
68
80
|
alias: 'm',
|
|
69
81
|
default: false,
|
|
70
82
|
},
|
|
83
|
+
strict: {
|
|
84
|
+
type: 'boolean',
|
|
85
|
+
default: false,
|
|
86
|
+
},
|
|
71
87
|
}
|
|
72
88
|
})
|
|
73
89
|
|
|
74
90
|
// Extract the input
|
|
75
91
|
|
|
76
92
|
const {
|
|
93
|
+
all: includeAllIssues,
|
|
77
94
|
json: outputJson,
|
|
78
95
|
markdown: outputMarkdown,
|
|
96
|
+
strict,
|
|
79
97
|
} = cli.flags
|
|
80
98
|
|
|
81
99
|
const [reportId, ...extraInput] = cli.input
|
|
@@ -92,17 +110,26 @@ function setupCommand (name, description, argv, importMeta) {
|
|
|
92
110
|
}
|
|
93
111
|
|
|
94
112
|
return {
|
|
113
|
+
includeAllIssues,
|
|
95
114
|
outputJson,
|
|
96
115
|
outputMarkdown,
|
|
97
116
|
reportId,
|
|
117
|
+
strict,
|
|
98
118
|
}
|
|
99
119
|
}
|
|
100
120
|
|
|
121
|
+
/**
|
|
122
|
+
* @typedef ReportData
|
|
123
|
+
* @property {import('@socketsecurity/sdk').SocketSdkReturnType<'getReport'>["data"]} data
|
|
124
|
+
* @property {Record<import('../../utils/format-issues').SocketIssue['severity'], number>} severityCount
|
|
125
|
+
*/
|
|
126
|
+
|
|
101
127
|
/**
|
|
102
128
|
* @param {string} reportId
|
|
103
|
-
* @
|
|
129
|
+
* @param {Pick<CommandContext, 'includeAllIssues' | 'strict'>} context
|
|
130
|
+
* @returns {Promise<void|ReportData>}
|
|
104
131
|
*/
|
|
105
|
-
export async function fetchReportData (reportId) {
|
|
132
|
+
export async function fetchReportData (reportId, { includeAllIssues, strict }) {
|
|
106
133
|
// Do the API call
|
|
107
134
|
|
|
108
135
|
const socketSdk = await setupSdk()
|
|
@@ -115,32 +142,40 @@ export async function fetchReportData (reportId) {
|
|
|
115
142
|
|
|
116
143
|
// Conclude the status of the API call
|
|
117
144
|
|
|
118
|
-
const
|
|
119
|
-
spinner.succeed(`Report contains ${issueSummary || 'no'} issues`)
|
|
145
|
+
const severityCount = getSeverityCount(result.data.issues, includeAllIssues ? undefined : 'high')
|
|
120
146
|
|
|
121
|
-
|
|
147
|
+
if (objectSome(severityCount)) {
|
|
148
|
+
const issueSummary = formatSeverityCount(severityCount)
|
|
149
|
+
spinner[strict ? 'fail' : 'succeed'](`Report has these issues: ${issueSummary}`)
|
|
150
|
+
} else {
|
|
151
|
+
spinner.succeed('Report has no issues')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
data: result.data,
|
|
156
|
+
severityCount,
|
|
157
|
+
}
|
|
122
158
|
}
|
|
123
159
|
|
|
124
160
|
/**
|
|
125
|
-
* @param {
|
|
126
|
-
* @param {{ name: string
|
|
161
|
+
* @param {ReportData} reportData
|
|
162
|
+
* @param {{ name: string } & CommandContext} context
|
|
127
163
|
* @returns {void}
|
|
128
164
|
*/
|
|
129
|
-
export function formatReportDataOutput (data, { name, outputJson, outputMarkdown, reportId }) {
|
|
130
|
-
// If JSON, output and return...
|
|
131
|
-
|
|
165
|
+
export function formatReportDataOutput ({ data, severityCount }, { name, outputJson, outputMarkdown, reportId, strict }) {
|
|
132
166
|
if (outputJson) {
|
|
133
167
|
console.log(JSON.stringify(data, undefined, 2))
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
// ...else do the CLI / Markdown output dance
|
|
168
|
+
} else {
|
|
169
|
+
const format = new ChalkOrMarkdown(!!outputMarkdown)
|
|
170
|
+
const url = `https://socket.dev/npm/reports/${encodeURIComponent(reportId)}`
|
|
138
171
|
|
|
139
|
-
|
|
140
|
-
|
|
172
|
+
console.log('\nDetailed info on socket.dev: ' + format.hyperlink(reportId, url, { fallbackToUrl: true }))
|
|
173
|
+
if (!outputMarkdown) {
|
|
174
|
+
console.log(chalk.dim('\nOr rerun', chalk.italic(name), 'using the', chalk.italic('--json'), 'flag to get full JSON output'))
|
|
175
|
+
}
|
|
176
|
+
}
|
|
141
177
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
console.log(chalk.dim('\nOr rerun', chalk.italic(name), 'using the', chalk.italic('--json'), 'flag to get full JSON output'))
|
|
178
|
+
if (strict && objectSome(severityCount)) {
|
|
179
|
+
process.exit(1)
|
|
145
180
|
}
|
|
146
181
|
}
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@socketsecurity/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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,7 +41,9 @@
|
|
|
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",
|
|
@@ -50,6 +51,7 @@
|
|
|
50
51
|
"@typescript-eslint/parser": "^5.44.0",
|
|
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
56
|
"eslint": "^8.28.0",
|
|
55
57
|
"eslint-config-standard": "^17.0.0",
|
|
@@ -61,17 +63,25 @@
|
|
|
61
63
|
"eslint-plugin-promise": "^6.1.1",
|
|
62
64
|
"eslint-plugin-react": "^7.31.11",
|
|
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.2.9",
|
|
67
72
|
"npm-run-all2": "^6.0.2",
|
|
68
73
|
"type-coverage": "^2.24.1",
|
|
69
74
|
"typescript": "~4.9.3"
|
|
70
75
|
},
|
|
71
76
|
"dependencies": {
|
|
77
|
+
"@apideck/better-ajv-errors": "^0.3.6",
|
|
78
|
+
"@socketsecurity/config": "^1.2.0",
|
|
72
79
|
"@socketsecurity/sdk": "^0.4.0",
|
|
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",
|