diff --git a/src/commands/audit-log/get-audit-log.ts b/src/commands/audit-log/get-audit-log.ts index 940e531eb..10400ec95 100644 --- a/src/commands/audit-log/get-audit-log.ts +++ b/src/commands/audit-log/get-audit-log.ts @@ -7,6 +7,7 @@ import { SocketSdkReturnType } from '@socketsecurity/sdk' import constants from '../../constants' import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' import { AuthError } from '../../utils/errors' +import { mdTable } from '../../utils/markdown' import { getDefaultToken, setupSdk } from '../../utils/sdk' import type { Choice } from '@socketsecurity/registry/lib/prompts' @@ -111,7 +112,7 @@ async function outputAsMarkdown( perPage: number ): Promise { try { - const table = mdTable(auditLogs, [ + const table = mdTable(auditLogs, [ 'event_id', 'created_at', 'type', @@ -144,46 +145,6 @@ ${table} } } -function mdTable< - T extends SocketSdkReturnType<'getAuditLogEvents'>['data']['results'] ->( - 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 -): string { - // Max col width required to fit all data in that column - const cws = cols.map(col => col.length) - - for (const log of logs) { - for (let i = 0; i < cols.length; ++i) { - // @ts-ignore - const val: unknown = log[cols[i] ?? ''] ?? '' - cws[i] = Math.max(cws[i] ?? 0, String(val).length) - } - } - - let div = '|' - for (const cw of cws) div += ' ' + '-'.repeat(cw) + ' |' - - let header = '|' - for (let i = 0; i < cols.length; ++i) - header += ' ' + String(cols[i]).padEnd(cws[i] ?? 0, ' ') + ' |' - - let body = '' - for (const log of logs) { - body += '|' - for (let i = 0; i < cols.length; ++i) { - // @ts-ignore - const val: unknown = log[cols[i] ?? ''] ?? '' - body += ' ' + String(val).padEnd(cws[i] ?? 0, ' ') + ' |' - } - body += '\n' - } - - return [div, header, div, body.trim(), div].filter(s => !!s.trim()).join('\n') -} - async function outputAsPrint( auditLogs: SocketSdkReturnType<'getAuditLogEvents'>['data']['results'], orgSlug: string, diff --git a/src/commands/raw-npm/cmd-raw-npm.ts b/src/commands/raw-npm/cmd-raw-npm.ts index 7d6774ab5..b25297c14 100644 --- a/src/commands/raw-npm/cmd-raw-npm.ts +++ b/src/commands/raw-npm/cmd-raw-npm.ts @@ -3,7 +3,6 @@ import { logger } from '@socketsecurity/registry/lib/logger' import { runRawNpm } from './run-raw-npm' import constants from '../../constants' import { meowOrExit } from '../../utils/meow-with-subcommands' -import { getFlagListOutput } from '../../utils/output-formatting' import type { CliCommandConfig } from '../../utils/meow-with-subcommands' @@ -14,13 +13,10 @@ const config: CliCommandConfig = { description: `Temporarily disable the Socket ${NPM} wrapper`, hidden: false, flags: {}, - help: (command, config) => ` + help: command => ` Usage $ ${command} - Options - ${getFlagListOutput(config.flags, 6)} - Examples $ ${command} install ` diff --git a/src/commands/raw-npx/cmd-raw-npx.ts b/src/commands/raw-npx/cmd-raw-npx.ts index 17c8a7688..695433b39 100644 --- a/src/commands/raw-npx/cmd-raw-npx.ts +++ b/src/commands/raw-npx/cmd-raw-npx.ts @@ -3,7 +3,6 @@ import { logger } from '@socketsecurity/registry/lib/logger' import { runRawNpx } from './run-raw-npx' import constants from '../../constants' import { meowOrExit } from '../../utils/meow-with-subcommands' -import { getFlagListOutput } from '../../utils/output-formatting' import type { CliCommandConfig } from '../../utils/meow-with-subcommands' @@ -14,13 +13,10 @@ const config: CliCommandConfig = { description: `Temporarily disable the Socket ${NPX} wrapper`, hidden: false, flags: {}, - help: (command, config) => ` + help: command => ` Usage $ ${command} - Options - ${getFlagListOutput(config.flags, 6)} - Examples $ ${command} install ` diff --git a/src/commands/report/cmd-report-create.ts b/src/commands/report/cmd-report-create.ts index 251b2c2e8..7b5329fff 100644 --- a/src/commands/report/cmd-report-create.ts +++ b/src/commands/report/cmd-report-create.ts @@ -10,7 +10,6 @@ import constants from '../../constants' import { commonFlags, outputFlags, validationFlags } from '../../flags' import { ColorOrMarkdown } from '../../utils/color-or-markdown' import { meowOrExit } from '../../utils/meow-with-subcommands' -import { getFlagListOutput } from '../../utils/output-formatting' import type { CliCommandConfig } from '../../utils/meow-with-subcommands' @@ -18,7 +17,7 @@ const { DRY_RUN_BAIL_TEXT } = constants const config: CliCommandConfig = { commandName: 'create', - description: 'Create a project report', + description: '[Deprecated] Create a project report', hidden: false, flags: { ...commonFlags, @@ -36,27 +35,9 @@ const config: CliCommandConfig = { description: 'Will wait for and return the created report' } }, - help: (command, config) => ` - Usage - $ ${command} - - Uploads the specified "package.json" and lock files for JavaScript, Python, and Go dependency manifests. - If any folder is specified, the ones found in there recursively are uploaded. - - Supports globbing such as "**/package.json", "**/requirements.txt", "**/pyproject.toml", and "**/go.mod". - - Ignores any file specified in your project's ".gitignore", your project's - "socket.yml" file's "projectIgnorePaths" and also has a sensible set of - default ignores from the "ignore-by-default" module. - - Options - ${getFlagListOutput(config.flags, 6)} - - Examples - $ ${command} . - $ ${command} '**/package.json' - $ ${command} /path/to/a/package.json /path/to/another/package.json - $ ${command} . --view --json + help: () => ` + This command is deprecated in favor of \`socket scan create\`. + It will be removed in the next major release of the CLI. ` } diff --git a/src/commands/report/cmd-report-view.ts b/src/commands/report/cmd-report-view.ts index df19a60e8..7f514b8c0 100644 --- a/src/commands/report/cmd-report-view.ts +++ b/src/commands/report/cmd-report-view.ts @@ -7,7 +7,6 @@ import { viewReport } from './view-report' import constants from '../../constants' import { commonFlags, outputFlags, validationFlags } from '../../flags' import { meowOrExit } from '../../utils/meow-with-subcommands' -import { getFlagListOutput } from '../../utils/output-formatting' import type { CliCommandConfig } from '../../utils/meow-with-subcommands' @@ -15,22 +14,16 @@ const { DRY_RUN_BAIL_TEXT } = constants const config: CliCommandConfig = { commandName: 'view', - description: 'View a project report', + description: '[Deprecated] View a project report', hidden: false, flags: { ...commonFlags, ...outputFlags, ...validationFlags }, - help: (command, config) => ` - Usage - $ ${command} - - Options - ${getFlagListOutput(config.flags, 6)} - - Examples - $ ${command} QXU8PmK7LfH608RAwfIKdbcHgwEd_ZeWJ9QEGv05FJUQ + help: () => ` + This command is deprecated in favor of \`socket scan view\`. + It will be removed in the next major release of the CLI. ` } diff --git a/src/commands/report/cmd-report.ts b/src/commands/report/cmd-report.ts index cc765f6db..de5794f05 100644 --- a/src/commands/report/cmd-report.ts +++ b/src/commands/report/cmd-report.ts @@ -8,6 +8,7 @@ const description = '[Deprecated] Project report related commands' export const cmdReport: CliSubcommand = { description, + hidden: true, // Deprecated in favor of `scan` async run(argv, importMeta, { parentName }) { await meowWithSubcommands( { diff --git a/src/commands/report/create-report.ts b/src/commands/report/create-report.ts index 60677c9ad..4ea7d10f6 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 { getPackageFiles } from '../../utils/path-resolve' +import { getPackageFilesFullScans } from '../../utils/path-resolve' import { setupSdk } from '../../utils/sdk' import type { SocketYml } from '@socketsecurity/config' @@ -40,13 +40,13 @@ export async function createReport( cause }) }) - const packagePaths = await getPackageFiles( + const packagePaths = await getPackageFilesFullScans( cwd, inputPaths, - socketConfig, - supportedFiles + supportedFiles, + socketConfig ) - const { length: packagePathsCount } = packagePaths + const packagePathsCount = packagePaths.length if (packagePathsCount && isDebug()) { for (const pkgPath of packagePaths) { debugLog(`Uploading: ${pkgPath}`) diff --git a/src/commands/scan/cmd-scan-create.ts b/src/commands/scan/cmd-scan-create.ts index 4e408d580..cee0afbfc 100644 --- a/src/commands/scan/cmd-scan-create.ts +++ b/src/commands/scan/cmd-scan-create.ts @@ -88,13 +88,30 @@ const config: CliCommandConfig = { default: false, description: 'Set the visibility (true/false) of the scan in your dashboard' + }, + view: { + type: 'boolean', + shortFlag: 'v', + default: true, + description: + 'Will wait for and return the created report. Use --no-view to disable.' } }, + // TODO: your project's "socket.yml" file's "projectIgnorePaths" help: (command, config) => ` Usage $ ${command} [...options] [TARGET...] - Where TARGET is a FILE or DIR that _must_ be inside the CWD. + Uploads the specified "package.json" and lock files for JavaScript, Python, + Go, Scala, Gradle, and Kotlin dependency manifests. + If any folder is specified, the ones found in there recursively are uploaded. + + Supports globbing such as "**/package.json", "**/requirements.txt", etc. + + Ignores any file specified in your project's ".gitignore" and also has a + sensible set of default ignores from the "ignore-by-default" module. + + TARGET should be a FILE or DIR that _must_ be inside the CWD. When a FILE is given only that FILE is targeted. Otherwise any eligible files in the given DIR will be considered. @@ -134,7 +151,7 @@ async function run( let { branch: branchName, repo: repoName } = cli.flags - const apiToken = getDefaultToken() + const apiToken = getDefaultToken() // This checks if we _can_ suggest anything if (!apiToken && (!orgSlug || !repoName || !branchName || !targets.length)) { // Without api token we cannot recover because we can't request more info diff --git a/src/commands/scan/cmd-scan-del.ts b/src/commands/scan/cmd-scan-del.ts index cb09dfe97..78ec29c53 100644 --- a/src/commands/scan/cmd-scan-del.ts +++ b/src/commands/scan/cmd-scan-del.ts @@ -6,10 +6,8 @@ import { logger } from '@socketsecurity/registry/lib/logger' import { deleteOrgFullScan } from './delete-full-scan' import constants from '../../constants' import { commonFlags, outputFlags } from '../../flags' -import { AuthError } from '../../utils/errors' import { meowOrExit } from '../../utils/meow-with-subcommands' import { getFlagListOutput } from '../../utils/output-formatting' -import { getDefaultToken } from '../../utils/sdk' import type { CliCommandConfig } from '../../utils/meow-with-subcommands' @@ -75,12 +73,5 @@ async function run( return } - 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 deleteOrgFullScan(orgSlug, fullScanId, apiToken) + await deleteOrgFullScan(orgSlug, fullScanId) } diff --git a/src/commands/scan/cmd-scan-list.ts b/src/commands/scan/cmd-scan-list.ts index f100d6a00..e8d15f843 100644 --- a/src/commands/scan/cmd-scan-list.ts +++ b/src/commands/scan/cmd-scan-list.ts @@ -6,10 +6,8 @@ import { logger } from '@socketsecurity/registry/lib/logger' import { listFullScans } from './list-full-scans' import constants from '../../constants' import { commonFlags, outputFlags } from '../../flags' -import { AuthError } from '../../utils/errors' import { meowOrExit } from '../../utils/meow-with-subcommands' import { getFlagListOutput } from '../../utils/output-formatting' -import { getDefaultToken } from '../../utils/sdk' import type { CliCommandConfig, @@ -113,37 +111,17 @@ async function run( return } - 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 listFullScans( + await listFullScans({ + direction: String(cli.flags['direction'] || ''), + from_time: String(cli.flags['fromTime'] || ''), orgSlug, - // TODO: refine this object to what we need - { - outputJson: cli.flags['json'], - outputMarkdown: cli.flags['markdown'], - orgSlug, - sort: cli.flags['sort'], - direction: cli.flags['direction'], - per_page: cli.flags['perPage'], - page: cli.flags['page'], - from_time: cli.flags['fromTime'], - until_time: cli.flags['untilTime'] - } as { - outputJson: boolean - outputMarkdown: boolean - orgSlug: string - sort: string - direction: string - per_page: number - page: number - from_time: string - until_time: string - }, - apiToken - ) + outputKind: cli.flags['json'] + ? 'json' + : cli.flags['markdown'] + ? 'markdown' + : 'print', + page: Number(cli.flags['page'] || 1), + per_page: Number(cli.flags['perPage'] || 30), + sort: String(cli.flags['sort'] || '') + }) } diff --git a/src/commands/scan/cmd-scan-metadata.ts b/src/commands/scan/cmd-scan-metadata.ts index 51696106f..a77c00340 100644 --- a/src/commands/scan/cmd-scan-metadata.ts +++ b/src/commands/scan/cmd-scan-metadata.ts @@ -6,10 +6,8 @@ import { logger } from '@socketsecurity/registry/lib/logger' import { getOrgScanMetadata } from './get-full-scan-metadata' import constants from '../../constants' import { commonFlags, outputFlags } from '../../flags' -import { AuthError } from '../../utils/errors' import { meowOrExit } from '../../utils/meow-with-subcommands' import { getFlagListOutput } from '../../utils/output-formatting' -import { getDefaultToken } from '../../utils/sdk' import type { CliCommandConfig, @@ -78,12 +76,9 @@ async function run( return } - 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 getOrgScanMetadata(orgSlug, fullScanId, apiToken) + await getOrgScanMetadata( + orgSlug, + fullScanId, + cli.flags['json'] ? 'json' : cli.flags['markdown'] ? 'markdown' : 'print' + ) } diff --git a/src/commands/scan/cmd-scan-stream.ts b/src/commands/scan/cmd-scan-view.ts similarity index 79% rename from src/commands/scan/cmd-scan-stream.ts rename to src/commands/scan/cmd-scan-view.ts index 55e58b65c..5d58a3307 100644 --- a/src/commands/scan/cmd-scan-stream.ts +++ b/src/commands/scan/cmd-scan-view.ts @@ -3,13 +3,12 @@ import colors from 'yoctocolors-cjs' import { logger } from '@socketsecurity/registry/lib/logger' -import { getFullScan } from './get-full-scan' +import { streamFullScan } from './stream-full-scan' +import { viewFullScan } from './view-full-scan' import constants from '../../constants' import { commonFlags, outputFlags } from '../../flags' -import { AuthError } from '../../utils/errors' import { meowOrExit } from '../../utils/meow-with-subcommands' import { getFlagListOutput } from '../../utils/output-formatting' -import { getDefaultToken } from '../../utils/sdk' import type { CliCommandConfig, @@ -19,8 +18,8 @@ import type { const { DRY_RUN_BAIL_TEXT } = constants const config: CliCommandConfig = { - commandName: 'stream', - description: 'Stream the output of a scan', + commandName: 'view', + description: 'View the raw results of a scan', hidden: false, flags: { ...commonFlags, @@ -40,7 +39,7 @@ const config: CliCommandConfig = { ` } -export const cmdScanStream: CliSubcommand = { +export const cmdScanView: CliSubcommand = { description: config.description, hidden: config.hidden, run @@ -82,12 +81,9 @@ async function run( return } - 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.' - ) + if (cli.flags['json']) { + await streamFullScan(orgSlug, fullScanId, file) + } else { + await viewFullScan(orgSlug, fullScanId, file) } - - await getFullScan(orgSlug, fullScanId, file, apiToken) } diff --git a/src/commands/scan/cmd-scan.ts b/src/commands/scan/cmd-scan.ts index 912c12838..56277d111 100644 --- a/src/commands/scan/cmd-scan.ts +++ b/src/commands/scan/cmd-scan.ts @@ -2,12 +2,12 @@ import { cmdScanCreate } from './cmd-scan-create' import { cmdScanDel } from './cmd-scan-del' import { cmdScanList } from './cmd-scan-list' import { cmdScanMetadata } from './cmd-scan-metadata' -import { cmdScanStream } from './cmd-scan-stream' +import { cmdScanView } from './cmd-scan-view' import { meowWithSubcommands } from '../../utils/meow-with-subcommands' import type { CliSubcommand } from '../../utils/meow-with-subcommands' -const description = 'Scans related commands' +const description = 'Full Scan related commands' export const cmdScan: CliSubcommand = { description, @@ -15,12 +15,20 @@ export const cmdScan: CliSubcommand = { await meowWithSubcommands( { create: cmdScanCreate, - stream: cmdScanStream, list: cmdScanList, del: cmdScanDel, - metadata: cmdScanMetadata + metadata: cmdScanMetadata, + view: cmdScanView }, { + aliases: { + // Backwards compat. TODO: Drop next major bump + stream: { + description: cmdScanView.description, + hidden: true, + argv: ['view'] // Original args will be appended (!) + } + }, argv, description, importMeta, diff --git a/src/commands/scan/create-full-scan.ts b/src/commands/scan/create-full-scan.ts index 521bddc21..5911818cd 100644 --- a/src/commands/scan/create-full-scan.ts +++ b/src/commands/scan/create-full-scan.ts @@ -77,10 +77,15 @@ export async function createFullScan({ 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 ) // We're going to need an api token to suggest data because those suggestions @@ -88,30 +93,33 @@ export async function createFullScan({ // If the api-token is not set, ignore it for the sake of suggestions. const apiToken = getDefaultToken() - if (apiToken && !orgSlug) { - const suggestion = await suggestOrgSlug(socketSdk) - if (suggestion) orgSlug = suggestion - 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 = '' - // (Don't bother asking for the rest if we didn't get an org slug above) - if (apiToken && orgSlug && !repoName) { - const suggestion = await suggestRepoSlug(socketSdk, orgSlug) - if (suggestion) { - ;({ defaultBranch: repoDefaultBranch, slug: repoName } = suggestion) + if (apiToken) { + if (!orgSlug) { + const suggestion = await suggestOrgSlug(socketSdk) + if (suggestion) orgSlug = suggestion + updatedInput = true } - updatedInput = true - } - // (Don't bother asking for the rest if we didn't get an org/repo above) - if (apiToken && orgSlug && repoName && !branchName) { - const suggestion = await suggestBranchSlug(repoDefaultBranch) - if (suggestion) branchName = 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(socketSdk, 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) { @@ -169,7 +177,7 @@ export async function createFullScan({ return } - spinner.start('Creating a scan...') + spinner.start(`Creating a scan with ${packagePaths.length} packages...`) const result = await handleApiCall( socketSdk.createOrgFullScan( diff --git a/src/commands/scan/delete-full-scan.ts b/src/commands/scan/delete-full-scan.ts index 710fe8414..4225e2164 100644 --- a/src/commands/scan/delete-full-scan.ts +++ b/src/commands/scan/delete-full-scan.ts @@ -1,8 +1,22 @@ import constants from '../../constants' import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' -import { setupSdk } from '../../utils/sdk' +import { AuthError } from '../../utils/errors' +import { getDefaultToken, setupSdk } from '../../utils/sdk' export async function deleteOrgFullScan( + orgSlug: string, + fullScanId: 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 deleteOrgFullScanWithToken(orgSlug, fullScanId, apiToken) +} +export async function deleteOrgFullScanWithToken( orgSlug: string, fullScanId: string, apiToken: string @@ -22,5 +36,6 @@ export async function deleteOrgFullScan( handleUnsuccessfulApiResponse('deleteOrgFullScan', result, spinner) return } + spinner.successAndStop('Scan deleted successfully') } diff --git a/src/commands/scan/get-full-scan-metadata.ts b/src/commands/scan/get-full-scan-metadata.ts index 1cc8743b1..6f11d86be 100644 --- a/src/commands/scan/get-full-scan-metadata.ts +++ b/src/commands/scan/get-full-scan-metadata.ts @@ -2,17 +2,33 @@ import { logger } from '@socketsecurity/registry/lib/logger' import constants from '../../constants' import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' -import { setupSdk } from '../../utils/sdk' +import { AuthError } from '../../utils/errors' +import { getDefaultToken, setupSdk } from '../../utils/sdk' export async function getOrgScanMetadata( orgSlug: string, scanId: string, - apiToken: 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("Getting scan's metadata...") + spinner.start('Fetching meta data for a full scan...') const socketSdk = await setupSdk(apiToken) const result = await handleApiCall( @@ -25,6 +41,38 @@ export async function getOrgScanMetadata( return } - spinner.stop('Scan metadata:') - logger.log(result.data) + 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/get-full-scan.ts b/src/commands/scan/get-full-scan.ts index eaa80c4d9..d1bfecce7 100644 --- a/src/commands/scan/get-full-scan.ts +++ b/src/commands/scan/get-full-scan.ts @@ -1,34 +1,57 @@ -import constants from '../../constants' -import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' -import { setupSdk } from '../../utils/sdk' +import colors from 'yoctocolors-cjs' + +import { logger } from '@socketsecurity/registry/lib/logger' +import { components } from '@socketsecurity/sdk/types/api' -import type { SocketSdkResultType } from '@socketsecurity/sdk' +import constants from '../../constants' +import { handleAPIError, queryAPI } from '../../utils/api' +import { AuthError } from '../../utils/errors' +import { getDefaultToken } from '../../utils/sdk' export async function getFullScan( orgSlug: string, - fullScanId: string, - file: string | undefined, - apiToken: string -): Promise> { + fullScanId: string +): Promise | undefined> { // Lazily access constants.spinner. const { spinner } = constants - spinner.start('Streaming scan...') + 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('Fetching full-scan...') - const socketSdk = await setupSdk(apiToken) - const data = await handleApiCall( - socketSdk.getOrgFullScan( - orgSlug, - fullScanId, - file === '-' ? undefined : file - ), - 'Streaming a scan' + const response = await queryAPI( + `orgs/${orgSlug}/full-scans/${encodeURIComponent(fullScanId)}`, + apiToken ) - if (data?.success) { - spinner.stop(file ? `Full scan details written to ${file}` : '') - } else { - handleUnsuccessfulApiResponse('getOrgFullScan', data, spinner) + spinner.stop('Fetch complete.') + + if (!response.ok) { + const err = await handleAPIError(response.status) + logger.error( + `${colors.bgRed(colors.white(response.statusText))}: Fetch error: ${err}` + ) + return } + + // This is nd-json; each line is a json object + const jsons = await response.text() + const lines = jsons.split('\n').filter(Boolean) + const data = lines.map(line => { + try { + return JSON.parse(line) + } catch { + console.error( + 'At least one line item was returned that could not be parsed as JSON...' + ) + return {} + } + }) as unknown as Array + return data } diff --git a/src/commands/scan/list-full-scans.ts b/src/commands/scan/list-full-scans.ts index adaa793c2..0abcb1141 100644 --- a/src/commands/scan/list-full-scans.ts +++ b/src/commands/scan/list-full-scans.ts @@ -6,32 +6,78 @@ import { logger } from '@socketsecurity/registry/lib/logger' import constants from '../../constants' import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' -import { setupSdk } from '../../utils/sdk' +import { AuthError } from '../../utils/errors' +import { getDefaultToken, setupSdk } from '../../utils/sdk' -export async function listFullScans( - orgSlug: string, - input: { - // TODO: what do we actually need for getOrgFullScanList ? - outputJson: boolean - outputMarkdown: boolean - orgSlug: string - sort: string - direction: string - per_page: number - page: number - from_time: string - until_time: string - }, +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 -): Promise { + 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('Listing scans...') + spinner.start('Fetching list of scans...') const socketSdk = await setupSdk(apiToken) const result = await handleApiCall( - socketSdk.getOrgFullScanList(orgSlug, input), + socketSdk.getOrgFullScanList(orgSlug, { + sort, + direction, + per_page, + page, + from: from_time + }), 'Listing scans' ) @@ -40,6 +86,13 @@ export async function listFullScans( return } + spinner.stop(`Fetch complete`) + + if (outputKind === 'json') { + logger.log(result.data) + return + } + const options = { columns: [ { field: 'id', name: colors.magenta('ID') }, @@ -64,6 +117,5 @@ export async function listFullScans( } }) - spinner.stop(`Listing scans for: ${orgSlug}`) logger.log(chalkTable(options, formattedResults)) } diff --git a/src/commands/scan/stream-full-scan.ts b/src/commands/scan/stream-full-scan.ts new file mode 100644 index 000000000..290b865fb --- /dev/null +++ b/src/commands/scan/stream-full-scan.ts @@ -0,0 +1,45 @@ +import constants from '../../constants' +import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' +import { AuthError } from '../../utils/errors' +import { getDefaultToken, setupSdk } from '../../utils/sdk' + +import type { SocketSdkResultType } from '@socketsecurity/sdk' + +export async function streamFullScan( + orgSlug: string, + fullScanId: string, + file: string | undefined +): Promise | undefined> { + // 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('Fetching scan...') + + const socketSdk = await setupSdk(apiToken) + const data = await handleApiCall( + socketSdk.getOrgFullScan( + orgSlug, + fullScanId, + file === '-' ? undefined : file + ), + 'Fetching a scan' + ) + + if (!data?.success) { + handleUnsuccessfulApiResponse('getOrgFullScan', data, spinner) + return + } + + spinner?.successAndStop( + file ? `Full scan details written to ${file}` : 'stdout' + ) + + return data +} diff --git a/src/commands/scan/view-full-scan.ts b/src/commands/scan/view-full-scan.ts new file mode 100644 index 000000000..31e97c871 --- /dev/null +++ b/src/commands/scan/view-full-scan.ts @@ -0,0 +1,64 @@ +import fs from 'node:fs/promises' + +import { logger } from '@socketsecurity/registry/lib/logger' +import { components } from '@socketsecurity/sdk/types/api' + +import { getFullScan } from './get-full-scan' +import { mdTable } from '../../utils/markdown' + +export async function viewFullScan( + orgSlug: string, + fullScanId: 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.' : ''}` + : art.author + return { + type: art.type, + name: art.name, + version: art.version, + author, + score: JSON.stringify(art.score) + } + }) + + const md = mdTable(display, [ + 'type', + 'version', + 'name', + 'author', + 'score' + ]) + + const report = + ` +# Scan Details + +These are the artifacts and their scores found. + +Sscan ID: ${fullScanId} + +${md} + +View this report at: https://socket.dev/dashboard/org/${orgSlug}/sbom/${fullScanId} + `.trim() + '\n' + + if (filePath && filePath !== '-') { + try { + await fs.writeFile(filePath, report, 'utf8') + logger.log(`Data successfully written to ${filePath}`) + } catch (e) { + logger.error('There was an error trying to write the json to disk') + logger.error(e) + process.exitCode = 1 + } + } else { + logger.log(report) + } +} diff --git a/src/utils/markdown.ts b/src/utils/markdown.ts index 4952c12d3..f868a5bdf 100644 --- a/src/utils/markdown.ts +++ b/src/utils/markdown.ts @@ -26,3 +26,41 @@ export function mdTableStringNumber( return lines.join('\n') } + +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 +): string { + // Max col width required to fit all data in that column + const cws = cols.map(col => col.length) + + for (const log of logs) { + for (let i = 0; i < cols.length; ++i) { + // @ts-ignore + const val: unknown = log[cols[i] ?? ''] ?? '' + cws[i] = Math.max(cws[i] ?? 0, String(val).length) + } + } + + let div = '|' + for (const cw of cws) div += ' ' + '-'.repeat(cw) + ' |' + + let header = '|' + for (let i = 0; i < cols.length; ++i) + header += ' ' + String(cols[i]).padEnd(cws[i] ?? 0, ' ') + ' |' + + let body = '' + for (const log of logs) { + body += '|' + for (let i = 0; i < cols.length; ++i) { + // @ts-ignore + const val: unknown = log[cols[i] ?? ''] ?? '' + body += ' ' + String(val).padEnd(cws[i] ?? 0, ' ') + ' |' + } + body += '\n' + } + + return [div, header, div, body.trim(), div].filter(s => !!s.trim()).join('\n') +} diff --git a/src/utils/path-resolve.ts b/src/utils/path-resolve.ts index e4df60bee..781321444 100644 --- a/src/utils/path-resolve.ts +++ b/src/utils/path-resolve.ts @@ -236,11 +236,11 @@ export function findNpmPathSync(npmBinPath: string): string | undefined { } } -export async function getPackageFiles( +export async function getPackageFilesFullScans( cwd: string, inputPaths: string[], - config: SocketYml | undefined, - supportedFiles: SocketSdkReturnType<'getReportSupportedFiles'>['data'] + supportedFiles: SocketSdkReturnType<'getReportSupportedFiles'>['data'], + config?: SocketYml | undefined ): Promise { debugLog(`Globbed resolving ${inputPaths.length} paths:`, inputPaths) @@ -266,32 +266,3 @@ export async function getPackageFiles( return packageFiles } - -export async function getPackageFilesFullScans( - cwd: string, - inputPaths: string[], - supportedFiles: SocketSdkReturnType<'getReportSupportedFiles'>['data'] -): Promise { - debugLog(`Globbed resolving ${inputPaths.length} paths:`, inputPaths) - - const entries = await globWithGitIgnore(pathsToPatterns(inputPaths), { - cwd - }) - - debugLog( - `Globbed resolved ${inputPaths.length} paths to ${entries.length} paths:`, - entries - ) - - const packageFiles = await filterGlobResultToSupportedFiles( - entries, - supportedFiles - ) - - debugLog( - `Mapped ${entries.length} entries to ${packageFiles.length} files:`, - packageFiles - ) - - return packageFiles -} diff --git a/test/dry-run.test.ts b/test/dry-run.test.ts index b8242d5c4..8831404cf 100644 --- a/test/dry-run.test.ts +++ b/test/dry-run.test.ts @@ -902,14 +902,14 @@ describe('dry-run on all commands', async () => { ) }) - cmdit(['scan', 'stream', '--dry-run'], 'should support', async cmd => { + cmdit(['scan', 'view', '--dry-run'], 'should support', async cmd => { const { code, stderr, stdout } = await invoke(entryPath, cmd) expect(`\n ${stdout}`).toMatchInlineSnapshot(` " _____ _ _ /--------------- | __|___ ___| |_ ___| |_ | Socket.dev CLI ver |__ | . | _| '_| -_| _| | Node: , API token set: - |_____|___|___|_,_|___|_|.dev | Command: \`socket scan stream\`, cwd: " + |_____|___|___|_,_|___|_|.dev | Command: \`socket scan view\`, cwd: " `) expect(stderr).toMatchInlineSnapshot(` "\\x1b[31m\\xd7\\x1b[39m \\x1b[41m\\x1b[37mInput error\\x1b[39m\\x1b[49m: Please provide the required fields: diff --git a/test/path-resolve.test.ts b/test/path-resolve.test.ts index 7d23d9c3a..79c090dea 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 { getPackageFiles } from './dist/path-resolve' +import { getPackageFilesFullScans } from './dist/path-resolve' const testPath = __dirname const mockPath = normalizePath(path.join(testPath, 'mock')) @@ -68,7 +68,7 @@ const sortedPromise = const result = await fn(...args) return result.sort() } -const sortedGetPackageFiles = sortedPromise(getPackageFiles) +const sortedGetPackageFiles = sortedPromise(getPackageFilesFullScans) describe('Path Resolve', () => { beforeEach(() => { @@ -92,8 +92,8 @@ describe('Path Resolve', () => { const actual = await sortedGetPackageFiles( mockPath, ['.'], - undefined, - globPatterns + globPatterns, + undefined ) expect(actual.map(normalizePath)).toEqual([`${mockPath}/package.json`]) }) @@ -109,13 +109,13 @@ describe('Path Resolve', () => { const actual = await sortedGetPackageFiles( mockPath, ['**/*'], + globPatterns, { version: 2, projectIgnorePaths: ['bar/*', '!bar/package.json'], issueRules: {}, githubApp: {} - }, - globPatterns + } ) expect(actual.map(normalizePath)).toEqual([ `${mockPath}/bar/package.json`, @@ -136,8 +136,8 @@ describe('Path Resolve', () => { const actual = await sortedGetPackageFiles( mockPath, ['**/*'], - undefined, - globPatterns + globPatterns, + undefined ) expect(actual.map(normalizePath)).toEqual([ `${mockPath}/bar/package.json`, @@ -165,8 +165,8 @@ describe('Path Resolve', () => { const actual = await sortedGetPackageFiles( mockPath, ['**/*'], - undefined, - globPatterns + globPatterns, + undefined ) expect(actual.map(normalizePath)).toEqual([ `${mockPath}/foo/package-lock.json`, @@ -185,8 +185,8 @@ describe('Path Resolve', () => { const actual = await sortedGetPackageFiles( mockPath, ['**/*'], - undefined, - globPatterns + globPatterns, + undefined ) expect(actual.map(normalizePath)).toEqual([ `${mockPath}/foo/package-lock.json`, @@ -204,8 +204,8 @@ describe('Path Resolve', () => { const actual = await sortedGetPackageFiles( mockPath, ['**/*'], - undefined, - globPatterns + globPatterns, + undefined ) expect(actual.map(normalizePath)).toEqual([]) }) @@ -219,8 +219,8 @@ describe('Path Resolve', () => { const actual = await sortedGetPackageFiles( mockPath, ['**/*'], - undefined, - globPatterns + globPatterns, + undefined ) expect(actual.map(normalizePath)).toEqual([ `${mockPath}/package-lock.json`, @@ -236,8 +236,8 @@ describe('Path Resolve', () => { const actual = await sortedGetPackageFiles( mockPath, ['**/*'], - undefined, - globPatterns + globPatterns, + undefined ) expect(actual.map(normalizePath)).toEqual([`${mockPath}/package.json`]) }) @@ -251,8 +251,8 @@ describe('Path Resolve', () => { const actual = await sortedGetPackageFiles( mockPath, ['**/*'], - undefined, - globPatterns + globPatterns, + undefined ) expect(actual.map(normalizePath)).toEqual([ `${mockPath}/package.json`, @@ -274,8 +274,8 @@ describe('Path Resolve', () => { const actual = await sortedGetPackageFiles( mockPath, ['**/*'], - undefined, - globPatterns + globPatterns, + undefined ) expect(actual.map(normalizePath)).toEqual([ `${mockPath}/abc/package.json`,