@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 CHANGED
@@ -1,5 +1,6 @@
1
1
  # Socket CLI
2
2
 
3
+ [![Socket Badge](https://socket.dev/api/badge/npm/pkg/@socketsecurity/cli)](https://socket.dev/npm/package/@socketsecurity/cli)
3
4
  [![npm version](https://img.shields.io/npm/v/@socketsecurity/cli.svg?style=flat)](https://www.npmjs.com/package/@socketsecurity/cli)
4
5
  [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](https://github.com/SocketDev/eslint-config)
5
6
  [![Follow @SocketSecurity](https://img.shields.io/twitter/follow/SocketSecurity?style=social)](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
- * `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
+
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
@@ -37,6 +37,7 @@ try {
37
37
  } else if (err instanceof InputError) {
38
38
  errorTitle = 'Invalid input'
39
39
  errorMessage = err.message
40
+ errorBody = err.body
40
41
  } else if (err instanceof Error) {
41
42
  errorTitle = 'Unexpected error'
42
43
  errorMessage = messageWithCauses(err)
@@ -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 { getSeveritySummary } from '../../utils/format-issues.js'
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 result = input && await fetchPackageData(input.pkgName, input.pkgVersion)
22
+ const packageData = input && await fetchPackageData(input.pkgName, input.pkgVersion, input)
22
23
 
23
- if (result) {
24
- formatPackageDataOutput(result.data, { name, ...input })
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|{ outputJson: boolean, outputMarkdown: boolean, pkgName: string, pkgVersion: string }}
47
+ * @returns {void|CommandContext}
37
48
  */
38
- function setupCommand (name, description, argv, importMeta) {
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
- * @returns {Promise<void|import('@socketsecurity/sdk').SocketSdkReturnType<'getIssuesByNPMPackage'>>}
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 issueSummary = getSeveritySummary(result.data)
124
- spinner.succeed(`Found ${issueSummary || 'no'} issues for version ${pkgVersion} of ${pkgName}`)
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 result
164
+ return {
165
+ data: result.data,
166
+ severityCount,
167
+ }
127
168
  }
128
169
 
129
170
  /**
130
- * @param {import('@socketsecurity/sdk').SocketSdkReturnType<'getIssuesByNPMPackage'>["data"]} data
131
- * @param {{ name: string, outputJson: boolean, outputMarkdown: boolean, pkgName: string, pkgVersion: string }} context
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
- return
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
- const format = new ChalkOrMarkdown(!!outputMarkdown)
145
- const url = `https://socket.dev/npm/package/${pkgName}/overview/${pkgVersion}`
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
- console.log('\nDetailed info on socket.dev: ' + format.hyperlink(`${pkgName} v${pkgVersion}`, url, { fallbackToUrl: true }))
148
- if (!outputMarkdown) {
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 reportResult = input && await fetchReportData(reportId)
46
+ const reportData = input && await fetchReportData(reportId, { includeAllIssues, strict })
43
47
 
44
- if (reportResult) {
45
- formatReportDataOutput(reportResult.data, { name, outputJson, outputMarkdown, reportId })
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|{ cwd: string, debugLog: typeof console.error, dryRun: boolean, outputJson: boolean, outputMarkdown: boolean, packagePaths: string[], view: boolean }>}
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} ../package-lock.json
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 packagePaths = await resolvePackagePaths(cwd, cli.input)
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 {{ cwd: string, debugLog: typeof console.error, dryRun: boolean }} context
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(`${logSymbols.info} Uploading:`, packagePaths.join(`\n${logSymbols.info} Uploading:`))
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 result = await handleApiCall(socketSdk.createReportFromFilePaths(packagePaths, cwd), spinner, 'creating report')
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 {{ outputJson: boolean, outputMarkdown: boolean }} context
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 { getSeveritySummary } from '../../utils/format-issues.js'
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.data, { name, ...input })
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|{ outputJson: boolean, outputMarkdown: boolean, reportId: string }}
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
- debug: {
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
- * @returns {Promise<void|import('@socketsecurity/sdk').SocketSdkReturnType<'getReport'>>}
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
- const issueSummary = getSeveritySummary(result.data.issues)
119
- spinner.succeed(`Report contains ${issueSummary || 'no'} issues`)
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 {import('@socketsecurity/sdk').SocketSdkReturnType<'getReport'>["data"]} data
126
- * @param {{ name: string, outputJson: boolean, outputMarkdown: boolean, reportId: string }} context
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
- return
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
- const format = new ChalkOrMarkdown(!!outputMarkdown)
140
- const url = `https://socket.dev/npm/reports/${encodeURIComponent(reportId)}`
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
- 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'))
177
+ if (strict && data.healthy === false) {
178
+ process.exit(1)
145
179
  }
146
180
  }
@@ -1,2 +1,14 @@
1
1
  export class AuthError extends Error {}
2
- export class InputError extends Error {}
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
- /** @type {Record<SocketIssue['severity'], number>} */
12
- const severityCount = { low: 0, middle: 0, high: 0, critical: 0 }
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 {SocketIssueList} issues
58
+ * @param {Record<SocketIssue['severity'], number>} severityCount
31
59
  * @returns {string}
32
60
  */
33
- export function getSeveritySummary (issues) {
34
- const severityCount = getSeverityCount(issues)
61
+ export function formatSeverityCount (severityCount) {
62
+ /** @type {string[]} */
63
+ const summary = []
35
64
 
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
- ])
65
+ for (const severity of SEVERITIES_BY_ORDER) {
66
+ if (severityCount[severity]) {
67
+ summary.push(`${severityCount[severity]} ${severity}`)
68
+ }
69
+ }
42
70
 
43
- return issueSummary
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
- if (printDebugLogs) {
8
+ return printDebugLogs
7
9
  // eslint-disable-next-line no-console
8
- return console.error.bind(console)
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 { SocketSdk } from '@socketsecurity/sdk'
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.2.1",
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.44.0",
50
- "@typescript-eslint/parser": "^5.44.0",
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.28.0",
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.2",
58
- "eslint-plugin-import": "^2.26.0",
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.5.1",
62
+ "eslint-plugin-n": "^15.6.1",
61
63
  "eslint-plugin-promise": "^6.1.1",
62
- "eslint-plugin-react": "^7.31.11",
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.3"
74
+ "typescript": "~4.9.4"
70
75
  },
71
76
  "dependencies": {
72
- "@socketsecurity/sdk": "^0.4.0",
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",