@socketsecurity/cli 0.6.0 → 0.7.2

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,9 +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 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
+ Uploads the specified `package.json` and lock files for JavaScript and Python dependency manifests.
30
+ If any folder is specified, the ones found in there recursively are uploaded.
30
31
 
31
- Supports globbing such as `**/package.json`.
32
+ Supports globbing such as `**/package.json`, `**/requirements.txt`, and `**/pyproject.toml`.
32
33
 
33
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)
34
35
 
@@ -2,3 +2,5 @@ export * from './info/index.js'
2
2
  export * from './report/index.js'
3
3
  export * from './npm/index.js'
4
4
  export * from './npx/index.js'
5
+ export * from './login/index.js'
6
+ export * from './logout/index.js'
@@ -0,0 +1,66 @@
1
+ import isInteractive from 'is-interactive'
2
+ import meow from 'meow'
3
+ import ora from 'ora'
4
+ import prompts from 'prompts'
5
+
6
+ import { ChalkOrMarkdown } from '../../utils/chalk-markdown.js'
7
+ import { AuthError, InputError } from '../../utils/errors.js'
8
+ import { setupSdk } from '../../utils/sdk.js'
9
+ import { getSetting, updateSetting } from '../../utils/settings.js'
10
+
11
+ const description = 'Socket API login'
12
+
13
+ /** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */
14
+ export const login = {
15
+ description,
16
+ run: async (argv, importMeta, { parentName }) => {
17
+ const name = parentName + ' login'
18
+ const cli = meow(`
19
+ Usage
20
+ $ ${name}
21
+
22
+ Logs into the Socket API by prompting for an API key
23
+
24
+ Examples
25
+ $ ${name}
26
+ `, {
27
+ argv,
28
+ description,
29
+ importMeta,
30
+ })
31
+
32
+ if (cli.input.length) cli.showHelp()
33
+
34
+ if (!isInteractive()) {
35
+ throw new InputError('cannot prompt for credentials in a non-interactive shell')
36
+ }
37
+ const format = new ChalkOrMarkdown(false)
38
+ const { apiKey } = await prompts({
39
+ type: 'password',
40
+ name: 'apiKey',
41
+ message: `Enter your ${format.hyperlink(
42
+ 'Socket.dev API key',
43
+ 'https://docs.socket.dev/docs/api-keys'
44
+ )}`,
45
+ })
46
+
47
+ if (!apiKey) {
48
+ ora('API key not updated').warn()
49
+ return
50
+ }
51
+
52
+ const spinner = ora('Verifying API key...').start()
53
+
54
+ const oldKey = getSetting('apiKey')
55
+ updateSetting('apiKey', apiKey)
56
+ 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'}`)
61
+ } catch (e) {
62
+ updateSetting('apiKey', oldKey)
63
+ spinner.fail('Invalid API key')
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,32 @@
1
+ import meow from 'meow'
2
+ import ora from 'ora'
3
+
4
+ import { updateSetting } from '../../utils/settings.js'
5
+
6
+ const description = 'Socket API logout'
7
+
8
+ /** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */
9
+ export const logout = {
10
+ description,
11
+ run: async (argv, importMeta, { parentName }) => {
12
+ const name = parentName + ' logout'
13
+ const cli = meow(`
14
+ Usage
15
+ $ ${name}
16
+
17
+ Logs out of the Socket API and clears all Socket credentials from disk
18
+
19
+ Examples
20
+ $ ${name}
21
+ `, {
22
+ argv,
23
+ description,
24
+ importMeta,
25
+ })
26
+
27
+ if (cli.input.length) cli.showHelp()
28
+
29
+ updateSetting('apiKey', null)
30
+ ora('Successfully logged out').succeed()
31
+ }
32
+ }
@@ -107,12 +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 and, if any folder is
111
- specified, the ones found in there. Also includes the complementary
112
- "package.json" and lock file to any specified. Currently "package-lock.json"
113
- and "yarn.lock" are supported.
110
+ Uploads the specified "package.json" and lock files for JavaScript and Python dependency manifests.
111
+ If any folder is specified, the ones found in there recursively are uploaded.
114
112
 
115
- Supports globbing such as "**/package.json".
113
+ Supports globbing such as "**/package.json", "**/requirements.txt", and "**/pyproject.toml".
116
114
 
117
115
  Ignores any file specified in your project's ".gitignore", your project's
118
116
  "socket.yml" file's "projectIgnorePaths" and also has a sensible set of
@@ -181,7 +179,17 @@ async function setupCommand (name, description, argv, importMeta) {
181
179
  }
182
180
  })
