From d0813ca13d5fb140f6ae0aec884be79572a18828 Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Fri, 21 Mar 2025 14:53:22 +0100 Subject: [PATCH 1/2] Apply handle pattern to scan --- src/commands/report/create-report.ts | 4 +- src/commands/report/view-report.ts | 4 +- src/commands/scan/cmd-scan-create.ts | 97 ++++++-- src/commands/scan/cmd-scan-del.ts | 10 +- src/commands/scan/cmd-scan-list.ts | 4 +- src/commands/scan/cmd-scan-metadata.ts | 12 +- src/commands/scan/cmd-scan-report.ts | 12 +- src/commands/scan/cmd-scan-view.ts | 14 +- src/commands/scan/create-full-scan.ts | 227 ------------------ .../scan/fetch-create-org-full-scan.ts | 50 ++++ ...-scan.ts => fetch-delete-org-full-scan.ts} | 30 ++- src/commands/scan/fetch-list-scans.ts | 84 +++++++ src/commands/scan/fetch-report-data.ts | 4 +- src/commands/scan/fetch-scan-metadata.ts | 47 ++++ .../scan/{get-full-scan.ts => fetch-scan.ts} | 6 +- .../scan/fetch-supported-scan-file-names.ts | 32 +++ src/commands/scan/get-full-scan-metadata.ts | 78 ------ src/commands/scan/handle-create-new-scan.ts | 87 +++++++ src/commands/scan/handle-delete-scan.ts | 12 + src/commands/scan/handle-list-scans.ts | 32 +++ src/commands/scan/handle-scan-metadata.ts | 13 + src/commands/scan/handle-scan-report.ts | 54 +++++ src/commands/scan/handle-scan-view.ts | 13 + src/commands/scan/list-full-scans.ts | 121 ---------- src/commands/scan/output-create-new-scan.ts | 30 +++ src/commands/scan/output-delete-scan.ts | 9 + src/commands/scan/output-list-scans.ts | 43 ++++ src/commands/scan/output-scan-metadata.ts | 40 +++ ...can.test.ts => output-scan-report.test.ts} | 4 +- ...ort-full-scan.ts => output-scan-report.ts} | 81 +++---- ...{view-full-scan.ts => output-scan-view.ts} | 14 +- .../{stream-full-scan.ts => streamScan.ts} | 21 +- src/utils/path-resolve.ts | 2 +- test/path-resolve.test.ts | 6 +- 34 files changed, 724 insertions(+), 573 deletions(-) delete mode 100644 src/commands/scan/create-full-scan.ts create mode 100644 src/commands/scan/fetch-create-org-full-scan.ts rename src/commands/scan/{delete-full-scan.ts => fetch-delete-org-full-scan.ts} (55%) create mode 100644 src/commands/scan/fetch-list-scans.ts create mode 100644 src/commands/scan/fetch-scan-metadata.ts rename src/commands/scan/{get-full-scan.ts => fetch-scan.ts} (92%) create mode 100644 src/commands/scan/fetch-supported-scan-file-names.ts delete mode 100644 src/commands/scan/get-full-scan-metadata.ts create mode 100644 src/commands/scan/handle-create-new-scan.ts create mode 100644 src/commands/scan/handle-delete-scan.ts create mode 100644 src/commands/scan/handle-list-scans.ts create mode 100644 src/commands/scan/handle-scan-metadata.ts create mode 100644 src/commands/scan/handle-scan-report.ts create mode 100644 src/commands/scan/handle-scan-view.ts delete mode 100644 src/commands/scan/list-full-scans.ts create mode 100644 src/commands/scan/output-create-new-scan.ts create mode 100644 src/commands/scan/output-delete-scan.ts create mode 100644 src/commands/scan/output-list-scans.ts create mode 100644 src/commands/scan/output-scan-metadata.ts rename src/commands/scan/{report-full-scan.test.ts => output-scan-report.test.ts} (98%) rename src/commands/scan/{report-full-scan.ts => output-scan-report.ts} (79%) rename src/commands/scan/{view-full-scan.ts => output-scan-view.ts} (80%) rename src/commands/scan/{stream-full-scan.ts => streamScan.ts} (86%) diff --git a/src/commands/report/create-report.ts b/src/commands/report/create-report.ts index 52926839e..23f3ba082 100644 --- a/src/commands/report/create-report.ts +++ b/src/commands/report/create-report.ts @@ -3,7 +3,7 @@ import { pluralize } from '@socketsecurity/registry/lib/words' import constants from '../../constants' import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' -import { getPackageFilesFullScans } from '../../utils/path-resolve' +import { getPackageFilesForScan } from '../../utils/path-resolve' import { setupSdk } from '../../utils/sdk' import type { SocketYml } from '@socketsecurity/config' @@ -40,7 +40,7 @@ export async function createReport( cause }) }) - const packagePaths = await getPackageFilesFullScans( + const packagePaths = await getPackageFilesForScan( cwd, inputPaths, supportedFiles, diff --git a/src/commands/report/view-report.ts b/src/commands/report/view-report.ts index a8b242907..41b407729 100644 --- a/src/commands/report/view-report.ts +++ b/src/commands/report/view-report.ts @@ -1,6 +1,6 @@ import { fetchReportData } from './fetch-report-data' import { formatReportDataOutput } from './format-report-data' -import { getFullScan } from '../scan/get-full-scan' +import { fetchScan } from '../scan/fetch-scan' import type { components } from '@socketsecurity/sdk/types/api' @@ -21,7 +21,7 @@ export async function viewReport( const result = await fetchReportData(reportId, all, strict) const artifacts: Array | undefined = - await getFullScan('socketdev', reportId) + await fetchScan('socketdev', reportId) if (result) { formatReportDataOutput( diff --git a/src/commands/scan/cmd-scan-create.ts b/src/commands/scan/cmd-scan-create.ts index 8048a402f..0d50aba24 100644 --- a/src/commands/scan/cmd-scan-create.ts +++ b/src/commands/scan/cmd-scan-create.ts @@ -5,11 +5,15 @@ import colors from 'yoctocolors-cjs' import { logger } from '@socketsecurity/registry/lib/logger' -import { createFullScan } from './create-full-scan' +import { handleCreateNewScan } from './handle-create-new-scan' +import { suggestOrgSlug } from './suggest-org-slug' +import { suggestRepoSlug } from './suggest-repo-slug' +import { suggestBranchSlug } from './suggest_branch_slug' +import { suggestTarget } from './suggest_target' import constants from '../../constants' import { meowOrExit } from '../../utils/meow-with-subcommands' import { getFlagListOutput } from '../../utils/output-formatting' -import { getDefaultToken } from '../../utils/sdk' +import { getDefaultToken, setupSdk } from '../../utils/sdk' import type { CliCommandConfig } from '../../utils/meow-with-subcommands' @@ -142,27 +146,78 @@ async function run( parentName }) - const [orgSlug = '', ...targets] = cli.input - + const { cwd: cwdOverride, dryRun } = cli.flags const cwd = - cli.flags['cwd'] && cli.flags['cwd'] !== 'process.cwd()' - ? String(cli.flags['cwd']) + cwdOverride && cwdOverride !== 'process.cwd()' + ? String(cwdOverride) : process.cwd() + let { branch: branchName, repo: repoName } = cli.flags + let [orgSlug = '', ...targets] = cli.input + + // We're going to need an api token to suggest data because those suggestions + // must come from data we already know. Don't error on missing api token yet. + // If the api-token is not set, ignore it for the sake of suggestions. + const apiToken = getDefaultToken() + + // If we updated any inputs then we should print the command line to repeat + // the command without requiring user input, as a suggestion. + let updatedInput = false + + if (!targets.length && !dryRun) { + const received = await suggestTarget() + targets = received ?? [] + updatedInput = true + } + + // If the current cwd is unknown and is used as a repo slug anyways, we will + // first need to register the slug before we can use it. + let repoDefaultBranch = '' + + // Only do suggestions with an apiToken and when not in dryRun mode + if (apiToken && !dryRun) { + const sockSdk = await setupSdk() + + if (!orgSlug) { + const suggestion = await suggestOrgSlug(sockSdk) + if (suggestion) orgSlug = suggestion + updatedInput = true + } + + // (Don't bother asking for the rest if we didn't get an org slug above) + if (orgSlug && !repoName) { + const suggestion = await suggestRepoSlug(sockSdk, orgSlug) + if (suggestion) { + repoDefaultBranch = suggestion.defaultBranch + repoName = suggestion.slug + } + updatedInput = true + } - const { branch: branchName, repo: repoName } = cli.flags + // (Don't bother asking for the rest if we didn't get an org/repo above) + if (orgSlug && repoName && !branchName) { + const suggestion = await suggestBranchSlug(repoDefaultBranch) + if (suggestion) branchName = suggestion + updatedInput = true + } + } - const apiToken = getDefaultToken() // This checks if we _can_ suggest anything + if (updatedInput) { + logger.error( + 'Note: You can invoke this command next time to skip the interactive questions:' + ) + logger.error('```') + logger.error( + ` socket scan create [other flags...] --repo ${repoName} --branch ${branchName} ${orgSlug} ${targets.join(' ')}` + ) + logger.error('```') + } - if (!apiToken && (!orgSlug || !repoName || !branchName || !targets.length)) { - // Without api token we cannot recover because we can't request more info - // from the server, to match and help with the current cwd/git status. - // + if (!orgSlug || !repoName || !branchName || !targets.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( - stripIndents` + logger.fail(stripIndents` ${colors.bgRed(colors.white('Input error'))}: Please provide the required fields: - Org name as the first argument ${!orgSlug ? colors.red('(missing!)') : colors.green('(ok)')} @@ -171,30 +226,26 @@ async function run( - Branch name using --branch ${!branchName ? colors.red('(missing!)') : colors.green('(ok)')} - - At least one TARGET (e.g. \`.\` or \`./package.json\`) ${!targets.length ? '(missing)' : colors.green('(ok)')} + - At least one TARGET (e.g. \`.\` or \`./package.json\`) ${!targets.length ? colors.red('(missing)') : colors.green('(ok)')} - (Additionally, no API Token was set so we cannot auto-discover these details) - ` - ) + ${!apiToken ? 'Note: was unable to make suggestions because no API Token was found; this would make the command fail regardless' : ''} + `) return } // Note exiting earlier to skirt a hidden auth requirement - if (cli.flags['dryRun']) { + if (dryRun) { logger.log(DRY_RUN_BAIL_TEXT) return } - await createFullScan({ + await handleCreateNewScan({ branchName: branchName as string, - commitHash: (cli.flags['commitHash'] as string) ?? '', commitMessage: (cli.flags['commitMessage'] as string) ?? '', - committers: (cli.flags['committers'] as string) ?? '', cwd, defaultBranch: Boolean(cli.flags['defaultBranch']), orgSlug, pendingHead: Boolean(cli.flags['pendingHead']), - pullRequest: (cli.flags['pullRequest'] as number) ?? undefined, readOnly: Boolean(cli.flags['readOnly']), repoName: repoName as string, targets, diff --git a/src/commands/scan/cmd-scan-del.ts b/src/commands/scan/cmd-scan-del.ts index 10c8fd59c..4dfd5569a 100644 --- a/src/commands/scan/cmd-scan-del.ts +++ b/src/commands/scan/cmd-scan-del.ts @@ -3,7 +3,7 @@ import colors from 'yoctocolors-cjs' import { logger } from '@socketsecurity/registry/lib/logger' -import { deleteOrgFullScan } from './delete-full-scan' +import { handleDeleteScan } from './handle-delete-scan' import constants from '../../constants' import { commonFlags, outputFlags } from '../../flags' import { meowOrExit } from '../../utils/meow-with-subcommands' @@ -51,9 +51,9 @@ async function run( parentName }) - const [orgSlug = '', fullScanId = ''] = cli.input + const [orgSlug = '', scanId = ''] = cli.input - if (!orgSlug || !fullScanId) { + if (!orgSlug || !scanId) { // 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 @@ -63,7 +63,7 @@ async function run( - Org name as the first argument ${!orgSlug ? colors.red('(missing!)') : colors.green('(ok)')} - - Full Scan ID to delete as second argument ${!fullScanId ? colors.red('(missing!)') : colors.green('(ok)')}` + - Full Scan ID to delete as second argument ${!scanId ? colors.red('(missing!)') : colors.green('(ok)')}` ) return } @@ -73,5 +73,5 @@ async function run( return } - await deleteOrgFullScan(orgSlug, fullScanId) + await handleDeleteScan(orgSlug, scanId) } diff --git a/src/commands/scan/cmd-scan-list.ts b/src/commands/scan/cmd-scan-list.ts index e531dc0a0..38f2003a7 100644 --- a/src/commands/scan/cmd-scan-list.ts +++ b/src/commands/scan/cmd-scan-list.ts @@ -3,7 +3,7 @@ import colors from 'yoctocolors-cjs' import { logger } from '@socketsecurity/registry/lib/logger' -import { listFullScans } from './list-full-scans' +import { handleListScans } from './handle-list-scans' import constants from '../../constants' import { commonFlags, outputFlags } from '../../flags' import { meowOrExit } from '../../utils/meow-with-subcommands' @@ -111,7 +111,7 @@ async function run( return } - await listFullScans({ + await handleListScans({ direction: String(cli.flags['direction'] || ''), from_time: String(cli.flags['fromTime'] || ''), orgSlug, diff --git a/src/commands/scan/cmd-scan-metadata.ts b/src/commands/scan/cmd-scan-metadata.ts index 0e4de04aa..0b9237184 100644 --- a/src/commands/scan/cmd-scan-metadata.ts +++ b/src/commands/scan/cmd-scan-metadata.ts @@ -3,7 +3,7 @@ import colors from 'yoctocolors-cjs' import { logger } from '@socketsecurity/registry/lib/logger' -import { getOrgScanMetadata } from './get-full-scan-metadata' +import { handleOrgScanMetadata } from './handle-scan-metadata' import constants from '../../constants' import { commonFlags, outputFlags } from '../../flags' import { meowOrExit } from '../../utils/meow-with-subcommands' @@ -54,9 +54,9 @@ async function run( parentName }) - const [orgSlug = '', fullScanId = ''] = cli.input + const [orgSlug = '', scanId = ''] = cli.input - if (!orgSlug || !fullScanId) { + if (!orgSlug || !scanId) { // 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 @@ -66,7 +66,7 @@ async function run( - Org name as the first argument ${!orgSlug ? colors.red('(missing!)') : colors.green('(ok)')} - - Full Scan ID to inspect as second argument ${!fullScanId ? colors.red('(missing!)') : colors.green('(ok)')}` + - Full Scan ID to inspect as second argument ${!scanId ? colors.red('(missing!)') : colors.green('(ok)')}` ) return } @@ -76,9 +76,9 @@ async function run( return } - await getOrgScanMetadata( + await handleOrgScanMetadata( orgSlug, - fullScanId, + scanId, cli.flags['json'] ? 'json' : cli.flags['markdown'] ? 'markdown' : 'print' ) } diff --git a/src/commands/scan/cmd-scan-report.ts b/src/commands/scan/cmd-scan-report.ts index 8469c3442..fabee68ec 100644 --- a/src/commands/scan/cmd-scan-report.ts +++ b/src/commands/scan/cmd-scan-report.ts @@ -3,7 +3,7 @@ import colors from 'yoctocolors-cjs' import { logger } from '@socketsecurity/registry/lib/logger' -import { reportFullScan } from './report-full-scan' +import { handleScanReport } from './handle-scan-report' import constants from '../../constants' import { commonFlags, outputFlags } from '../../flags' import { meowOrExit } from '../../utils/meow-with-subcommands' @@ -105,11 +105,11 @@ async function run( security } = cli.flags - const [orgSlug = '', fullScanId = '', file = '-'] = cli.input + const [orgSlug = '', scanId = '', file = '-'] = cli.input if ( !orgSlug || - !fullScanId || + !scanId || // (!license && !security) || (json && markdown) ) { @@ -123,7 +123,7 @@ async function run( - Org name as the first argument ${!orgSlug ? colors.red('(missing!)') : colors.green('(ok)')} - - Full Scan ID to fetch as second argument ${!fullScanId ? colors.red('(missing!)') : colors.green('(ok)')} + - Full Scan ID to fetch as second argument ${!scanId ? colors.red('(missing!)') : colors.green('(ok)')} - Not both the --json and --markdown flags ${json && markdown ? colors.red('(pick one!)') : colors.green('(ok)')} ` @@ -137,9 +137,9 @@ async function run( return } - await reportFullScan({ + await handleScanReport({ orgSlug, - fullScanId, + scanId: scanId, includeLicensePolicy: false, // !!license, includeSecurityPolicy: typeof security === 'boolean' ? security : true, outputKind: json ? 'json' : markdown ? 'markdown' : 'text', diff --git a/src/commands/scan/cmd-scan-view.ts b/src/commands/scan/cmd-scan-view.ts index e709519f5..67fc91836 100644 --- a/src/commands/scan/cmd-scan-view.ts +++ b/src/commands/scan/cmd-scan-view.ts @@ -3,8 +3,8 @@ import colors from 'yoctocolors-cjs' import { logger } from '@socketsecurity/registry/lib/logger' -import { streamFullScan } from './stream-full-scan' -import { viewFullScan } from './view-full-scan' +import { handleScanView } from './handle-scan-view' +import { streamScan } from './streamScan' import constants from '../../constants' import { commonFlags, outputFlags } from '../../flags' import { meowOrExit } from '../../utils/meow-with-subcommands' @@ -57,9 +57,9 @@ async function run( parentName }) - const [orgSlug = '', fullScanId = '', file = '-'] = cli.input + const [orgSlug = '', scanId = '', file = '-'] = cli.input - if (!orgSlug || !fullScanId) { + if (!orgSlug || !scanId) { // 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 @@ -70,7 +70,7 @@ async function run( - Org name as the first argument ${!orgSlug ? colors.red('(missing!)') : colors.green('(ok)')} - - Full Scan ID to fetch as second argument ${!fullScanId ? colors.red('(missing!)') : colors.green('(ok)')} + - Full Scan ID to fetch as second argument ${!scanId ? colors.red('(missing!)') : colors.green('(ok)')} ` ) return @@ -82,8 +82,8 @@ async function run( } if (cli.flags['json']) { - await streamFullScan(orgSlug, fullScanId, file) + await streamScan(orgSlug, scanId, file) } else { - await viewFullScan(orgSlug, fullScanId, file) + await handleScanView(orgSlug, scanId, file) } } diff --git a/src/commands/scan/create-full-scan.ts b/src/commands/scan/create-full-scan.ts deleted file mode 100644 index 00a0ead8a..000000000 --- a/src/commands/scan/create-full-scan.ts +++ /dev/null @@ -1,227 +0,0 @@ -import assert from 'node:assert' -import process from 'node:process' -import readline from 'node:readline/promises' - -import { stripIndents } from 'common-tags' -import open from 'open' -import colors from 'yoctocolors-cjs' - -import { logger } from '@socketsecurity/registry/lib/logger' - -import { suggestOrgSlug } from './suggest-org-slug' -import { suggestRepoSlug } from './suggest-repo-slug' -import { suggestBranchSlug } from './suggest_branch_slug' -import { suggestTarget } from './suggest_target' -import constants from '../../constants' -import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' -import { AuthError } from '../../utils/errors' -import { getPackageFilesFullScans } from '../../utils/path-resolve' -import { getDefaultToken, setupSdk } from '../../utils/sdk' - -export async function createFullScan({ - branchName, - commitHash: _commitHash, - commitMessage, - committers: _committers, - cwd, - defaultBranch, - orgSlug, - pendingHead, - pullRequest: _pullRequest, - readOnly, - repoName, - targets, - tmp -}: { - branchName: string - commitHash: string - commitMessage: string - committers: string - cwd: string - defaultBranch: boolean - orgSlug: string - pendingHead: boolean - pullRequest: number | undefined - readOnly: boolean - repoName: string - targets: string[] - tmp: boolean -}): Promise { - // Lazily access constants.spinner. - const { spinner } = constants - - // We're going to need an api token to suggest data because those suggestions - // must come from data we already know. Don't error on missing api token yet. - // If the api-token is not set, ignore it for the sake of suggestions. - const apiToken = getDefaultToken() - - const sockSdk = await setupSdk(apiToken) - const supportedFiles = await sockSdk - .getReportSupportedFiles() - .then(res => { - if (!res.success) { - handleUnsuccessfulApiResponse('getReportSupportedFiles', res) - assert( - false, - 'handleUnsuccessfulApiResponse should unconditionally throw' - ) - } - - return res.data - }) - .catch((cause: Error) => { - throw new Error('Failed getting supported files for report', { cause }) - }) - - // If we updated any inputs then we should print the command line to repeat - // the command without requiring user input, as a suggestion. - let updatedInput = false - - if (!targets.length) { - const received = await suggestTarget() - targets = received ?? [] - updatedInput = true - } - - // // TODO: we'll probably use socket.json or something else soon... - // const absoluteConfigPath = path.join(cwd, 'socket.yml') - // const socketConfig = await getSocketConfig(absoluteConfigPath) - - const packagePaths = await getPackageFilesFullScans( - cwd, - targets, - supportedFiles - // socketConfig - ) - - // If the current cwd is unknown and is used as a repo slug anyways, we will - // first need to register the slug before we can use it. - let repoDefaultBranch = '' - - if (apiToken) { - if (!orgSlug) { - const suggestion = await suggestOrgSlug(sockSdk) - if (suggestion) { - orgSlug = suggestion - } - updatedInput = true - } - - // (Don't bother asking for the rest if we didn't get an org slug above) - if (orgSlug && !repoName) { - const suggestion = await suggestRepoSlug(sockSdk, orgSlug) - if (suggestion) { - repoDefaultBranch = suggestion.defaultBranch - repoName = suggestion.slug - } - updatedInput = true - } - - // (Don't bother asking for the rest if we didn't get an org/repo above) - if (orgSlug && repoName && !branchName) { - const suggestion = await suggestBranchSlug(repoDefaultBranch) - if (suggestion) { - branchName = suggestion - } - updatedInput = true - } - } - - if (!orgSlug || !repoName || !branchName || !packagePaths.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( - stripIndents` - ${colors.bgRed(colors.white('Input error'))}: Please provide the required fields: - - - Org name as the first argument ${!orgSlug ? colors.red('(missing!)') : colors.green('(ok)')} - - - Repository name using --repo ${!repoName ? colors.red('(missing!)') : colors.green('(ok)')} - - - Branch name using --branch ${!branchName ? colors.red('(missing!)') : colors.green('(ok)')} - - - At least one TARGET (e.g. \`.\` or \`./package.json\`) ${ - !packagePaths.length - ? colors.red( - targets.length > 0 - ? '(TARGET' + - (targets.length ? 's' : '') + - ' contained no matching/supported files!)' - : '(missing)' - ) - : colors.green('(ok)') - } - - ${!apiToken ? 'Note: was unable to make suggestions because no API Token was found; this would make command fail regardless' : ''} - ` - ) - return - } - - if (updatedInput) { - logger.log( - 'Note: You can invoke this command next time to skip the interactive questions:' - ) - logger.log('```') - logger.log( - ` socket scan create [other flags...] --repo ${repoName} --branch ${branchName} ${orgSlug} ${targets.join(' ')}` - ) - logger.log('```') - } - - 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.' - ) - } - - if (readOnly) { - logger.log('[ReadOnly] Bailing now') - return - } - - spinner.start(`Creating a scan with ${packagePaths.length} packages...`) - - const result = await handleApiCall( - sockSdk.createOrgFullScan( - orgSlug, - { - repo: repoName, - branch: branchName, - commit_message: commitMessage, - make_default_branch: String(defaultBranch), - set_as_pending_head: String(pendingHead), - tmp: String(tmp) - }, - packagePaths, - cwd - ), - 'Creating scan' - ) - - if (!result.success) { - handleUnsuccessfulApiResponse('CreateOrgFullScan', result) - return - } - - spinner.successAndStop('Scan created successfully') - - const link = colors.underline(colors.cyan(`${result.data.html_report_url}`)) - logger.log(`Available at: ${link}`) - - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }) - - const answer = await rl.question( - 'Would you like to open it in your browser? (y/n)' - ) - - if (answer.toLowerCase() === 'y') { - await open(`${result.data.html_report_url}`) - } - rl.close() -} diff --git a/src/commands/scan/fetch-create-org-full-scan.ts b/src/commands/scan/fetch-create-org-full-scan.ts new file mode 100644 index 000000000..a8b777c6d --- /dev/null +++ b/src/commands/scan/fetch-create-org-full-scan.ts @@ -0,0 +1,50 @@ +import constants from '../../constants' +import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' +import { setupSdk } from '../../utils/sdk' + +import type { SocketSdkReturnType } from '@socketsecurity/sdk' + +export async function fetchCreateOrgFullScan( + packagePaths: string[], + orgSlug: string, + repoName: string, + branchName: string, + commitMessage: string, + defaultBranch: boolean, + pendingHead: boolean, + tmp: boolean, + cwd: string +): Promise['data'] | undefined> { + const sockSdk = await setupSdk() + + // Lazily access constants.spinner. + const { spinner } = constants + + spinner.start(`Creating a scan with ${packagePaths.length} packages...`) + + const result = await handleApiCall( + sockSdk.createOrgFullScan( + orgSlug, + { + repo: repoName, + branch: branchName, + commit_message: commitMessage, + make_default_branch: String(defaultBranch), + set_as_pending_head: String(pendingHead), + tmp: String(tmp) + }, + packagePaths, + cwd + ), + 'Creating scan' + ) + + spinner.successAndStop('Scan created successfully') + + if (!result.success) { + handleUnsuccessfulApiResponse('CreateOrgFullScan', result) + return + } + + return result.data +} diff --git a/src/commands/scan/delete-full-scan.ts b/src/commands/scan/fetch-delete-org-full-scan.ts similarity index 55% rename from src/commands/scan/delete-full-scan.ts rename to src/commands/scan/fetch-delete-org-full-scan.ts index c3749e198..2afa94fab 100644 --- a/src/commands/scan/delete-full-scan.ts +++ b/src/commands/scan/fetch-delete-org-full-scan.ts @@ -3,10 +3,12 @@ import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' import { AuthError } from '../../utils/errors' import { getDefaultToken, setupSdk } from '../../utils/sdk' -export async function deleteOrgFullScan( +import type { SocketSdkReturnType } from '@socketsecurity/sdk' + +export async function fetchDeleteOrgFullScan( orgSlug: string, - fullScanId: string -): Promise { + scanId: string +): Promise['data'] | void> { const apiToken = getDefaultToken() if (!apiToken) { throw new AuthError( @@ -14,28 +16,32 @@ export async function deleteOrgFullScan( ) } - await deleteOrgFullScanWithToken(orgSlug, fullScanId, apiToken) + await fetchDeleteOrgFullScanWithToken(apiToken, orgSlug, scanId) } -export async function deleteOrgFullScanWithToken( + +async function fetchDeleteOrgFullScanWithToken( + apiToken: string, orgSlug: string, - fullScanId: string, - apiToken: string -): Promise { + scanId: string +): Promise['data'] | void> { // Lazily access constants.spinner. const { spinner } = constants - spinner.start('Deleting scan...') - const sockSdk = await setupSdk(apiToken) + + spinner.start('Requesting the scan to be deleted...') + const result = await handleApiCall( - sockSdk.deleteOrgFullScan(orgSlug, fullScanId), + sockSdk.deleteOrgFullScan(orgSlug, scanId), 'Deleting scan' ) + spinner.successAndStop('Received response for deleting a scan.') + if (!result.success) { handleUnsuccessfulApiResponse('deleteOrgFullScan', result) return } - spinner.successAndStop('Scan deleted successfully') + return result.data } diff --git a/src/commands/scan/fetch-list-scans.ts b/src/commands/scan/fetch-list-scans.ts new file mode 100644 index 000000000..0860a3a84 --- /dev/null +++ b/src/commands/scan/fetch-list-scans.ts @@ -0,0 +1,84 @@ +import constants from '../../constants' +import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' +import { AuthError } from '../../utils/errors' +import { getDefaultToken, setupSdk } from '../../utils/sdk' + +import type { SocketSdkReturnType } from '@socketsecurity/sdk' + +export async function fetchListScans({ + direction, + from_time, + orgSlug, + page, + per_page, + sort +}: { + direction: string + from_time: string + orgSlug: string + page: number + per_page: number + sort: string +}): Promise['data'] | void> { + 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.' + ) + } + + await fetchListScansWithToken(apiToken, { + direction, + from_time, + orgSlug, + page, + per_page, + sort + }) +} + +async function fetchListScansWithToken( + apiToken: string, + { + direction, + from_time, + orgSlug, + page, + per_page, + sort + }: { + direction: string + from_time: string // seconds + orgSlug: string + page: number + per_page: number + sort: string + } +): Promise['data'] | void> { + // Lazily access constants.spinner. + const { spinner } = constants + + const sockSdk = await setupSdk(apiToken) + + spinner.start('Fetching list of scans...') + + const result = await handleApiCall( + sockSdk.getOrgFullScanList(orgSlug, { + sort, + direction, + per_page: String(per_page), + page: String(page), + from: from_time + }), + 'Listing scans' + ) + + spinner.successAndStop(`Received response for list of scans.`) + + if (!result.success) { + handleUnsuccessfulApiResponse('getOrgFullScanList', result) + return + } + + return result.data +} diff --git a/src/commands/scan/fetch-report-data.ts b/src/commands/scan/fetch-report-data.ts index 4bed292e9..82eb95c33 100644 --- a/src/commands/scan/fetch-report-data.ts +++ b/src/commands/scan/fetch-report-data.ts @@ -20,7 +20,7 @@ import type { components } from '@socketsecurity/sdk/types/api' */ export async function fetchReportData( orgSlug: string, - fullScanId: string, + scanId: string, // includeLicensePolicy: boolean, includeSecurityPolicy: boolean ): Promise< @@ -103,7 +103,7 @@ export async function fetchReportData( (async () => { try { const response = await queryApi( - `orgs/${orgSlug}/full-scans/${encodeURIComponent(fullScanId)}`, + `orgs/${orgSlug}/full-scans/${encodeURIComponent(scanId)}`, apiToken ) diff --git a/src/commands/scan/fetch-scan-metadata.ts b/src/commands/scan/fetch-scan-metadata.ts new file mode 100644 index 000000000..a162d8c23 --- /dev/null +++ b/src/commands/scan/fetch-scan-metadata.ts @@ -0,0 +1,47 @@ +import constants from '../../constants' +import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' +import { AuthError } from '../../utils/errors' +import { getDefaultToken, setupSdk } from '../../utils/sdk' + +import type { SocketSdkReturnType } from '@socketsecurity/sdk' + +export async function fetchScanMetadata( + orgSlug: string, + scanId: string +): Promise['data'] | void> { + 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.' + ) + } + + await fetchScanMetadataWithToken(apiToken, orgSlug, scanId) +} + +async function fetchScanMetadataWithToken( + apiToken: string, + orgSlug: string, + scanId: string +): Promise['data'] | void> { + // Lazily access constants.spinner. + const { spinner } = constants + + const sockSdk = await setupSdk(apiToken) + + spinner.start('Fetching meta data for a full scan...') + + const result = await handleApiCall( + sockSdk.getOrgFullScanMetadata(orgSlug, scanId), + 'Listing scans' + ) + + spinner.successAndStop('Received response for scan meta data.') + + if (!result.success) { + handleUnsuccessfulApiResponse('getOrgFullScanMetadata', result) + return + } + + return result.data +} diff --git a/src/commands/scan/get-full-scan.ts b/src/commands/scan/fetch-scan.ts similarity index 92% rename from src/commands/scan/get-full-scan.ts rename to src/commands/scan/fetch-scan.ts index 2a1a3fb83..130ca25d8 100644 --- a/src/commands/scan/get-full-scan.ts +++ b/src/commands/scan/fetch-scan.ts @@ -9,9 +9,9 @@ import { getDefaultToken } from '../../utils/sdk' import type { components } from '@socketsecurity/sdk/types/api' -export async function getFullScan( +export async function fetchScan( orgSlug: string, - fullScanId: string + scanId: string ): Promise | undefined> { // Lazily access constants.spinner. const { spinner } = constants @@ -26,7 +26,7 @@ export async function getFullScan( spinner.start('Fetching full-scan...') const response = await queryApi( - `orgs/${orgSlug}/full-scans/${encodeURIComponent(fullScanId)}`, + `orgs/${orgSlug}/full-scans/${encodeURIComponent(scanId)}`, apiToken ) diff --git a/src/commands/scan/fetch-supported-scan-file-names.ts b/src/commands/scan/fetch-supported-scan-file-names.ts new file mode 100644 index 000000000..8ac4bcbf0 --- /dev/null +++ b/src/commands/scan/fetch-supported-scan-file-names.ts @@ -0,0 +1,32 @@ +import constants from '../../constants' +import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' +import { setupSdk } from '../../utils/sdk' + +import type { SocketSdkReturnType } from '@socketsecurity/sdk' + +export async function fetchSupportedScanFileNames(): Promise< + SocketSdkReturnType<'getReportSupportedFiles'>['data'] | undefined +> { + const sockSdk = await setupSdk() + + // Lazily access constants.spinner. + const { spinner } = constants + + spinner.start('Requesting supported scan file types from API...') + + const result = await handleApiCall( + sockSdk.getReportSupportedFiles(), + 'fetching supported scan file types' + ) + + spinner.successAndStop( + 'Received response while fetched supported scan file types.' + ) + + if (!result.success) { + handleUnsuccessfulApiResponse('getReportSupportedFiles', result) + return + } + + return result.data +} diff --git a/src/commands/scan/get-full-scan-metadata.ts b/src/commands/scan/get-full-scan-metadata.ts deleted file mode 100644 index 87ad5e20a..000000000 --- a/src/commands/scan/get-full-scan-metadata.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - -import constants from '../../constants' -import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' -import { AuthError } from '../../utils/errors' -import { getDefaultToken, setupSdk } from '../../utils/sdk' - -export async function getOrgScanMetadata( - orgSlug: string, - scanId: string, - outputKind: 'json' | 'markdown' | 'print' -): Promise { - 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.' - ) - } - - await getOrgScanMetadataWithToken(orgSlug, scanId, apiToken, outputKind) -} -export async function getOrgScanMetadataWithToken( - orgSlug: string, - scanId: string, - apiToken: string, - outputKind: 'json' | 'markdown' | 'print' -): Promise { - // Lazily access constants.spinner. - const { spinner } = constants - - spinner.start('Fetching meta data for a full scan...') - - const sockSdk = await setupSdk(apiToken) - const result = await handleApiCall( - sockSdk.getOrgFullScanMetadata(orgSlug, scanId), - 'Listing scans' - ) - - if (!result.success) { - handleUnsuccessfulApiResponse('getOrgFullScanMetadata', result) - return - } - - spinner?.successAndStop('Fetched the meta data\n') - - if (outputKind === 'json') { - logger.log(result.data) - } else { - // Markdown = print - if (outputKind === 'markdown') { - logger.log('# Scan meta data\n') - } - logger.log(`Scan ID: ${scanId}\n`) - for (const [key, value] of Object.entries(result.data)) { - if ( - [ - 'id', - 'updated_at', - 'organization_id', - 'repository_id', - 'commit_hash', - 'html_report_url' - ].includes(key) - ) - continue - logger.log(`- ${key}:`, value) - } - if (outputKind === 'markdown') { - logger.log( - `\nYou can view this report at: [${result.data.html_report_url}](${result.data.html_report_url})\n` - ) - } else { - logger.log( - `\nYou can view this report at: ${result.data.html_report_url}]\n` - ) - } - } -} diff --git a/src/commands/scan/handle-create-new-scan.ts b/src/commands/scan/handle-create-new-scan.ts new file mode 100644 index 000000000..bbc7952bc --- /dev/null +++ b/src/commands/scan/handle-create-new-scan.ts @@ -0,0 +1,87 @@ +import process from 'node:process' + +import { stripIndents } from 'common-tags' +import colors from 'yoctocolors-cjs' + +import { logger } from '@socketsecurity/registry/lib/logger' + +import { fetchCreateOrgFullScan } from './fetch-create-org-full-scan' +import { fetchSupportedScanFileNames } from './fetch-supported-scan-file-names' +import { outputCreateNewScan } from './output-create-new-scan' +import { AuthError } from '../../utils/errors' +import { getPackageFilesForScan } from '../../utils/path-resolve' +import { getDefaultToken } from '../../utils/sdk' + +export async function handleCreateNewScan({ + branchName, + commitMessage, + cwd, + defaultBranch, + orgSlug, + pendingHead, + readOnly, + repoName, + targets, + tmp +}: { + branchName: string + commitMessage: string + cwd: string + defaultBranch: boolean + orgSlug: string + pendingHead: boolean + readOnly: boolean + repoName: string + targets: string[] + tmp: boolean +}): Promise { + const apiToken = getDefaultToken() + + // Note: you need an apiToken to request supportedScanFileNames from the API + if (!apiToken) { + throw new AuthError( + 'User must be authenticated to create and submit new scans. To log in, run the command `socket login` and enter your API key.' + ) + } + + const supportedFileNames = await fetchSupportedScanFileNames() + if (!supportedFileNames) return + + const packagePaths = await getPackageFilesForScan( + cwd, + targets, + supportedFileNames + // socketConfig + ) + + if (!packagePaths.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(stripIndents` + ${colors.bgRed(colors.white('Input error'))}: The TARGET did not contain any matching / supported files for a scan + `) + return + } + + if (readOnly) { + logger.log('[ReadOnly] Bailing now') + return + } + + const data = await fetchCreateOrgFullScan( + packagePaths, + orgSlug, + repoName, + branchName, + commitMessage, + defaultBranch, + pendingHead, + tmp, + cwd + ) + if (!data) return + + await outputCreateNewScan(data) +} diff --git a/src/commands/scan/handle-delete-scan.ts b/src/commands/scan/handle-delete-scan.ts new file mode 100644 index 000000000..19936e66c --- /dev/null +++ b/src/commands/scan/handle-delete-scan.ts @@ -0,0 +1,12 @@ +import { fetchDeleteOrgFullScan } from './fetch-delete-org-full-scan' +import { outputDeleteScan } from './output-delete-scan' + +export async function handleDeleteScan( + orgSlug: string, + scanId: string +): Promise { + const data = await fetchDeleteOrgFullScan(orgSlug, scanId) + if (!data) return + + await outputDeleteScan(data) +} diff --git a/src/commands/scan/handle-list-scans.ts b/src/commands/scan/handle-list-scans.ts new file mode 100644 index 000000000..f098a6aea --- /dev/null +++ b/src/commands/scan/handle-list-scans.ts @@ -0,0 +1,32 @@ +import { fetchListScans } from './fetch-list-scans' +import { outputListScans } from './output-list-scans' + +export async function handleListScans({ + direction, + from_time, + orgSlug, + outputKind, + page, + per_page, + sort +}: { + direction: string + from_time: string + orgSlug: string + outputKind: 'json' | 'markdown' | 'print' + page: number + per_page: number + sort: string +}): Promise { + const data = await fetchListScans({ + direction, + from_time, + orgSlug, + page, + per_page, + sort + }) + if (!data) return + + await outputListScans(data, outputKind) +} diff --git a/src/commands/scan/handle-scan-metadata.ts b/src/commands/scan/handle-scan-metadata.ts new file mode 100644 index 000000000..3ebcc7fee --- /dev/null +++ b/src/commands/scan/handle-scan-metadata.ts @@ -0,0 +1,13 @@ +import { fetchScanMetadata } from './fetch-scan-metadata' +import { outputScanMetadata } from './output-scan-metadata' + +export async function handleOrgScanMetadata( + orgSlug: string, + scanId: string, + outputKind: 'json' | 'markdown' | 'print' +): Promise { + const data = await fetchScanMetadata(orgSlug, scanId) + if (!data) return + + await outputScanMetadata(data, scanId, outputKind) +} diff --git a/src/commands/scan/handle-scan-report.ts b/src/commands/scan/handle-scan-report.ts new file mode 100644 index 000000000..63f6f06df --- /dev/null +++ b/src/commands/scan/handle-scan-report.ts @@ -0,0 +1,54 @@ +import { fetchReportData } from './fetch-report-data' +import { outputScanReport } from './output-scan-report' + +export async function handleScanReport({ + filePath, + fold, + includeLicensePolicy, + includeSecurityPolicy, + orgSlug, + outputKind, + reportLevel, + scanId, + short +}: { + orgSlug: string + scanId: string + includeLicensePolicy: boolean + includeSecurityPolicy: boolean + outputKind: 'json' | 'markdown' | 'text' + filePath: string + fold: 'pkg' | 'version' | 'file' | 'none' + reportLevel: 'defer' | 'ignore' | 'monitor' | 'warn' | 'error' + short: boolean +}): Promise { + if (!includeLicensePolicy && !includeSecurityPolicy) { + process.exitCode = 1 + return // caller should assert + } + + const { + // licensePolicy, + ok, + scan, + securityPolicy + } = await fetchReportData( + orgSlug, + scanId, + // includeLicensePolicy + includeSecurityPolicy + ) + if (!ok) return + + await outputScanReport(scan, securityPolicy, { + filePath, + fold, + scanId: scanId, + includeLicensePolicy, + includeSecurityPolicy, + orgSlug, + outputKind, + reportLevel, + short + }) +} diff --git a/src/commands/scan/handle-scan-view.ts b/src/commands/scan/handle-scan-view.ts new file mode 100644 index 000000000..5602e266b --- /dev/null +++ b/src/commands/scan/handle-scan-view.ts @@ -0,0 +1,13 @@ +import { fetchScan } from './fetch-scan' +import { outputScanView } from './output-scan-view' + +export async function handleScanView( + orgSlug: string, + scanId: string, + filePath: string +): Promise { + const data = await fetchScan(orgSlug, scanId) + if (!data) return + + await outputScanView(data, orgSlug, scanId, filePath) +} diff --git a/src/commands/scan/list-full-scans.ts b/src/commands/scan/list-full-scans.ts deleted file mode 100644 index 8aa97ff1b..000000000 --- a/src/commands/scan/list-full-scans.ts +++ /dev/null @@ -1,121 +0,0 @@ -// @ts-ignore -import chalkTable from 'chalk-table' -import colors from 'yoctocolors-cjs' - -import { logger } from '@socketsecurity/registry/lib/logger' - -import constants from '../../constants' -import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' -import { AuthError } from '../../utils/errors' -import { getDefaultToken, setupSdk } from '../../utils/sdk' - -export async function listFullScans({ - direction, - from_time, - orgSlug, - outputKind, - page, - per_page, - sort -}: { - direction: string - from_time: string - orgSlug: string - outputKind: 'json' | 'markdown' | 'print' - page: number - per_page: number - sort: string -}): Promise { - 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.' - ) - } - - await listFullScansWithToken({ - apiToken, - direction, - from_time, - orgSlug, - outputKind, - page, - per_page, - sort - }) -} - -async function listFullScansWithToken({ - apiToken, - direction, - from_time, - orgSlug, - outputKind, - page, - per_page, - sort -}: { - apiToken: string - direction: string - from_time: string // seconds - orgSlug: string - outputKind: 'json' | 'markdown' | 'print' - page: number - per_page: number - sort: string -}): Promise { - // Lazily access constants.spinner. - const { spinner } = constants - - spinner.start('Fetching list of scans...') - - const sockSdk = await setupSdk(apiToken) - const result = await handleApiCall( - sockSdk.getOrgFullScanList(orgSlug, { - sort, - direction, - per_page: String(per_page), - page: String(page), - from: from_time - }), - 'Listing scans' - ) - - if (!result.success) { - handleUnsuccessfulApiResponse('getOrgFullScanList', result) - return - } - - spinner.stop(`Fetch complete`) - - if (outputKind === 'json') { - logger.log(result.data) - return - } - - const options = { - columns: [ - { field: 'id', name: colors.magenta('ID') }, - { field: 'report_url', name: colors.magenta('Scan URL') }, - { field: 'branch', name: colors.magenta('Branch') }, - { field: 'created_at', name: colors.magenta('Created at') } - ] - } - - const formattedResults = result.data.results.map(d => { - return { - id: d.id, - report_url: colors.underline(`${d.html_report_url}`), - created_at: d.created_at - ? new Date(d.created_at).toLocaleDateString('en-us', { - year: 'numeric', - month: 'numeric', - day: 'numeric' - }) - : '', - branch: d.branch - } - }) - - logger.log(chalkTable(options, formattedResults)) -} diff --git a/src/commands/scan/output-create-new-scan.ts b/src/commands/scan/output-create-new-scan.ts new file mode 100644 index 000000000..99a8aeae2 --- /dev/null +++ b/src/commands/scan/output-create-new-scan.ts @@ -0,0 +1,30 @@ +import process from 'node:process' +import readline from 'node:readline/promises' + +import open from 'open' +import colors from 'yoctocolors-cjs' + +import { logger } from '@socketsecurity/registry/lib/logger' + +import type { SocketSdkReturnType } from '@socketsecurity/sdk' + +export async function outputCreateNewScan( + data: SocketSdkReturnType<'CreateOrgFullScan'>['data'] +) { + const link = colors.underline(colors.cyan(`${data.html_report_url}`)) + logger.log(`Available at: ${link}`) + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }) + + const answer = await rl.question( + 'Would you like to open it in your browser? (y/n)' + ) + + if (answer.toLowerCase() === 'y') { + await open(`${data.html_report_url}`) + } + rl.close() +} diff --git a/src/commands/scan/output-delete-scan.ts b/src/commands/scan/output-delete-scan.ts new file mode 100644 index 000000000..2570ee9bb --- /dev/null +++ b/src/commands/scan/output-delete-scan.ts @@ -0,0 +1,9 @@ +import { logger } from '@socketsecurity/registry/lib/logger' + +import type { SocketSdkReturnType } from '@socketsecurity/sdk' + +export async function outputDeleteScan( + _data: SocketSdkReturnType<'deleteOrgFullScan'>['data'] +): Promise { + logger.success('Scan deleted successfully') +} diff --git a/src/commands/scan/output-list-scans.ts b/src/commands/scan/output-list-scans.ts new file mode 100644 index 000000000..fcae6196f --- /dev/null +++ b/src/commands/scan/output-list-scans.ts @@ -0,0 +1,43 @@ +// @ts-ignore +import chalkTable from 'chalk-table' +import colors from 'yoctocolors-cjs' + +import { logger } from '@socketsecurity/registry/lib/logger' + +import type { SocketSdkReturnType } from '@socketsecurity/sdk' + +export async function outputListScans( + data: SocketSdkReturnType<'getOrgFullScanList'>['data'], + outputKind: 'json' | 'markdown' | 'print' +): Promise { + if (outputKind === 'json') { + logger.log(data) + return + } + + const options = { + columns: [ + { field: 'id', name: colors.magenta('ID') }, + { field: 'report_url', name: colors.magenta('Scan URL') }, + { field: 'branch', name: colors.magenta('Branch') }, + { field: 'created_at', name: colors.magenta('Created at') } + ] + } + + const formattedResults = data.results.map(d => { + return { + id: d.id, + report_url: colors.underline(`${d.html_report_url}`), + created_at: d.created_at + ? new Date(d.created_at).toLocaleDateString('en-us', { + year: 'numeric', + month: 'numeric', + day: 'numeric' + }) + : '', + branch: d.branch + } + }) + + logger.log(chalkTable(options, formattedResults)) +} diff --git a/src/commands/scan/output-scan-metadata.ts b/src/commands/scan/output-scan-metadata.ts new file mode 100644 index 000000000..006be9d82 --- /dev/null +++ b/src/commands/scan/output-scan-metadata.ts @@ -0,0 +1,40 @@ +import { logger } from '@socketsecurity/registry/lib/logger' + +import type { SocketSdkReturnType } from '@socketsecurity/sdk' + +export async function outputScanMetadata( + data: SocketSdkReturnType<'getOrgFullScanMetadata'>['data'], + scanId: string, + outputKind: 'json' | 'markdown' | 'print' +): Promise { + if (outputKind === 'json') { + logger.log(data) + } else { + // Markdown = print + if (outputKind === 'markdown') { + logger.log('# Scan meta data\n') + } + logger.log(`Scan ID: ${scanId}\n`) + for (const [key, value] of Object.entries(data)) { + if ( + [ + 'id', + 'updated_at', + 'organization_id', + 'repository_id', + 'commit_hash', + 'html_report_url' + ].includes(key) + ) + continue + logger.log(`- ${key}:`, value) + } + if (outputKind === 'markdown') { + logger.log( + `\nYou can view this report at: [${data.html_report_url}](${data.html_report_url})\n` + ) + } else { + logger.log(`\nYou can view this report at: ${data.html_report_url}]\n`) + } + } +} diff --git a/src/commands/scan/report-full-scan.test.ts b/src/commands/scan/output-scan-report.test.ts similarity index 98% rename from src/commands/scan/report-full-scan.test.ts rename to src/commands/scan/output-scan-report.test.ts index d3dc2d247..2dd991798 100644 --- a/src/commands/scan/report-full-scan.test.ts +++ b/src/commands/scan/output-scan-report.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it } from 'vitest' -import { toJsonReport, toMarkdownReport } from './report-full-scan' +import { toJsonReport, toMarkdownReport } from './output-scan-report' import type { ScanReport } from './generate-report' -describe('report-full-scan', () => { +describe('output-scan-report', () => { describe('toJsonReport', () => { it('should be able to generate a healthy json report', () => { expect(toJsonReport(getHealthyReport())).toMatchInlineSnapshot(` diff --git a/src/commands/scan/report-full-scan.ts b/src/commands/scan/output-scan-report.ts similarity index 79% rename from src/commands/scan/report-full-scan.ts rename to src/commands/scan/output-scan-report.ts index 9f76c6028..68061d1c1 100644 --- a/src/commands/scan/report-full-scan.ts +++ b/src/commands/scan/output-scan-report.ts @@ -2,75 +2,56 @@ import fs from 'node:fs/promises' import { logger } from '@socketsecurity/registry/lib/logger' -import { fetchReportData } from './fetch-report-data' import { generateReport } from './generate-report' import { mapToObject } from '../../utils/map-to-object' import { mdTable } from '../../utils/markdown' import { walkNestedMap } from '../../utils/walk-nested-map' import type { ReportLeafNode, ScanReport } from './generate-report' - -export async function reportFullScan({ - filePath, - fold, - fullScanId, - includeLicensePolicy, - includeSecurityPolicy, - orgSlug, - outputKind, - reportLevel, - short -}: { - orgSlug: string - fullScanId: string - includeLicensePolicy: boolean - includeSecurityPolicy: boolean - outputKind: 'json' | 'markdown' | 'text' - filePath: string - fold: 'pkg' | 'version' | 'file' | 'none' - reportLevel: 'defer' | 'ignore' | 'monitor' | 'warn' | 'error' - short: boolean -}): Promise { - logger.error( - 'output:', - outputKind, - ', file:', +import type { SocketSdkReturnType } from '@socketsecurity/sdk' +import type { components } from '@socketsecurity/sdk/types/api' + +export async function outputScanReport( + scan: Array, + // licensePolicy: undefined | SocketSdkReturnType<'getOrgSecurityPolicy'>, + securityPolicy: undefined | SocketSdkReturnType<'getOrgSecurityPolicy'>, + { filePath, - ', fold:', fold, - ', reportLevel:', - reportLevel - ) + includeLicensePolicy, + includeSecurityPolicy, + orgSlug, + outputKind, + reportLevel, + scanId, + short + }: { + orgSlug: string + scanId: string + includeLicensePolicy: boolean + includeSecurityPolicy: boolean + outputKind: 'json' | 'markdown' | 'text' + filePath: string + fold: 'pkg' | 'version' | 'file' | 'none' + reportLevel: 'defer' | 'ignore' | 'monitor' | 'warn' | 'error' + short: boolean + } +): Promise { if (!includeLicensePolicy && !includeSecurityPolicy) { + process.exitCode = 1 return // caller should assert } - const { - // licensePolicy, - ok, - scan, - securityPolicy - } = await fetchReportData( - orgSlug, - fullScanId, - // includeLicensePolicy - includeSecurityPolicy - ) - - if (!ok) { - return - } - const scanReport = generateReport( scan, undefined, // licensePolicy, securityPolicy, { orgSlug, - scanId: fullScanId, + scanId, fold, - short, - reportLevel + reportLevel, + short } ) diff --git a/src/commands/scan/view-full-scan.ts b/src/commands/scan/output-scan-view.ts similarity index 80% rename from src/commands/scan/view-full-scan.ts rename to src/commands/scan/output-scan-view.ts index 956ca279c..ab0964f92 100644 --- a/src/commands/scan/view-full-scan.ts +++ b/src/commands/scan/output-scan-view.ts @@ -2,20 +2,16 @@ import fs from 'node:fs/promises' import { logger } from '@socketsecurity/registry/lib/logger' -import { getFullScan } from './get-full-scan' import { mdTable } from '../../utils/markdown' import type { components } from '@socketsecurity/sdk/types/api' -export async function viewFullScan( +export async function outputScanView( + artifacts: Array, orgSlug: string, - fullScanId: string, + scanId: string, filePath: string ): Promise { - const artifacts: Array | undefined = - await getFullScan(orgSlug, fullScanId) - if (!artifacts) return - const display = artifacts.map(art => { const author = Array.isArray(art.author) ? `${art.author[0]}${art.author.length > 1 ? ' et.al.' : ''}` @@ -43,11 +39,11 @@ export async function viewFullScan( These are the artifacts and their scores found. -Sscan ID: ${fullScanId} +Scan ID: ${scanId} ${md} -View this report at: https://socket.dev/dashboard/org/${orgSlug}/sbom/${fullScanId} +View this report at: https://socket.dev/dashboard/org/${orgSlug}/sbom/${scanId} `.trim() + '\n' if (filePath && filePath !== '-') { diff --git a/src/commands/scan/stream-full-scan.ts b/src/commands/scan/streamScan.ts similarity index 86% rename from src/commands/scan/stream-full-scan.ts rename to src/commands/scan/streamScan.ts index 2fc1b8fe7..ababc1c4c 100644 --- a/src/commands/scan/stream-full-scan.ts +++ b/src/commands/scan/streamScan.ts @@ -5,9 +5,9 @@ import { getDefaultToken, setupSdk } from '../../utils/sdk' import type { SocketSdkResultType } from '@socketsecurity/sdk' -export async function streamFullScan( +export async function streamScan( orgSlug: string, - fullScanId: string, + scanId: string, file: string | undefined ): Promise | undefined> { // Lazily access constants.spinner. @@ -20,26 +20,23 @@ export async function streamFullScan( ) } + const sockSdk = await setupSdk(apiToken) + spinner.start('Fetching scan...') - const sockSdk = await setupSdk(apiToken) const data = await handleApiCall( - sockSdk.getOrgFullScan( - orgSlug, - fullScanId, - file === '-' ? undefined : file - ), + sockSdk.getOrgFullScan(orgSlug, scanId, file === '-' ? undefined : file), 'Fetching a scan' ) + spinner?.successAndStop( + file ? `Full scan details written to ${file}` : 'stdout' + ) + if (!data?.success) { handleUnsuccessfulApiResponse('getOrgFullScan', data) return } - spinner?.successAndStop( - file ? `Full scan details written to ${file}` : 'stdout' - ) - return data } diff --git a/src/utils/path-resolve.ts b/src/utils/path-resolve.ts index 4dcdb2218..a55cbf903 100644 --- a/src/utils/path-resolve.ts +++ b/src/utils/path-resolve.ts @@ -248,7 +248,7 @@ export function findNpmPathSync(npmBinPath: string): string | undefined { } } -export async function getPackageFilesFullScans( +export async function getPackageFilesForScan( cwd: string, inputPaths: string[], supportedFiles: SocketSdkReturnType<'getReportSupportedFiles'>['data'], diff --git a/test/path-resolve.test.ts b/test/path-resolve.test.ts index f80e6d109..a52ec2c9b 100644 --- a/test/path-resolve.test.ts +++ b/test/path-resolve.test.ts @@ -6,7 +6,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { normalizePath } from '@socketsecurity/registry/lib/path' -import { getPackageFilesFullScans } from './dist/path-resolve' +import { getPackageFilesForScan } from './dist/path-resolve' const testPath = __dirname const rootNmPath = path.join(testPath, '../node_modules') @@ -79,7 +79,7 @@ const sortedPromise = const result = await fn(...args) return result.sort() } -const sortedGetPackageFilesFullScans = sortedPromise(getPackageFilesFullScans) +const sortedGetPackageFilesFullScans = sortedPromise(getPackageFilesForScan) describe('Path Resolve', () => { beforeEach(() => { @@ -94,7 +94,7 @@ describe('Path Resolve', () => { } }) - describe('getPackageFilesFullScans()', () => { + describe('getPackageFilesForScan()', () => { it('should handle a "." inputPath', async () => { mockTestFs({ [`${mockFixturePath}/package.json`]: '{}' From 24eb10f273b6752331edf84551e90c857f9beb93 Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Fri, 21 Mar 2025 16:20:36 +0100 Subject: [PATCH 2/2] Improve suggestion handling --- src/commands/scan/cmd-scan-create.ts | 13 +++++-------- src/commands/scan/suggest-org-slug.ts | 12 +++++++----- src/commands/scan/suggest-repo-slug.ts | 13 +++++++------ 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/commands/scan/cmd-scan-create.ts b/src/commands/scan/cmd-scan-create.ts index 0d50aba24..64d27dabc 100644 --- a/src/commands/scan/cmd-scan-create.ts +++ b/src/commands/scan/cmd-scan-create.ts @@ -13,7 +13,7 @@ import { suggestTarget } from './suggest_target' import constants from '../../constants' import { meowOrExit } from '../../utils/meow-with-subcommands' import { getFlagListOutput } from '../../utils/output-formatting' -import { getDefaultToken, setupSdk } from '../../utils/sdk' +import { getDefaultToken } from '../../utils/sdk' import type { CliCommandConfig } from '../../utils/meow-with-subcommands' @@ -172,20 +172,17 @@ async function run( // If the current cwd is unknown and is used as a repo slug anyways, we will // first need to register the slug before we can use it. let repoDefaultBranch = '' - // Only do suggestions with an apiToken and when not in dryRun mode if (apiToken && !dryRun) { - const sockSdk = await setupSdk() - if (!orgSlug) { - const suggestion = await suggestOrgSlug(sockSdk) + const suggestion = await suggestOrgSlug() if (suggestion) orgSlug = suggestion updatedInput = true } // (Don't bother asking for the rest if we didn't get an org slug above) if (orgSlug && !repoName) { - const suggestion = await suggestRepoSlug(sockSdk, orgSlug) + const suggestion = await suggestRepoSlug(orgSlug) if (suggestion) { repoDefaultBranch = suggestion.defaultBranch repoName = suggestion.slug @@ -201,7 +198,7 @@ async function run( } } - if (updatedInput) { + if (updatedInput && repoName && branchName && orgSlug && targets?.length) { logger.error( 'Note: You can invoke this command next time to skip the interactive questions:' ) @@ -209,7 +206,7 @@ async function run( logger.error( ` socket scan create [other flags...] --repo ${repoName} --branch ${branchName} ${orgSlug} ${targets.join(' ')}` ) - logger.error('```') + logger.error('```\n') } if (!orgSlug || !repoName || !branchName || !targets.length) { diff --git a/src/commands/scan/suggest-org-slug.ts b/src/commands/scan/suggest-org-slug.ts index 591326d98..310a3de81 100644 --- a/src/commands/scan/suggest-org-slug.ts +++ b/src/commands/scan/suggest-org-slug.ts @@ -1,11 +1,11 @@ +import { logger } from '@socketsecurity/registry/lib/logger' import { select } from '@socketsecurity/registry/lib/prompts' -import { SocketSdk } from '@socketsecurity/sdk' import { handleApiCall } from '../../utils/api' +import { setupSdk } from '../../utils/sdk' -export async function suggestOrgSlug( - sockSdk: SocketSdk -): Promise { +export async function suggestOrgSlug(): Promise { + const sockSdk = await setupSdk() const result = await handleApiCall( sockSdk.getOrganizations(), 'looking up organizations' @@ -33,6 +33,8 @@ export async function suggestOrgSlug( return proceed } } else { - // TODO: in verbose mode, report this error to stderr + logger.fail( + 'Failed to lookup organization list from API, unable to suggest.' + ) } } diff --git a/src/commands/scan/suggest-repo-slug.ts b/src/commands/scan/suggest-repo-slug.ts index d34b74cd2..b1dc73c6b 100644 --- a/src/commands/scan/suggest-repo-slug.ts +++ b/src/commands/scan/suggest-repo-slug.ts @@ -1,18 +1,18 @@ import path from 'node:path' import process from 'node:process' +import { logger } from '@socketsecurity/registry/lib/logger' import { select } from '@socketsecurity/registry/lib/prompts' -import { SocketSdk } from '@socketsecurity/sdk' import { handleApiCall } from '../../utils/api' +import { setupSdk } from '../../utils/sdk' -export async function suggestRepoSlug( - sockSdk: SocketSdk, - orgSlug: string -): Promise<{ +export async function suggestRepoSlug(orgSlug: string): Promise<{ slug: string defaultBranch: string } | void> { + const sockSdk = await setupSdk() + // Same as above, but if there's a repo with the same name as cwd then // default the selection to that name. const result = await handleApiCall( @@ -28,6 +28,7 @@ export async function suggestRepoSlug( }), 'looking up known repos' ) + // Ignore a failed request here. It was not the primary goal of // running this command and reporting it only leads to end-user confusion. if (result.success) { @@ -92,7 +93,7 @@ export async function suggestRepoSlug( return { slug: repoName, defaultBranch: repoDefaultBranch } } } else { - // TODO: in verbose mode, report this error to stderr + logger.fail('Failed to lookup repo list from API, unable to suggest.') } }