From d1748ceec1965cf8b6c926c48764db549890e22b Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Thu, 13 Feb 2025 17:45:49 +0100 Subject: [PATCH] Refactor the scan commands --- src/cli.ts | 4 +- src/commands/scan/cmd-create.ts | 166 +++++++++++++ src/commands/scan/cmd-delete.ts | 71 ++++++ src/commands/scan/cmd-list.ts | 136 +++++++++++ src/commands/scan/cmd-metadata.ts | 77 ++++++ src/commands/scan/cmd-scan.ts | 31 +++ src/commands/scan/cmd-stream.ts | 76 ++++++ src/commands/scan/create-full-scan.ts | 82 +++++++ src/commands/scan/create.ts | 254 -------------------- src/commands/scan/delete-full-scan.ts | 25 ++ src/commands/scan/delete.ts | 112 --------- src/commands/scan/get-full-scan-metadata.ts | 28 +++ src/commands/scan/get-full-scan.ts | 36 +++ src/commands/scan/index.ts | 31 --- src/commands/scan/list-full-scans.ts | 67 ++++++ src/commands/scan/list.ts | 187 -------------- src/commands/scan/metadata.ts | 109 --------- src/commands/scan/stream.ts | 116 --------- 18 files changed, 797 insertions(+), 811 deletions(-) create mode 100644 src/commands/scan/cmd-create.ts create mode 100644 src/commands/scan/cmd-delete.ts create mode 100644 src/commands/scan/cmd-list.ts create mode 100644 src/commands/scan/cmd-metadata.ts create mode 100644 src/commands/scan/cmd-scan.ts create mode 100644 src/commands/scan/cmd-stream.ts create mode 100644 src/commands/scan/create-full-scan.ts delete mode 100644 src/commands/scan/create.ts create mode 100644 src/commands/scan/delete-full-scan.ts delete mode 100644 src/commands/scan/delete.ts create mode 100644 src/commands/scan/get-full-scan-metadata.ts create mode 100644 src/commands/scan/get-full-scan.ts delete mode 100644 src/commands/scan/index.ts create mode 100644 src/commands/scan/list-full-scans.ts delete mode 100644 src/commands/scan/list.ts delete mode 100644 src/commands/scan/metadata.ts delete mode 100644 src/commands/scan/stream.ts diff --git a/src/cli.ts b/src/cli.ts index d4970c20b..8058a2777 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -26,7 +26,7 @@ import { rawNpmCommand } from './commands/raw-npm' import { rawNpxCommand } from './commands/raw-npx' import { reportCommand } from './commands/report' import { reposCommand } from './commands/repos' -import { scanCommand } from './commands/scan' +import { cmdScan } from './commands/scan/cmd-scan.ts' import { threatFeedCommand } from './commands/threat-feed' import { wrapperCommand } from './commands/wrapper' import constants from './constants' @@ -61,7 +61,7 @@ void (async () => { 'raw-npx': rawNpxCommand, report: reportCommand, wrapper: wrapperCommand, - scan: scanCommand, + scan: cmdScan, 'audit-log': auditLogCommand, repos: reposCommand, dependencies: dependenciesCommand, diff --git a/src/commands/scan/cmd-create.ts b/src/commands/scan/cmd-create.ts new file mode 100644 index 000000000..9cc17fb22 --- /dev/null +++ b/src/commands/scan/cmd-create.ts @@ -0,0 +1,166 @@ +import process from 'node:process' + +import meowOrDie from 'meow' +import colors from 'yoctocolors-cjs' + +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import { createFullScan } from './create-full-scan.ts' +import { handleUnsuccessfulApiResponse } from '../../utils/api' +import { AuthError } from '../../utils/errors' +import { getFlagListOutput } from '../../utils/output-formatting' +import { getPackageFilesFullScans } from '../../utils/path-resolve' +import { getDefaultToken, setupSdk } from '../../utils/sdk' + +import type { CliCommandConfig } from '../../utils/meow-with-subcommands' + +const config: CliCommandConfig = { + commandName: 'create', + description: 'Create a scan', + hidden: false, + flags: { + repo: { + type: 'string', + shortFlag: 'r', + default: '', + description: 'Repository name' + }, + branch: { + type: 'string', + shortFlag: 'b', + default: '', + description: 'Branch name' + }, + commitMessage: { + type: 'string', + shortFlag: 'm', + default: '', + description: 'Commit message' + }, + commitHash: { + type: 'string', + shortFlag: 'ch', + default: '', + description: 'Commit hash' + }, + pullRequest: { + type: 'number', + shortFlag: 'pr', + description: 'Commit hash' + }, + committers: { + type: 'string', + shortFlag: 'c', + default: '', + description: 'Committers' + }, + defaultBranch: { + type: 'boolean', + shortFlag: 'db', + default: false, + description: 'Make default branch' + }, + pendingHead: { + type: 'boolean', + shortFlag: 'ph', + default: false, + description: 'Set as pending head' + }, + tmp: { + type: 'boolean', + shortFlag: 't', + default: false, + description: + 'Set the visibility (true/false) of the scan in your dashboard' + } + }, + help: (parentName, config) => ` + Usage + $ ${parentName} ${config.commandName} [...options] + + Options + ${getFlagListOutput(config.flags, 6)} + + Examples + $ ${parentName} ${config.commandName} --org=FakeOrg --repo=test-repo --branch=main ./package.json + ` +} + +export const cmdScanCreate = { + description: config.description, + hidden: config.hidden, + run +} + +async function run( + argv: readonly string[], + importMeta: ImportMeta, + { parentName }: { parentName: string } +): Promise { + const cli = meowOrDie(config.help(parentName, config), { + argv, + description: config.description, + importMeta, + flags: config.flags + }) + + const orgSlug = cli.input[0] ?? '' // TODO: if nobody uses this then get rid of it in favor of --org + const cwd = process.cwd() + + const socketSdk = await setupSdk() + const supportedFiles = await socketSdk + .getReportSupportedFiles() + .then(res => { + if (!res.success) + handleUnsuccessfulApiResponse( + 'getReportSupportedFiles', + res, + new Spinner() + ) + // TODO: verify type at runtime? Consider it trusted data and assume type? + return (res as any).data + }) + .catch((cause: Error) => { + throw new Error('Failed getting supported files for report', { cause }) + }) + + const packagePaths = await getPackageFilesFullScans( + cwd, + cli.input, + supportedFiles + ) + + const { branch: branchName, repo: repoName } = cli.flags + + if (!orgSlug || !repoName || !branchName || !packagePaths.length) { + console.error(`${colors.bgRed(colors.white('Input error'))}: Please provide the required fields:\n + - Org name as the argument ${!orgSlug ? colors.red('(missing!)') : colors.green('(ok)')}\n + - Repository name using --repo ${!repoName ? colors.red('(missing!)') : colors.green('(ok)')}\n + - Branch name using --branch ${!branchName ? colors.red('(missing!)') : colors.green('(ok)')}\n + - At least one file path (e.g. ./package.json) ${!packagePaths.length ? colors.red('(missing or no matching/supported files found!)') : colors.green('(ok)')}`) + config.help(parentName, config) + 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 createFullScan({ + apiToken, + orgSlug, + repoName: repoName as string, + branchName: branchName as string, + commitMessage: (cli.flags['commitMessage'] as string) ?? '', + defaultBranch: Boolean(cli.flags['defaultBranch']), + pendingHead: Boolean(cli.flags['pendingHead']), + tmp: Boolean(cli.flags['tmp']), + packagePaths, + commitHash: (cli.flags['commitHash'] as string) ?? '', + committers: (cli.flags['committers'] as string) ?? '', + pullRequest: (cli.flags['pullRequest'] as number) ?? undefined + }) +} diff --git a/src/commands/scan/cmd-delete.ts b/src/commands/scan/cmd-delete.ts new file mode 100644 index 000000000..802b332e1 --- /dev/null +++ b/src/commands/scan/cmd-delete.ts @@ -0,0 +1,71 @@ +import meow from 'meow' +import colors from 'yoctocolors-cjs' + +import { deleteOrgFullScan } from './delete-full-scan.ts' +import { commonFlags, outputFlags } from '../../flags' +import { AuthError } from '../../utils/errors' +import { getFlagListOutput } from '../../utils/output-formatting' +import { getDefaultToken } from '../../utils/sdk' + +import type { CliCommandConfig } from '../../utils/meow-with-subcommands' + +const config: CliCommandConfig = { + commandName: 'del', + description: 'Delete a scan', + hidden: false, + flags: { + ...commonFlags, + ...outputFlags + }, + help: (parentName, config) => ` + Usage + $ ${parentName} ${config.commandName} + + Options + ${getFlagListOutput(config.flags, 6)} + + Examples + $ ${parentName} ${config.commandName} FakeOrg 000aaaa1-0000-0a0a-00a0-00a0000000a0 + ` +} + +export const cmdScanDelete = { + description: config.description, + hidden: config.hidden, + run +} + +async function run( + argv: readonly string[], + importMeta: ImportMeta, + { parentName }: { parentName: string } +): Promise { + const cli = meow(config.help(parentName, config), { + argv, + description: config.description, + importMeta, + flags: config.flags + }) + + const [orgSlug = '', fullScanId = ''] = cli.input + + if (!orgSlug || !fullScanId) { + console.error( + `${colors.bgRed(colors.white('Input error'))}: Please provide the required fields:\n + - Org name as the first argument ${!orgSlug ? colors.red('(missing!)') : colors.green('(ok)')}\n + - Full Scan ID to delete as second argument ${!fullScanId ? colors.red('(missing!)') : colors.green('(ok)')} + ` + ) + config.help(parentName, config) + 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) +} diff --git a/src/commands/scan/cmd-list.ts b/src/commands/scan/cmd-list.ts new file mode 100644 index 000000000..22e0c59e3 --- /dev/null +++ b/src/commands/scan/cmd-list.ts @@ -0,0 +1,136 @@ +import meow from 'meow' +import colors from 'yoctocolors-cjs' + +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import { listFullScans } from './list-full-scans.ts' +import { commonFlags, outputFlags } from '../../flags' +import { AuthError } from '../../utils/errors' +import { getFlagListOutput } from '../../utils/output-formatting' +import { getDefaultToken } from '../../utils/sdk' + +import type { + CliCommandConfig, + CliSubcommand +} from '../../utils/meow-with-subcommands' + +const config: CliCommandConfig = { + commandName: 'list', + description: 'List the full scans for an organization', + hidden: false, + flags: { + ...commonFlags, + ...outputFlags, + sort: { + type: 'string', + shortFlag: 's', + default: 'created_at', + description: + 'Sorting option (`name` or `created_at`) - default is `created_at`' + }, + direction: { + type: 'string', + shortFlag: 'd', + default: 'desc', + description: 'Direction option (`desc` or `asc`) - Default is `desc`' + }, + perPage: { + type: 'number', + shortFlag: 'pp', + default: 30, + description: 'Results per page - Default is 30' + }, + page: { + type: 'number', + shortFlag: 'p', + default: 1, + description: 'Page number - Default is 1' + }, + fromTime: { + type: 'string', + shortFlag: 'f', + default: '', + description: 'From time - as a unix timestamp' + }, + untilTime: { + type: 'string', + shortFlag: 'u', + default: '', + description: 'Until time - as a unix timestamp' + } + }, + help: (parentName, config) => ` + Usage + $ ${parentName} ${config.commandName} + + Options + ${getFlagListOutput(config.flags, 6)} + + Examples + $ ${parentName} ${config.commandName} FakeOrg + ` +} + +export const cmdScanList: CliSubcommand = { + description: config.description, + hidden: config.hidden, + run +} + +async function run( + argv: readonly string[], + importMeta: ImportMeta, + { parentName }: { parentName: string } +) { + const cli = meow(config.help(parentName, config), { + argv, + description: config.description, + importMeta, + flags: config.flags + }) + + const orgSlug = cli.input[0] + + if (!orgSlug) { + console.error(`${colors.bgRed(colors.white('Input error'))}: Please provide the required fields:\n + - Org name as the argument ${!orgSlug ? colors.red('(missing!)') : colors.green('(ok)')}`) + config.help(parentName, config) + 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.' + ) + } + const spinnerText = 'Listing scans... \n' + const spinner = new Spinner({ text: spinnerText }).start() + await listFullScans( + 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 + }, + spinner, + apiToken + ) +} diff --git a/src/commands/scan/cmd-metadata.ts b/src/commands/scan/cmd-metadata.ts new file mode 100644 index 000000000..0ec281702 --- /dev/null +++ b/src/commands/scan/cmd-metadata.ts @@ -0,0 +1,77 @@ +import meow from 'meow' +import colors from 'yoctocolors-cjs' + +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import { getOrgScanMetadata } from './get-full-scan-metadata.ts' +import { commonFlags, outputFlags } from '../../flags' +import { AuthError } from '../../utils/errors' +import { getFlagListOutput } from '../../utils/output-formatting' +import { getDefaultToken } from '../../utils/sdk' + +import type { + CliCommandConfig, + CliSubcommand +} from '../../utils/meow-with-subcommands' + +const config: CliCommandConfig = { + commandName: 'metadata', + description: "Get a full scan's metadata", + hidden: false, + flags: { + ...commonFlags, + ...outputFlags + }, + help: (parentName, config) => ` + Usage + $ ${parentName} ${config.commandName} + + Options + ${getFlagListOutput(config.flags, 6)} + + Examples + $ ${parentName} ${config.commandName} FakeOrg 000aaaa1-0000-0a0a-00a0-00a0000000a0 + ` +} + +export const cmdScanMetadata: CliSubcommand = { + description: config.description, + hidden: config.hidden, + run +} + +async function run( + argv: readonly string[], + importMeta: ImportMeta, + { parentName }: { parentName: string } +): Promise { + const cli = meow(config.help(parentName, config), { + argv, + description: config.description, + importMeta, + flags: config.flags + }) + + const [orgSlug = '', fullScanId = ''] = cli.input + + if (!orgSlug || !fullScanId) { + console.error( + `${colors.bgRed(colors.white('Input error'))}: Please provide the required fields:\n + - Org name as the first argument ${!orgSlug ? colors.red('(missing!)') : colors.green('(ok)')}\n + - Full Scan ID to inspect as second argument ${!fullScanId ? colors.red('(missing!)') : colors.green('(ok)')} + ` + ) + config.help(parentName, config) + 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.' + ) + } + const spinnerText = "Getting scan's metadata... \n" + const spinner = new Spinner({ text: spinnerText }).start() + await getOrgScanMetadata(orgSlug, fullScanId, spinner, apiToken) +} diff --git a/src/commands/scan/cmd-scan.ts b/src/commands/scan/cmd-scan.ts new file mode 100644 index 000000000..b33210a5a --- /dev/null +++ b/src/commands/scan/cmd-scan.ts @@ -0,0 +1,31 @@ +import { cmdScanCreate } from './cmd-create' +import { cmdScanDelete } from './cmd-delete.ts' +import { cmdScanList } from './cmd-list.ts' +import { cmdScanMetadata } from './cmd-metadata.ts' +import { cmdScanStream } from './cmd-stream.ts' +import { meowWithSubcommands } from '../../utils/meow-with-subcommands' + +import type { CliSubcommand } from '../../utils/meow-with-subcommands' + +const description = 'Scans related commands' + +export const cmdScan: CliSubcommand = { + description, + async run(argv, importMeta, { parentName }) { + await meowWithSubcommands( + { + create: cmdScanCreate, + stream: cmdScanStream, + list: cmdScanList, + del: cmdScanDelete, + metadata: cmdScanMetadata + }, + { + argv, + description, + importMeta, + name: parentName + ' scan' + } + ) + } +} diff --git a/src/commands/scan/cmd-stream.ts b/src/commands/scan/cmd-stream.ts new file mode 100644 index 000000000..804b09803 --- /dev/null +++ b/src/commands/scan/cmd-stream.ts @@ -0,0 +1,76 @@ +import meow from 'meow' +import colors from 'yoctocolors-cjs' + +import { getFullScan } from './get-full-scan.ts' +import { commonFlags, outputFlags } from '../../flags' +import { AuthError } from '../../utils/errors' +import { getFlagListOutput } from '../../utils/output-formatting' +import { getDefaultToken } from '../../utils/sdk' + +import type { + CliCommandConfig, + CliSubcommand +} from '../../utils/meow-with-subcommands' + +const config: CliCommandConfig = { + commandName: 'stream', + description: 'Stream the output of a scan', + hidden: false, + flags: { + ...commonFlags, + ...outputFlags + }, + help: (parentName, config) => ` + Usage + $ ${parentName} ${config.commandName} [path to output file] + + When no output path is given the contents is sent to stdout. + + Options + ${getFlagListOutput(config.flags, 6)} + + Examples + $ ${parentName} ${config.commandName} FakeOrg 000aaaa1-0000-0a0a-00a0-00a0000000a0 ./stream.txt + ` +} + +export const cmdScanStream: CliSubcommand = { + description: config.description, + hidden: config.hidden, + run +} + +async function run( + argv: readonly string[], + importMeta: ImportMeta, + { parentName }: { parentName: string } +): Promise { + const cli = meow(config.help(parentName, config), { + argv, + description: config.description, + importMeta, + flags: config.flags + }) + + const [orgSlug = '', fullScanId = '', file = '-'] = cli.input + + if (!orgSlug || !fullScanId) { + console.error( + `${colors.bgRed(colors.white('Input error'))}: Please provide the required fields:\n + - Org name as the first argument ${!orgSlug ? colors.red('(missing!)') : colors.green('(ok)')}\n + - Full Scan ID to fetch as second argument ${!fullScanId ? colors.red('(missing!)') : colors.green('(ok)')} + ` + ) + config.help(parentName, config) + 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 getFullScan(orgSlug, fullScanId, file, apiToken) +} diff --git a/src/commands/scan/create-full-scan.ts b/src/commands/scan/create-full-scan.ts new file mode 100644 index 000000000..753b08985 --- /dev/null +++ b/src/commands/scan/create-full-scan.ts @@ -0,0 +1,82 @@ +import process from 'node:process' +import readline from 'node:readline/promises' + +import open from 'open' +import colors from 'yoctocolors-cjs' + +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' +import { setupSdk } from '../../utils/sdk' + +export async function createFullScan({ + apiToken, + branchName, + commitHash: _commitHash, + commitMessage, + committers: _committers, + defaultBranch, + orgSlug, + packagePaths, + pendingHead, + pullRequest: _pullRequest, + repoName, + tmp +}: { + apiToken: string + orgSlug: string + repoName: string + branchName: string + committers: string + commitMessage: string + commitHash: string + pullRequest: number | undefined + defaultBranch: boolean + pendingHead: boolean + tmp: boolean + packagePaths: string[] +}): Promise { + const spinnerText = 'Creating a scan... \n' + const spinner = new Spinner({ text: spinnerText }).start() + + const socketSdk = await setupSdk(apiToken) + const result = await handleApiCall( + socketSdk.createOrgFullScan( + orgSlug, + { + repo: repoName, + branch: branchName, + commit_message: commitMessage, + make_default_branch: defaultBranch, + set_as_pending_head: pendingHead, + tmp + }, + packagePaths + ), + 'Creating scan' + ) + + if (!result.success) { + handleUnsuccessfulApiResponse('CreateOrgFullScan', result, spinner) + return + } + + spinner.success('Scan created successfully') + + const link = colors.underline(colors.cyan(`${result.data.html_report_url}`)) + console.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/create.ts b/src/commands/scan/create.ts deleted file mode 100644 index f7d6799ba..000000000 --- a/src/commands/scan/create.ts +++ /dev/null @@ -1,254 +0,0 @@ -import process from 'node:process' -import readline from 'node:readline/promises' - -import meow from 'meow' -import open from 'open' -import colors from 'yoctocolors-cjs' - -import { Spinner } from '@socketsecurity/registry/lib/spinner' - -import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' -import { AuthError } from '../../utils/errors' -import { getFlagListOutput } from '../../utils/output-formatting' -import { getPackageFilesFullScans } from '../../utils/path-resolve' -import { getDefaultToken, setupSdk } from '../../utils/sdk' - -import type { CliSubcommand } from '../../utils/meow-with-subcommands' - -export const create: CliSubcommand = { - description: 'Create a scan', - async run(argv, importMeta, { parentName }) { - const name = `${parentName} create` - const input = await setupCommand(name, create.description, argv, importMeta) - if (input) { - 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.' - ) - } - const spinnerText = 'Creating a scan... \n' - const spinner = new Spinner({ text: spinnerText }).start() - await createFullScan(input, spinner, apiToken) - } - } -} - -const createFullScanFlags: { [key: string]: any } = { - repo: { - type: 'string', - shortFlag: 'r', - default: '', - description: 'Repository name' - }, - branch: { - type: 'string', - shortFlag: 'b', - default: '', - description: 'Branch name' - }, - commitMessage: { - type: 'string', - shortFlag: 'm', - default: '', - description: 'Commit message' - }, - commitHash: { - type: 'string', - shortFlag: 'ch', - default: '', - description: 'Commit hash' - }, - pullRequest: { - type: 'number', - shortFlag: 'pr', - description: 'Commit hash' - }, - committers: { - type: 'string', - shortFlag: 'c', - default: '', - description: 'Committers' - }, - defaultBranch: { - type: 'boolean', - shortFlag: 'db', - default: false, - description: 'Make default branch' - }, - pendingHead: { - type: 'boolean', - shortFlag: 'ph', - default: false, - description: 'Set as pending head' - }, - tmp: { - type: 'boolean', - shortFlag: 't', - default: false, - description: 'Set the visibility (true/false) of the scan in your dashboard' - } -} - -// Internal functions - -type CommandContext = { - orgSlug: string - repoName: string - branchName: string - committers: string - commitMessage: string - commitHash: string - pullRequest: number | undefined - defaultBranch: boolean - pendingHead: boolean - tmp: boolean - packagePaths: string[] -} - -async function setupCommand( - name: string, - description: string, - argv: readonly string[], - importMeta: ImportMeta -): Promise { - const flags: { [key: string]: any } = { - ...createFullScanFlags - } - const cli = meow( - ` - Usage - $ ${name} [...options] - - Options - ${getFlagListOutput(flags, 6)} - - Examples - $ ${name} --org=FakeOrg --repo=test-repo --branch=main ./package.json - `, - { - argv, - description, - importMeta, - flags - } - ) - let showHelp = cli.flags['help'] - if (!cli.input[0]) { - showHelp = true - } - if (showHelp) { - cli.showHelp() - return - } - const { 0: orgSlug = '' } = cli.input - const cwd = process.cwd() - const socketSdk = await setupSdk() - const supportedFiles = await socketSdk - .getReportSupportedFiles() - .then(res => { - if (!res.success) - handleUnsuccessfulApiResponse( - 'getReportSupportedFiles', - res, - new Spinner() - ) - return (res as any).data - }) - .catch( - /** @type {(cause: Error) => never} */ - cause => { - throw new Error('Failed getting supported files for report', { - cause - }) - } - ) - - const packagePaths = await getPackageFilesFullScans( - cwd, - cli.input, - supportedFiles - ) - const { branch: branchName, repo: repoName } = cli.flags - if (!repoName || !branchName || !packagePaths.length) { - showHelp = true - console.error(`${colors.bgRed(colors.white('Input error'))}: Please provide the required fields:\n - - Repository name using --repo ${!repoName ? colors.red('(missing!)') : colors.green('(ok)')}\n - - Branch name using --branch ${!branchName ? colors.red('(missing!)') : colors.green('(ok)')}\n - - At least one file path (e.g. ./package.json) ${!packagePaths.length ? colors.red('(missing or no matching/supported files found!)') : colors.green('(ok)')}`) - } - if (showHelp) { - cli.showHelp() - return - } - return { - orgSlug, - repoName, - branchName, - commitMessage: cli.flags['commitMessage'], - defaultBranch: cli.flags['defaultBranch'], - pendingHead: cli.flags['pendingHead'], - tmp: cli.flags['tmp'], - packagePaths, - commitHash: cli.flags['commitHash'], - committers: cli.flags['committers'], - pullRequest: cli.flags['pullRequest'] - } -} - -async function createFullScan( - input: CommandContext, - spinner: Spinner, - apiToken: string -): Promise { - const socketSdk = await setupSdk(apiToken) - const { - branchName, - commitMessage, - defaultBranch, - orgSlug, - packagePaths, - pendingHead, - repoName, - tmp - } = input - const result = await handleApiCall( - socketSdk.createOrgFullScan( - orgSlug, - { - repo: repoName, - branch: branchName, - commit_message: commitMessage, - make_default_branch: defaultBranch, - set_as_pending_head: pendingHead, - tmp - }, - packagePaths - ), - 'Creating scan' - ) - - if (!result.success) { - handleUnsuccessfulApiResponse('CreateOrgFullScan', result, spinner) - return - } - - spinner.success('Scan created successfully') - - const link = colors.underline(colors.cyan(`${result.data.html_report_url}`)) - console.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/delete-full-scan.ts b/src/commands/scan/delete-full-scan.ts new file mode 100644 index 000000000..c662ac84b --- /dev/null +++ b/src/commands/scan/delete-full-scan.ts @@ -0,0 +1,25 @@ +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' +import { setupSdk } from '../../utils/sdk' + +export async function deleteOrgFullScan( + orgSlug: string, + fullScanId: string, + apiToken: string +): Promise { + const spinnerText = 'Deleting scan...' + const spinner = new Spinner({ text: spinnerText }).start() + + const socketSdk = await setupSdk(apiToken) + const result = await handleApiCall( + socketSdk.deleteOrgFullScan(orgSlug, fullScanId), + 'Deleting scan' + ) + + if (result.success) { + spinner.success('Scan deleted successfully') + } else { + handleUnsuccessfulApiResponse('deleteOrgFullScan', result, spinner) + } +} diff --git a/src/commands/scan/delete.ts b/src/commands/scan/delete.ts deleted file mode 100644 index 29c40a0ef..000000000 --- a/src/commands/scan/delete.ts +++ /dev/null @@ -1,112 +0,0 @@ -import meow from 'meow' -import colors from 'yoctocolors-cjs' - -import { Spinner } from '@socketsecurity/registry/lib/spinner' - -import { commonFlags, outputFlags } from '../../flags' -import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' -import { AuthError } from '../../utils/errors' -import { getFlagListOutput } from '../../utils/output-formatting' -import { getDefaultToken, setupSdk } from '../../utils/sdk' - -import type { CliSubcommand } from '../../utils/meow-with-subcommands' - -export const del: CliSubcommand = { - description: 'Delete a scan', - async run(argv, importMeta, { parentName }) { - const name = `${parentName} del` - const input = setupCommand(name, del.description, argv, importMeta) - if (input) { - 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.' - ) - } - const spinnerText = 'Deleting scan...' - const spinner = new Spinner({ text: spinnerText }).start() - await deleteOrgFullScan( - input.orgSlug, - input.fullScanId, - spinner, - apiToken - ) - } - } -} - -// Internal functions - -type CommandContext = { - outputJson: boolean - outputMarkdown: boolean - orgSlug: string - fullScanId: string -} - -function setupCommand( - name: string, - description: string, - argv: readonly string[], - importMeta: ImportMeta -): CommandContext | undefined { - const flags: { [key: string]: any } = { - ...commonFlags, - ...outputFlags - } - const cli = meow( - ` - Usage - $ ${name} - - Options - ${getFlagListOutput(flags, 6)} - - Examples - $ ${name} FakeOrg 000aaaa1-0000-0a0a-00a0-00a0000000a0 - `, - { - argv, - description, - importMeta, - flags - } - ) - let showHelp = cli.flags['help'] - if (cli.input.length < 2) { - showHelp = true - console.error( - `${colors.bgRed(colors.white('Input error'))}: Please specify an organization slug and a scan ID.` - ) - } - if (showHelp) { - cli.showHelp() - return - } - const { 0: orgSlug = '', 1: fullScanId = '' } = cli.input - return { - outputJson: cli.flags['json'], - outputMarkdown: cli.flags['markdown'], - orgSlug, - fullScanId - } -} - -async function deleteOrgFullScan( - orgSlug: string, - fullScanId: string, - spinner: Spinner, - apiToken: string -): Promise { - const socketSdk = await setupSdk(apiToken) - const result = await handleApiCall( - socketSdk.deleteOrgFullScan(orgSlug, fullScanId), - 'Deleting scan' - ) - - if (result.success) { - spinner.success('Scan deleted successfully') - } else { - handleUnsuccessfulApiResponse('deleteOrgFullScan', result, spinner) - } -} diff --git a/src/commands/scan/get-full-scan-metadata.ts b/src/commands/scan/get-full-scan-metadata.ts new file mode 100644 index 000000000..aecd6ea0f --- /dev/null +++ b/src/commands/scan/get-full-scan-metadata.ts @@ -0,0 +1,28 @@ +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import { + handleApiCall, + handleUnsuccessfulApiResponse +} from '../../utils/api.ts' +import { setupSdk } from '../../utils/sdk.ts' + +export async function getOrgScanMetadata( + orgSlug: string, + scanId: string, + spinner: Spinner, + apiToken: string +): Promise { + const socketSdk = await setupSdk(apiToken) + const result = await handleApiCall( + socketSdk.getOrgFullScanMetadata(orgSlug, scanId), + 'Listing scans' + ) + + if (!result.success) { + handleUnsuccessfulApiResponse('getOrgFullScanMetadata', result, spinner) + return + } + + spinner.stop('Scan metadata:') + console.log(result.data) +} diff --git a/src/commands/scan/get-full-scan.ts b/src/commands/scan/get-full-scan.ts new file mode 100644 index 000000000..ae26dfb49 --- /dev/null +++ b/src/commands/scan/get-full-scan.ts @@ -0,0 +1,36 @@ +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import { + handleApiCall, + handleUnsuccessfulApiResponse +} from '../../utils/api.ts' +import { setupSdk } from '../../utils/sdk.ts' + +import type { SocketSdkResultType } from '@socketsecurity/sdk' + +export async function getFullScan( + orgSlug: string, + fullScanId: string, + file: string | undefined, + apiToken: string +): Promise> { + const spinner = new Spinner({ text: 'Streaming scan...' }).start() + + const socketSdk = await setupSdk(apiToken) + const data = await handleApiCall( + socketSdk.getOrgFullScan( + orgSlug, + fullScanId, + file === '-' ? undefined : file + ), + 'Streaming a scan' + ) + + if (data?.success) { + spinner.stop(file ? `Full scan details written to ${file}` : '') + } else { + handleUnsuccessfulApiResponse('getOrgFullScan', data, spinner) + } + + return data +} diff --git a/src/commands/scan/index.ts b/src/commands/scan/index.ts deleted file mode 100644 index 6583f5b65..000000000 --- a/src/commands/scan/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { create } from './create' -import { del } from './delete' -import { list } from './list' -import { metadata } from './metadata' -import { stream } from './stream' -import { meowWithSubcommands } from '../../utils/meow-with-subcommands' - -import type { CliSubcommand } from '../../utils/meow-with-subcommands' - -const description = 'Scans related commands' - -export const scanCommand: CliSubcommand = { - description, - async run(argv, importMeta, { parentName }) { - await meowWithSubcommands( - { - create, - stream, - list, - del, - metadata - }, - { - argv, - description, - importMeta, - name: parentName + ' scan' - } - ) - } -} diff --git a/src/commands/scan/list-full-scans.ts b/src/commands/scan/list-full-scans.ts new file mode 100644 index 000000000..13efae309 --- /dev/null +++ b/src/commands/scan/list-full-scans.ts @@ -0,0 +1,67 @@ +// @ts-ignore +import chalkTable from 'chalk-table' +import colors from 'yoctocolors-cjs' + +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import { + handleApiCall, + handleUnsuccessfulApiResponse +} from '../../utils/api.ts' +import { setupSdk } from '../../utils/sdk.ts' + +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 + }, + spinner: Spinner, + apiToken: string +): Promise { + const socketSdk = await setupSdk(apiToken) + const result = await handleApiCall( + socketSdk.getOrgFullScanList(orgSlug, input), + 'Listing scans' + ) + + if (!result.success) { + handleUnsuccessfulApiResponse('getOrgFullScanList', result, spinner) + 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 + } + }) + + spinner.stop(`Listing scans for: ${orgSlug}`) + console.log(chalkTable(options, formattedResults)) +} diff --git a/src/commands/scan/list.ts b/src/commands/scan/list.ts deleted file mode 100644 index d073140aa..000000000 --- a/src/commands/scan/list.ts +++ /dev/null @@ -1,187 +0,0 @@ -// @ts-ignore -import chalkTable from 'chalk-table' -import meow from 'meow' -import colors from 'yoctocolors-cjs' - -import { Spinner } from '@socketsecurity/registry/lib/spinner' - -import { commonFlags, outputFlags } from '../../flags' -import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' -import { AuthError } from '../../utils/errors' -import { getFlagListOutput } from '../../utils/output-formatting' -import { getDefaultToken, setupSdk } from '../../utils/sdk' - -import type { CliSubcommand } from '../../utils/meow-with-subcommands' - -export const list: CliSubcommand = { - description: 'List scans for an organization', - async run(argv, importMeta, { parentName }) { - const name = `${parentName} list` - const input = setupCommand(name, list.description, argv, importMeta) - if (input) { - 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.' - ) - } - const spinnerText = 'Listing scans... \n' - const spinner = new Spinner({ text: spinnerText }).start() - await listOrgFullScan(input.orgSlug, input, spinner, apiToken) - } - } -} - -const listFullScanFlags: { [key: string]: any } = { - sort: { - type: 'string', - shortFlag: 's', - default: 'created_at', - description: - 'Sorting option (`name` or `created_at`) - default is `created_at`' - }, - direction: { - type: 'string', - shortFlag: 'd', - default: 'desc', - description: 'Direction option (`desc` or `asc`) - Default is `desc`' - }, - perPage: { - type: 'number', - shortFlag: 'pp', - default: 30, - description: 'Results per page - Default is 30' - }, - page: { - type: 'number', - shortFlag: 'p', - default: 1, - description: 'Page number - Default is 1' - }, - fromTime: { - type: 'string', - shortFlag: 'f', - default: '', - description: 'From time - as a unix timestamp' - }, - untilTime: { - type: 'string', - shortFlag: 'u', - default: '', - description: 'Until time - as a unix timestamp' - } -} - -// Internal functions - -type CommandContext = { - outputJson: boolean - outputMarkdown: boolean - orgSlug: string - sort: string - direction: string - per_page: number - page: number - from_time: string - until_time: string -} - -function setupCommand( - name: string, - description: string, - argv: readonly string[], - importMeta: ImportMeta -): CommandContext | undefined { - const flags: { [key: string]: any } = { - ...commonFlags, - ...listFullScanFlags, - ...outputFlags - } - - const cli = meow( - ` - Usage - $ ${name} - - Options - ${getFlagListOutput(flags, 6)} - - Examples - $ ${name} FakeOrg - `, - { - argv, - description, - importMeta, - flags - } - ) - let showHelp = cli.flags['help'] - if (!cli.input[0]) { - showHelp = true - console.error( - `${colors.bgRed(colors.white('Input error'))}: Please specify an organization slug.` - ) - } - if (showHelp) { - cli.showHelp() - return - } - const { 0: orgSlug = '' } = cli.input - return { - 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'] - } -} - -async function listOrgFullScan( - orgSlug: string, - input: CommandContext, - spinner: Spinner, - apiToken: string -): Promise { - const socketSdk = await setupSdk(apiToken) - const result = await handleApiCall( - socketSdk.getOrgFullScanList(orgSlug, input), - 'Listing scans' - ) - - if (!result.success) { - handleUnsuccessfulApiResponse('getOrgFullScanList', result, spinner) - 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 - } - }) - - spinner.stop(`Listing scans for: ${orgSlug}`) - console.log(chalkTable(options, formattedResults)) -} diff --git a/src/commands/scan/metadata.ts b/src/commands/scan/metadata.ts deleted file mode 100644 index 097feea24..000000000 --- a/src/commands/scan/metadata.ts +++ /dev/null @@ -1,109 +0,0 @@ -import meow from 'meow' -import colors from 'yoctocolors-cjs' - -import { Spinner } from '@socketsecurity/registry/lib/spinner' - -import { commonFlags, outputFlags } from '../../flags' -import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' -import { AuthError } from '../../utils/errors' -import { getFlagListOutput } from '../../utils/output-formatting' -import { getDefaultToken, setupSdk } from '../../utils/sdk' - -import type { CliSubcommand } from '../../utils/meow-with-subcommands' - -export const metadata: CliSubcommand = { - description: "Get a scan's metadata", - async run(argv, importMeta, { parentName }) { - const name = `${parentName} metadata` - const input = setupCommand(name, metadata.description, argv, importMeta) - if (input) { - 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.' - ) - } - const spinnerText = "Getting scan's metadata... \n" - const spinner = new Spinner({ text: spinnerText }).start() - await getOrgScanMetadata(input.orgSlug, input.scanID, spinner, apiToken) - } - } -} - -// Internal functions - -type CommandContext = { - outputJson: boolean - outputMarkdown: boolean - orgSlug: string - scanID: string -} - -function setupCommand( - name: string, - description: string, - argv: readonly string[], - importMeta: ImportMeta -): CommandContext | undefined { - const flags: { [key: string]: any } = { - ...commonFlags, - ...outputFlags - } - const cli = meow( - ` - Usage - $ ${name} - - Options - ${getFlagListOutput(flags, 6)} - - Examples - $ ${name} FakeOrg 000aaaa1-0000-0a0a-00a0-00a0000000a0 - `, - { - argv, - description, - importMeta, - flags - } - ) - let showHelp = cli.flags['help'] - if (cli.input.length < 2) { - showHelp = true - console.error( - `${colors.bgRed(colors.white('Input error'))}: Please specify an organization slug and a scan ID.` - ) - } - if (showHelp) { - cli.showHelp() - return - } - const { 0: orgSlug = '', 1: scanID = '' } = cli.input - return { - outputJson: cli.flags['json'], - outputMarkdown: cli.flags['markdown'], - orgSlug, - scanID - } -} - -async function getOrgScanMetadata( - orgSlug: string, - scanId: string, - spinner: Spinner, - apiToken: string -): Promise { - const socketSdk = await setupSdk(apiToken) - const result = await handleApiCall( - socketSdk.getOrgFullScanMetadata(orgSlug, scanId), - 'Listing scans' - ) - - if (!result.success) { - handleUnsuccessfulApiResponse('getOrgFullScanMetadata', result, spinner) - return - } - - spinner.stop('Scan metadata:') - console.log(result.data) -} diff --git a/src/commands/scan/stream.ts b/src/commands/scan/stream.ts deleted file mode 100644 index 837a0659a..000000000 --- a/src/commands/scan/stream.ts +++ /dev/null @@ -1,116 +0,0 @@ -import meow from 'meow' -import colors from 'yoctocolors-cjs' - -import { Spinner } from '@socketsecurity/registry/lib/spinner' - -import { commonFlags, outputFlags } from '../../flags' -import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' -import { AuthError } from '../../utils/errors' -import { getFlagListOutput } from '../../utils/output-formatting' -import { getDefaultToken, setupSdk } from '../../utils/sdk' - -import type { CliSubcommand } from '../../utils/meow-with-subcommands' -import type { SocketSdkResultType } from '@socketsecurity/sdk' - -export const stream: CliSubcommand = { - description: 'Stream the output of a scan', - async run(argv, importMeta, { parentName }) { - const name = `${parentName} stream` - const input = setupCommand(name, stream.description, argv, importMeta) - if (input) { - 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.' - ) - } - const spinner = new Spinner({ text: 'Streaming scan...' }).start() - const result = await getOrgFullScan( - input.orgSlug, - input.fullScanId, - input.file, - apiToken - ) - - if (result?.success) { - spinner.stop( - input.file ? `Full scan details written to ${input.file}` : '' - ) - } else { - handleUnsuccessfulApiResponse('getOrgFullScan', result, spinner) - } - } - } -} - -// Internal functions - -type CommandContext = { - outputJson: boolean - outputMarkdown: boolean - orgSlug: string - fullScanId: string - file: string | undefined -} - -function setupCommand( - name: string, - description: string, - argv: readonly string[], - importMeta: ImportMeta -): CommandContext | undefined { - const flags: { [key: string]: any } = { - ...commonFlags, - ...outputFlags - } - const cli = meow( - ` - Usage - $ ${name} - - Options - ${getFlagListOutput(flags, 6)} - - Examples - $ ${name} FakeOrg 000aaaa1-0000-0a0a-00a0-00a0000000a0 ./stream.txt - `, - { - argv, - description, - importMeta, - flags - } - ) - let showHelp = cli.flags['help'] - if (cli.input.length < 2) { - showHelp = true - console.error( - `${colors.bgRed(colors.white('Input error'))}: Please specify an organization slug and a scan ID.` - ) - } - if (showHelp) { - cli.showHelp() - return - } - const { 0: orgSlug = '', 1: fullScanId = '', 2: file } = cli.input - return { - outputJson: cli.flags['json'], - outputMarkdown: cli.flags['markdown'], - orgSlug, - fullScanId, - file - } -} - -async function getOrgFullScan( - orgSlug: string, - fullScanId: string, - file: string | undefined, - apiToken: string -): Promise> { - const socketSdk = await setupSdk(apiToken) - return await handleApiCall( - socketSdk.getOrgFullScan(orgSlug, fullScanId, file), - 'Streaming a scan' - ) -}