183
181
 
184
- const packagePaths = await getPackageFiles(cwd, cli.input, config, debugLog)
182
+ // TODO: setupSdk(getDefaultKey() || FREE_API_KEY)
183
+ const socketSdk = await setupSdk()
184
+ const supportedFiles = await socketSdk.getReportSupportedFiles()
185
+ .then(res => {
186
+ if (!res.success) handleUnsuccessfulApiResponse('getReportSupportedFiles', res, ora())
187
+ return res.data
188
+ }).catch(cause => {
189
+ throw new ErrorWithCause('Failed getting supported files for report', { cause })
190
+ })
191
+
192
+ const packagePaths = await getPackageFiles(cwd, cli.input, config, supportedFiles, debugLog)
185
193
 
186
194
  return {
187
195
  config,
@@ -12,6 +12,7 @@ const oraPromise = import('ora')
12
12
  const isInteractivePromise = import('is-interactive')
13
13
  const chalkPromise = import('chalk')
14
14
  const chalkMarkdownPromise = import('../utils/chalk-markdown.js')
15
+ const settingsPromise = import('../utils/settings.js')
15
16
  const ipc_version = require('../../package.json').version
16
17
 
17
18
  try {
@@ -32,7 +33,9 @@ try {
32
33
  * @typedef {import('stream').Writable} Writable
33
34
  */
34
35
 
35
- const pubToken = 'sktsec_t_--RAN5U4ivauy4w37-6aoKyYPDt5ZbaT5JBVMqiwKo_api'
36
+ const pubTokenPromise = settingsPromise.then(({ getSetting }) =>
37
+ getSetting('apiKey') || 'sktsec_t_--RAN5U4ivauy4w37-6aoKyYPDt5ZbaT5JBVMqiwKo_api'
38
+ );
36
39
 
37
40
  // shadow `npm` and `npx` to mitigate subshells
38
41
  require('./link.cjs')(fs.realpathSync(path.join(__dirname, 'bin')), 'npm')
@@ -64,6 +67,7 @@ const pkgidParts = (pkgid) => {
64
67
  async function * batchScan (
65
68
  pkgids
66
69
  ) {
70
+ const pubToken = await pubTokenPromise
67
71
  const query = {
68
72
  packages: pkgids.map(pkgid => {
69
73
  const { name, version } = pkgidParts(pkgid)
@@ -8,7 +8,7 @@ import { AuthError } from './errors.js'
8
8
  * @param {T} _name
9
9
  * @param {import('@socketsecurity/sdk').SocketSdkErrorType<T>} result
10
10
  * @param {import('ora').Ora} spinner
11
- * @returns {void}
11
+ * @returns {never}
12
12
  */
13
13
  export function handleUnsuccessfulApiResponse (_name, result, spinner) {
14
14
  const resultError = 'error' in result && result.error && typeof result.error === 'object' ? result.error : {}
@@ -5,17 +5,12 @@ import { globby } from 'globby'
5
5
  import ignore from 'ignore'
6
6
  // @ts-ignore This package provides no types
7
7
  import { directories } from 'ignore-by-default'
8
+ import micromatch from 'micromatch'
8
9
  import { ErrorWithCause } from 'pony-cause'
9
10
 
10
11
  import { InputError } from './errors.js'
11
12
  import { isErrnoException } from './type-helpers.js'
12
13
 
13
- /** @type {readonly string[]} */
14
- const SUPPORTED_LOCKFILES = [
15
- 'package-lock.json',
16
- 'yarn.lock',
17
- ]
18
-
19
14
  /**
20
15
  * There are a lot of possible folders that we should not be looking in and "ignore-by-default" helps us with defining those
21
16
  *
@@ -23,10 +18,17 @@ const SUPPORTED_LOCKFILES = [
23
18
  */
24
19
  const ignoreByDefault = directories()
25
20
 
26
- /** @type {readonly string[]} */
27
- const GLOB_IGNORE = [
28
- ...ignoreByDefault.map(item => '**/' + item)
29
- ]
21
+ /** @type {import('globby').Options} */
22
+ const BASE_GLOBBY_OPTS = {
23
+ absolute: true,
24
+ expandDirectories: false,
25
+ gitignore: true,
26
+ ignore: [
27
+ ...ignoreByDefault.map(item => '**/' + item)
28
+ ],
29
+ markDirectories: true,
30
+ unique: true,
31
+ }
30
32
 
31
33
  /**
32
34
  * Resolves package.json and lockfiles from (globbed) input paths, applying relevant ignores
@@ -34,28 +36,24 @@ const GLOB_IGNORE = [
34
36
  * @param {string} cwd The working directory to use when resolving paths
35
37
  * @param {string[]} inputPaths A list of paths to folders, package.json files and/or recognized lockfiles. Supports globs.
36
38
  * @param {import('@socketsecurity/config').SocketYml|undefined} config
39
+ * @param {import('@socketsecurity/sdk').SocketSdkReturnType<"getReportSupportedFiles">['data']} supportedFiles
37
40
  * @param {typeof console.error} debugLog
38
41
  * @returns {Promise<string[]>}
39
42
  * @throws {InputError}
40
43
  */
41
- export async function getPackageFiles (cwd, inputPaths, config, debugLog) {
44
+ export async function getPackageFiles (cwd, inputPaths, config, supportedFiles, debugLog) {
42
45
  debugLog(`Globbed resolving ${inputPaths.length} paths:`, inputPaths)
43
46
 
44
47
  // TODO: Does not support `~/` paths
45
48
  const entries = await globby(inputPaths, {
46
- absolute: true,
49
+ ...BASE_GLOBBY_OPTS,
47
50
  cwd,
48
- expandDirectories: false,
49
- gitignore: true,
50
- ignore: [...GLOB_IGNORE],
51
- markDirectories: true,
52
- onlyFiles: false,
53
- unique: true,
51
+ onlyFiles: false
54
52
  })
55
53
 
56
54
  debugLog(`Globbed resolved ${inputPaths.length} paths to ${entries.length} paths:`, entries)
57
55
 
58
- const packageFiles = await mapGlobResultToFiles(entries)
56
+ const packageFiles = await mapGlobResultToFiles(entries, supportedFiles)
59
57
 
60
58
  debugLog(`Mapped ${entries.length} entries to ${packageFiles.length} files:`, packageFiles)
61
59
 
@@ -73,11 +71,14 @@ export async function getPackageFiles (cwd, inputPaths, config, debugLog) {
73
71
  * Takes paths to folders, package.json and/or recognized lock files and resolves them to package.json + lockfile pairs (where possible)
74
72
  *
75
73
  * @param {string[]} entries
74
+ * @param {import('@socketsecurity/sdk').SocketSdkReturnType<"getReportSupportedFiles">['data']} supportedFiles
76
75
  * @returns {Promise<string[]>}
77
76
  * @throws {InputError}
78
77
  */
79
- export async function mapGlobResultToFiles (entries) {
80
- const packageFiles = await Promise.all(entries.map(mapGlobEntryToFiles))
78
+ export async function mapGlobResultToFiles (entries, supportedFiles) {
79
+ const packageFiles = await Promise.all(
80
+ entries.map(entry => mapGlobEntryToFiles(entry, supportedFiles))
81
+ )
81
82
 
82
83
  const uniquePackageFiles = [...new Set(packageFiles.flat())]
83
84
 
@@ -88,46 +89,58 @@ export async function mapGlobResultToFiles (entries) {
88
89
  * Takes a single path to a folder, package.json or a recognized lock file and resolves to a package.json + lockfile pair (where possible)
89
90
  *
90
91
  * @param {string} entry
92
+ * @param {import('@socketsecurity/sdk').SocketSdkReturnType<'getReportSupportedFiles'>['data']} supportedFiles
91
93
  * @returns {Promise<string[]>}
92
94
  * @throws {InputError}
93
95
  */
94
- export async function mapGlobEntryToFiles (entry) {
96
+ export async function mapGlobEntryToFiles (entry, supportedFiles) {
95
97
  /** @type {string|undefined} */
96
- let pkgFile
97
- /** @type {string|undefined} */
98
- let lockFile
99
-
98
+ let pkgJSFile
99
+ /** @type {string[]} */
100
+ let jsLockFiles = []
101
+ /** @type {string[]} */
102
+ let pyFiles = []
103
+
104
+ const jsSupported = supportedFiles['npm'] || {}
105
+ const jsLockFilePatterns = Object.keys(jsSupported)
106
+ .filter(key => key !== 'packagejson')
107
+ .map(key => /** @type {{ pattern: string }} */ (jsSupported[key]).pattern)
108
+
109
+ const pyFilePatterns = Object.values(supportedFiles['pypi'] || {}).map(p => p.pattern)
100
110
  if (entry.endsWith('/')) {
101
111
  // If the match is a folder and that folder contains a package.json file, then include it
102
112
  const filePath = path.resolve(entry, 'package.json')
103
- pkgFile = await fileExists(filePath) ? filePath : undefined
104
- } else if (path.basename(entry) === 'package.json') {
105
- // If the match is a package.json file, then include it
106
- pkgFile = entry
107
- } else if (SUPPORTED_LOCKFILES.includes(path.basename(entry))) {
108
- // If the match is a lock file, include both it and the corresponding package.json file
109
- lockFile = entry
110
- pkgFile = path.resolve(path.dirname(entry), 'package.json')
113
+ if (await fileExists(filePath)) pkgJSFile = filePath
114
+ pyFiles = await globby(pyFilePatterns, {
115
+ ...BASE_GLOBBY_OPTS,
116
+ cwd: entry
117
+ })
118
+ } else {
119
+ const entryFile = path.basename(entry)
120
+
121
+ if (entryFile === 'package.json') {
122
+ // If the match is a package.json file, then include it
123
+ pkgJSFile = entry
124
+ } else if (micromatch.isMatch(entryFile, jsLockFilePatterns)) {
125
+ jsLockFiles = [entry]
126
+ pkgJSFile = path.resolve(path.dirname(entry), 'package.json')
127
+ if (!(await fileExists(pkgJSFile))) return []
128
+ } else if (micromatch.isMatch(entryFile, pyFilePatterns)) {
129
+ pyFiles = [entry]
130
+ }
111
131
  }
112
132
 
113
133
  // If we will include a package.json file but don't already have a corresponding lockfile, then look for one
114
- if (!lockFile && pkgFile) {
115
- const pkgDir = path.dirname(pkgFile)
116
-
117
- for (const name of SUPPORTED_LOCKFILES) {
118
- const lockFileAlternative = path.resolve(pkgDir, name)
119
- if (await fileExists(lockFileAlternative)) {
120
- lockFile = lockFileAlternative
121
- break
122
- }
123
- }
124
- }
134
+ if (!jsLockFiles.length && pkgJSFile) {
135
+ const pkgDir = path.dirname(pkgJSFile)
125
136
 
126
- if (pkgFile && lockFile) {
127
- return [pkgFile, lockFile]
137
+ jsLockFiles = await globby(jsLockFilePatterns, {
138
+ ...BASE_GLOBBY_OPTS,
139
+ cwd: pkgDir
140
+ })
128
141
  }
129
142
 
130
- return pkgFile ? [pkgFile] : []
143
+ return [...jsLockFiles, ...pyFiles].concat(pkgJSFile ? [pkgJSFile] : [])
131
144
  }
132
145
 
133
146
  /**
package/lib/utils/sdk.js CHANGED
@@ -7,28 +7,27 @@ import isInteractive from 'is-interactive'
7
7
  import prompts from 'prompts'
8
8
 
9
9
  import { AuthError } from './errors.js'
10
+ import { getSetting } from './settings.js'
10
11
 
11
12
  /**
12
- * The API key should be stored globally for the duration of the CLI execution
13
+ * This API key should be stored globally for the duration of the CLI execution
13
14
  *
14
15
  * @type {string | undefined}
15
16
  */
16
- let apiKey
17
+ let sessionAPIKey
17
18
 
18
19
  /** @returns {Promise<import('@socketsecurity/sdk').SocketSdk>} */
19
20
  export async function setupSdk () {
20
- if (!apiKey) {
21
- apiKey = process.env['SOCKET_SECURITY_API_KEY']
22
- }
21
+ let apiKey = getSetting('apiKey') || process.env['SOCKET_SECURITY_API_KEY'] || sessionAPIKey
23
22
 
24
23
  if (!apiKey && isInteractive()) {
25
24
  const input = await prompts({
26
25
  type: 'password',
27
26
  name: 'apiKey',
28
- message: 'Enter your Socket.dev API key',
27
+ message: 'Enter your Socket.dev API key (not saved)',
29
28
  })
30
29
 
31
- apiKey = input.apiKey
30
+ apiKey = sessionAPIKey = input.apiKey
32
31
  }
33
32
 
34
33
  if (!apiKey) {
@@ -0,0 +1,57 @@
1
+ import * as fs from 'fs'
2
+ import * as os from 'os'
3
+ import * as path from 'path'
4
+
5
+ import ora from 'ora'
6
+
7
+ let dataHome = process.platform === 'win32'
8
+ ? process.env['LOCALAPPDATA']
9
+ : process.env['XDG_DATA_HOME']
10
+
11
+ if (!dataHome) {
12
+ if (process.platform === 'win32') throw new Error('missing %LOCALAPPDATA%')
13
+ const home = os.homedir()
14
+ dataHome = path.join(home, ...(process.platform === 'darwin'
15
+ ? ['Library', 'Application Support']
16
+ : ['.local', 'share']
17
+ ))
18
+ }
19
+
20
+ const settingsPath = path.join(dataHome, 'socket', 'settings')
21
+
22
+ /** @type {{apiKey?: string | null}} */
23
+ let settings = {}
24
+
25
+ if (fs.existsSync(settingsPath)) {
26
+ const raw = fs.readFileSync(settingsPath, 'utf-8')
27
+ try {
28
+ settings = JSON.parse(Buffer.from(raw, 'base64').toString())
29
+ } catch (e) {
30
+ ora(`Failed to parse settings at ${settingsPath}`).warn()
31
+ }
32
+ } else {
33
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true })
34
+ }
35
+
36
+ /**
37
+ * @template {keyof typeof settings} Key
38
+ * @param {Key} key
39
+ * @returns {typeof settings[Key]}
40
+ */
41
+ export function getSetting (key) {
42
+ return settings[key]
43
+ }
44
+
45
+ /**
46
+ * @template {keyof typeof settings} Key
47
+ * @param {Key} key
48
+ * @param {typeof settings[Key]} value
49
+ * @returns {void}
50
+ */
51
+ export function updateSetting (key, value) {
52
+ settings[key] = value
53
+ fs.writeFileSync(
54
+ settingsPath,
55
+ Buffer.from(JSON.stringify(settings)).toString('base64')
56
+ )
57
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@socketsecurity/cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.2",
4
4
  "description": "CLI tool for Socket.dev",
5
5
  "homepage": "http://github.com/SocketDev/socket-cli-js",
6
6
  "repository": {
@@ -43,10 +43,11 @@
43
43
  "test": "run-s check test:*"
44
44
  },
45
45
  "devDependencies": {
46
- "@socketsecurity/eslint-config": "^2.0.0",
46
+ "@socketsecurity/eslint-config": "^3.0.1",
47
47
  "@tsconfig/node14": "^1.0.3",
48
48
  "@types/chai": "^4.3.3",
49
49
  "@types/chai-as-promised": "^7.1.5",
50
+ "@types/micromatch": "^4.0.2",
50
51
  "@types/mocha": "^10.0.1",
51
52
  "@types/mock-fs": "^4.13.1",
52
53
  "@types/node": "^14.18.31",
@@ -84,7 +85,7 @@
84
85
  "dependencies": {
85
86
  "@apideck/better-ajv-errors": "^0.3.6",
86
87
  "@socketsecurity/config": "^2.0.0",
87
- "@socketsecurity/sdk": "^0.5.4",
88
+ "@socketsecurity/sdk": "^0.6.0",
88
89
  "chalk": "^5.1.2",
89
90
  "globby": "^13.1.3",
90
91
  "hpagent": "^1.2.0",
@@ -93,6 +94,7 @@
93
94
  "is-interactive": "^2.0.0",
94
95
  "is-unicode-supported": "^1.3.0",
95
96
  "meow": "^11.0.0",
97
+ "micromatch": "^4.0.5",
96
98
  "ora": "^6.1.2",
97
99
  "pony-cause": "^2.1.8",
98
100
  "prompts": "^2.4.2",