@@ -7,8 +7,8 @@ import ora from 'ora'
7
7
import { outputFlags , validationFlags } from '../../flags/index.js'
8
8
import { handleApiCall , handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js'
9
9
import { ChalkOrMarkdown } from '../../utils/chalk-markdown.js'
10
- import { InputError } from '../../utils/errors .js'
11
- import { getSeverityCount , formatSeverityCount } from '../../utils/format-issues.js'
10
+ import { prepareFlags } from '../../utils/flags .js'
11
+ import { formatSeverityCount , getCountSeverity } from '../../utils/format-issues.js'
12
12
import { printFlagList } from '../../utils/formatting.js'
13
13
import { objectSome } from '../../utils/misc.js'
14
14
import { FREE_API_KEY , getDefaultKey , setupSdk } from '../../utils/sdk.js'
@@ -21,25 +21,41 @@ export const info = {
21
21
22
22
const input = setupCommand ( name , info . description , argv , importMeta )
23
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`
24
+ const spinnerText = `Looking up data for packages: ${ input . packages . join ( ', ' ) } \n`
25
25
const spinner = ora ( spinnerText ) . start ( )
26
- const packageData = await fetchPackageData ( input . pkgName , input . pkgVersion , input , spinner )
26
+ const packageData = await fetchPackageData ( input . packages , input . includeAlerts , spinner )
27
27
if ( packageData ) {
28
28
formatPackageDataOutput ( packageData , { name, ...input } , spinner )
29
29
}
30
30
}
31
31
}
32
32
}
33
33
34
+ const infoFlags = prepareFlags ( {
35
+ // At the moment in API v0, alerts and license do the same thing.
36
+ // The license parameter will be implemented later.
37
+ // license: {
38
+ // type: 'boolean',
39
+ // shortFlag: 'l',
40
+ // default: false,
41
+ // description: 'Include license - Default is false',
42
+ // },
43
+ alerts : {
44
+ type : 'boolean' ,
45
+ shortFlag : 'a' ,
46
+ default : false ,
47
+ description : 'Include alerts - Default is false' ,
48
+ }
49
+ } )
50
+
34
51
// Internal functions
35
52
36
53
/**
37
54
* @typedef CommandContext
38
- * @property {boolean } includeAllIssues
55
+ * @property {boolean } includeAlerts
39
56
* @property {boolean } outputJson
40
57
* @property {boolean } outputMarkdown
41
- * @property {string } pkgName
42
- * @property {string } pkgVersion
58
+ * @property {string[] } packages
43
59
* @property {boolean } strict
44
60
*/
45
61
@@ -54,18 +70,19 @@ function setupCommand (name, description, argv, importMeta) {
54
70
const flags = {
55
71
...outputFlags ,
56
72
...validationFlags ,
73
+ ...infoFlags
57
74
}
58
75
59
76
const cli = meow ( `
60
77
Usage
61
- $ ${ name } <name>
78
+ $ ${ name } <ecosystem>:< name>@<version >
62
79
63
80
Options
64
81
${ printFlagList ( flags , 6 ) }
65
82
66
83
Examples
67
- $ ${ name } webtorrent
68
-
84
+ $ ${ name } npm: webtorrent
85
+
69
86
` , {
70
87
argv,
71
88
description,
@@ -74,138 +91,162 @@ function setupCommand (name, description, argv, importMeta) {
74
91
} )
75
92
76
93
const {
77
- all : includeAllIssues ,
94
+ alerts : includeAlerts ,
78
95
json : outputJson ,
79
96
markdown : outputMarkdown ,
80
97
strict,
81
98
} = cli . flags
82
99
83
- if ( cli . input . length > 1 ) {
84
- throw new InputError ( 'Only one package lookup supported at once' )
85
- }
86
-
87
100
const [ rawPkgName = '' ] = cli . input
88
101
89
102
if ( ! rawPkgName ) {
103
+ console . error ( `${ chalk . bgRed ( 'Input error' ) } : Please provide an ecosystem and package name` )
90
104
cli . showHelp ( )
91
105
return
92
106
}
93
107
94
- const versionSeparator = rawPkgName . lastIndexOf ( '@' )
108
+ const /** @type { string[] } */ inputPkgs = [ ]
95
109
96
- const pkgName = versionSeparator < 1 ? rawPkgName : rawPkgName . slice ( 0 , versionSeparator )
97
- const pkgVersion = versionSeparator < 1 ? 'latest' : rawPkgName . slice ( versionSeparator + 1 )
110
+ cli . input . map ( pkg => {
111
+ const ecosystem = pkg . split ( ':' ) [ 0 ]
112
+ if ( ! ecosystem ) {
113
+ console . error ( `Package name ${ pkg } formatted incorrectly.` )
114
+ return cli . showHelp ( )
115
+ } else {
116
+ const versionSeparator = pkg . lastIndexOf ( '@' )
117
+ const ecosystemSeparator = pkg . lastIndexOf ( ecosystem )
118
+ const pkgName = versionSeparator < 1 ? pkg . slice ( ecosystemSeparator + ecosystem . length + 1 ) : pkg . slice ( ecosystemSeparator + ecosystem . length + 1 , versionSeparator )
119
+ const pkgVersion = versionSeparator < 1 ? 'latest' : pkg . slice ( versionSeparator + 1 )
120
+ inputPkgs . push ( `${ ecosystem } /${ pkgName } @${ pkgVersion } ` )
121
+ }
122
+ return inputPkgs
123
+ } )
98
124
99
125
return {
100
- includeAllIssues ,
126
+ includeAlerts ,
101
127
outputJson,
102
128
outputMarkdown,
103
- pkgName,
104
- pkgVersion,
129
+ packages : inputPkgs ,
105
130
strict,
106
131
}
107
132
}
108
133
109
134
/**
110
135
* @typedef PackageData
111
- * @property {import('@socketsecurity/sdk').SocketSdkReturnType<'getIssuesByNPMPackage'>["data"] } data
112
- * @property {Record<import('../../utils/format-issues.js').SocketIssue['severity'], number> } severityCount
113
- * @property {import('@socketsecurity/sdk').SocketSdkReturnType<'getScoreByNPMPackage'>["data"] } score
136
+ * @property {import('@socketsecurity/sdk').SocketSdkReturnType<'batchPackageFetch'>["data"] } data
114
137
*/
115
138
116
139
/**
117
- * @param {string } pkgName
118
- * @param {string } pkgVersion
119
- * @param {Pick<CommandContext, 'includeAllIssues'> } context
140
+ * @param {string[] } packages
141
+ * @param {boolean } includeAlerts
120
142
* @param {import('ora').Ora } spinner
121
143
* @returns {Promise<void|PackageData> }
122
144
*/
123
- async function fetchPackageData ( pkgName , pkgVersion , { includeAllIssues } , spinner ) {
145
+ async function fetchPackageData ( packages , includeAlerts , spinner ) {
124
146
const socketSdk = await setupSdk ( getDefaultKey ( ) || FREE_API_KEY )
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' )
127
147
128
- if ( result . success === false ) {
129
- return handleUnsuccessfulApiResponse ( 'getIssuesByNPMPackage' , result , spinner )
130
- }
148
+ const components = packages . map ( pkg => {
149
+ return { 'purl' : `pkg: ${ pkg } ` }
150
+ } )
131
151
132
- if ( scoreResult . success === false ) {
133
- return handleUnsuccessfulApiResponse ( 'getScoreByNPMPackage' , scoreResult , spinner )
152
+ const result = await handleApiCall ( socketSdk . batchPackageFetch (
153
+ { alerts : includeAlerts . toString ( ) } ,
154
+ {
155
+ components
156
+ } ) , 'looking up package' )
157
+
158
+ if ( ! result . success ) {
159
+ return handleUnsuccessfulApiResponse ( 'batchPackageFetch' , result , spinner )
134
160
}
135
161
136
- // Conclude the status of the API call
137
- const severityCount = getSeverityCount ( result . data , includeAllIssues ? undefined : 'high' )
162
+ // @ts -ignore
163
+ result . data . map ( pkg => {
164
+ const severityCount = pkg . alerts && getCountSeverity ( pkg . alerts , includeAlerts ? undefined : 'high' )
165
+ pkg . severityCount = severityCount
166
+ return pkg
167
+ } )
168
+
169
+ spinner . stop ( )
138
170
139
171
return {
140
- data : result . data ,
141
- severityCount,
142
- score : scoreResult . data
172
+ data : result . data
143
173
}
144
174
}
145
175
146
176
/**
147
- * @param {PackageData } packageData
177
+ * @param {CommandContext } data
148
178
* @param {{ name: string } & CommandContext } context
149
179
* @param {import('ora').Ora } spinner
150
180
* @returns {void }
151
181
*/
152
- function formatPackageDataOutput ( { data , severityCount , score } , { name , outputJson , outputMarkdown , pkgName , pkgVersion , strict } , spinner ) {
182
+ function formatPackageDataOutput ( /** @type { { [key: string]: any } } */ { data } , { outputJson , outputMarkdown , strict } , spinner ) {
153
183
if ( outputJson ) {
154
184
console . log ( JSON . stringify ( data , undefined , 2 ) )
155
185
} 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
- }
186
+ data . map ( ( /** @type {{[key:string]: any} } */ d ) => {
187
+ const { score, license, name, severityCount, version } = d
188
+ console . log ( `\nPackage metrics for ${ name } :` )
189
+
190
+ const scoreResult = {
191
+ 'Supply Chain Risk' : Math . floor ( score . supplyChain * 100 ) ,
192
+ 'Maintenance' : Math . floor ( score . maintenance * 100 ) ,
193
+ 'Quality' : Math . floor ( score . quality * 100 ) ,
194
+ 'Vulnerabilities' : Math . floor ( score . vulnerability * 100 ) ,
195
+ 'License' : Math . floor ( score . license * 100 ) ,
196
+ 'Overall' : Math . floor ( score . overall * 100 )
197
+ }
176
198
177
- // Link to issues list
178
- const format = new ChalkOrMarkdown ( ! ! outputMarkdown )
179
- const url = `https://socket.dev/npm/package/${ pkgName } /overview/${ pkgVersion } `
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
- }
185
- if ( ! outputMarkdown ) {
186
- console . log ( chalk . dim ( '\nOr rerun' , chalk . italic ( name ) , 'using the' , chalk . italic ( '--json' ) , 'flag to get full JSON output' ) )
187
- }
188
- }
199
+ Object . entries ( scoreResult ) . map ( score => console . log ( `- ${ score [ 0 ] } : ${ formatScore ( score [ 1 ] ) } ` ) )
200
+
201
+ // Package license
202
+ console . log ( '\nPackage license:' )
203
+ console . log ( `${ license } ` )
204
+
205
+ // Package issues list
206
+ if ( objectSome ( severityCount ) ) {
207
+ const issueSummary = formatSeverityCount ( severityCount )
208
+ console . log ( '\n' )
209
+ spinner [ strict ? 'fail' : 'succeed' ] ( `Package has these issues: ${ issueSummary } ` )
210
+ formatPackageIssuesDetails ( data . alerts , outputMarkdown )
211
+ } else if ( severityCount && ! objectSome ( severityCount ) ) {
212
+ console . log ( '\n' )
213
+ spinner . succeed ( 'Package has no issues' )
214
+ }
189
215
190
- if ( strict && objectSome ( severityCount ) ) {
191
- process . exit ( 1 )
216
+ // Link to issues list
217
+ const format = new ChalkOrMarkdown ( ! ! outputMarkdown )
218
+ const url = `https://socket.dev/npm/package/${ name } /overview/${ version } `
219
+ if ( version === 'latest' ) {
220
+ console . log ( '\nDetailed info on socket.dev: ' + format . hyperlink ( `${ name } ` , url , { fallbackToUrl : true } ) )
221
+ } else {
222
+ console . log ( '\nDetailed info on socket.dev: ' + format . hyperlink ( `${ name } v${ version } ` , url , { fallbackToUrl : true } ) )
223
+ }
224
+ if ( ! outputMarkdown ) {
225
+ console . log ( chalk . dim ( '\nOr rerun' , chalk . italic ( name ) , 'using the' , chalk . italic ( '--json' ) , 'flag to get full JSON output' ) )
226
+ }
227
+
228
+ if ( strict && objectSome ( severityCount ) ) {
229
+ process . exit ( 1 )
230
+ }
231
+ return d
232
+ } )
192
233
}
193
234
}
194
235
195
236
/**
196
- * @param {import('@socketsecurity/sdk').SocketSdkReturnType<'getIssuesByNPMPackage'>["data"] } packageData
237
+ * @param {{[key: string]: any}[] } alertsData
197
238
* @param {boolean } outputMarkdown
198
239
* @returns {void[] }
199
240
*/
200
- function formatPackageIssuesDetails ( packageData , outputMarkdown ) {
201
- const issueDetails = packageData . filter ( d => d . value ?. severity === 'high' || d . value ?. severity === 'critical' )
241
+ function formatPackageIssuesDetails ( alertsData , outputMarkdown ) {
242
+ const issueDetails = alertsData . filter ( d => d [ ' severity' ] === 'high' || d [ ' severity' ] === 'critical' )
202
243
203
244
const uniqueIssues = issueDetails . reduce ( ( /** @type {{ [key: string]: {count: Number, label: string | undefined} } } */ acc , issue ) => {
204
245
const { type } = issue
205
246
if ( type ) {
206
247
if ( ! acc [ type ] ) {
207
248
acc [ type ] = {
208
- label : issue . value ?. label ,
249
+ label : issue [ 'type' ] ,
209
250
count : 1
210
251
}
211
252
} else {
@@ -217,6 +258,7 @@ function formatPackageIssuesDetails (packageData, outputMarkdown) {
217
258
} , { } )
218
259
219
260
const format = new ChalkOrMarkdown ( ! ! outputMarkdown )
261
+
220
262
return Object . keys ( uniqueIssues ) . map ( issue => {
221
263
const issueWithLink = format . hyperlink ( `${ uniqueIssues [ issue ] ?. label } ` , `https://socket.dev/npm/issue/${ issue } ` , { fallbackToUrl : true } )
222
264
if ( uniqueIssues [ issue ] ?. count === 1 ) {
0 commit comments