Skip to content

Commit 9e9c636

Browse files
Merge pull request #123 from SocketDev/cg/updateInfoUsePURLEndpoint
Update info command with PURL endpoint
2 parents e35d387 + c754d36 commit 9e9c636

File tree

4 files changed

+155
-86
lines changed

4 files changed

+155
-86
lines changed

lib/commands/info/index.js

Lines changed: 122 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import ora from 'ora'
77
import { outputFlags, validationFlags } from '../../flags/index.js'
88
import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js'
99
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'
1212
import { printFlagList } from '../../utils/formatting.js'
1313
import { objectSome } from '../../utils/misc.js'
1414
import { FREE_API_KEY, getDefaultKey, setupSdk } from '../../utils/sdk.js'
@@ -21,25 +21,41 @@ export const info = {
2121

2222
const input = setupCommand(name, info.description, argv, importMeta)
2323
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`
2525
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)
2727
if (packageData) {
2828
formatPackageDataOutput(packageData, { name, ...input }, spinner)
2929
}
3030
}
3131
}
3232
}
3333

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+
3451
// Internal functions
3552

3653
/**
3754
* @typedef CommandContext
38-
* @property {boolean} includeAllIssues
55+
* @property {boolean} includeAlerts
3956
* @property {boolean} outputJson
4057
* @property {boolean} outputMarkdown
41-
* @property {string} pkgName
42-
* @property {string} pkgVersion
58+
* @property {string[]} packages
4359
* @property {boolean} strict
4460
*/
4561

@@ -54,18 +70,19 @@ function setupCommand (name, description, argv, importMeta) {
5470
const flags = {
5571
...outputFlags,
5672
...validationFlags,
73+
...infoFlags
5774
}
5875

5976
const cli = meow(`
6077
Usage
61-
$ ${name} <name>
78+
$ ${name} <ecosystem>:<name>@<version>
6279
6380
Options
6481
${printFlagList(flags, 6)}
6582
6683
Examples
67-
$ ${name} webtorrent
68-
84+
$ ${name} npm:webtorrent
85+
$ ${name} npm:[email protected]
6986
`, {
7087
argv,
7188
description,
@@ -74,138 +91,162 @@ function setupCommand (name, description, argv, importMeta) {
7491
})
7592

7693
const {
77-
all: includeAllIssues,
94+
alerts: includeAlerts,
7895
json: outputJson,
7996
markdown: outputMarkdown,
8097
strict,
8198
} = cli.flags
8299

83-
if (cli.input.length > 1) {
84-
throw new InputError('Only one package lookup supported at once')
85-
}
86-
87100
const [rawPkgName = ''] = cli.input
88101

89102
if (!rawPkgName) {
103+
console.error(`${chalk.bgRed('Input error')}: Please provide an ecosystem and package name`)
90104
cli.showHelp()
91105
return
92106
}
93107

94-
const versionSeparator = rawPkgName.lastIndexOf('@')
108+
const /** @type {string[]} */inputPkgs = []
95109

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+
})
98124

99125
return {
100-
includeAllIssues,
126+
includeAlerts,
101127
outputJson,
102128
outputMarkdown,
103-
pkgName,
104-
pkgVersion,
129+
packages: inputPkgs,
105130
strict,
106131
}
107132
}
108133

109134
/**
110135
* @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
114137
*/
115138

116139
/**
117-
* @param {string} pkgName
118-
* @param {string} pkgVersion
119-
* @param {Pick<CommandContext, 'includeAllIssues'>} context
140+
* @param {string[]} packages
141+
* @param {boolean} includeAlerts
120142
* @param {import('ora').Ora} spinner
121143
* @returns {Promise<void|PackageData>}
122144
*/
123-
async function fetchPackageData (pkgName, pkgVersion, { includeAllIssues }, spinner) {
145+
async function fetchPackageData (packages, includeAlerts, spinner) {
124146
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')
127147

128-
if (result.success === false) {
129-
return handleUnsuccessfulApiResponse('getIssuesByNPMPackage', result, spinner)
130-
}
148+
const components = packages.map(pkg => {
149+
return { 'purl': `pkg:${pkg}` }
150+
})
131151

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)
134160
}
135161

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()
138170

139171
return {
140-
data: result.data,
141-
severityCount,
142-
score: scoreResult.data
172+
data: result.data
143173
}
144174
}
145175

146176
/**
147-
* @param {PackageData} packageData
177+
* @param {CommandContext} data
148178
* @param {{ name: string } & CommandContext} context
149179
* @param {import('ora').Ora} spinner
150180
* @returns {void}
151181
*/
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) {
153183
if (outputJson) {
154184
console.log(JSON.stringify(data, undefined, 2))
155185
} 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+
}
176198

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+
}
189215

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+
})
192233
}
193234
}
194235

195236
/**
196-
* @param {import('@socketsecurity/sdk').SocketSdkReturnType<'getIssuesByNPMPackage'>["data"]} packageData
237+
* @param {{[key: string]: any}[]} alertsData
197238
* @param {boolean} outputMarkdown
198239
* @returns {void[]}
199240
*/
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')
202243

203244
const uniqueIssues = issueDetails.reduce((/** @type {{ [key: string]: {count: Number, label: string | undefined} }} */ acc, issue) => {
204245
const { type } = issue
205246
if (type) {
206247
if (!acc[type]) {
207248
acc[type] = {
208-
label: issue.value?.label,
249+
label: issue['type'],
209250
count: 1
210251
}
211252
} else {
@@ -217,6 +258,7 @@ function formatPackageIssuesDetails (packageData, outputMarkdown) {
217258
}, {})
218259

219260
const format = new ChalkOrMarkdown(!!outputMarkdown)
261+
220262
return Object.keys(uniqueIssues).map(issue => {
221263
const issueWithLink = format.hyperlink(`${uniqueIssues[issue]?.label}`, `https://socket.dev/npm/issue/${issue}`, { fallbackToUrl: true })
222264
if (uniqueIssues[issue]?.count === 1) {

lib/utils/format-issues.js

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const SEVERITIES_BY_ORDER = /** @type {const} */ ([
2727

2828
return result
2929
}
30-
30+
/* TODO: Delete this function when we remove the report command */
3131
/**
3232
* @param {SocketIssueList} issues
3333
* @param {SocketIssue['severity']} [lowestToInclude]
@@ -54,6 +54,33 @@ export function getSeverityCount (issues, lowestToInclude) {
5454
return severityCount
5555
}
5656

57+
/* The following function is the updated one */
58+
/**
59+
* @param {Array<SocketIssue>} issues
60+
* @param {SocketIssue['severity']} [lowestToInclude]
61+
* @returns {Record<SocketIssue['severity'], number>}
62+
*/
63+
export function getCountSeverity (issues, lowestToInclude) {
64+
const severityCount = pick(
65+
{ low: 0, middle: 0, high: 0, critical: 0 },
66+
getDesiredSeverities(lowestToInclude)
67+
)
68+
69+
for (const issue of issues) {
70+
const severity = issue.severity
71+
72+
if (!severity) {
73+
continue
74+
}
75+
76+
if (severityCount[severity] !== undefined) {
77+
severityCount[severity] += 1
78+
}
79+
}
80+
81+
return severityCount
82+
}
83+
5784
/**
5885
* @param {Record<SocketIssue['severity'], number>} severityCount
5986
* @returns {string}

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
"@cyclonedx/cdxgen": "^10.7.0",
8888
"@inquirer/select": "^2.3.5",
8989
"@socketsecurity/config": "^2.1.3",
90-
"@socketsecurity/sdk": "^1.1.1",
90+
"@socketsecurity/sdk": "^1.2.0",
9191
"chalk": "^5.3.0",
9292
"chalk-table": "^1.0.2",
9393
"execa": "^9.1.0",

0 commit comments

Comments
 (0)