From ad4e5683448902584e9669d8bc8aa07b240679d3 Mon Sep 17 00:00:00 2001 From: Charlie Gerard Date: Fri, 21 Jun 2024 13:34:20 -0700 Subject: [PATCH 1/8] Update info command with PURL endpoint --- lib/commands/info/new.js | 139 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 lib/commands/info/new.js diff --git a/lib/commands/info/new.js b/lib/commands/info/new.js new file mode 100644 index 00000000..10e6def0 --- /dev/null +++ b/lib/commands/info/new.js @@ -0,0 +1,139 @@ +/* eslint-disable no-console */ + +import meow from 'meow' +import ora from 'ora' + +import { outputFlags, validationFlags } from '../../flags/index.js' +import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js' +import { InputError } from '../../utils/errors.js' +import { printFlagList } from '../../utils/formatting.js' +import { FREE_API_KEY, getDefaultKey, setupSdk } from '../../utils/sdk.js' + +/** @type {import('../../utils/meow-with-subcommands.js').CliSubcommand} */ +export const info2 = { + description: 'Look up info regarding a package', + async run (argv, importMeta, { parentName }) { + const name = parentName + ' info2' + + const input = setupCommand(name, info2.description, argv, importMeta) + if (input) { + 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` + const spinner = ora(spinnerText).start() + await fetchPackageData(input.ecosystem, input.pkgName, input.pkgVersion, spinner) + } + } +} + +// Internal functions + +/** + * @typedef CommandContext + * @property {boolean} includeAllIssues + * @property {boolean} outputJson + * @property {boolean} outputMarkdown + * @property {string} pkgName + * @property {string} pkgVersion + * @property {boolean} strict + * @property {string} ecosystem + */ + +/** + * @param {string} name + * @param {string} description + * @param {readonly string[]} argv + * @param {ImportMeta} importMeta + * @returns {void|CommandContext} + */ +function setupCommand (name, description, argv, importMeta) { + const flags = { + ...outputFlags, + ...validationFlags, + } + + const cli = meow(` + Usage + $ ${name} + + Options + ${printFlagList(flags, 6)} + + Examples + $ ${name} npm webtorrent + $ ${name} npm webtorrent@1.9.1 + `, { + argv, + description, + importMeta, + flags + }) + + const { + all: includeAllIssues, + json: outputJson, + markdown: outputMarkdown, + strict, + } = cli.flags + + if (cli.input.length > 2) { + throw new InputError('Only one package lookup supported at once') + } + + const [ecosystem = '', rawPkgName = ''] = cli.input + + if (!rawPkgName) { + cli.showHelp() + return + } + + const versionSeparator = rawPkgName.lastIndexOf('@') + + const pkgName = versionSeparator < 1 ? rawPkgName : rawPkgName.slice(0, versionSeparator) + const pkgVersion = versionSeparator < 1 ? 'latest' : rawPkgName.slice(versionSeparator + 1) + + return { + includeAllIssues, + outputJson, + outputMarkdown, + pkgName, + pkgVersion, + strict, + ecosystem + } +} + +/** + * @typedef PackageData + * @property {import('@socketsecurity/sdk').SocketSdkReturnType<'batchPackageFetch'>["data"]} data + */ + +/** + * @param {string} ecosystem + * @param {string} pkgName + * @param {string} pkgVersion + * @param {import('ora').Ora} spinner + * @returns {Promise} + */ +async function fetchPackageData (ecosystem, pkgName, pkgVersion, spinner) { + const socketSdk = await setupSdk(getDefaultKey() || FREE_API_KEY) + // @ts-ignore + const result = await handleApiCall(socketSdk.batchPackageFetch( + { license: false, alerts: false }, + { + components: + [{ + 'purl': `pkg:${ecosystem}/${pkgName}@${pkgVersion}` + }] + }), 'looking up package') + + if (!result.success) { + return handleUnsuccessfulApiResponse('batchPackageFetch', result, spinner) + } + + console.log(result.data) + + spinner.stop() + + return { + data: result.data + } +} From 0c5f031cce369bc908a0b615e1758a85aa2f0f45 Mon Sep 17 00:00:00 2001 From: Charlie Gerard Date: Mon, 24 Jun 2024 09:28:10 -0700 Subject: [PATCH 2/8] wip --- lib/commands/index.js | 2 + lib/commands/info/new.js | 79 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/lib/commands/index.js b/lib/commands/index.js index ade80f5c..9e9899b9 100644 --- a/lib/commands/index.js +++ b/lib/commands/index.js @@ -1,5 +1,7 @@ export * from './cyclonedx/index.js' export * from './info/index.js' +// @ts-ignore +export * from './info/new.js' export * from './login/index.js' export * from './logout/index.js' export * from './npm/index.js' diff --git a/lib/commands/info/new.js b/lib/commands/info/new.js index 10e6def0..b723fbfb 100644 --- a/lib/commands/info/new.js +++ b/lib/commands/info/new.js @@ -1,10 +1,12 @@ /* eslint-disable no-console */ +import chalk from 'chalk' import meow from 'meow' import ora from 'ora' import { outputFlags, validationFlags } from '../../flags/index.js' import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js' +import { ChalkOrMarkdown } from '../../utils/chalk-markdown.js' import { InputError } from '../../utils/errors.js' import { printFlagList } from '../../utils/formatting.js' import { FREE_API_KEY, getDefaultKey, setupSdk } from '../../utils/sdk.js' @@ -19,7 +21,10 @@ export const info2 = { if (input) { 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` const spinner = ora(spinnerText).start() - await fetchPackageData(input.ecosystem, input.pkgName, input.pkgVersion, spinner) + const packageData = await fetchPackageData(input.ecosystem, input.pkgName, input.pkgVersion, spinner) + if (packageData) { + formatPackageDataOutput(packageData, { name, ...input }, spinner) + } } } } @@ -117,7 +122,7 @@ async function fetchPackageData (ecosystem, pkgName, pkgVersion, spinner) { const socketSdk = await setupSdk(getDefaultKey() || FREE_API_KEY) // @ts-ignore const result = await handleApiCall(socketSdk.batchPackageFetch( - { license: false, alerts: false }, + { license: true, alerts: false }, { components: [{ @@ -129,11 +134,77 @@ async function fetchPackageData (ecosystem, pkgName, pkgVersion, spinner) { return handleUnsuccessfulApiResponse('batchPackageFetch', result, spinner) } - console.log(result.data) - spinner.stop() return { data: result.data } } + +/** + * @param {CommandContext} data + * @param {{ name: string } & CommandContext} context + * @param {import('ora').Ora} spinner + * @returns {void} + */ +function formatPackageDataOutput (/** @type {{ [key: string]: any }} */ data, { name, outputJson, outputMarkdown, pkgName, pkgVersion }, spinner) { + if (outputJson) { + console.log(JSON.stringify(data, undefined, 2)) + } else { + console.log('\nPackage report card:') + + const scoreResult = { + 'Supply Chain Risk': Math.floor(data['score'].supplyChain * 100), + 'Maintenance': Math.floor(data['score'].maintenance * 100), + 'Quality': Math.floor(data['score'].quality * 100), + 'Vulnerabilities': Math.floor(data['score'].vulnerability * 100), + 'License': Math.floor(data['score'].license * 100) + } + Object.entries(scoreResult).map(score => console.log(`- ${score[0]}: ${formatScore(score[1])}`)) + + // Package issues list + // if (objectSome(severityCount)) { + // const issueSummary = formatSeverityCount(severityCount) + // console.log('\n') + // spinner[strict ? 'fail' : 'succeed'](`Package has these issues: ${issueSummary}`) + // // formatPackageIssuesDetails(data, outputMarkdown) + // } else { + // console.log('\n') + spinner.succeed('Package has no issues') + // } + + // Link to issues list + const format = new ChalkOrMarkdown(!!outputMarkdown) + const url = `https://socket.dev/npm/package/${pkgName}/overview/${pkgVersion}` + if (pkgVersion === 'latest') { + console.log('\nDetailed info on socket.dev: ' + format.hyperlink(`${pkgName}`, url, { fallbackToUrl: true })) + } else { + console.log('\nDetailed info on socket.dev: ' + format.hyperlink(`${pkgName} v${pkgVersion}`, url, { fallbackToUrl: true })) + } + if (!outputMarkdown) { + console.log(chalk.dim('\nOr rerun', chalk.italic(name), 'using the', chalk.italic('--json'), 'flag to get full JSON output')) + } + } + + // if (strict && objectSome(severityCount)) { + // process.exit(1) + // } +} + +/** + * @param {number} score + * @returns {string} + */ +function formatScore (score) { + const error = chalk.hex('#de7c7b') + const warning = chalk.hex('#e59361') + const success = chalk.hex('#a4cb9d') + + if (score > 80) { + return `${success(score)}` + } else if (score < 80 && score > 60) { + return `${warning(score)}` + } else { + return `${error(score)}` + } +} From c2c474dd1a1fd066a1a45016395b343fe5a4af13 Mon Sep 17 00:00:00 2001 From: Charlie Gerard Date: Mon, 24 Jun 2024 13:47:34 -0700 Subject: [PATCH 3/8] wip --- lib/commands/index.js | 2 - lib/commands/info/index.js | 78 ++++++++------ lib/commands/info/new.js | 210 ------------------------------------- 3 files changed, 45 insertions(+), 245 deletions(-) delete mode 100644 lib/commands/info/new.js diff --git a/lib/commands/index.js b/lib/commands/index.js index 9e9899b9..ade80f5c 100644 --- a/lib/commands/index.js +++ b/lib/commands/index.js @@ -1,7 +1,5 @@ export * from './cyclonedx/index.js' export * from './info/index.js' -// @ts-ignore -export * from './info/new.js' export * from './login/index.js' export * from './logout/index.js' export * from './npm/index.js' diff --git a/lib/commands/info/index.js b/lib/commands/info/index.js index c6feb37c..33430f89 100644 --- a/lib/commands/info/index.js +++ b/lib/commands/info/index.js @@ -8,7 +8,7 @@ import { outputFlags, validationFlags } from '../../flags/index.js' import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js' import { ChalkOrMarkdown } from '../../utils/chalk-markdown.js' import { InputError } from '../../utils/errors.js' -import { getSeverityCount, formatSeverityCount } from '../../utils/format-issues.js' +import { formatSeverityCount, getSeverityCount } from '../../utils/format-issues.js' import { printFlagList } from '../../utils/formatting.js' import { objectSome } from '../../utils/misc.js' import { FREE_API_KEY, getDefaultKey, setupSdk } from '../../utils/sdk.js' @@ -23,7 +23,7 @@ export const info = { if (input) { 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` const spinner = ora(spinnerText).start() - const packageData = await fetchPackageData(input.pkgName, input.pkgVersion, input, spinner) + const packageData = await fetchPackageData(input.ecosystem, input.pkgName, input.pkgVersion, input, spinner) if (packageData) { formatPackageDataOutput(packageData, { name, ...input }, spinner) } @@ -41,6 +41,7 @@ export const info = { * @property {string} pkgName * @property {string} pkgVersion * @property {boolean} strict + * @property {string} ecosystem */ /** @@ -58,14 +59,14 @@ function setupCommand (name, description, argv, importMeta) { const cli = meow(` Usage - $ ${name} + $ ${name} Options ${printFlagList(flags, 6)} Examples - $ ${name} webtorrent - $ ${name} webtorrent@1.9.1 + $ ${name} npm webtorrent + $ ${name} npm webtorrent@1.9.1 `, { argv, description, @@ -80,11 +81,11 @@ function setupCommand (name, description, argv, importMeta) { strict, } = cli.flags - if (cli.input.length > 1) { + if (cli.input.length > 2) { throw new InputError('Only one package lookup supported at once') } - const [rawPkgName = ''] = cli.input + const [ecosystem = '', rawPkgName = ''] = cli.input if (!rawPkgName) { cli.showHelp() @@ -103,63 +104,73 @@ function setupCommand (name, description, argv, importMeta) { pkgName, pkgVersion, strict, + ecosystem } } /** * @typedef PackageData - * @property {import('@socketsecurity/sdk').SocketSdkReturnType<'getIssuesByNPMPackage'>["data"]} data + * @property {import('@socketsecurity/sdk').SocketSdkReturnType<'batchPackageFetch'>["data"]} data * @property {Record} severityCount - * @property {import('@socketsecurity/sdk').SocketSdkReturnType<'getScoreByNPMPackage'>["data"]} score */ /** + * @param {string} ecosystem * @param {string} pkgName * @param {string} pkgVersion * @param {Pick} context * @param {import('ora').Ora} spinner * @returns {Promise} */ -async function fetchPackageData (pkgName, pkgVersion, { includeAllIssues }, spinner) { +async function fetchPackageData (ecosystem, pkgName, pkgVersion, { includeAllIssues }, spinner) { const socketSdk = await setupSdk(getDefaultKey() || FREE_API_KEY) - const result = await handleApiCall(socketSdk.getIssuesByNPMPackage(pkgName, pkgVersion), 'looking up package') - const scoreResult = await handleApiCall(socketSdk.getScoreByNPMPackage(pkgName, pkgVersion), 'looking up package score') - - if (result.success === false) { - return handleUnsuccessfulApiResponse('getIssuesByNPMPackage', result, spinner) + // @ts-ignore + const result = await handleApiCall(socketSdk.batchPackageFetch( + { license: 'true', alerts: 'true' }, + { + components: + [{ + 'purl': `pkg:${ecosystem}/${pkgName}@${pkgVersion}` + }] + }), 'looking up package') + const alerts = await handleApiCall(socketSdk.getIssuesByNPMPackage(pkgName, pkgVersion), 'looking up package') + + if (!result.success) { + return handleUnsuccessfulApiResponse('batchPackageFetch', result, spinner) } - if (scoreResult.success === false) { - return handleUnsuccessfulApiResponse('getScoreByNPMPackage', scoreResult, spinner) + if (!alerts.success) { + return handleUnsuccessfulApiResponse('getIssuesByNPMPackage', alerts, spinner) } - // Conclude the status of the API call - const severityCount = getSeverityCount(result.data, includeAllIssues ? undefined : 'high') + const severityCount = getSeverityCount(alerts.data, includeAllIssues ? undefined : 'high') + + spinner.stop() return { data: result.data, - severityCount, - score: scoreResult.data + severityCount } } /** - * @param {PackageData} packageData + * @param {CommandContext} data * @param {{ name: string } & CommandContext} context * @param {import('ora').Ora} spinner * @returns {void} */ - function formatPackageDataOutput ({ data, severityCount, score }, { name, outputJson, outputMarkdown, pkgName, pkgVersion, strict }, spinner) { +function formatPackageDataOutput (/** @type {{ [key: string]: any }} */ { data, severityCount }, { name, outputJson, outputMarkdown, pkgName, pkgVersion, strict }, spinner) { if (outputJson) { console.log(JSON.stringify(data, undefined, 2)) } else { console.log('\nPackage report card:') + const scoreResult = { - 'Supply Chain Risk': Math.floor(score.supplyChainRisk.score * 100), - 'Maintenance': Math.floor(score.maintenance.score * 100), - 'Quality': Math.floor(score.quality.score * 100), - 'Vulnerabilities': Math.floor(score.vulnerability.score * 100), - 'License': Math.floor(score.license.score * 100) + 'Supply Chain Risk': Math.floor(data['score'].supplyChain * 100), + 'Maintenance': Math.floor(data['score'].maintenance * 100), + 'Quality': Math.floor(data['score'].quality * 100), + 'Vulnerabilities': Math.floor(data['score'].vulnerability * 100), + 'License': Math.floor(data['score'].license * 100) } Object.entries(scoreResult).map(score => console.log(`- ${score[0]}: ${formatScore(score[1])}`)) @@ -168,7 +179,7 @@ async function fetchPackageData (pkgName, pkgVersion, { includeAllIssues }, spin const issueSummary = formatSeverityCount(severityCount) console.log('\n') spinner[strict ? 'fail' : 'succeed'](`Package has these issues: ${issueSummary}`) - formatPackageIssuesDetails(data, outputMarkdown) + formatPackageIssuesDetails(data.alerts, outputMarkdown) } else { console.log('\n') spinner.succeed('Package has no issues') @@ -193,19 +204,19 @@ async function fetchPackageData (pkgName, pkgVersion, { includeAllIssues }, spin } /** - * @param {import('@socketsecurity/sdk').SocketSdkReturnType<'getIssuesByNPMPackage'>["data"]} packageData + * @param {{[key: string]: any}[]} alertsData * @param {boolean} outputMarkdown * @returns {void[]} */ -function formatPackageIssuesDetails (packageData, outputMarkdown) { - const issueDetails = packageData.filter(d => d.value?.severity === 'high' || d.value?.severity === 'critical') +function formatPackageIssuesDetails (alertsData, outputMarkdown) { + const issueDetails = alertsData.filter(d => d['severity'] === 'high' || d['severity'] === 'critical') const uniqueIssues = issueDetails.reduce((/** @type {{ [key: string]: {count: Number, label: string | undefined} }} */ acc, issue) => { const { type } = issue if (type) { if (!acc[type]) { acc[type] = { - label: issue.value?.label, + label: issue['type'], count: 1 } } else { @@ -217,6 +228,7 @@ function formatPackageIssuesDetails (packageData, outputMarkdown) { }, {}) const format = new ChalkOrMarkdown(!!outputMarkdown) + return Object.keys(uniqueIssues).map(issue => { const issueWithLink = format.hyperlink(`${uniqueIssues[issue]?.label}`, `https://socket.dev/npm/issue/${issue}`, { fallbackToUrl: true }) if (uniqueIssues[issue]?.count === 1) { diff --git a/lib/commands/info/new.js b/lib/commands/info/new.js deleted file mode 100644 index b723fbfb..00000000 --- a/lib/commands/info/new.js +++ /dev/null @@ -1,210 +0,0 @@ -/* eslint-disable no-console */ - -import chalk from 'chalk' -import meow from 'meow' -import ora from 'ora' - -import { outputFlags, validationFlags } from '../../flags/index.js' -import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js' -import { ChalkOrMarkdown } from '../../utils/chalk-markdown.js' -import { InputError } from '../../utils/errors.js' -import { printFlagList } from '../../utils/formatting.js' -import { FREE_API_KEY, getDefaultKey, setupSdk } from '../../utils/sdk.js' - -/** @type {import('../../utils/meow-with-subcommands.js').CliSubcommand} */ -export const info2 = { - description: 'Look up info regarding a package', - async run (argv, importMeta, { parentName }) { - const name = parentName + ' info2' - - const input = setupCommand(name, info2.description, argv, importMeta) - if (input) { - 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` - const spinner = ora(spinnerText).start() - const packageData = await fetchPackageData(input.ecosystem, input.pkgName, input.pkgVersion, spinner) - if (packageData) { - formatPackageDataOutput(packageData, { name, ...input }, spinner) - } - } - } -} - -// Internal functions - -/** - * @typedef CommandContext - * @property {boolean} includeAllIssues - * @property {boolean} outputJson - * @property {boolean} outputMarkdown - * @property {string} pkgName - * @property {string} pkgVersion - * @property {boolean} strict - * @property {string} ecosystem - */ - -/** - * @param {string} name - * @param {string} description - * @param {readonly string[]} argv - * @param {ImportMeta} importMeta - * @returns {void|CommandContext} - */ -function setupCommand (name, description, argv, importMeta) { - const flags = { - ...outputFlags, - ...validationFlags, - } - - const cli = meow(` - Usage - $ ${name} - - Options - ${printFlagList(flags, 6)} - - Examples - $ ${name} npm webtorrent - $ ${name} npm webtorrent@1.9.1 - `, { - argv, - description, - importMeta, - flags - }) - - const { - all: includeAllIssues, - json: outputJson, - markdown: outputMarkdown, - strict, - } = cli.flags - - if (cli.input.length > 2) { - throw new InputError('Only one package lookup supported at once') - } - - const [ecosystem = '', rawPkgName = ''] = cli.input - - if (!rawPkgName) { - cli.showHelp() - return - } - - const versionSeparator = rawPkgName.lastIndexOf('@') - - const pkgName = versionSeparator < 1 ? rawPkgName : rawPkgName.slice(0, versionSeparator) - const pkgVersion = versionSeparator < 1 ? 'latest' : rawPkgName.slice(versionSeparator + 1) - - return { - includeAllIssues, - outputJson, - outputMarkdown, - pkgName, - pkgVersion, - strict, - ecosystem - } -} - -/** - * @typedef PackageData - * @property {import('@socketsecurity/sdk').SocketSdkReturnType<'batchPackageFetch'>["data"]} data - */ - -/** - * @param {string} ecosystem - * @param {string} pkgName - * @param {string} pkgVersion - * @param {import('ora').Ora} spinner - * @returns {Promise} - */ -async function fetchPackageData (ecosystem, pkgName, pkgVersion, spinner) { - const socketSdk = await setupSdk(getDefaultKey() || FREE_API_KEY) - // @ts-ignore - const result = await handleApiCall(socketSdk.batchPackageFetch( - { license: true, alerts: false }, - { - components: - [{ - 'purl': `pkg:${ecosystem}/${pkgName}@${pkgVersion}` - }] - }), 'looking up package') - - if (!result.success) { - return handleUnsuccessfulApiResponse('batchPackageFetch', result, spinner) - } - - spinner.stop() - - return { - data: result.data - } -} - -/** - * @param {CommandContext} data - * @param {{ name: string } & CommandContext} context - * @param {import('ora').Ora} spinner - * @returns {void} - */ -function formatPackageDataOutput (/** @type {{ [key: string]: any }} */ data, { name, outputJson, outputMarkdown, pkgName, pkgVersion }, spinner) { - if (outputJson) { - console.log(JSON.stringify(data, undefined, 2)) - } else { - console.log('\nPackage report card:') - - const scoreResult = { - 'Supply Chain Risk': Math.floor(data['score'].supplyChain * 100), - 'Maintenance': Math.floor(data['score'].maintenance * 100), - 'Quality': Math.floor(data['score'].quality * 100), - 'Vulnerabilities': Math.floor(data['score'].vulnerability * 100), - 'License': Math.floor(data['score'].license * 100) - } - Object.entries(scoreResult).map(score => console.log(`- ${score[0]}: ${formatScore(score[1])}`)) - - // Package issues list - // if (objectSome(severityCount)) { - // const issueSummary = formatSeverityCount(severityCount) - // console.log('\n') - // spinner[strict ? 'fail' : 'succeed'](`Package has these issues: ${issueSummary}`) - // // formatPackageIssuesDetails(data, outputMarkdown) - // } else { - // console.log('\n') - spinner.succeed('Package has no issues') - // } - - // Link to issues list - const format = new ChalkOrMarkdown(!!outputMarkdown) - const url = `https://socket.dev/npm/package/${pkgName}/overview/${pkgVersion}` - if (pkgVersion === 'latest') { - console.log('\nDetailed info on socket.dev: ' + format.hyperlink(`${pkgName}`, url, { fallbackToUrl: true })) - } else { - console.log('\nDetailed info on socket.dev: ' + format.hyperlink(`${pkgName} v${pkgVersion}`, url, { fallbackToUrl: true })) - } - if (!outputMarkdown) { - console.log(chalk.dim('\nOr rerun', chalk.italic(name), 'using the', chalk.italic('--json'), 'flag to get full JSON output')) - } - } - - // if (strict && objectSome(severityCount)) { - // process.exit(1) - // } -} - -/** - * @param {number} score - * @returns {string} - */ -function formatScore (score) { - const error = chalk.hex('#de7c7b') - const warning = chalk.hex('#e59361') - const success = chalk.hex('#a4cb9d') - - if (score > 80) { - return `${success(score)}` - } else if (score < 80 && score > 60) { - return `${warning(score)}` - } else { - return `${error(score)}` - } -} From 1a9b4c0539106ff2dc8af64a499a9320028dc1a5 Mon Sep 17 00:00:00 2001 From: Charlie Gerard Date: Tue, 25 Jun 2024 14:05:56 -0700 Subject: [PATCH 4/8] wip --- lib/commands/info/index.js | 10 +++------- lib/utils/format-issues.js | 29 ++++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/lib/commands/info/index.js b/lib/commands/info/index.js index 33430f89..70b5f447 100644 --- a/lib/commands/info/index.js +++ b/lib/commands/info/index.js @@ -8,7 +8,7 @@ import { outputFlags, validationFlags } from '../../flags/index.js' import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js' import { ChalkOrMarkdown } from '../../utils/chalk-markdown.js' import { InputError } from '../../utils/errors.js' -import { formatSeverityCount, getSeverityCount } from '../../utils/format-issues.js' +import { formatSeverityCount, getCountSeverity } from '../../utils/format-issues.js' import { printFlagList } from '../../utils/formatting.js' import { objectSome } from '../../utils/misc.js' import { FREE_API_KEY, getDefaultKey, setupSdk } from '../../utils/sdk.js' @@ -133,17 +133,13 @@ async function fetchPackageData (ecosystem, pkgName, pkgVersion, { includeAllIss 'purl': `pkg:${ecosystem}/${pkgName}@${pkgVersion}` }] }), 'looking up package') - const alerts = await handleApiCall(socketSdk.getIssuesByNPMPackage(pkgName, pkgVersion), 'looking up package') if (!result.success) { return handleUnsuccessfulApiResponse('batchPackageFetch', result, spinner) } - if (!alerts.success) { - return handleUnsuccessfulApiResponse('getIssuesByNPMPackage', alerts, spinner) - } - - const severityCount = getSeverityCount(alerts.data, includeAllIssues ? undefined : 'high') + // @ts-ignore + const severityCount = getCountSeverity(result.data.alerts, includeAllIssues ? undefined : 'high') spinner.stop() diff --git a/lib/utils/format-issues.js b/lib/utils/format-issues.js index bf4a1b72..5c09d2cd 100644 --- a/lib/utils/format-issues.js +++ b/lib/utils/format-issues.js @@ -27,7 +27,7 @@ const SEVERITIES_BY_ORDER = /** @type {const} */ ([ return result } - +/* TODO: Delete this function when we remove the report command */ /** * @param {SocketIssueList} issues * @param {SocketIssue['severity']} [lowestToInclude] @@ -54,6 +54,33 @@ export function getSeverityCount (issues, lowestToInclude) { return severityCount } +/* The following function is the updated one */ +/** + * @param {Array} issues + * @param {SocketIssue['severity']} [lowestToInclude] + * @returns {Record} + */ +export function getCountSeverity (issues, lowestToInclude) { + const severityCount = pick( + { low: 0, middle: 0, high: 0, critical: 0 }, + getDesiredSeverities(lowestToInclude) + ) + + for (const issue of issues) { + const severity = issue.severity + + if (!severity) { + continue + } + + if (severityCount[severity] !== undefined) { + severityCount[severity] += 1 + } + } + + return severityCount +} + /** * @param {Record} severityCount * @returns {string} From 844e3cb773cd26a38919e42fc4719400f7f359f4 Mon Sep 17 00:00:00 2001 From: Charlie Gerard Date: Tue, 25 Jun 2024 14:34:19 -0700 Subject: [PATCH 5/8] wip --- lib/commands/info/index.js | 63 +++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/lib/commands/info/index.js b/lib/commands/info/index.js index 70b5f447..3a07395a 100644 --- a/lib/commands/info/index.js +++ b/lib/commands/info/index.js @@ -7,7 +7,7 @@ import ora from 'ora' import { outputFlags, validationFlags } from '../../flags/index.js' import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js' import { ChalkOrMarkdown } from '../../utils/chalk-markdown.js' -import { InputError } from '../../utils/errors.js' +import { prepareFlags } from '../../utils/flags.js' import { formatSeverityCount, getCountSeverity } from '../../utils/format-issues.js' import { printFlagList } from '../../utils/formatting.js' import { objectSome } from '../../utils/misc.js' @@ -31,17 +31,33 @@ export const info = { } } +const infoFlags = prepareFlags({ + license: { + type: 'boolean', + shortFlag: 'l', + default: false, + description: 'Include license - Default is false', + }, + alerts: { + type: 'boolean', + shortFlag: 'a', + default: false, + description: 'Include alerts - Default is false', + } +}) + // Internal functions /** * @typedef CommandContext - * @property {boolean} includeAllIssues + * @property {boolean} includeAlerts * @property {boolean} outputJson * @property {boolean} outputMarkdown * @property {string} pkgName * @property {string} pkgVersion * @property {boolean} strict * @property {string} ecosystem + * @property {boolean} includeLicense */ /** @@ -55,6 +71,7 @@ function setupCommand (name, description, argv, importMeta) { const flags = { ...outputFlags, ...validationFlags, + ...infoFlags } const cli = meow(` @@ -75,19 +92,17 @@ function setupCommand (name, description, argv, importMeta) { }) const { - all: includeAllIssues, + alerts: includeAlerts, + license: includeLicense, json: outputJson, markdown: outputMarkdown, strict, } = cli.flags - if (cli.input.length > 2) { - throw new InputError('Only one package lookup supported at once') - } - const [ecosystem = '', rawPkgName = ''] = cli.input - if (!rawPkgName) { + if (!ecosystem && !rawPkgName) { + console.error('Please provide an ecosystem and a package name') cli.showHelp() return } @@ -98,7 +113,8 @@ function setupCommand (name, description, argv, importMeta) { const pkgVersion = versionSeparator < 1 ? 'latest' : rawPkgName.slice(versionSeparator + 1) return { - includeAllIssues, + includeAlerts, + includeLicense, outputJson, outputMarkdown, pkgName, @@ -111,22 +127,22 @@ function setupCommand (name, description, argv, importMeta) { /** * @typedef PackageData * @property {import('@socketsecurity/sdk').SocketSdkReturnType<'batchPackageFetch'>["data"]} data - * @property {Record} severityCount + * @property {Record | undefined} severityCount */ /** * @param {string} ecosystem * @param {string} pkgName * @param {string} pkgVersion - * @param {Pick} context + * @param {Pick} context * @param {import('ora').Ora} spinner * @returns {Promise} */ -async function fetchPackageData (ecosystem, pkgName, pkgVersion, { includeAllIssues }, spinner) { +async function fetchPackageData (ecosystem, pkgName, pkgVersion, { includeAlerts, includeLicense }, spinner) { const socketSdk = await setupSdk(getDefaultKey() || FREE_API_KEY) // @ts-ignore const result = await handleApiCall(socketSdk.batchPackageFetch( - { license: 'true', alerts: 'true' }, + { license: includeLicense.toString(), alerts: includeAlerts.toString() }, { components: [{ @@ -139,7 +155,7 @@ async function fetchPackageData (ecosystem, pkgName, pkgVersion, { includeAllIss } // @ts-ignore - const severityCount = getCountSeverity(result.data.alerts, includeAllIssues ? undefined : 'high') + const severityCount = result.data.alerts && getCountSeverity(result.data.alerts, includeAlerts ? undefined : 'high') spinner.stop() @@ -159,24 +175,29 @@ function formatPackageDataOutput (/** @type {{ [key: string]: any }} */ { data, if (outputJson) { console.log(JSON.stringify(data, undefined, 2)) } else { - console.log('\nPackage report card:') + console.log('\nPackage metrics:') const scoreResult = { - 'Supply Chain Risk': Math.floor(data['score'].supplyChain * 100), - 'Maintenance': Math.floor(data['score'].maintenance * 100), - 'Quality': Math.floor(data['score'].quality * 100), - 'Vulnerabilities': Math.floor(data['score'].vulnerability * 100), - 'License': Math.floor(data['score'].license * 100) + 'Supply Chain Risk': Math.floor(data.score.supplyChain * 100), + 'Maintenance': Math.floor(data.score.maintenance * 100), + 'Quality': Math.floor(data.score.quality * 100), + 'Vulnerabilities': Math.floor(data.score.vulnerability * 100), + 'License': Math.floor(data.score.license * 100), + 'Overall': Math.floor(data.score.overall * 100) } Object.entries(scoreResult).map(score => console.log(`- ${score[0]}: ${formatScore(score[1])}`)) + // Package license + console.log('\nPackage license:') + console.log(`${data.license}`) + // Package issues list if (objectSome(severityCount)) { const issueSummary = formatSeverityCount(severityCount) console.log('\n') spinner[strict ? 'fail' : 'succeed'](`Package has these issues: ${issueSummary}`) formatPackageIssuesDetails(data.alerts, outputMarkdown) - } else { + } else if (severityCount && !objectSome(severityCount)) { console.log('\n') spinner.succeed('Package has no issues') } From c0d5caa1919d75bf1499dca6b70fb96eec138bd1 Mon Sep 17 00:00:00 2001 From: Charlie Gerard Date: Wed, 26 Jun 2024 16:37:36 -0700 Subject: [PATCH 6/8] wip --- lib/commands/info/index.js | 176 ++++++++++++++++++++----------------- 1 file changed, 95 insertions(+), 81 deletions(-) diff --git a/lib/commands/info/index.js b/lib/commands/info/index.js index 3a07395a..cb9399c9 100644 --- a/lib/commands/info/index.js +++ b/lib/commands/info/index.js @@ -21,9 +21,9 @@ export const info = { const input = setupCommand(name, info.description, argv, importMeta) if (input) { - 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` + const spinnerText = `Looking up data for packages: ${input.packages.join(', ')}\n` const spinner = ora(spinnerText).start() - const packageData = await fetchPackageData(input.ecosystem, input.pkgName, input.pkgVersion, input, spinner) + const packageData = await fetchPackageData(input.packages, input.includeAlerts, spinner) if (packageData) { formatPackageDataOutput(packageData, { name, ...input }, spinner) } @@ -32,12 +32,14 @@ export const info = { } const infoFlags = prepareFlags({ - license: { - type: 'boolean', - shortFlag: 'l', - default: false, - description: 'Include license - Default is false', - }, + // At the moment in API v0, alerts and license do the same thing. + // The license parameter will be implemented later. + // license: { + // type: 'boolean', + // shortFlag: 'l', + // default: false, + // description: 'Include license - Default is false', + // }, alerts: { type: 'boolean', shortFlag: 'a', @@ -53,11 +55,8 @@ const infoFlags = prepareFlags({ * @property {boolean} includeAlerts * @property {boolean} outputJson * @property {boolean} outputMarkdown - * @property {string} pkgName - * @property {string} pkgVersion + * @property {string[]} packages * @property {boolean} strict - * @property {string} ecosystem - * @property {boolean} includeLicense */ /** @@ -76,14 +75,14 @@ function setupCommand (name, description, argv, importMeta) { const cli = meow(` Usage - $ ${name} + $ ${name} :@ Options ${printFlagList(flags, 6)} Examples - $ ${name} npm webtorrent - $ ${name} npm webtorrent@1.9.1 + $ ${name} npm:webtorrent + $ ${name} npm:webtorrent@1.9.1 `, { argv, description, @@ -93,61 +92,68 @@ function setupCommand (name, description, argv, importMeta) { const { alerts: includeAlerts, - license: includeLicense, json: outputJson, markdown: outputMarkdown, strict, } = cli.flags - const [ecosystem = '', rawPkgName = ''] = cli.input + const [rawPkgName = ''] = cli.input - if (!ecosystem && !rawPkgName) { - console.error('Please provide an ecosystem and a package name') + if (!rawPkgName) { + console.error('Please provide an ecosystem and package name') cli.showHelp() return } - const versionSeparator = rawPkgName.lastIndexOf('@') + const /** @type {string[]} */inputPkgs = [] - const pkgName = versionSeparator < 1 ? rawPkgName : rawPkgName.slice(0, versionSeparator) - const pkgVersion = versionSeparator < 1 ? 'latest' : rawPkgName.slice(versionSeparator + 1) + cli.input.map(pkg => { + const ecosystem = pkg.split(':')[0] + if (!ecosystem) { + console.error(`Package name ${pkg} formatted incorrectly.`) + return cli.showHelp() + } else { + const versionSeparator = pkg.lastIndexOf('@') + const ecosystemSeparator = pkg.lastIndexOf(ecosystem) + const pkgName = versionSeparator < 1 ? pkg.slice(ecosystemSeparator + ecosystem.length + 1) : pkg.slice(ecosystemSeparator + ecosystem.length + 1, versionSeparator) + const pkgVersion = versionSeparator < 1 ? 'latest' : pkg.slice(versionSeparator + 1) + inputPkgs.push(`${ecosystem}/${pkgName}@${pkgVersion}`) + } + return inputPkgs + }) return { includeAlerts, - includeLicense, outputJson, outputMarkdown, - pkgName, - pkgVersion, + packages: inputPkgs, strict, - ecosystem } } /** * @typedef PackageData * @property {import('@socketsecurity/sdk').SocketSdkReturnType<'batchPackageFetch'>["data"]} data - * @property {Record | undefined} severityCount */ /** - * @param {string} ecosystem - * @param {string} pkgName - * @param {string} pkgVersion - * @param {Pick} context + * @param {string[]} packages + * @param {boolean} includeAlerts * @param {import('ora').Ora} spinner * @returns {Promise} */ -async function fetchPackageData (ecosystem, pkgName, pkgVersion, { includeAlerts, includeLicense }, spinner) { +async function fetchPackageData (packages, includeAlerts, spinner) { const socketSdk = await setupSdk(getDefaultKey() || FREE_API_KEY) + + const components = packages.map(pkg => { + return { 'purl': `pkg:${pkg}` } + }) + // @ts-ignore const result = await handleApiCall(socketSdk.batchPackageFetch( - { license: includeLicense.toString(), alerts: includeAlerts.toString() }, + { alerts: includeAlerts.toString() }, { - components: - [{ - 'purl': `pkg:${ecosystem}/${pkgName}@${pkgVersion}` - }] + components }), 'looking up package') if (!result.success) { @@ -155,13 +161,16 @@ async function fetchPackageData (ecosystem, pkgName, pkgVersion, { includeAlerts } // @ts-ignore - const severityCount = result.data.alerts && getCountSeverity(result.data.alerts, includeAlerts ? undefined : 'high') + result.data.map(pkg => { + const severityCount = pkg.alerts && getCountSeverity(pkg.alerts, includeAlerts ? undefined : 'high') + pkg.severityCount = severityCount + return pkg + }) spinner.stop() return { - data: result.data, - severityCount + data: result.data } } @@ -171,52 +180,57 @@ async function fetchPackageData (ecosystem, pkgName, pkgVersion, { includeAlerts * @param {import('ora').Ora} spinner * @returns {void} */ -function formatPackageDataOutput (/** @type {{ [key: string]: any }} */ { data, severityCount }, { name, outputJson, outputMarkdown, pkgName, pkgVersion, strict }, spinner) { +function formatPackageDataOutput (/** @type {{ [key: string]: any }} */ { data }, { outputJson, outputMarkdown, strict }, spinner) { if (outputJson) { console.log(JSON.stringify(data, undefined, 2)) } else { - console.log('\nPackage metrics:') - - const scoreResult = { - 'Supply Chain Risk': Math.floor(data.score.supplyChain * 100), - 'Maintenance': Math.floor(data.score.maintenance * 100), - 'Quality': Math.floor(data.score.quality * 100), - 'Vulnerabilities': Math.floor(data.score.vulnerability * 100), - 'License': Math.floor(data.score.license * 100), - 'Overall': Math.floor(data.score.overall * 100) - } - Object.entries(scoreResult).map(score => console.log(`- ${score[0]}: ${formatScore(score[1])}`)) - - // Package license - console.log('\nPackage license:') - console.log(`${data.license}`) - - // Package issues list - if (objectSome(severityCount)) { - const issueSummary = formatSeverityCount(severityCount) - console.log('\n') - spinner[strict ? 'fail' : 'succeed'](`Package has these issues: ${issueSummary}`) - formatPackageIssuesDetails(data.alerts, outputMarkdown) - } else if (severityCount && !objectSome(severityCount)) { - console.log('\n') - spinner.succeed('Package has no issues') - } + data.map((/** @type {{[key:string]: any}} */ d) => { + const { score, license, name, severityCount, version } = d + console.log(`\nPackage metrics for ${name}:`) + + const scoreResult = { + 'Supply Chain Risk': Math.floor(score.supplyChain * 100), + 'Maintenance': Math.floor(score.maintenance * 100), + 'Quality': Math.floor(score.quality * 100), + 'Vulnerabilities': Math.floor(score.vulnerability * 100), + 'License': Math.floor(score.license * 100), + 'Overall': Math.floor(score.overall * 100) + } - // Link to issues list - const format = new ChalkOrMarkdown(!!outputMarkdown) - const url = `https://socket.dev/npm/package/${pkgName}/overview/${pkgVersion}` - if (pkgVersion === 'latest') { - console.log('\nDetailed info on socket.dev: ' + format.hyperlink(`${pkgName}`, url, { fallbackToUrl: true })) - } else { - console.log('\nDetailed info on socket.dev: ' + format.hyperlink(`${pkgName} v${pkgVersion}`, url, { fallbackToUrl: true })) - } - if (!outputMarkdown) { - console.log(chalk.dim('\nOr rerun', chalk.italic(name), 'using the', chalk.italic('--json'), 'flag to get full JSON output')) - } - } + Object.entries(scoreResult).map(score => console.log(`- ${score[0]}: ${formatScore(score[1])}`)) + + // Package license + console.log('\nPackage license:') + console.log(`${license}`) + + // Package issues list + if (objectSome(severityCount)) { + const issueSummary = formatSeverityCount(severityCount) + console.log('\n') + spinner[strict ? 'fail' : 'succeed'](`Package has these issues: ${issueSummary}`) + formatPackageIssuesDetails(data.alerts, outputMarkdown) + } else if (severityCount && !objectSome(severityCount)) { + console.log('\n') + spinner.succeed('Package has no issues') + } - if (strict && objectSome(severityCount)) { - process.exit(1) + // Link to issues list + const format = new ChalkOrMarkdown(!!outputMarkdown) + const url = `https://socket.dev/npm/package/${name}/overview/${version}` + if (version === 'latest') { + console.log('\nDetailed info on socket.dev: ' + format.hyperlink(`${name}`, url, { fallbackToUrl: true })) + } else { + console.log('\nDetailed info on socket.dev: ' + format.hyperlink(`${name} v${version}`, url, { fallbackToUrl: true })) + } + if (!outputMarkdown) { + console.log(chalk.dim('\nOr rerun', chalk.italic(name), 'using the', chalk.italic('--json'), 'flag to get full JSON output')) + } + + if (strict && objectSome(severityCount)) { + process.exit(1) + } + return d + }) } } From 8a800cc8c089743e0c24cee28584768dbf7f8209 Mon Sep 17 00:00:00 2001 From: Charlie Gerard Date: Thu, 27 Jun 2024 13:25:01 -0700 Subject: [PATCH 7/8] wip --- lib/commands/info/index.js | 1 - package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/commands/info/index.js b/lib/commands/info/index.js index cb9399c9..bfce19a6 100644 --- a/lib/commands/info/index.js +++ b/lib/commands/info/index.js @@ -149,7 +149,6 @@ async function fetchPackageData (packages, includeAlerts, spinner) { return { 'purl': `pkg:${pkg}` } }) - // @ts-ignore const result = await handleApiCall(socketSdk.batchPackageFetch( { alerts: includeAlerts.toString() }, { diff --git a/package-lock.json b/package-lock.json index 0f6e6386..3a312bdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@cyclonedx/cdxgen": "^10.5.2", "@inquirer/select": "^2.3.5", "@socketsecurity/config": "^2.1.3", - "@socketsecurity/sdk": "^1.1.1", + "@socketsecurity/sdk": "^1.2.0", "chalk": "^5.3.0", "chalk-table": "^1.0.2", "execa": "^9.1.0", @@ -1615,9 +1615,9 @@ } }, "node_modules/@socketsecurity/sdk": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@socketsecurity/sdk/-/sdk-1.1.1.tgz", - "integrity": "sha512-ACVBe0UiZxy3S1C/2OvetAOoaZDumanKRzr2gXq5qOjhI6neLr2tZxC5xI+UcMKAcTDkwOZM8mpqvMdSjd6EEw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@socketsecurity/sdk/-/sdk-1.2.0.tgz", + "integrity": "sha512-XvOIJJsmzivaJWyUwNOcCAxcBBQtRLoE4mYbdrpgi1gagdgmau3dzSq/OC3vgrTV27iS9zfJLP8gqjrposuhGQ==", "dependencies": { "formdata-node": "^5.0.0", "got": "^12.5.3", diff --git a/package.json b/package.json index 2c9dda8e..d1f5eb88 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "@cyclonedx/cdxgen": "^10.5.2", "@inquirer/select": "^2.3.5", "@socketsecurity/config": "^2.1.3", - "@socketsecurity/sdk": "^1.1.1", + "@socketsecurity/sdk": "^1.2.0", "chalk": "^5.3.0", "chalk-table": "^1.0.2", "execa": "^9.1.0", From c754d36a3debf44d0e152f86264a8ed5d81dd44a Mon Sep 17 00:00:00 2001 From: Charlie Gerard Date: Thu, 27 Jun 2024 13:26:25 -0700 Subject: [PATCH 8/8] wip --- lib/commands/info/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/commands/info/index.js b/lib/commands/info/index.js index bfce19a6..65fdf849 100644 --- a/lib/commands/info/index.js +++ b/lib/commands/info/index.js @@ -100,7 +100,7 @@ function setupCommand (name, description, argv, importMeta) { const [rawPkgName = ''] = cli.input if (!rawPkgName) { - console.error('Please provide an ecosystem and package name') + console.error(`${chalk.bgRed('Input error')}: Please provide an ecosystem and package name`) cli.showHelp() return }