@socketsecurity/cli 0.7.2 → 0.8.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
@@ -26,10 +26,10 @@ socket report view QXU8PmK7LfH608RAwfIKdbcHgwEd_ZeWJ9QEGv05FJUQ
26
26
 
27
27
  * `socket report create <path(s)-to-folder-or-file>` - creates a report on [socket.dev](https://socket.dev/)
28
28
 
29
- Uploads the specified `package.json` and lock files for JavaScript and Python dependency manifests.
29
+ Uploads the specified `package.json` and lock files for JavaScript, Python, and Go dependency manifests.
30
30
  If any folder is specified, the ones found in there recursively are uploaded.
31
31
 
32
- Supports globbing such as `**/package.json`, `**/requirements.txt`, and `**/pyproject.toml`.
32
+ Supports globbing such as `**/package.json`, `**/requirements.txt`, `**/pyproject.toml`, and `**/go.mod`.
33
33
 
34
34
  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)
35
35
 
@@ -11,7 +11,7 @@ import { InputError } from '../../utils/errors.js'
11
11
  import { getSeverityCount, formatSeverityCount } from '../../utils/format-issues.js'
12
12
  import { printFlagList } from '../../utils/formatting.js'
13
13
  import { objectSome } from '../../utils/misc.js'
14
- import { setupSdk } from '../../utils/sdk.js'
14
+ import { FREE_API_KEY, getDefaultKey, setupSdk } from '../../utils/sdk.js'
15
15
 
16
16
  /** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */
