@socketsecurity/cli 0.8.2 → 0.9.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.
@@ -20,10 +20,13 @@ export const info = {
20
20
  const name = parentName + ' info'
21
21
 
22
22
  const input = setupCommand(name, info.description, argv, importMeta)
23
- const packageData = input && await fetchPackageData(input.pkgName, input.pkgVersion, input)
24
-
25
- if (packageData) {
26
- formatPackageDataOutput(packageData, { name, ...input })
23
+ if (input) {
24
+ const spinnerText = input.pkgVersion === 'latest' ? `Looking up data for the latest version of ${input.pkgName}\n` : `Looking up data for version ${input.pkgVersion} of ${input.pkgName}\n`
25
+ const spinner = ora(spinnerText).start()
26
+ const packageData = await fetchPackageData(input.pkgName, input.pkgVersion, input, spinner)
27
+ if (packageData) {
28
+ formatPackageDataOutput(packageData, { name, ...input }, spinner)
29
+ }
27
30
  }
28
31
  }
29
32
  }
@@ -90,16 +93,8 @@ function setupCommand (name, description, argv, importMeta) {
90
93
 
91
94
  const versionSeparator = rawPkgName.lastIndexOf('@')
92
95
 
93
- if (versionSeparator < 1) {
94
- throw new InputError('Need to specify a full package identifier, like eg: webtorrent@1.0.0')
95
- }
96
-
97
- const pkgName = rawPkgName.slice(0, versionSeparator)
98
- const pkgVersion = rawPkgName.slice(versionSeparator + 1)
99
-
100
- if (!pkgVersion) {
101
- throw new InputError('Need to specify a version, like eg: webtorrent@1.0.0')
102
- }
96
+ const pkgName = versionSeparator < 1 ? rawPkgName : rawPkgName.slice(0, versionSeparator)
97
+ const pkgVersion = versionSeparator < 1 ? 'latest' : rawPkgName.slice(versionSeparator + 1)
103
98
 
104
99
  return {
105
100
  includeAllIssues,
@@ -115,53 +110,78 @@ function setupCommand (name, description, argv, importMeta) {
115
110
  * @typedef PackageData
116
111
  * @property {import('@socketsecurity/sdk').SocketSdkReturnType<'getIssuesByNPMPackage'>["data"]} data
117
112
  * @property {Record<import('../../utils/format-issues').SocketIssue['severity'], number>} severityCount
113
+ * @property {import('@socketsecurity/sdk').SocketSdkReturnType<'getScoreByNPMPackage'>["data"]} score
118
114
  */
119
115
 
120
116
  /**
121
117
  * @param {string} pkgName
122
118
  * @param {string} pkgVersion
123
- * @param {Pick<CommandContext, 'includeAllIssues' | 'strict'>} context
119
+ * @param {Pick<CommandContext, 'includeAllIssues'>} context
120
+ * @param {import('ora').Ora} spinner
124
121
  * @returns {Promise<void|PackageData>}
125
122
  */
126
- async function fetchPackageData (pkgName, pkgVersion, { includeAllIssues, strict }) {
123
+ async function fetchPackageData (pkgName, pkgVersion, { includeAllIssues }, spinner) {
127
124
  const socketSdk = await setupSdk(getDefaultKey() || FREE_API_KEY)
128
- const spinner = ora(`Looking up data for version ${pkgVersion} of ${pkgName}`).start()
129
125
  const result = await handleApiCall(socketSdk.getIssuesByNPMPackage(pkgName, pkgVersion), 'looking up package')
126
+ const scoreResult = await handleApiCall(socketSdk.getScoreByNPMPackage(pkgName, pkgVersion), 'looking up package score')
130
127
 
131
128
  if (result.success === false) {
132
129
  return handleUnsuccessfulApiResponse('getIssuesByNPMPackage', result, spinner)
133
130
  }
134
131
 
135
- // Conclude the status of the API call
132
+ if (scoreResult.success === false) {
133
+ return handleUnsuccessfulApiResponse('getScoreByNPMPackage', scoreResult, spinner)
134
+ }
136
135
 
136
+ // Conclude the status of the API call
137
137
  const severityCount = getSeverityCount(result.data, includeAllIssues ? undefined : 'high')
138
138
 
139
- if (objectSome(severityCount)) {
140
- const issueSummary = formatSeverityCount(severityCount)
141
- spinner[strict ? 'fail' : 'succeed'](`Package has these issues: ${issueSummary}`)
142
- } else {
143
- spinner.succeed('Package has no issues')
144
- }
145
-
146
139
  return {
147
140
  data: result.data,
148
141
  severityCount,
142
+ score: scoreResult.data
149
143
  }
150
144
  }
151
145
 
152
146
  /**
153
147
  * @param {PackageData} packageData
154
148
  * @param {{ name: string } & CommandContext} context
149
+ * @param {import('ora').Ora} spinner
155
150
  * @returns {void}
156
151
  */
157
- function formatPackageDataOutput ({ data, severityCount }, { name, outputJson, outputMarkdown, pkgName, pkgVersion, strict }) {
152
+ function formatPackageDataOutput ({ data, severityCount, score }, { name, outputJson, outputMarkdown, pkgName, pkgVersion, strict }, spinner) {
158
153
  if (outputJson) {
159
154
  console.log(JSON.stringify(data, undefined, 2))
160
155
  } else {
156
+ console.log('\nPackage report card:')
157
+ const scoreResult = {
158
+ 'Supply Chain Risk': Math.floor(score.supplyChainRisk.score * 100),
159
+ 'Maintenance': Math.floor(score.maintenance.score * 100),
160
+ 'Quality': Math.floor(score.quality.score * 100),
161
+ 'Vulnerabilities': Math.floor(score.vulnerability.score * 100),
162
+ 'License': Math.floor(score.license.score * 100)
163
+ }
164
+ Object.entries(scoreResult).map(score => console.log(`- ${score[0]}: ${formatScore(score[1])}`))
165
+
166
+ // Package issues list
167
+ if (objectSome(severityCount)) {
168
+ const issueSummary = formatSeverityCount(severityCount)
169
+ console.log('\n')
170
+ spinner[strict ? 'fail' : 'succeed'](`Package has these issues: ${issueSummary}`)
171
+ formatPackageIssuesDetails(data, outputMarkdown)
172
+ } else {
173
+ console.log('\n')
174
+ spinner.succeed('Package has no issues')
175
+ }
176
+
177
+ // Link to issues list
161
178
  const format = new ChalkOrMarkdown(!!outputMarkdown)
162
179
  const url = `https://socket.dev/npm/package/${pkgName}/overview/${pkgVersion}`
163
-
164
- console.log('\nDetailed info on socket.dev: ' + format.hyperlink(`${pkgName} v${pkgVersion}`, url, { fallbackToUrl: true }))
180
+ if (pkgVersion === 'latest') {
181
+ console.log('\nDetailed info on socket.dev: ' + format.hyperlink(`${pkgName}`, url, { fallbackToUrl: true }))
182
+ } else {
183
+ console.log('\nDetailed info on socket.dev: ' + format.hyperlink(`${pkgName} v${pkgVersion}`, url, { fallbackToUrl: true }))
184
+ }
165
185
  if (!outputMarkdown) {
166
186
  console.log(chalk.dim('\nOr rerun', chalk.italic(name), 'using the', chalk.italic('--json'), 'flag to get full JSON output'))
167
187
  }
@@ -171,3 +191,55 @@ async function fetchPackageData (pkgName, pkgVersion, { includeAllIssues, strict
171
191
  process.exit(1)
172
192
  }
173
193
  }
194
+
195
+ /**
196
+ * @param {import('@socketsecurity/sdk').SocketSdkReturnType<'getIssuesByNPMPackage'>["data"]} packageData
197
+ * @param {boolean} outputMarkdown
198
+ * @returns {void[]}
199
+ */
200
+ function formatPackageIssuesDetails (packageData, outputMarkdown) {
201
+ const issueDetails = packageData.filter(d => d.value?.severity === 'high' || d.value?.severity === 'critical')
202
+
203
+ const uniqueIssues = issueDetails.reduce((/** @type {{ [key: string]: {count: Number, label: string | undefined} }} */ acc, issue) => {
204
+ const { type } = issue
205
+ if (type) {
206
+ if (!acc[type]) {
207
+ acc[type] = {
208
+ label: issue.value?.label,
209
+ count: 1
210
+ }
211
+ } else {
212
+ // @ts-ignore
213
+ acc[type].count += 1
214
+ }
215
+ }
216
+ return acc
217
+ }, {})
218
+
219
+ const format = new ChalkOrMarkdown(!!outputMarkdown)
220
+ return Object.keys(uniqueIssues).map(issue => {
221
+ const issueWithLink = format.hyperlink(`${uniqueIssues[issue]?.label}`, `https://socket.dev/npm/issue/${issue}`, { fallbackToUrl: true })
222
+ if (uniqueIssues[issue]?.count === 1) {
223
+ return console.log(`- ${issueWithLink}`)
224
+ }
225
+ return console.log(`- ${issueWithLink}: ${uniqueIssues[issue]?.count}`)
226
+ })
227
+ }
228
+
229
+ /**
230
+ * @param {number} score
231
+ * @returns {string}
232
+ */
233
+ function formatScore (score) {
234
+ const error = chalk.hex('#de7c7b')
235
+ const warning = chalk.hex('#e59361')
236
+ const success = chalk.hex('#a4cb9d')
237
+
238
+ if (score > 80) {
239
+ return `${success(score)}`
240
+ } else if (score < 80 && score > 60) {
241
+ return `${warning(score)}`
242
+ } else {
243
+ return `${error(score)}`
244
+ }
245
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@socketsecurity/cli",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "description": "CLI tool for Socket.dev",
5
5
  "homepage": "http://github.com/SocketDev/socket-cli-js",
6
6
  "repository": {
@@ -82,7 +82,7 @@
82
82
  "dependencies": {
83
83
  "@apideck/better-ajv-errors": "^0.3.6",
84
84
  "@socketsecurity/config": "^2.0.0",
85
- "@socketsecurity/sdk": "^0.7.2",
85
+ "@socketsecurity/sdk": "^0.7.3",
86
86
  "chalk": "^5.1.2",
87
87
  "globby": "^13.1.3",
88
88
  "hpagent": "^1.2.0",