From 22ffec6fedb76f008a779cdcb3f42585bbcdfe1c Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Thu, 20 Mar 2025 15:09:41 +0100 Subject: [PATCH 1/7] Add the package score command, for realzies --- .../package/cmd-package-score.test.ts | 110 ++++++++++++++++ src/commands/package/cmd-package-score.ts | 103 +++++++++++++++ src/commands/package/cmd-package.ts | 6 +- src/commands/package/fetch-purl-deep-score.ts | 46 +++++++ .../package/handle-purl-deep-score.ts | 12 ++ src/commands/package/output-purl-score.ts | 122 ++++++++++++++++++ 6 files changed, 397 insertions(+), 2 deletions(-) create mode 100644 src/commands/package/cmd-package-score.test.ts create mode 100644 src/commands/package/cmd-package-score.ts create mode 100644 src/commands/package/fetch-purl-deep-score.ts create mode 100644 src/commands/package/handle-purl-deep-score.ts create mode 100644 src/commands/package/output-purl-score.ts diff --git a/src/commands/package/cmd-package-score.test.ts b/src/commands/package/cmd-package-score.test.ts new file mode 100644 index 000000000..df3508a0c --- /dev/null +++ b/src/commands/package/cmd-package-score.test.ts @@ -0,0 +1,110 @@ +import path from 'node:path' + +import { describe, expect } from 'vitest' + +import constants from '../../../dist/constants.js' +import { cmdit, invokeNpm } from '../../../test/utils' + +const { CLI } = constants + +describe('socket package score', async () => { + // Lazily access constants.rootBinPath. + const entryPath = path.join(constants.rootBinPath, `${CLI}.js`) + + cmdit(['package', 'score', '--help'], 'should support --help', async cmd => { + const { code, stderr, stdout } = await invokeNpm(entryPath, cmd) + expect(stdout).toMatchInlineSnapshot( + ` + "Look up score for one package which reflects all of its transitive dependencies as well + + Usage + $ socket package score < | > + + Options + --dryRun Do input validation for a command and exit 0 when input is ok + --help Print this help. + --json Output result as json + --markdown Output result as markdown + + Requirements + - quota: 100 + - scope: \`packages:list\` + + Show deep scoring details for one package. The score will reflect the package + itself, any of its dependencies, and any of its transitive dependencies. + + When you want to know whether to trust a package, this is the command to run. + + See also the \`socket package shallow\` command, which returns the shallow + score for any number of packages. That will not reflect the dependency scores. + + Only a few ecosystems are supported like npm, golang, and maven. + + A "purl" is a standard package name formatting: \`pkg:eco/name@version\` + This command will automatically prepend "pkg:" when not present. + + The version is optional but when given should be a direct match. + + Examples + $ socket package score npm babel-cli + $ socket package score npm babel-cli@1.9.1 + $ socket package score npm/babel-cli@1.9.1 + $ socket package score pkg:npm/babel-cli@1.9.1" + ` + ) + expect(`\n ${stderr}`).toMatchInlineSnapshot(` + " + _____ _ _ /--------------- + | __|___ ___| |_ ___| |_ | Socket.dev CLI ver + |__ | . | _| '_| -_| _| | Node: , API token set: + |_____|___|___|_,_|___|_|.dev | Command: \`socket package score\`, cwd: " + `) + + expect(code, 'help should exit with code 2').toBe(2) + expect(stderr, 'header should include command (without params)').toContain( + '`socket package score`' + ) + }) + + cmdit( + ['package', 'score', '--dry-run'], + 'should require args with just dry-run', + async cmd => { + const { code, stderr, stdout } = await invokeNpm(entryPath, cmd) + expect(stdout).toMatchInlineSnapshot(`""`) + expect(`\n ${stderr}`).toMatchInlineSnapshot(` + " + _____ _ _ /--------------- + | __|___ ___| |_ ___| |_ | Socket.dev CLI ver + |__ | . | _| '_| -_| _| | Node: , API token set: + |_____|___|___|_,_|___|_|.dev | Command: \`socket package score\`, cwd: + + \\x1b[31m\\xd7\\x1b[39m \\x1b[41m\\x1b[37mInput error\\x1b[39m\\x1b[49m: Please provide the required fields: + + - First parameter should be an ecosystem or the arg must be a purl \\x1b[31m(bad!)\\x1b[39m + + - Expecting the package to check \\x1b[31m(missing!)\\x1b[39m" + `) + + expect(code, 'dry-run should exit with code 2 if missing input').toBe(2) + } + ) + + cmdit( + ['package', 'score', 'npm', 'babel', '--dry-run'], + 'should require args with just dry-run', + async cmd => { + const { code, stderr, stdout } = await invokeNpm(entryPath, cmd) + expect(stdout).toMatchInlineSnapshot(`"[DryRun]: Bailing now"`) + expect(`\n ${stderr}`).toMatchInlineSnapshot(` + " + _____ _ _ /--------------- + | __|___ ___| |_ ___| |_ | Socket.dev CLI ver + |__ | . | _| '_| -_| _| | Node: , API token set: + |_____|___|___|_,_|___|_|.dev | Command: \`socket package score\`, cwd: " + `) + + expect(code, 'dry-run should exit with code 0 if input ok').toBe(0) + } + ) +}) diff --git a/src/commands/package/cmd-package-score.ts b/src/commands/package/cmd-package-score.ts new file mode 100644 index 000000000..f3c3b0028 --- /dev/null +++ b/src/commands/package/cmd-package-score.ts @@ -0,0 +1,103 @@ +import colors from 'yoctocolors-cjs' + +import { logger } from '@socketsecurity/registry/lib/logger' + +import { handlePurlDeepScore } from './handle-purl-deep-score' +import { parsePackageSpecifiers } from './parse-package-specifiers' +import constants from '../../constants' +import { commonFlags, outputFlags } from '../../flags' +import { meowOrExit } from '../../utils/meow-with-subcommands' +import { getFlagListOutput } from '../../utils/output-formatting' + +import type { CliCommandConfig } from '../../utils/meow-with-subcommands' + +const { DRY_RUN_BAIL_TEXT } = constants + +const config: CliCommandConfig = { + commandName: 'score', + description: + 'Look up score for one package which reflects all of its transitive dependencies as well', + hidden: true, + flags: { + ...commonFlags, + ...outputFlags + }, + help: (command, config) => ` + Usage + $ ${command} < | > + + Options + ${getFlagListOutput(config.flags, 6)} + + Requirements + - quota: 100 + - scope: \`packages:list\` + + Show deep scoring details for one package. The score will reflect the package + itself, any of its dependencies, and any of its transitive dependencies. + + When you want to know whether to trust a package, this is the command to run. + + See also the \`socket package shallow\` command, which returns the shallow + score for any number of packages. That will not reflect the dependency scores. + + Only a few ecosystems are supported like npm, golang, and maven. + + A "purl" is a standard package name formatting: \`pkg:eco/name@version\` + This command will automatically prepend "pkg:" when not present. + + The version is optional but when given should be a direct match. + + Examples + $ ${command} npm babel-cli + $ ${command} npm babel-cli@1.9.1 + $ ${command} npm/babel-cli@1.9.1 + $ ${command} pkg:npm/babel-cli@1.9.1 + ` +} + +export const cmdPackageScore = { + description: config.description, + hidden: config.hidden, + run +} + +async function run( + argv: string[] | readonly string[], + importMeta: ImportMeta, + { parentName }: { parentName: string } +): Promise { + const cli = meowOrExit({ + argv, + config, + importMeta, + parentName + }) + + const { json, markdown } = cli.flags + const [ecosystem = '', purl] = cli.input + + const { purls, valid } = parsePackageSpecifiers(ecosystem, purl ? [purl] : []) + + if (!valid || !purls.length) { + // Use exit status of 2 to indicate incorrect usage, generally invalid + // options or missing arguments. + // https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html + process.exitCode = 2 + logger.fail(`${colors.bgRed(colors.white('Input error'))}: Please provide the required fields:\n + - First parameter should be an ecosystem or the arg must be a purl ${!valid ? colors.red('(bad!)') : colors.green('(ok)')}\n + - Expecting the package to check ${!purls.length ? colors.red('(missing!)') : colors.green('(ok)')}\n + `) + return + } + + if (cli.flags['dryRun']) { + logger.log(DRY_RUN_BAIL_TEXT) + return + } + + await handlePurlDeepScore( + purls[0] || '', + json ? 'json' : markdown ? 'markdown' : 'text' + ) +} diff --git a/src/commands/package/cmd-package.ts b/src/commands/package/cmd-package.ts index 9bfad8d39..60e564981 100644 --- a/src/commands/package/cmd-package.ts +++ b/src/commands/package/cmd-package.ts @@ -1,3 +1,4 @@ +import { cmdPackageScore } from './cmd-package-score' import { cmdPackageShallow } from './cmd-package-shallow' import { meowWithSubcommands } from '../../utils/meow-with-subcommands' @@ -11,14 +12,15 @@ export const cmdPackage: CliSubcommand = { async run(argv, importMeta, { parentName }) { await meowWithSubcommands( { + score: cmdPackageScore, shallow: cmdPackageShallow }, { aliases: { - pkg: { + deep: { description, hidden: true, - argv: [] + argv: ['score'] } }, argv, diff --git a/src/commands/package/fetch-purl-deep-score.ts b/src/commands/package/fetch-purl-deep-score.ts new file mode 100644 index 000000000..13d5c85b6 --- /dev/null +++ b/src/commands/package/fetch-purl-deep-score.ts @@ -0,0 +1,46 @@ +import colors from 'yoctocolors-cjs' + +import { logger } from '@socketsecurity/registry/lib/logger' + +import constants from '../../constants' +import { handleAPIError, handleApiCall, queryAPI } from '../../utils/api' +import { AuthError } from '../../utils/errors' +import { getDefaultToken } from '../../utils/sdk' + +export async function fetchPurlDeepScore(purl: string) { + // Lazily access constants.spinner. + const { spinner } = constants + + const apiToken = getDefaultToken() + if (!apiToken) { + throw new AuthError( + 'User must be authenticated to run this command. To log in, run the command `socket login` and enter your API key.' + ) + } + + spinner.start('Getting deep package score...') + const response = await queryAPI( + `purl/score/${encodeURIComponent(purl)}`, + apiToken + ) + spinner?.successAndStop('Received deep package score response.') + + if (!response.ok) { + const err = await handleAPIError(response.status) + logger.log('\nThere was an error', err) + spinner.errorAndStop( + `${colors.bgRed(colors.white(response.statusText))}: ${err}` + ) + return + } + + const result = await handleApiCall(await response.text(), 'Reading text') + + try { + return JSON.parse(result) + } catch (e) { + throw new Error( + 'Was unable to JSON parse the input from the server. It may not have been a proper JSON response. Please report this problem.' + ) + } +} diff --git a/src/commands/package/handle-purl-deep-score.ts b/src/commands/package/handle-purl-deep-score.ts new file mode 100644 index 000000000..ea9b80828 --- /dev/null +++ b/src/commands/package/handle-purl-deep-score.ts @@ -0,0 +1,12 @@ +import { fetchPurlDeepScore } from './fetch-purl-deep-score' +import { outputPurlScore } from './output-purl-score' + +export async function handlePurlDeepScore( + purl: string, + outputKind: 'json' | 'markdown' | 'text' +) { + const data = await fetchPurlDeepScore(purl) + if (!data) return + + await outputPurlScore(purl, data, outputKind) +} diff --git a/src/commands/package/output-purl-score.ts b/src/commands/package/output-purl-score.ts new file mode 100644 index 000000000..cde718f2c --- /dev/null +++ b/src/commands/package/output-purl-score.ts @@ -0,0 +1,122 @@ +import { logger } from '@socketsecurity/registry/lib/logger' + +export async function outputPurlScore( + purl: string, + data: unknown, + outputKind: 'json' | 'markdown' | 'text' +) { + if (outputKind === 'json') { + let json + try { + json = JSON.stringify(data, null, 2) + } catch { + console.error( + 'Failed to convert the server response to JSON... Please try again or reach out to customer support.' + ) + process.exitCode = 1 + return + } + + logger.error(`Score report for "${purl}":\n`) + logger.log(json) + logger.log('') + return + } + + if (outputKind === 'markdown') { + const { alerts, func, score, worst } = data as { + func: string + score: { + license: number + maintenance: number + overall: number + quality: number + supplyChain: number + vulnerability: number + } + worst: { + license: string + maintenance: string + overall: string + quality: string + supplyChain: string + vulnerability: string + } + alerts: Array<{ + name: string + severity: string + category: string + }> + } + + logger.error(`Score report for "${purl}":\n`) + logger.log('# Deep Package Score') + logger.log('') + logger.log( + 'This Socket report contains the response for requesting a deep package score for' + ) + logger.log( + `a package and any of its dependencies or transitive dependencies.` + ) + logger.log('') + logger.log(`The package is: ${purl}`) + logger.log('') + logger.log('## Score') + logger.log('') + logger.log('Please note:') + logger.log( + ' The listed scores reflect the scores from the requested package, its' + ) + logger.log( + ' dependencies, and any transitive dependencies. An aggregation function' + ) + logger.log( + ' computes the final score which is presented in this report.' + ) + logger.log('') + logger.log(`The aggregation function that was used is: "${func}"`) + logger.log('') + logger.log('- Overall: ' + score.overall) + logger.log('- Maintenance: ' + score.maintenance) + logger.log('- Quality: ' + score.quality) + logger.log('- Supply Chain: ' + score.supplyChain) + logger.log('- Vulnerability: ' + score.vulnerability) + logger.log('- License: ' + score.license) + logger.log('') + logger.log('## Worst score examples') + logger.log('') + logger.log( + 'These are packages with the worst score in each category. Only one package is' + ) + logger.log( + 'listed even if multiple have that lowest score. Each of these packages is the' + ) + logger.log('package itself or a (transitive) dependency.') + logger.log('') + logger.log('- Overall: ' + worst.overall) + logger.log('- Maintenance: ' + worst.maintenance) + logger.log('- Quality: ' + worst.quality) + logger.log('- Supply Chain: ' + worst.supplyChain) + logger.log('- Vulnerability: ' + worst.vulnerability) + logger.log('- License: ' + worst.license) + logger.log('') + logger.log('## Alerts') + logger.log('') + logger.log( + 'Here is a list of the alerts emitted by this package or any of its (transitive)' + ) + logger.log( + 'dependencies in aggregate. Only the first 100 or shown, ordered by severity.' + ) + logger.log('') + alerts.forEach(({ name, severity }) => { + logger.log(`- [${severity}] ${name}`) + }) + logger.log('') + return + } + + logger.log(`Score report for "${purl}":`) + logger.log(data) + logger.log('') +} From df5e9e0211f4611b88e73bdcd98d2e45f25962a0 Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Wed, 26 Mar 2025 18:20:20 +0100 Subject: [PATCH 2/7] Update to match current endpoint output --- src/commands/package/fetch-purl-deep-score.ts | 6 +- src/commands/package/output-purl-score.ts | 282 +++++++++++++----- 2 files changed, 210 insertions(+), 78 deletions(-) diff --git a/src/commands/package/fetch-purl-deep-score.ts b/src/commands/package/fetch-purl-deep-score.ts index 13d5c85b6..b1c6850c9 100644 --- a/src/commands/package/fetch-purl-deep-score.ts +++ b/src/commands/package/fetch-purl-deep-score.ts @@ -3,7 +3,7 @@ import colors from 'yoctocolors-cjs' import { logger } from '@socketsecurity/registry/lib/logger' import constants from '../../constants' -import { handleAPIError, handleApiCall, queryAPI } from '../../utils/api' +import { handleApiCall, handleApiError, queryApi } from '../../utils/api' import { AuthError } from '../../utils/errors' import { getDefaultToken } from '../../utils/sdk' @@ -19,14 +19,14 @@ export async function fetchPurlDeepScore(purl: string) { } spinner.start('Getting deep package score...') - const response = await queryAPI( + const response = await queryApi( `purl/score/${encodeURIComponent(purl)}`, apiToken ) spinner?.successAndStop('Received deep package score response.') if (!response.ok) { - const err = await handleAPIError(response.status) + const err = await handleApiError(response.status) logger.log('\nThere was an error', err) spinner.errorAndStop( `${colors.bgRed(colors.white(response.statusText))}: ${err}` diff --git a/src/commands/package/output-purl-score.ts b/src/commands/package/output-purl-score.ts index cde718f2c..2a98a2c99 100644 --- a/src/commands/package/output-purl-score.ts +++ b/src/commands/package/output-purl-score.ts @@ -24,99 +24,231 @@ export async function outputPurlScore( } if (outputKind === 'markdown') { - const { alerts, func, score, worst } = data as { - func: string - score: { - license: number - maintenance: number - overall: number - quality: number - supplyChain: number - vulnerability: number + const { + purl: requestedPurl, + self: { + alerts: selfAlerts, + capabilities: selfCaps, + purl, + score: selfScore + }, + transitively: { + alerts, + capabilities, + dependencyCount, + func, + lowest, + score } - worst: { - license: string - maintenance: string - overall: string - quality: string - supplyChain: string - vulnerability: string + } = data as { + purl: string + self: { + purl: string + score: { + license: number + maintenance: number + overall: number + quality: number + supplyChain: number + vulnerability: number + } + capabilities: string[] + alerts: Array<{ + name: string + severity: string + category: string + example: string + }> + } + transitively: { + dependencyCount: number + func: string + score: { + license: number + maintenance: number + overall: number + quality: number + supplyChain: number + vulnerability: number + } + lowest: { + license: string + maintenance: string + overall: string + quality: string + supplyChain: string + vulnerability: string + } + capabilities: string[] + alerts: Array<{ + name: string + severity: string + category: string + example: string + }> } - alerts: Array<{ - name: string - severity: string - category: string - }> } - logger.error(`Score report for "${purl}":\n`) - logger.log('# Deep Package Score') - logger.log('') - logger.log( - 'This Socket report contains the response for requesting a deep package score for' - ) - logger.log( - `a package and any of its dependencies or transitive dependencies.` - ) - logger.log('') - logger.log(`The package is: ${purl}`) + logger.error(`Score report for "${requestedPurl}" ("${purl}"):\n`) + logger.log('# Complete Package Score') logger.log('') - logger.log('## Score') + if (dependencyCount) { + logger.log( + `This is a Socket report for the package *"${purl}"* and its *${dependencyCount}* direct/transitive dependencies.` + ) + } else { + logger.log( + `This is a Socket report for the package *"${purl}"*. It has *no dependencies*.` + ) + } logger.log('') - logger.log('Please note:') - logger.log( - ' The listed scores reflect the scores from the requested package, its' - ) - logger.log( - ' dependencies, and any transitive dependencies. An aggregation function' - ) - logger.log( - ' computes the final score which is presented in this report.' - ) + if (dependencyCount) { + logger.log( + `It will show you the shallow score for just the package itself and a deep score for all the transitives combined. Additionally you can see which capabilities were found and the top alerts as well as a package that was responsible for it.` + ) + } else { + logger.log( + `It will show you the shallow score for the package itself, which capabilities were found, and its top alerts.` + ) + logger.log('') + logger.log( + 'Since it has no dependencies, the shallow score is also the deep score.' + ) + } logger.log('') - logger.log(`The aggregation function that was used is: "${func}"`) + if (dependencyCount) { + // This doesn't make much sense if there are no dependencies. Better to omit it. + logger.log( + 'The report should give you a good insight into the status of this package.' + ) + logger.log('') + logger.log('## Package itself') + logger.log('') + logger.log( + 'Here are results for the package itself (excluding data from dependencies).' + ) + } else { + logger.log('## Report') + logger.log('') + logger.log( + 'The report should give you a good insight into the status of this package.' + ) + } logger.log('') - logger.log('- Overall: ' + score.overall) - logger.log('- Maintenance: ' + score.maintenance) - logger.log('- Quality: ' + score.quality) - logger.log('- Supply Chain: ' + score.supplyChain) - logger.log('- Vulnerability: ' + score.vulnerability) - logger.log('- License: ' + score.license) + logger.log('### Shallow Score') logger.log('') - logger.log('## Worst score examples') + logger.log('This score is just for the package itself:') logger.log('') - logger.log( - 'These are packages with the worst score in each category. Only one package is' - ) - logger.log( - 'listed even if multiple have that lowest score. Each of these packages is the' - ) - logger.log('package itself or a (transitive) dependency.') + logger.log('- Overall: ' + selfScore.overall) + logger.log('- Maintenance: ' + selfScore.maintenance) + logger.log('- Quality: ' + selfScore.quality) + logger.log('- Supply Chain: ' + selfScore.supplyChain) + logger.log('- Vulnerability: ' + selfScore.vulnerability) + logger.log('- License: ' + selfScore.license) logger.log('') - logger.log('- Overall: ' + worst.overall) - logger.log('- Maintenance: ' + worst.maintenance) - logger.log('- Quality: ' + worst.quality) - logger.log('- Supply Chain: ' + worst.supplyChain) - logger.log('- Vulnerability: ' + worst.vulnerability) - logger.log('- License: ' + worst.license) + logger.log('### Capabilities') logger.log('') - logger.log('## Alerts') + if (selfCaps.length) { + logger.log('These are the capabilities detected in the package itself:') + logger.log('') + selfCaps.forEach(cap => { + logger.log(`- ${cap}`) + }) + } else { + logger.log('No capabilities were found in the package.') + } logger.log('') - logger.log( - 'Here is a list of the alerts emitted by this package or any of its (transitive)' - ) - logger.log( - 'dependencies in aggregate. Only the first 100 or shown, ordered by severity.' - ) + logger.log('### Alerts for this package') logger.log('') - alerts.forEach(({ name, severity }) => { - logger.log(`- [${severity}] ${name}`) - }) + if (selfAlerts.length) { + if (dependencyCount) { + logger.log('These are the alerts found for the package itself:') + } else { + logger.log('These are the alerts found for this package:') + } + logger.log('') + selfAlerts.forEach(alert => { + logger.log(`- [${alert.severity}] ${alert.name}`) + }) + } else { + logger.log('There are currently no alerts for this package.') + } logger.log('') + if (dependencyCount) { + logger.log('## Transitive Package Results') + logger.log('') + logger.log( + 'Here are results for the package and its direct/transitive dependencies.' + ) + logger.log('') + logger.log('### Deep Score') + logger.log('') + logger.log( + 'This score represents the package and and its direct/transitive dependencies:' + ) + logger.log( + `The function used to calculate the values in aggregate is: *"${func}"*` + ) + logger.log('') + logger.log('- Overall: ' + score.overall) + logger.log('- Maintenance: ' + score.maintenance) + logger.log('- Quality: ' + score.quality) + logger.log('- Supply Chain: ' + score.supplyChain) + logger.log('- Vulnerability: ' + score.vulnerability) + logger.log('- License: ' + score.license) + logger.log('') + logger.log('### Capabilities') + logger.log('') + logger.log( + 'These are the packages with the lowest recorded score. If there is more than one with the lowest score, just one is shown here. This may help you figure out the source of low scores.' + ) + logger.log('') + logger.log('- Overall: ' + lowest.overall) + logger.log('- Maintenance: ' + lowest.maintenance) + logger.log('- Quality: ' + lowest.quality) + logger.log('- Supply Chain: ' + lowest.supplyChain) + logger.log('- Vulnerability: ' + lowest.vulnerability) + logger.log('- License: ' + lowest.license) + logger.log('') + logger.log('### Capabilities') + logger.log('') + if (capabilities.length) { + logger.log( + 'These are the capabilities detected in at least one package:' + ) + logger.log('') + capabilities.forEach(cap => { + logger.log(`- ${cap}`) + }) + } else { + logger.log( + 'This package had no capabilities and neither did any of its direct/transitive dependencies.' + ) + } + logger.log('') + logger.log('### Alerts') + logger.log('') + if (alerts.length) { + logger.log('These are the alerts found:') + logger.log('') + alerts.forEach(alert => { + logger.log( + `- [${alert.severity}] ${alert.name} (at least in ${alert.example})` + ) + }) + } else { + logger.log( + 'This package had no alerts and neither did any of its direct/transitive dependencies.' + ) + } + logger.log('') + } return } - logger.log(`Score report for "${purl}":`) + logger.log( + `Score report for "${purl}" (use --json for raw and --markdown for formatted reports):` + ) logger.log(data) logger.log('') } From 1172be334cfc92630b0e17504c21003bfb0ef79d Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Thu, 27 Mar 2025 11:12:15 +0100 Subject: [PATCH 3/7] Improve error handling --- src/commands/package/fetch-purl-deep-score.ts | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/src/commands/package/fetch-purl-deep-score.ts b/src/commands/package/fetch-purl-deep-score.ts index b1c6850c9..21c63daf7 100644 --- a/src/commands/package/fetch-purl-deep-score.ts +++ b/src/commands/package/fetch-purl-deep-score.ts @@ -19,25 +19,39 @@ export async function fetchPurlDeepScore(purl: string) { } spinner.start('Getting deep package score...') - const response = await queryApi( - `purl/score/${encodeURIComponent(purl)}`, - apiToken - ) - spinner?.successAndStop('Received deep package score response.') - - if (!response.ok) { - const err = await handleApiError(response.status) - logger.log('\nThere was an error', err) - spinner.errorAndStop( - `${colors.bgRed(colors.white(response.statusText))}: ${err}` + let result + try { + result = await queryApi(`purl/score/${encodeURIComponent(purl)}`, apiToken) + spinner?.successAndStop('Received deep package score response.') + } catch (e) { + spinner?.failAndStop('The request was unsuccessful.') + const msg = (e as undefined | { message: string })?.message + if (msg) { + logger.fail(msg) + logger.log( + 'Please report this if the error persists or use the cli version that includes error reporting to automate that' + ) + } else { + logger.log( + 'An error happened but no reason was given. If this persists please let us know about it and what you were trying to achieve. Thank you.' + ) + } + return + } + + if (!result.ok) { + const err = await handleApiError(result.status) + logger.fail( + `${colors.bgRed(colors.bold(colors.white(' ' + result.statusText + ' ')))}: ${err}` ) + process.exitCode = 1 return } - const result = await handleApiCall(await response.text(), 'Reading text') + const data = await handleApiCall(await result.text(), 'Reading text') try { - return JSON.parse(result) + return JSON.parse(data) } catch (e) { throw new Error( 'Was unable to JSON parse the input from the server. It may not have been a proper JSON response. Please report this problem.' From 8fe146495025baffd7b62a1a1cc717b7d7cf6dd8 Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Thu, 27 Mar 2025 11:23:46 +0100 Subject: [PATCH 4/7] lint --- src/commands/package/handle-purl-deep-score.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/commands/package/handle-purl-deep-score.ts b/src/commands/package/handle-purl-deep-score.ts index ea9b80828..f77fc563d 100644 --- a/src/commands/package/handle-purl-deep-score.ts +++ b/src/commands/package/handle-purl-deep-score.ts @@ -6,7 +6,9 @@ export async function handlePurlDeepScore( outputKind: 'json' | 'markdown' | 'text' ) { const data = await fetchPurlDeepScore(purl) - if (!data) return + if (!data) { + return + } await outputPurlScore(purl, data, outputKind) } From 87fed0a4c24f973fd3697aec9034f0b06190439f Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Fri, 28 Mar 2025 14:16:26 +0100 Subject: [PATCH 5/7] Prevent double forward slash in queryApi --- src/utils/api.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/utils/api.ts b/src/utils/api.ts index 2e3547a44..aede245ed 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -77,8 +77,11 @@ export function getDefaultApiBaseUrl(): string | undefined { } export async function queryApi(path: string, apiToken: string) { - const API_V0_URL = getDefaultApiBaseUrl() - return await fetch(`${API_V0_URL}/${path}`, { + const API_V0_URL = getDefaultApiBaseUrl() || '' + if (!API_V0_URL) { + logger.warn('API endpoint is not set and default was empty. Request is likely to fail.') + } + return await fetch(`${API_V0_URL}${API_V0_URL.endsWith('/')?'':'/'}${path}`, { method: 'GET', headers: { Authorization: `Basic ${btoa(`${apiToken}:`)}` From 0e973278ea14362382200159a255a38344974122 Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Fri, 28 Mar 2025 14:18:28 +0100 Subject: [PATCH 6/7] lint --- src/utils/api.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/utils/api.ts b/src/utils/api.ts index aede245ed..bc2b5de18 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -79,12 +79,17 @@ export function getDefaultApiBaseUrl(): string | undefined { export async function queryApi(path: string, apiToken: string) { const API_V0_URL = getDefaultApiBaseUrl() || '' if (!API_V0_URL) { - logger.warn('API endpoint is not set and default was empty. Request is likely to fail.') + logger.warn( + 'API endpoint is not set and default was empty. Request is likely to fail.' + ) } - return await fetch(`${API_V0_URL}${API_V0_URL.endsWith('/')?'':'/'}${path}`, { - method: 'GET', - headers: { - Authorization: `Basic ${btoa(`${apiToken}:`)}` + return await fetch( + `${API_V0_URL}${API_V0_URL.endsWith('/') ? '' : '/'}${path}`, + { + method: 'GET', + headers: { + Authorization: `Basic ${btoa(`${apiToken}:`)}` + } } - }) + ) } From b4ee969251f051521168c6bbe5e7ba6c4873d978 Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Fri, 28 Mar 2025 15:44:57 +0100 Subject: [PATCH 7/7] Print the alerts as a table --- src/commands/audit-log/output-audit-log.ts | 2 +- src/commands/package/output-purl-score.ts | 19 ++++++++++++------- src/utils/markdown.ts | 13 +++++++++---- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/commands/audit-log/output-audit-log.ts b/src/commands/audit-log/output-audit-log.ts index 56f31beed..419afd71e 100644 --- a/src/commands/audit-log/output-audit-log.ts +++ b/src/commands/audit-log/output-audit-log.ts @@ -88,7 +88,7 @@ async function outputAsJson( logger.log(json) } -async function outputAsMarkdown( +export async function outputAsMarkdown( auditLogs: SocketSdkReturnType<'getAuditLogEvents'>['data']['results'], orgSlug: string, logType: string, diff --git a/src/commands/package/output-purl-score.ts b/src/commands/package/output-purl-score.ts index 2a98a2c99..4d9801d34 100644 --- a/src/commands/package/output-purl-score.ts +++ b/src/commands/package/output-purl-score.ts @@ -1,5 +1,7 @@ import { logger } from '@socketsecurity/registry/lib/logger' +import { mdTable } from '../../utils/markdown' + export async function outputPurlScore( purl: string, data: unknown, @@ -167,9 +169,9 @@ export async function outputPurlScore( logger.log('These are the alerts found for this package:') } logger.log('') - selfAlerts.forEach(alert => { - logger.log(`- [${alert.severity}] ${alert.name}`) - }) + logger.log( + mdTable(selfAlerts, ['severity', 'name'], ['Severity', 'Alert Name']) + ) } else { logger.log('There are currently no alerts for this package.') } @@ -231,11 +233,14 @@ export async function outputPurlScore( if (alerts.length) { logger.log('These are the alerts found:') logger.log('') - alerts.forEach(alert => { - logger.log( - `- [${alert.severity}] ${alert.name} (at least in ${alert.example})` + + logger.log( + mdTable( + alerts, + ['severity', 'name', 'example'], + ['Severity', 'Alert Name', 'Example package reporting it'] ) - }) + ) } else { logger.log( 'This package had no alerts and neither did any of its direct/transitive dependencies.' diff --git a/src/utils/markdown.ts b/src/utils/markdown.ts index 2d7741748..68edeef80 100644 --- a/src/utils/markdown.ts +++ b/src/utils/markdown.ts @@ -31,7 +31,8 @@ export function mdTable>>( logs: T, // This is saying "an array of strings and the strings are a valid key of elements of T" // In turn, T is defined above as the audit log event type from our OpenAPI docs. - cols: Array + cols: Array, + titles: string[] = cols ): string { // Max col width required to fit all data in that column const cws = cols.map(col => col.length) @@ -40,7 +41,11 @@ export function mdTable>>( for (let i = 0, { length } = cols; i < length; i += 1) { // @ts-ignore const val: unknown = log[cols[i] ?? ''] ?? '' - cws[i] = Math.max(cws[i] ?? 0, String(val).length) + cws[i] = Math.max( + cws[i] ?? 0, + String(val).length, + (titles[i] || '').length + ) } } @@ -50,8 +55,8 @@ export function mdTable>>( } let header = '|' - for (let i = 0, { length } = cols; i < length; i += 1) { - header += ' ' + String(cols[i]).padEnd(cws[i] ?? 0, ' ') + ' |' + for (let i = 0, { length } = titles; i < length; i += 1) { + header += ' ' + String(titles[i]).padEnd(cws[i] ?? 0, ' ') + ' |' } let body = ''