17
17
  export const info = {
@@ -124,9 +124,9 @@ function setupCommand (name, description, argv, importMeta) {
124
124
  * @returns {Promise<void|PackageData>}
125
125
  */
126
126
  async function fetchPackageData (pkgName, pkgVersion, { includeAllIssues, strict }) {
127
- const socketSdk = await setupSdk()
127
+ const socketSdk = await setupSdk(getDefaultKey() || FREE_API_KEY)
128
128
  const spinner = ora(`Looking up data for version ${pkgVersion} of ${pkgName}`).start()
129
- const result = await handleApiCall(socketSdk.getIssuesByNPMPackage(pkgName, pkgVersion), spinner, 'looking up package')
129
+ const result = await handleApiCall(socketSdk.getIssuesByNPMPackage(pkgName, pkgVersion), 'looking up package')
130
130
 
131
131
  if (result.success === false) {
132
132
  return handleUnsuccessfulApiResponse('getIssuesByNPMPackage', result, spinner)
@@ -2,10 +2,10 @@ import isInteractive from 'is-interactive'
2
2
  import meow from 'meow'
3
3
  import ora from 'ora'
4
4
  import prompts from 'prompts'
5
+ import terminalLink from 'terminal-link'
5
6
 
6
- import { ChalkOrMarkdown } from '../../utils/chalk-markdown.js'
7
7
  import { AuthError, InputError } from '../../utils/errors.js'
8
- import { setupSdk } from '../../utils/sdk.js'
8
+ import { FREE_API_KEY, setupSdk } from '../../utils/sdk.js'
9
9
  import { getSetting, updateSetting } from '../../utils/settings.js'
10
10
 
11
11
  const description = 'Socket API login'
@@ -29,38 +29,108 @@ export const login = {
29
29
  importMeta,
30
30
  })
31
31
 
32
+ /**
33
+ * @param {{aborted: boolean}} state
34
+ */
35
+ const promptAbortHandler = (state) => {
36
+ if (state.aborted) {
37
+ process.nextTick(() => process.exit(1))
38
+ }
39
+ }
40
+
32
41
  if (cli.input.length) cli.showHelp()
33
42
 
34
43
  if (!isInteractive()) {
35
44
  throw new InputError('cannot prompt for credentials in a non-interactive shell')
36
45
  }
37
- const format = new ChalkOrMarkdown(false)
38
- const { apiKey } = await prompts({
46
+ /**
47
+ * @type {{ apiKey: string }}
48
+ */
49
+ const result = await prompts({
39
50
  type: 'password',
40
51
  name: 'apiKey',
41
- message: `Enter your ${format.hyperlink(
52
+ message: `Enter your ${terminalLink(
42
53
  'Socket.dev API key',
43
54
  'https://docs.socket.dev/docs/api-keys'
44
- )}`,
55
+ )} (leave blank for a public key)`,
56
+ onState: promptAbortHandler
45
57
  })
46
58
 
47
- if (!apiKey) {
48
- ora('API key not updated').warn()
49
- return
50
- }
59
+ const apiKey = result.apiKey || FREE_API_KEY
51
60
 
52
61
  const spinner = ora('Verifying API key...').start()
53
62
 
54
- const oldKey = getSetting('apiKey')
55
- updateSetting('apiKey', apiKey)
63
+ /** @type {import('@socketsecurity/sdk').SocketSdkReturnType<'getOrganizations'>['data']} */
64
+ let orgs
65
+
56
66
  try {
57
- const sdk = await setupSdk()
58
- const quota = await sdk.getQuota()
59
- if (!quota.success) throw new AuthError()
60
- spinner.succeed(`API key ${oldKey ? 'updated' : 'set'}`)
67
+ const sdk = await setupSdk(apiKey)
68
+ const result = await sdk.getOrganizations()
69
+ if (!result.success) throw new AuthError()
70
+ orgs = result.data
71
+ spinner.succeed('API key verified\n')
61
72
  } catch (e) {
62
- updateSetting('apiKey', oldKey)
63
73
  spinner.fail('Invalid API key')
74
+ return
64
75
  }
76
+
77
+ /**
78
+ * @template T
79
+ * @param {T | null | undefined} value
80
+ * @returns {value is T}
81
+ */
82
+ const nonNullish = value => value != null
83
+
84
+ /** @type {prompts.Choice[]} */
85
+ const enforcedChoices = Object.values(orgs.organizations)
86
+ .filter(nonNullish)
87
+ .filter(org => org.plan === 'enterprise')
88
+ .map(org => ({
89
+ title: org.name,
90
+ value: org.id
91
+ }))
92
+
93
+ /** @type {string[]} */
94
+ let enforcedOrgs = []
95
+
96
+ if (enforcedChoices.length > 1) {
97
+ /**
98
+ * @type { {id: string} }
99
+ */
100
+ const { id } = await prompts({
101
+ type: 'select',
102
+ name: 'id',
103
+ hint: '\n Pick "None" if this is a personal device',
104
+ message: 'Which organization\'s policies should Socket enforce system-wide?',
105
+ choices: enforcedChoices.concat({
106
+ title: 'None',
107
+ value: null
108
+ }),
109
+ onState: promptAbortHandler
110
+ })
111
+ if (id) enforcedOrgs = [id]
112
+ } else if (enforcedChoices.length) {
113
+ /**
114
+ * @type { {confirmOrg: boolean} }
115
+ */
116
+ const { confirmOrg } = await prompts({
117
+ type: 'confirm',
118
+ name: 'confirmOrg',
119
+ message: `Should Socket enforce ${enforcedChoices[0]?.title}'s security policies system-wide?`,
120
+ initial: true,
121
+ onState: promptAbortHandler
122
+ })
123
+ if (confirmOrg) {
124
+ const existing = /** @type {undefined | {value: string}} */(enforcedChoices[0])
125
+ if (existing) {
126
+ enforcedOrgs = [existing.value]
127
+ }
128
+ }
129
+ }
130
+ // MUST DO all updateSetting ON SAME TICK TO AVOID PARTIAL WRITE
131
+ updateSetting('enforcedOrgs', enforcedOrgs)
132
+ const oldKey = getSetting('apiKey')
133
+ updateSetting('apiKey', apiKey)
134
+ spinner.succeed(`API credentials ${oldKey ? 'updated' : 'set'}`)
65
135
  }
66
136
  }
@@ -27,6 +27,7 @@ export const logout = {
27
27
  if (cli.input.length) cli.showHelp()
28
28
 
29
29
  updateSetting('apiKey', null)
30
+ updateSetting('enforcedOrgs', null)
30
31
  ora('Successfully logged out').succeed()
31
32
  }
32
33
  }
@@ -86,7 +86,7 @@ async function setupCommand (name, description, argv, importMeta) {
86
86
  ...validationFlags,
87
87
  debug: {
88
88
  type: 'boolean',
89
- alias: 'd',
89
+ shortFlag: 'd',
90
90
  default: false,
91
91
  description: 'Output debug information',
92
92
  },
@@ -97,7 +97,7 @@ async function setupCommand (name, description, argv, importMeta) {
97
97
  },
98
98
  view: {
99
99
  type: 'boolean',
100
- alias: 'v',
100
+ shortFlag: 'v',
101
101
  default: false,
102
102
  description: 'Will wait for and return the created report'
103
103
  },
@@ -107,10 +107,10 @@ async function setupCommand (name, description, argv, importMeta) {
107
107
  Usage
108
108
  $ ${name} <paths-to-package-folders-and-files>
109
109
 
110
- Uploads the specified "package.json" and lock files for JavaScript and Python dependency manifests.
110
+ Uploads the specified "package.json" and lock files for JavaScript, Python, and Go dependency manifests.
111
111
  If any folder is specified, the ones found in there recursively are uploaded.
112
112
 
113
- Supports globbing such as "**/package.json", "**/requirements.txt", and "**/pyproject.toml".
113
+ Supports globbing such as "**/package.json", "**/requirements.txt", "**/pyproject.toml", and "**/go.mod".
114
114
 
115
115
  Ignores any file specified in your project's ".gitignore", your project's
116
116
  "socket.yml" file's "projectIgnorePaths" and also has a sensible set of
@@ -118,13 +118,13 @@ async function setupCommand (name, description, argv, importMeta) {
118
118
 
119
119
  Options
120
120
  ${printFlagList({
121
- '--all': 'Include all issues',
122
- '--debug': 'Output debug information',
123
- '--dry-run': 'Only output what will be done without actually doing it',
124
- '--json': 'Output result as json',
125
- '--markdown': 'Output result as markdown',
126
- '--strict': 'Exits with an error code if any matching issues are found',
127
- '--view': 'Will wait for and return the created report'
121
+ 'all': 'Include all issues',
122
+ 'debug': 'Output debug information',
123
+ 'dry-run': 'Only output what will be done without actually doing it',
124
+ 'json': 'Output result as json',
125
+ 'markdown': 'Output result as markdown',
126
+ 'strict': 'Exits with an error code if any matching issues are found',
127
+ 'view': 'Will wait for and return the created report'
128
128
  }, 6)}
129
129
 
130
130
  Examples
@@ -185,9 +185,11 @@ async function setupCommand (name, description, argv, importMeta) {
185
185
  .then(res => {
186
186
  if (!res.success) handleUnsuccessfulApiResponse('getReportSupportedFiles', res, ora())
187
187
  return res.data
188
- }).catch(cause => {
189
- throw new ErrorWithCause('Failed getting supported files for report', { cause })
190
- })
188
+ }).catch(
189
+ /** @type {(cause: Error) => never} */
190
+ (cause) => {
191
+ throw new ErrorWithCause('Failed getting supported files for report', { cause })
192
+ })
191
193
 
192
194
  const packagePaths = await getPackageFiles(cwd, cli.input, config, supportedFiles, debugLog)
193
195
 
@@ -220,7 +222,7 @@ async function createReport (packagePaths, { config, cwd, debugLog, dryRun }) {
220
222
  const socketSdk = await setupSdk()
221
223
  const spinner = ora(`Creating report with ${packagePaths.length} package files`).start()
222
224
  const apiCall = socketSdk.createReportFromFilePaths(packagePaths, cwd, config?.issueRules)
223
- const result = await handleApiCall(apiCall, spinner, 'creating report')
225
+ const result = await handleApiCall(apiCall, 'creating report')
224
226
 
225
227
  if (result.success === false) {
226
228
  return handleUnsuccessfulApiResponse('createReport', result, spinner)
@@ -3,6 +3,7 @@
3
3
  import chalk from 'chalk'
4
4
  import meow from 'meow'
5
5
  import ora from 'ora'
6
+ import { ErrorWithCause } from 'pony-cause'
6
7
 
7
8
  import { outputFlags, validationFlags } from '../../flags/index.js'
8
9
  import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js'
@@ -102,6 +103,8 @@ function setupCommand (name, description, argv, importMeta) {
102
103
  * @typedef {import('@socketsecurity/sdk').SocketSdkReturnType<'getReport'>["data"]} ReportData
103
104
  */
104
105
 
106
+ const MAX_TIMEOUT_RETRY = 5
107
+
105
108
  /**
106
109
  * @param {string} reportId
107
110
  * @param {Pick<CommandContext, 'includeAllIssues' | 'strict'>} context
@@ -111,8 +114,22 @@ export async function fetchReportData (reportId, { includeAllIssues, strict }) {
111
114
  // Do the API call
112
115
 
113
116
  const socketSdk = await setupSdk()
114
- const spinner = ora(`Fetching report with ID ${reportId}`).start()
115
- const result = await handleApiCall(socketSdk.getReport(reportId), spinner, 'fetching report')
117
+ const spinner = ora(`Fetching report with ID ${reportId} (this could take a while)`).start()
118
+ /** @type {import('@socketsecurity/sdk').SocketSdkResultType<'getReport'> | undefined} */
119
+ let result
120
+ for (let retry = 1; !result; ++retry) {
121
+ try {
122
+ result = await handleApiCall(socketSdk.getReport(reportId), 'fetching report')
123
+ } catch (err) {
124
+ if (
125
+ retry >= MAX_TIMEOUT_RETRY ||
126
+ !(err instanceof ErrorWithCause) ||
127
+ err.cause?.cause?.response?.statusCode !== 524
128
+ ) {
129
+ throw err
130
+ }
131
+ }
132
+ }
116
133
 
117
134
  if (result.success === false) {
118
135
  return handleUnsuccessfulApiResponse('getReport', result, spinner)
@@ -147,9 +164,7 @@ export function formatReportDataOutput (data, { name, outputJson, outputMarkdown
147
164
  console.log(JSON.stringify(data, undefined, 2))
148
165
  } else {
149
166
  const format = new ChalkOrMarkdown(!!outputMarkdown)
150
- const url = `https://socket.dev/npm/reports/${encodeURIComponent(reportId)}`
151
-
152
- console.log('\nDetailed info on socket.dev: ' + format.hyperlink(reportId, url, { fallbackToUrl: true }))
167
+ console.log('\nDetailed info on socket.dev: ' + format.hyperlink(reportId, data.url, { fallbackToUrl: true }))
153
168
  if (!outputMarkdown) {
154
169
  console.log(chalk.dim('\nOr rerun', chalk.italic(name), 'using the', chalk.italic('--json'), 'flag to get full JSON output'))
155
170
  }
@@ -3,13 +3,13 @@ import { prepareFlags } from '../utils/flags.js'
3
3
  export const outputFlags = prepareFlags({
4
4
  json: {
5
5
  type: 'boolean',
6
- alias: 'j',
6
+ shortFlag: 'j',
7
7
  default: false,
8
8
  description: 'Output result as json',
9
9
  },
10
10
  markdown: {
11
11
  type: 'boolean',
12
- alias: 'm',
12
+ shortFlag: 'm',
13
13
  default: false,
14
14
  description: 'Output result as markdown',
15
15
  },
@@ -5,34 +5,34 @@ const path = require('path')
5
5
  const which = require('which')
6
6
 
7
7
  if (process.platform === 'win32') {
8
- console.error('Socket npm and socket npx wrapper Windows suppport is limited to WSL at this time.')
8
+ console.error('Socket dependency manager Windows suppport is limited to WSL at this time.')
9
9
  process.exit(1)
10
10
  }
11
11
 
12
12
  /**
13
13
  * @param {string} realDirname path to shadow/bin
14
14
  * @param {'npm' | 'npx'} binname
15
- * @returns {string} path to npm provided cli / npx bin
15
+ * @returns {string} path to original bin
16
16
  */
17
17
  function installLinks (realDirname, binname) {
18
- const realNpmShadowBinDir = realDirname
19
- // find npm being shadowed by this process
20
- const npms = which.sync(binname, {
18
+ const realShadowBinDir = realDirname
19
+ // find package manager being shadowed by this process
20
+ const bins = which.sync(binname, {
21
21
  all: true
22
22
  })
23
23
  let shadowIndex = -1
24
- const npmpath = npms.find((npmPath, i) => {
25
- const isShadow = realpathSync(path.dirname(npmPath)) === realNpmShadowBinDir
24
+ const binpath = bins.find((binPath, i) => {
25
+ const isShadow = realpathSync(path.dirname(binPath)) === realShadowBinDir
26
26
  if (isShadow) {
27
27
  shadowIndex = i
28
28
  }
29
29
  return !isShadow
30
30
  })
31
- if (npmpath && process.platform === 'win32') {
32
- return npmpath
31
+ if (binpath && process.platform === 'win32') {
32
+ return binpath
33
33
  }
34
- if (!npmpath) {
35
- console.error('Socket unable to locate npm ensure it is available in the PATH environment variable')
34
+ if (!binpath) {
35
+ console.error(`Socket unable to locate ${binname}; ensure it is available in the PATH environment variable`)
36
36
  process.exit(127)
37
37
  }
38
38
  if (shadowIndex === -1) {
@@ -45,6 +45,6 @@ function installLinks (realDirname, binname) {
45
45
  process.env['PATH']
46
46
  }`
47
47
  }
48
- return npmpath
48
+ return binpath
49
49
  }
50
50
  module.exports = installLinks