From fd6268b98d225a3b8a43af4f0c541883872282df Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Tue, 18 Feb 2025 14:14:56 +0100 Subject: [PATCH 1/2] Refactor many commands into consistency --- package.json | 1 + src/cli.ts | 40 +-- src/commands/action/cmd-action.ts | 61 ++++ src/commands/action/index.ts | 108 ------- src/commands/action/run-action.ts | 87 ++++++ ...{analytics-command.ts => cmd-analytics.ts} | 39 ++- ...{run-analytics.ts => display-analytics.ts} | 124 ++++---- src/commands/audit-log.ts | 182 ----------- src/commands/audit-log/cmd-audit-log.ts | 95 ++++++ src/commands/audit-log/get-audit-log.ts | 86 ++++++ src/commands/cdxgen.ts | 210 ------------- src/commands/cdxgen/cmd-cdxgen.ts | 170 ++++++++++ src/commands/cdxgen/run-cyclonedx.ts | 97 ++++++ src/commands/dependencies.ts | 145 --------- src/commands/dependencies/cmd-dependencies.ts | 66 ++++ .../dependencies/find-dependencies.ts | 59 ++++ src/commands/diff-scan/cmd-diff-scan-get.ts | 108 +++++++ .../diff-scan/{index.ts => cmd-diff-scan.ts} | 6 +- src/commands/diff-scan/get-diff-scan.ts | 73 +++++ src/commands/diff-scan/get.ts | 180 ----------- src/commands/fix.ts | 56 ---- src/commands/fix/cmd-fix.ts | 87 ++++++ src/commands/info.ts | 291 ------------------ src/commands/info/cmd-info.ts | 77 +++++ src/commands/info/fetch-package-info.ts | 53 ++++ src/commands/info/format-package-info.ts | 142 +++++++++ src/commands/info/get-package-info.ts | 58 ++++ src/commands/report/cmd-report-create.ts | 120 ++++++++ src/commands/report/cmd-report-view.ts | 69 +++++ .../report/{index.ts => cmd-report.ts} | 10 +- src/commands/report/create-report.ts | 79 +++++ src/commands/report/create.ts | 277 ----------------- src/commands/report/fetch-report-data.ts | 76 +++++ src/commands/report/format-report-data.ts | 37 +++ src/commands/report/get-socket-config.ts | 41 +++ src/commands/report/view-report.ts | 31 ++ src/commands/report/view.ts | 207 ------------- src/commands/repos/cmd-repos-create.ts | 111 +++++++ src/commands/repos/cmd-repos-delete.ts | 63 ++++ src/commands/repos/cmd-repos-list.ts | 102 ++++++ src/commands/repos/cmd-repos-update.ts | 112 +++++++ src/commands/repos/cmd-repos-view.ts | 71 +++++ src/commands/repos/{index.ts => cmd-repos.ts} | 22 +- src/commands/repos/create-repo.ts | 53 ++++ src/commands/repos/create.ts | 155 ---------- src/commands/repos/delete-repo.ts | 28 ++ src/commands/repos/delete.ts | 93 ------ src/commands/repos/list-repos.ts | 65 ++++ src/commands/repos/list.ts | 153 --------- src/commands/repos/update-repo.ts | 53 ++++ src/commands/repos/update.ts | 156 ---------- src/commands/repos/view-repo.ts | 45 +++ src/commands/repos/view.ts | 127 -------- .../{cmd-create.ts => cmd-scan-create.ts} | 0 .../{cmd-delete.ts => cmd-scan-delete.ts} | 0 .../scan/{cmd-list.ts => cmd-scan-list.ts} | 0 .../{cmd-metadata.ts => cmd-scan-metadata.ts} | 0 .../{cmd-stream.ts => cmd-scan-stream.ts} | 0 src/commands/scan/cmd-scan.ts | 10 +- src/commands/types.ts | 7 - 60 files changed, 2592 insertions(+), 2482 deletions(-) create mode 100644 src/commands/action/cmd-action.ts delete mode 100644 src/commands/action/index.ts create mode 100644 src/commands/action/run-action.ts rename src/commands/analytics/{analytics-command.ts => cmd-analytics.ts} (70%) rename src/commands/analytics/{run-analytics.ts => display-analytics.ts} (77%) delete mode 100644 src/commands/audit-log.ts create mode 100644 src/commands/audit-log/cmd-audit-log.ts create mode 100644 src/commands/audit-log/get-audit-log.ts delete mode 100644 src/commands/cdxgen.ts create mode 100644 src/commands/cdxgen/cmd-cdxgen.ts create mode 100644 src/commands/cdxgen/run-cyclonedx.ts delete mode 100644 src/commands/dependencies.ts create mode 100644 src/commands/dependencies/cmd-dependencies.ts create mode 100644 src/commands/dependencies/find-dependencies.ts create mode 100644 src/commands/diff-scan/cmd-diff-scan-get.ts rename src/commands/diff-scan/{index.ts => cmd-diff-scan.ts} (77%) create mode 100644 src/commands/diff-scan/get-diff-scan.ts delete mode 100644 src/commands/diff-scan/get.ts delete mode 100644 src/commands/fix.ts create mode 100644 src/commands/fix/cmd-fix.ts delete mode 100644 src/commands/info.ts create mode 100644 src/commands/info/cmd-info.ts create mode 100644 src/commands/info/fetch-package-info.ts create mode 100644 src/commands/info/format-package-info.ts create mode 100644 src/commands/info/get-package-info.ts create mode 100644 src/commands/report/cmd-report-create.ts create mode 100644 src/commands/report/cmd-report-view.ts rename src/commands/report/{index.ts => cmd-report.ts} (67%) create mode 100644 src/commands/report/create-report.ts delete mode 100644 src/commands/report/create.ts create mode 100644 src/commands/report/fetch-report-data.ts create mode 100644 src/commands/report/format-report-data.ts create mode 100644 src/commands/report/get-socket-config.ts create mode 100644 src/commands/report/view-report.ts delete mode 100644 src/commands/report/view.ts create mode 100644 src/commands/repos/cmd-repos-create.ts create mode 100644 src/commands/repos/cmd-repos-delete.ts create mode 100644 src/commands/repos/cmd-repos-list.ts create mode 100644 src/commands/repos/cmd-repos-update.ts create mode 100644 src/commands/repos/cmd-repos-view.ts rename src/commands/repos/{index.ts => cmd-repos.ts} (50%) create mode 100644 src/commands/repos/create-repo.ts delete mode 100644 src/commands/repos/create.ts create mode 100644 src/commands/repos/delete-repo.ts delete mode 100644 src/commands/repos/delete.ts create mode 100644 src/commands/repos/list-repos.ts delete mode 100644 src/commands/repos/list.ts create mode 100644 src/commands/repos/update-repo.ts delete mode 100644 src/commands/repos/update.ts create mode 100644 src/commands/repos/view-repo.ts delete mode 100644 src/commands/repos/view.ts rename src/commands/scan/{cmd-create.ts => cmd-scan-create.ts} (100%) rename src/commands/scan/{cmd-delete.ts => cmd-scan-delete.ts} (100%) rename src/commands/scan/{cmd-list.ts => cmd-scan-list.ts} (100%) rename src/commands/scan/{cmd-metadata.ts => cmd-scan-metadata.ts} (100%) rename src/commands/scan/{cmd-stream.ts => cmd-scan-stream.ts} (100%) delete mode 100644 src/commands/types.ts diff --git a/package.json b/package.json index a9f478c93..2300b4b2f 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "clean": "run-p --aggregate-output clean:*", "clean:dist": "del-cli 'dist' 'test/dist'", "clean:node_modules": "del-cli '**/node_modules'", + "fix": "npm run lint:fix ; npm run check:lint -- --fix", "knip:dependencies": "knip --dependencies", "knip:exports": "knip --include exports,duplicates", "lint": "oxlint -c=./.oxlintrc.json --ignore-path=./.oxlintignore --tsconfig=./tsconfig.json .", diff --git a/src/cli.ts b/src/cli.ts index 8058a2777..665e21b24 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -7,14 +7,14 @@ import { messageWithCauses, stackWithCauses } from 'pony-cause' import updateNotifier from 'tiny-updater' import colors from 'yoctocolors-cjs' -import { actionCommand } from './commands/action' -import { analyticsCommand } from './commands/analytics/analytics-command' -import { auditLogCommand } from './commands/audit-log' -import { cdxgenCommand } from './commands/cdxgen' -import { dependenciesCommand } from './commands/dependencies' -import { diffScanCommand } from './commands/diff-scan' -import { fixCommand } from './commands/fix' -import { infoCommand } from './commands/info' +import { cmdAction } from './commands/action/cmd-action.ts' +import { cmdAnalytics } from './commands/analytics/cmd-analytics.ts' +import { cmdAuditLog } from './commands/audit-log/cmd-audit-log.ts' +import { cmdCdxgen } from './commands/cdxgen/cmd-cdxgen.ts' +import { cmdScanCreate } from './commands/dependencies/cmd-dependencies.ts' +import { cmdDiffScan } from './commands/diff-scan/cmd-diff-scan.ts' +import { cmdFix } from './commands/fix/cmd-fix.ts' +import { cmdInfo } from './commands/info/cmd-info.ts' import { loginCommand } from './commands/login' import { logoutCommand } from './commands/logout' import { manifestCommand } from './commands/manifest' @@ -24,8 +24,8 @@ import { optimizeCommand } from './commands/optimize' import { organizationCommand } from './commands/organization' import { rawNpmCommand } from './commands/raw-npm' import { rawNpxCommand } from './commands/raw-npx' -import { reportCommand } from './commands/report' -import { reposCommand } from './commands/repos' +import { cmdReport } from './commands/report/cmd-report.ts' +import { cmdRepos } from './commands/repos/cmd-repos.ts' import { cmdScan } from './commands/scan/cmd-scan.ts' import { threatFeedCommand } from './commands/threat-feed' import { wrapperCommand } from './commands/wrapper' @@ -47,10 +47,10 @@ void (async () => { try { await meowWithSubcommands( { - action: actionCommand, - cdxgen: cdxgenCommand, - fix: fixCommand, - info: infoCommand, + action: cmdAction, + cdxgen: cmdCdxgen, + fix: cmdFix, + info: cmdInfo, login: loginCommand, logout: logoutCommand, npm: npmCommand, @@ -59,14 +59,14 @@ void (async () => { organization: organizationCommand, 'raw-npm': rawNpmCommand, 'raw-npx': rawNpxCommand, - report: reportCommand, + report: cmdReport, wrapper: wrapperCommand, scan: cmdScan, - 'audit-log': auditLogCommand, - repos: reposCommand, - dependencies: dependenciesCommand, - analytics: analyticsCommand, - 'diff-scan': diffScanCommand, + 'audit-log': cmdAuditLog, + repos: cmdRepos, + dependencies: cmdScanCreate, + analytics: cmdAnalytics, + 'diff-scan': cmdDiffScan, 'threat-feed': threatFeedCommand, manifest: manifestCommand }, diff --git a/src/commands/action/cmd-action.ts b/src/commands/action/cmd-action.ts new file mode 100644 index 000000000..df0f85efc --- /dev/null +++ b/src/commands/action/cmd-action.ts @@ -0,0 +1,61 @@ +// https://github.com/SocketDev/socket-python-cli/blob/6d4fc56faee68d3a4764f1f80f84710635bdaf05/socketsecurity/socketcli.py +import meowOrDie from 'meow' + +import { runAction } from './run-action.ts' +import { type CliCommandConfig } from '../../utils/meow-with-subcommands' +import { getFlagListOutput } from '../../utils/output-formatting.ts' + +const config: CliCommandConfig = { + commandName: 'action', + description: 'Socket action command', // GitHub Action ? + hidden: true, + flags: { + // This flag is unused + // socketSecurityApiKey: { // deprecate this asap. + // type: 'string', + // default: 'env var SOCKET_SECURITY_API_KEY', + // description: 'Socket API token' + // }, + githubEventBefore: { + type: 'string', + default: '', + description: 'Before marker' + }, + githubEventAfter: { + type: 'string', + default: '', + description: 'After marker' + } + }, + help: (parentName, { commandName, flags }) => ` + Usage + $ ${parentName} ${commandName} [options] + + Options + ${getFlagListOutput(flags, 6)} + ` +} + +export const cmdAction = { + description: config.description, + hidden: config.hidden, + run: 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 githubEventBefore = String(cli.flags['githubEventBefore'] || '') + const githubEventAfter = String(cli.flags['githubEventAfter'] || '') + + await runAction(githubEventBefore, githubEventAfter) +} diff --git a/src/commands/action/index.ts b/src/commands/action/index.ts deleted file mode 100644 index d33d2beff..000000000 --- a/src/commands/action/index.ts +++ /dev/null @@ -1,108 +0,0 @@ -// https://github.com/SocketDev/socket-python-cli/blob/6d4fc56faee68d3a4764f1f80f84710635bdaf05/socketsecurity/socketcli.py -import { parseArgs } from 'util' - -import micromatch from 'micromatch' -import { simpleGit } from 'simple-git' - -import { SocketSdk } from '@socketsecurity/sdk' - -import { Core } from './core' -import { GitHub } from './core/github' -import * as Messages from './core/messages' -import * as SCMComments from './core/scm_comments' -import { CliSubcommand } from '../../utils/meow-with-subcommands' -import { getDefaultToken } from '../../utils/sdk' - -const socket = new SocketSdk(getDefaultToken()!) - -export const actionCommand: CliSubcommand = { - description: 'Socket action command', - hidden: true, - async run(args: readonly string[]) { - const { values } = parseArgs({ - ...args, - options: { - socketSecurityApiKey: { - type: 'string', - default: process.env['SOCKET_SECURITY_API_KEY'] - }, - githubEventBefore: { - type: 'string', - default: '' - }, - githubEventAfter: { - type: 'string', - default: '' - } - }, - strict: true, - allowPositionals: true - }) - - const git = simpleGit() - const changedFiles = ( - await git.diff( - process.env['GITHUB_EVENT_NAME'] === 'pull_request' - ? ['--name-only', 'HEAD^1', 'HEAD'] - : ['--name-only', values.githubEventBefore, values.githubEventAfter] - ) - ).split('\n') - - console.log({ changedFiles }) - // supportedFiles have 3-level deep globs - const patterns = Object.values(await socket.getReportSupportedFiles()) - .flatMap((i: Record) => Object.values(i)) - .flatMap((i: Record) => Object.values(i)) - .flatMap((i: Record) => Object.values(i)) - - const files = micromatch(changedFiles, patterns) - - const scm = new GitHub() - - if (scm.checkEventType() === 'comment') { - console.log('Comment initiated flow') - const comments = await scm.getCommentsForPR() - await scm.removeCommentAlerts({ comments }) - } else if (scm.checkEventType() === 'diff') { - console.log('Push initiated flow') - const core = new Core({ owner: scm.owner, repo: scm.repo, files, socket }) - const diff = await core.createNewDiff({}) - const comments = await scm.getCommentsForPR() - diff.newAlerts = SCMComments.removeAlerts({ - comments, - newAlerts: diff.newAlerts - }) - const overviewComment = Messages.dependencyOverviewTemplate(diff) - const securityComment = Messages.securityCommentTemplate(diff) - let newSecurityComment = true - let newOverviewComment = true - let updateOldSecurityComment = comments.security !== undefined - let updateOldOverviewComment = comments.overview !== undefined - if (diff.newAlerts.length === 0) { - if (!updateOldSecurityComment) { - newSecurityComment = false - console.log('No new alerts or security issue comment disabled') - } else { - console.log('Updated security comment with no new alerts') - } - } - if (diff.newPackages.length === 0 && diff.removedPackages.length === 0) { - if (!updateOldOverviewComment) { - newOverviewComment = false - console.log( - 'No new/removed packages or Dependency Overview comment disabled' - ) - } else { - console.log('Updated overview comment with no dependencies') - } - } - await scm.addSocketComments({ - securityComment, - overviewComment, - comments, - newSecurityComment, - newOverviewComment - }) - } - } -} diff --git a/src/commands/action/run-action.ts b/src/commands/action/run-action.ts new file mode 100644 index 000000000..23eac288b --- /dev/null +++ b/src/commands/action/run-action.ts @@ -0,0 +1,87 @@ +// https://github.com/SocketDev/socket-python-cli/blob/6d4fc56faee68d3a4764f1f80f84710635bdaf05/socketsecurity/socketcli.py + +import micromatch from 'micromatch' +import { simpleGit } from 'simple-git' + +import { SocketSdk } from '@socketsecurity/sdk' + +import { Core } from './core' +import { GitHub } from './core/github' +import * as Messages from './core/messages' +import * as SCMComments from './core/scm_comments' +import { getDefaultToken } from '../../utils/sdk' + +// TODO: is this a github action handler? +export async function runAction( + githubEventBefore: string, + githubEventAfter: string +) { + //TODO + const socket = new SocketSdk(getDefaultToken()!) + + const git = simpleGit() + const changedFiles = ( + await git.diff( + process.env['GITHUB_EVENT_NAME'] === 'pull_request' + ? ['--name-only', 'HEAD^1', 'HEAD'] + : ['--name-only', githubEventBefore, githubEventAfter] + ) + ).split('\n') + + console.log({ changedFiles }) + // supportedFiles have 3-level deep globs + const patterns = Object.values(await socket.getReportSupportedFiles()) + .flatMap((i: Record) => Object.values(i)) + .flatMap((i: Record) => Object.values(i)) + .flatMap((i: Record) => Object.values(i)) + + const files = micromatch(changedFiles, patterns) + + const scm = new GitHub() + + if (scm.checkEventType() === 'comment') { + console.log('Comment initiated flow') + const comments = await scm.getCommentsForPR() + await scm.removeCommentAlerts({ comments }) + } else if (scm.checkEventType() === 'diff') { + console.log('Push initiated flow') + const core = new Core({ owner: scm.owner, repo: scm.repo, files, socket }) + const diff = await core.createNewDiff({}) + const comments = await scm.getCommentsForPR() + diff.newAlerts = SCMComments.removeAlerts({ + comments, + newAlerts: diff.newAlerts + }) + const overviewComment = Messages.dependencyOverviewTemplate(diff) + const securityComment = Messages.securityCommentTemplate(diff) + let newSecurityComment = true + let newOverviewComment = true + let updateOldSecurityComment = comments.security !== undefined + let updateOldOverviewComment = comments.overview !== undefined + if (diff.newAlerts.length === 0) { + if (!updateOldSecurityComment) { + newSecurityComment = false + console.log('No new alerts or security issue comment disabled') + } else { + console.log('Updated security comment with no new alerts') + } + } + if (diff.newPackages.length === 0 && diff.removedPackages.length === 0) { + if (!updateOldOverviewComment) { + newOverviewComment = false + console.log( + 'No new/removed packages or Dependency Overview comment disabled' + ) + } else { + console.log('Updated overview comment with no dependencies') + } + } + await scm.addSocketComments({ + securityComment, + overviewComment, + comments, + newSecurityComment, + newOverviewComment + }) + } +} diff --git a/src/commands/analytics/analytics-command.ts b/src/commands/analytics/cmd-analytics.ts similarity index 70% rename from src/commands/analytics/analytics-command.ts rename to src/commands/analytics/cmd-analytics.ts index 3a44f0765..66d2321a5 100644 --- a/src/commands/analytics/analytics-command.ts +++ b/src/commands/analytics/cmd-analytics.ts @@ -1,14 +1,13 @@ import meowOrDie from 'meow' import colors from 'yoctocolors-cjs' -import { runAnalytics } from './run-analytics.ts' +import { displayAnalytics } from './display-analytics.ts' import { commonFlags, outputFlags } from '../../flags' -import { AuthError, InputError } from '../../utils/errors' +import { AuthError } from '../../utils/errors' import { getFlagListOutput } from '../../utils/output-formatting' import { getDefaultToken } from '../../utils/sdk.ts' import type { CliCommandConfig } from '../../utils/meow-with-subcommands' -import type { CommandContext } from '../types.ts' const config: CliCommandConfig = { commandName: 'analytics', @@ -60,7 +59,7 @@ const config: CliCommandConfig = { ` } -export const analyticsCommand = { +export const cmdAnalytics = { description: config.description, hidden: config.hidden, run: run @@ -80,19 +79,18 @@ async function run( const { repo, scope, time } = cli.flags - if (scope !== 'org' && scope !== 'repo') { - throw new InputError("The scope must either be 'org' or 'repo'") - } - - if (time !== 7 && time !== 30 && time !== 90) { - throw new InputError('The time filter must either be 7, 30 or 90') - } + const badScope = scope !== 'org' && scope !== 'repo' + const badTime = time !== 7 && time !== 30 && time !== 90 + const badRepo = scope === 'repo' && !repo - if (scope === 'repo' && !repo) { - console.error( - `${colors.bgRed(colors.white('Input error'))}: Please provide a repository name when using the repository scope.` - ) + if (badScope || badTime || badRepo) { + console.error(`${colors.bgRed(colors.white('Input error'))}: Please provide the required fields:\n + - Scope must be "repo" or "org" ${badScope ? colors.red('(bad!)') : colors.green('(ok)')}\n + - The time filter must either be 7, 30 or 90 ${badTime ? colors.red('(bad!)') : colors.green('(ok)')}\n + - Repository name using --repo when scope is "repo" ${badRepo ? colors.red('(bad!)') : colors.green('(ok)')}\n + `) cli.showHelp() + return } const apiToken = getDefaultToken() @@ -102,11 +100,12 @@ async function run( ) } - return await runAnalytics(apiToken, { + return await displayAnalytics({ + apiToken, scope, time, - repo, - outputJson: cli.flags['json'], - file: cli.flags['file'] - } as CommandContext) + repo: String(repo || ''), + outputJson: Boolean(cli.flags['json']), + filePath: String(cli.flags['file'] || '') + }) } diff --git a/src/commands/analytics/run-analytics.ts b/src/commands/analytics/display-analytics.ts similarity index 77% rename from src/commands/analytics/run-analytics.ts rename to src/commands/analytics/display-analytics.ts index 907d8158a..0436e66e2 100644 --- a/src/commands/analytics/run-analytics.ts +++ b/src/commands/analytics/display-analytics.ts @@ -8,8 +8,6 @@ import { Spinner } from '@socketsecurity/registry/lib/spinner' import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' import { setupSdk } from '../../utils/sdk' -import type { CommandContext } from '../types.ts' - type FormattedData = { top_five_alert_types: { [key: string]: number } total_critical_alerts: { [key: string]: number } @@ -57,31 +55,44 @@ const METRICS = [ 'total_low_prevented' ] as const -export async function runAnalytics( - apiToken: string, - input: CommandContext -): Promise { - if (input) { - const spinner = new Spinner({ text: 'Fetching analytics data' }).start() - if (input.scope === 'org') { - await fetchOrgAnalyticsData( - input.time, - spinner, - apiToken, - input.outputJson, - input.file - ) - } else { - if (input.repo) { - await fetchRepoAnalyticsData( - input.repo, - input.time, - spinner, - apiToken, - input.outputJson, - input.file - ) +export async function displayAnalytics({ + apiToken, + filePath, + outputJson, + repo, + scope, + time +}: { + apiToken: string + scope: string + time: number + repo: string + outputJson: boolean + filePath: string +}): Promise { + const spinner = new Spinner({ text: 'Fetching analytics data' }).start() + + let data: undefined | { [key: string]: any }[] + if (scope === 'org') { + data = await fetchOrgAnalyticsData(time, spinner, apiToken) + } else if (repo) { + data = await fetchRepoAnalyticsData(repo, time, spinner, apiToken) + } + + if (data) { + if (outputJson && !filePath) { + console.log(data) + } else if (filePath) { + try { + await fs.writeFile(filePath, JSON.stringify(data), 'utf8') + console.log(`Data successfully written to ${filePath}`) + } catch (e: any) { + console.error(e) } + } else { + const fdata = + scope === 'org' ? formatData(data, 'org') : formatData(data, 'repo') + displayAnalyticsScreen(fdata) } } } @@ -171,10 +182,8 @@ function displayAnalyticsScreen(data: FormattedData): void { async function fetchOrgAnalyticsData( time: number, spinner: Spinner, - apiToken: string, - outputJson: boolean, - filePath: string -): Promise { + apiToken: string +): Promise<{ [key: string]: any }[] | undefined> { const socketSdk = await setupSdk(apiToken) const result = await handleApiCall( socketSdk.getOrgAnalytics(time.toString()), @@ -182,41 +191,26 @@ async function fetchOrgAnalyticsData( ) if (result.success === false) { - return handleUnsuccessfulApiResponse('getOrgAnalytics', result, spinner) + handleUnsuccessfulApiResponse('getOrgAnalytics', result, spinner) + return undefined } spinner.stop() if (!result.data.length) { - return console.log( - 'No analytics data is available for this organization yet.' - ) + console.log('No analytics data is available for this organization yet.') + return undefined } - const data = formatData(result.data, 'org') - if (outputJson && !filePath) { - console.log(result.data) - return - } - if (filePath) { - try { - await fs.writeFile(filePath, JSON.stringify(result.data), 'utf8') - console.log(`Data successfully written to ${filePath}`) - } catch (e: any) { - console.error(e) - } - return - } - return displayAnalyticsScreen(data) + + return result.data } async function fetchRepoAnalyticsData( repo: string, time: number, spinner: Spinner, - apiToken: string, - outputJson: boolean, - filePath: string -): Promise { + apiToken: string +): Promise<{ [key: string]: any }[] | undefined> { const socketSdk = await setupSdk(apiToken) const result = await handleApiCall( socketSdk.getRepoAnalytics(repo, time.toString()), @@ -224,30 +218,18 @@ async function fetchRepoAnalyticsData( ) if (result.success === false) { - return handleUnsuccessfulApiResponse('getRepoAnalytics', result, spinner) + handleUnsuccessfulApiResponse('getRepoAnalytics', result, spinner) + return undefined } spinner.stop() if (!result.data.length) { - return console.log( - 'No analytics data is available for this organization yet.' - ) + console.log('No analytics data is available for this organization yet.') + return undefined } - const data = formatData(result.data, 'repo') - if (outputJson && !filePath) { - return console.log(result.data) - } - if (filePath) { - try { - await fs.writeFile(filePath, JSON.stringify(result.data), 'utf8') - console.log(`Data successfully written to ${filePath}`) - } catch (e: any) { - console.error(e) - } - return - } - return displayAnalyticsScreen(data) + + return result.data } function formatData( diff --git a/src/commands/audit-log.ts b/src/commands/audit-log.ts deleted file mode 100644 index aab22968b..000000000 --- a/src/commands/audit-log.ts +++ /dev/null @@ -1,182 +0,0 @@ -import meow from 'meow' -import colors from 'yoctocolors-cjs' - -import { Separator, select } from '@socketsecurity/registry/lib/prompts' -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' - -const description = 'Look up the audit log for an organization' - -export const auditLogCommand: CliSubcommand = { - description, - async run(argv, importMeta, { parentName }) { - const name = parentName + ' audit-log' - - const input = setupCommand(name, 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: `Looking up audit log for ${input.orgSlug}\n` - }).start() - await fetchOrgAuditLog(input.orgSlug, input, spinner, apiToken) - } - } -} - -const auditLogFlags: { [key: string]: any } = { - type: { - type: 'string', - shortFlag: 't', - default: '', - description: 'Type of log event' - }, - 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' - } -} - -// Internal functions - -type CommandContext = { - outputJson: boolean - outputMarkdown: boolean - orgSlug: string - type: string - page: number - per_page: number -} - -function setupCommand( - name: string, - description: string, - argv: readonly string[], - importMeta: ImportMeta -): CommandContext | undefined { - const flags: { [key: string]: any } = { - ...auditLogFlags, - ...commonFlags, - ...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.length < 1) { - showHelp = true - console.error( - `${colors.bgRed(colors.white('Input error'))}: Please provide an organization slug.` - ) - } - if (showHelp) { - cli.showHelp() - return - } - const { - json: outputJson, - markdown: outputMarkdown, - page, - perPage - } = cli.flags - const type = cli.flags['type'] - const { 0: orgSlug = '' } = cli.input - return { - outputJson, - outputMarkdown, - orgSlug, - type: type && type.charAt(0).toUpperCase() + type.slice(1), - page, - per_page: perPage - } -} - -type Choice = { - value: Value - name?: string - description?: string - disabled?: boolean | string - type?: never -} - -type AuditChoice = Choice - -type AuditChoices = (Separator | AuditChoice)[] - -async function fetchOrgAuditLog( - orgSlug: string, - input: CommandContext, - spinner: Spinner, - apiToken: string -): Promise { - const socketSdk = await setupSdk(apiToken) - const result = await handleApiCall( - socketSdk.getAuditLogEvents(orgSlug, input), - `Looking up audit log for ${orgSlug}\n` - ) - - if (!result.success) { - handleUnsuccessfulApiResponse('getAuditLogEvents', result, spinner) - return - } - - spinner.stop() - - const data: AuditChoices = [] - const logDetails: { [key: string]: string } = {} - - for (const d of result.data.results) { - const { created_at } = d - if (created_at) { - const name = `${new Date(created_at).toLocaleDateString('en-us', { year: 'numeric', month: 'numeric', day: 'numeric' })} - ${d.user_email} - ${d.type} - ${d.ip_address} - ${d.user_agent}` - data.push({ name }, new Separator()) - logDetails[name] = JSON.stringify(d.payload) - } - } - - console.log( - logDetails[ - (await select({ - message: input.type - ? `\n Audit log for: ${orgSlug} with type: ${input.type}\n` - : `\n Audit log for: ${orgSlug}\n`, - choices: data, - pageSize: 30 - })) as any - ] - ) -} diff --git a/src/commands/audit-log/cmd-audit-log.ts b/src/commands/audit-log/cmd-audit-log.ts new file mode 100644 index 000000000..63c259899 --- /dev/null +++ b/src/commands/audit-log/cmd-audit-log.ts @@ -0,0 +1,95 @@ +import meowOrDie from 'meow' +import colors from 'yoctocolors-cjs' + +import { getAuditLog } from './get-audit-log.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: 'audit-log', + description: 'Look up the audit log for an organization', + hidden: false, + flags: { + type: { + type: 'string', + shortFlag: 't', + default: '', + description: 'Type of log event' + }, + 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' + }, + ...commonFlags, + ...outputFlags + }, + help: (parentName, config) => ` + Usage + $ ${parentName} ${config.commandName} + + Options + ${getFlagListOutput(config.flags, 6)} + + Examples + $ ${parentName} ${config.commandName} FakeOrg + ` +} + +export const cmdAuditLog = { + 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 type = String(cli.flags['type'] || '') + const [orgSlug = ''] = cli.input + + if (!orgSlug) { + 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 + `) + 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 getAuditLog({ + apiToken, + orgSlug, + outputJson: Boolean(cli.flags['json']), + outputMarkdown: Boolean(cli.flags['markdown']), + page: Number(cli.flags['page'] || 0), + perPage: Number(cli.flags['perPage'] || 0), + type: type.charAt(0).toUpperCase() + type.slice(1) + }) +} diff --git a/src/commands/audit-log/get-audit-log.ts b/src/commands/audit-log/get-audit-log.ts new file mode 100644 index 000000000..d2cc875f0 --- /dev/null +++ b/src/commands/audit-log/get-audit-log.ts @@ -0,0 +1,86 @@ +import { Separator, select } from '@socketsecurity/registry/lib/prompts' +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import { + handleApiCall, + handleUnsuccessfulApiResponse +} from '../../utils/api.ts' +import { setupSdk } from '../../utils/sdk.ts' + +type Choice = { + description?: string + disabled?: boolean | string + name?: string + type?: never + value: Value +} + +type AuditChoice = Choice + +type AuditChoices = (Separator | AuditChoice)[] + +export async function getAuditLog({ + apiToken, + orgSlug, + outputJson, + outputMarkdown, + page, + perPage, + type +}: { + apiToken: string + outputJson: boolean + outputMarkdown: boolean + orgSlug: string + page: number + perPage: number + type: string +}): Promise { + const spinner = new Spinner({ + text: `Looking up audit log for ${orgSlug}\n` + }).start() + + const socketSdk = await setupSdk(apiToken) + const result = await handleApiCall( + socketSdk.getAuditLogEvents(orgSlug, { + outputJson, + outputMarkdown, + orgSlug, + type, + page, + per_page: perPage + }), + `Looking up audit log for ${orgSlug}\n` + ) + + if (!result.success) { + handleUnsuccessfulApiResponse('getAuditLogEvents', result, spinner) + return + } + + spinner.stop() + + const data: AuditChoices = [] + const logDetails: { [key: string]: string } = {} + + for (const d of result.data.results) { + const { created_at } = d + if (created_at) { + const name = `${new Date(created_at).toLocaleDateString('en-us', { year: 'numeric', month: 'numeric', day: 'numeric' })} - ${d.user_email} - ${d.type} - ${d.ip_address} - ${d.user_agent}` + data.push({ name }, new Separator()) + logDetails[name] = JSON.stringify(d.payload) + } + } + + console.log( + logDetails[ + (await select({ + message: type + ? `\n Audit log for: ${orgSlug} with type: ${type}\n` + : `\n Audit log for: ${orgSlug}\n`, + choices: data, + pageSize: 30 + })) as any + ] + ) +} diff --git a/src/commands/cdxgen.ts b/src/commands/cdxgen.ts deleted file mode 100644 index f4e8d2186..000000000 --- a/src/commands/cdxgen.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { existsSync, promises as fs } from 'node:fs' -import path from 'node:path' -import process from 'node:process' - -import yargsParse from 'yargs-parser' -import colors from 'yoctocolors-cjs' - -import { runBin } from '@socketsecurity/registry/lib/npm' -import { pluralize } from '@socketsecurity/registry/lib/words' - -import constants from '../constants' - -import type { CliSubcommand } from '../utils/meow-with-subcommands' - -const { NPM, PNPM, cdxgenBinPath, synpBinPath } = constants - -const { - SBOM_SIGN_ALGORITHM, // Algorithm. Example: RS512 - SBOM_SIGN_PRIVATE_KEY, // Location to the RSA private key - SBOM_SIGN_PUBLIC_KEY // Optional. Location to the RSA public key -} = process.env - -const toLower = (arg: string) => arg.toLowerCase() -const arrayToLower = (arg: string[]) => arg.map(toLower) - -const nodejsPlatformTypes = new Set([ - 'javascript', - 'js', - 'nodejs', - NPM, - PNPM, - 'ts', - 'tsx', - 'typescript' -]) - -const yargsConfig = { - configuration: { - 'camel-case-expansion': false, - 'strip-aliased': true, - 'parse-numbers': false, - 'populate--': true, - 'unknown-options-as-args': true - }, - coerce: { - author: arrayToLower, - filter: arrayToLower, - only: arrayToLower, - profile: toLower, - standard: arrayToLower, - type: toLower - }, - default: { - //author: ['OWASP Foundation'], - //'auto-compositions': true, - //babel: true, - //evidence: false, - //'include-crypto': false, - //'include-formulation': false, - - // Default 'install-deps' to `false` and 'lifecycle' to 'pre-build' to - // sidestep arbitrary code execution during a cdxgen scan. - // https://github.com/CycloneDX/cdxgen/issues/1328 - 'install-deps': false, - lifecycle: 'pre-build', - - //output: 'bom.json', - //profile: 'generic', - //'project-version': '', - //recurse: true, - //'server-host': '127.0.0.1', - //'server-port': '9090', - //'spec-version': '1.5', - type: 'js' - //validate: true, - }, - alias: { - help: ['h'], - output: ['o'], - print: ['p'], - recurse: ['r'], - 'resolve-class': ['c'], - type: ['t'], - version: ['v'] - }, - array: [ - { key: 'author', type: 'string' }, - { key: 'exclude', type: 'string' }, - { key: 'filter', type: 'string' }, - { key: 'only', type: 'string' }, - { key: 'standard', type: 'string' } - ], - boolean: [ - 'auto-compositions', - 'babel', - 'deep', - 'evidence', - 'fail-on-error', - 'generate-key-and-sign', - 'help', - 'include-formulation', - 'include-crypto', - 'install-deps', - 'print', - 'required-only', - 'server', - 'validate', - 'version' - ], - string: [ - 'api-key', - 'lifecycle', - 'output', - 'parent-project-id', - 'profile', - 'project-group', - 'project-name', - 'project-version', - 'project-id', - 'server-host', - 'server-port', - 'server-url', - 'spec-version' - ] -} - -function argvToArray(argv: { - [key: string]: boolean | null | number | string | (string | number)[] -}): string[] { - if (argv['help']) return ['--help'] - const result = [] - for (const { 0: key, 1: value } of Object.entries(argv)) { - if (key === '_' || key === '--') continue - if (key === 'babel' || key === 'install-deps' || key === 'validate') { - // cdxgen documents no-babel, no-install-deps, and no-validate flags so - // use them when relevant. - result.push(`--${value ? key : `no-${key}`}`) - } else if (value === true) { - result.push(`--${key}`) - } else if (typeof value === 'string') { - result.push(`--${key}`, String(value)) - } else if (Array.isArray(value)) { - result.push(`--${key}`, ...value.map(String)) - } - } - if (argv['--']) { - result.push('--', ...(argv as any)['--']) - } - return result -} - -export const cdxgenCommand: CliSubcommand = { - description: 'Create an SBOM with CycloneDX generator (cdxgen)', - async run(argv_) { - const yargv = { - ...yargsParse(argv_, yargsConfig) - } - const unknown: string[] = yargv._ - const { length: unknownLength } = unknown - if (unknownLength) { - process.exitCode = 1 - console.error( - `Unknown ${pluralize('argument', unknownLength)}: ${yargv._.join(', ')}` - ) - return - } - let cleanupPackageLock = false - if ( - yargv.type !== 'yarn' && - nodejsPlatformTypes.has(yargv.type) && - existsSync('./yarn.lock') - ) { - if (existsSync('./package-lock.json')) { - yargv.type = NPM - } else { - // Use synp to create a package-lock.json from the yarn.lock, - // based on the node_modules folder, for a more accurate SBOM. - try { - await runBin(await fs.realpath(synpBinPath), [ - '--source-file', - './yarn.lock' - ]) - yargv.type = NPM - cleanupPackageLock = true - } catch {} - } - } - if (yargv.output === undefined) { - yargv.output = 'socket-cdx.json' - } - await runBin(await fs.realpath(cdxgenBinPath), argvToArray(yargv), { - env: { - NODE_ENV: '', - SBOM_SIGN_ALGORITHM, - SBOM_SIGN_PRIVATE_KEY, - SBOM_SIGN_PUBLIC_KEY - }, - stdio: 'inherit' - }) - if (cleanupPackageLock) { - try { - await fs.rm('./package-lock.json') - } catch {} - } - const fullOutputPath = path.join(process.cwd(), yargv.output) - if (existsSync(fullOutputPath)) { - console.log(colors.cyanBright(`${yargv.output} created!`)) - } - } -} diff --git a/src/commands/cdxgen/cmd-cdxgen.ts b/src/commands/cdxgen/cmd-cdxgen.ts new file mode 100644 index 000000000..9f23c575c --- /dev/null +++ b/src/commands/cdxgen/cmd-cdxgen.ts @@ -0,0 +1,170 @@ +// import meowOrDie from 'meow' +import process from 'node:process' + +import yargsParse from 'yargs-parser' + +import { pluralize } from '@socketsecurity/registry/lib/words' + +import { runCycloneDX } from './run-cyclonedx.ts' +import { getFlagListOutput } from '../../utils/output-formatting.ts' + +import type { CliCommandConfig } from '../../utils/meow-with-subcommands.ts' + +// TODO: convert yargs to meow. Or convert all the other things to yargs. +const toLower = (arg: string) => arg.toLowerCase() +const arrayToLower = (arg: string[]) => arg.map(toLower) +const yargsConfig = { + configuration: { + 'camel-case-expansion': false, + 'strip-aliased': true, + 'parse-numbers': false, + 'populate--': true, + 'unknown-options-as-args': true + }, + coerce: { + author: arrayToLower, + filter: arrayToLower, + only: arrayToLower, + profile: toLower, + standard: arrayToLower, + type: toLower + }, + default: { + //author: ['OWASP Foundation'], + //'auto-compositions': true, + //babel: true, + //evidence: false, + //'include-crypto': false, + //'include-formulation': false, + + // Default 'install-deps' to `false` and 'lifecycle' to 'pre-build' to + // sidestep arbitrary code execution during a cdxgen scan. + // https://github.com/CycloneDX/cdxgen/issues/1328 + 'install-deps': false, + lifecycle: 'pre-build', + + //output: 'bom.json', + //profile: 'generic', + //'project-version': '', + //recurse: true, + //'server-host': '127.0.0.1', + //'server-port': '9090', + //'spec-version': '1.5', + type: 'js' + //validate: true, + }, + alias: { + help: ['h'], + output: ['o'], + print: ['p'], + recurse: ['r'], + 'resolve-class': ['c'], + type: ['t'], + version: ['v'] + }, + array: [ + { key: 'author', type: 'string' }, + { key: 'exclude', type: 'string' }, + { key: 'filter', type: 'string' }, + { key: 'only', type: 'string' }, + { key: 'standard', type: 'string' } + ], + boolean: [ + 'auto-compositions', + 'babel', + 'deep', + 'evidence', + 'fail-on-error', + 'generate-key-and-sign', + 'help', + 'include-formulation', + 'include-crypto', + 'install-deps', + 'print', + 'required-only', + 'server', + 'validate', + 'version' + ], + string: [ + 'api-key', + 'lifecycle', + 'output', + 'parent-project-id', + 'profile', + 'project-group', + 'project-name', + 'project-version', + 'project-id', + 'server-host', + 'server-port', + 'server-url', + 'spec-version' + ] +} + +const config: CliCommandConfig = { + commandName: 'cdxgen', + description: 'Create an SBOM with CycloneDX generator (cdxgen)', + hidden: false, + flags: { + // TODO: convert from yargsConfig + }, + help: (parentName, config) => ` + Usage + $ ${parentName} ${config.commandName} [options] + + Options + ${getFlagListOutput(config.flags, 6)} + ` +} + +export const cmdCdxgen = { + description: config.description, + hidden: config.hidden, + run +} + +async function run( + argv: readonly string[], + _importMeta: ImportMeta, + { parentName: _parentName }: { parentName: string } +): Promise { + // const cli = meowOrDie(config.help(parentName, config), { + // argv, + // description: config.description, + // importMeta, + // // Note: we pass the args through so unknown args are allowed for this command + // flags: config.flags + // }) + // + // + // if (cli.input.length) + // console.error(`${colors.bgRed(colors.white('Input error'))}: Please provide the required fields:\n + // - Unexpected arguments\n + // `) + // config.help(parentName, config) + // return + // } + + // TODO: convert to meow + const yargv = { + ...yargsParse(argv as Array, yargsConfig) + } as any // as Record; + + const unknown: Array = yargv._ + const { length: unknownLength } = unknown + if (unknownLength) { + process.exitCode = 1 + console.error( + `Unknown ${pluralize('argument', unknownLength)}: ${yargv._.join(', ')}` + ) + return + } + + if (yargv.output === undefined) { + yargv.output = 'socket-cdx.json' + } + + await runCycloneDX(yargv) +} diff --git a/src/commands/cdxgen/run-cyclonedx.ts b/src/commands/cdxgen/run-cyclonedx.ts new file mode 100644 index 000000000..cfe6eecf4 --- /dev/null +++ b/src/commands/cdxgen/run-cyclonedx.ts @@ -0,0 +1,97 @@ +import { promises as fs } from 'fs' +import { existsSync } from 'node:fs' +import path from 'node:path' +import process from 'node:process' + +import colors from 'yoctocolors-cjs' + +import { runBin } from '@socketsecurity/registry/lib/npm' + +import constants from '../../constants' + +const { + SBOM_SIGN_ALGORITHM, // Algorithm. Example: RS512 + SBOM_SIGN_PRIVATE_KEY, // Location to the RSA private key + SBOM_SIGN_PUBLIC_KEY // Optional. Location to the RSA public key +} = process.env + +const { NPM, PNPM, cdxgenBinPath, synpBinPath } = constants + +const nodejsPlatformTypes = new Set([ + 'javascript', + 'js', + 'nodejs', + NPM, + PNPM, + 'ts', + 'tsx', + 'typescript' +]) + +export async function runCycloneDX(yargv: any) { + let cleanupPackageLock = false + if ( + yargv.type !== 'yarn' && + nodejsPlatformTypes.has(yargv.type) && + existsSync('./yarn.lock') + ) { + if (existsSync('./package-lock.json')) { + yargv.type = NPM + } else { + // Use synp to create a package-lock.json from the yarn.lock, + // based on the node_modules folder, for a more accurate SBOM. + try { + await runBin(await fs.realpath(synpBinPath), [ + '--source-file', + './yarn.lock' + ]) + yargv.type = NPM + cleanupPackageLock = true + } catch {} + } + } + + await runBin(await fs.realpath(cdxgenBinPath), argvToArray(yargv), { + env: { + NODE_ENV: '', + SBOM_SIGN_ALGORITHM, + SBOM_SIGN_PRIVATE_KEY, + SBOM_SIGN_PUBLIC_KEY + }, + stdio: 'inherit' + }) + if (cleanupPackageLock) { + try { + await fs.rm('./package-lock.json') + } catch {} + } + const fullOutputPath = path.join(process.cwd(), yargv.output) + if (existsSync(fullOutputPath)) { + console.log(colors.cyanBright(`${yargv.output} created!`)) + } +} + +function argvToArray(argv: { + [key: string]: boolean | null | number | string | (string | number)[] +}): string[] { + if (argv['help']) return ['--help'] + const result = [] + for (const { 0: key, 1: value } of Object.entries(argv)) { + if (key === '_' || key === '--') continue + if (key === 'babel' || key === 'install-deps' || key === 'validate') { + // cdxgen documents no-babel, no-install-deps, and no-validate flags so + // use them when relevant. + result.push(`--${value ? key : `no-${key}`}`) + } else if (value === true) { + result.push(`--${key}`) + } else if (typeof value === 'string') { + result.push(`--${key}`, String(value)) + } else if (Array.isArray(value)) { + result.push(`--${key}`, ...value.map(String)) + } + } + if (argv['--']) { + result.push('--', ...(argv as any)['--']) + } + return result +} diff --git a/src/commands/dependencies.ts b/src/commands/dependencies.ts deleted file mode 100644 index f3466c77f..000000000 --- a/src/commands/dependencies.ts +++ /dev/null @@ -1,145 +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' - -const description = - 'Search for any dependency that is being used in your organization' - -export const dependenciesCommand: CliSubcommand = { - description, - async run(argv, importMeta, { parentName }) { - const name = parentName + ' dependencies' - - const input = setupCommand(name, description, argv, importMeta) - if (input) { - await searchDeps(input) - } - } -} - -const dependenciesFlags = { - limit: { - type: 'number', - shortFlag: 'l', - default: 50, - description: 'Maximum number of dependencies returned' - }, - offset: { - type: 'number', - shortFlag: 'o', - default: 0, - description: 'Page number' - } -} - -// Internal functions - -type CommandContext = { - outputJson: boolean - outputMarkdown: boolean - limit: number - offset: number -} - -function setupCommand( - name: string, - description: string, - argv: readonly string[], - importMeta: ImportMeta -): CommandContext | undefined { - const flags: { [key: string]: any } = { - ...commonFlags, - ...dependenciesFlags, - ...outputFlags - } - - const cli = meow( - ` - Usage - $ ${name} - - Options - ${getFlagListOutput(flags, 6)} - - Examples - $ ${name} - `, - { - argv, - description, - importMeta, - flags - } - ) - - const { - json: outputJson, - limit, - markdown: outputMarkdown, - offset - } = cli.flags - - return { - outputJson, - outputMarkdown, - limit, - offset - } -} - -async function searchDeps({ - limit, - offset, - outputJson -}: CommandContext): 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.' - ) - } - const spinner = new Spinner({ text: 'Searching dependencies...' }).start() - const socketSdk = await setupSdk(apiToken) - - const result = await handleApiCall( - socketSdk.searchDependencies({ limit, offset }), - 'Searching dependencies' - ) - - if (!result.success) { - handleUnsuccessfulApiResponse('searchDependencies', result, spinner) - return - } - - spinner.stop('Organization dependencies:') - - if (outputJson) { - console.log(result.data) - return - } - - const options = { - columns: [ - { field: 'namespace', name: colors.cyan('Namespace') }, - { field: 'name', name: colors.cyan('Name') }, - { field: 'version', name: colors.cyan('Version') }, - { field: 'repository', name: colors.cyan('Repository') }, - { field: 'branch', name: colors.cyan('Branch') }, - { field: 'type', name: colors.cyan('Type') }, - { field: 'direct', name: colors.cyan('Direct') } - ] - } - - console.log(chalkTable(options, result.data.rows)) -} diff --git a/src/commands/dependencies/cmd-dependencies.ts b/src/commands/dependencies/cmd-dependencies.ts new file mode 100644 index 000000000..26210a5d3 --- /dev/null +++ b/src/commands/dependencies/cmd-dependencies.ts @@ -0,0 +1,66 @@ +import meowOrDie from 'meow' + +import { findDependencies } from './find-dependencies.ts' +import { commonFlags, outputFlags } from '../../flags' +import { getFlagListOutput } from '../../utils/output-formatting' + +import type { CliCommandConfig } from '../../utils/meow-with-subcommands.ts' + +const config: CliCommandConfig = { + commandName: 'dependencies', + description: + 'Search for any dependency that is being used in your organization', + hidden: false, + flags: { + ...commonFlags, + limit: { + type: 'number', + shortFlag: 'l', + default: 50, + description: 'Maximum number of dependencies returned' + }, + offset: { + type: 'number', + shortFlag: 'o', + default: 0, + description: 'Page number' + }, + ...outputFlags + }, + help: (parentName, config) => ` + Usage + ${parentName} ${config.commandName} + + Options + ${getFlagListOutput(config.flags, 6)} + + Examples + ${parentName} ${config.commandName} + ` +} + +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 + }) + + // TODO: markdown flag is ignored + await findDependencies({ + limit: Number(cli.flags['limit'] || 0) || 0, + offset: Number(cli.flags['offset'] || 0) || 0, + outputJson: Boolean(cli.flags['json']) + }) +} diff --git a/src/commands/dependencies/find-dependencies.ts b/src/commands/dependencies/find-dependencies.ts new file mode 100644 index 000000000..28e1253b0 --- /dev/null +++ b/src/commands/dependencies/find-dependencies.ts @@ -0,0 +1,59 @@ +// @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' +import { AuthError } from '../../utils/errors' +import { getDefaultToken, setupSdk } from '../../utils/sdk' + +export async function findDependencies({ + limit, + offset, + outputJson +}: { + outputJson: boolean + limit: number + offset: number +}): 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.' + ) + } + const spinner = new Spinner({ text: 'Searching dependencies...' }).start() + const socketSdk = await setupSdk(apiToken) + + const result = await handleApiCall( + socketSdk.searchDependencies({ limit, offset }), + 'Searching dependencies' + ) + + if (!result.success) { + handleUnsuccessfulApiResponse('searchDependencies', result, spinner) + return + } + + spinner.stop('Organization dependencies:') + + if (outputJson) { + console.log(result.data) + return + } + + const options = { + columns: [ + { field: 'namespace', name: colors.cyan('Namespace') }, + { field: 'name', name: colors.cyan('Name') }, + { field: 'version', name: colors.cyan('Version') }, + { field: 'repository', name: colors.cyan('Repository') }, + { field: 'branch', name: colors.cyan('Branch') }, + { field: 'type', name: colors.cyan('Type') }, + { field: 'direct', name: colors.cyan('Direct') } + ] + } + + console.log(chalkTable(options, result.data.rows)) +} diff --git a/src/commands/diff-scan/cmd-diff-scan-get.ts b/src/commands/diff-scan/cmd-diff-scan-get.ts new file mode 100644 index 000000000..db4c5754f --- /dev/null +++ b/src/commands/diff-scan/cmd-diff-scan-get.ts @@ -0,0 +1,108 @@ +import meowOrDie from 'meow' +import colors from 'yoctocolors-cjs' + +import { getDiffScan } from './get-diff-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: 'get', + description: 'Get a diff scan for an organization', + hidden: false, + flags: { + ...commonFlags, + before: { + type: 'string', + shortFlag: 'b', + default: '', + description: 'The full scan ID of the base scan' + }, + after: { + type: 'string', + shortFlag: 'a', + default: '', + description: 'The full scan ID of the head scan' + }, + preview: { + type: 'boolean', + shortFlag: 'p', + default: true, + description: 'A boolean flag to persist or not the diff scan result' + }, + file: { + type: 'string', + shortFlag: 'f', + default: '', + description: 'Path to a local file where the output should be saved' + }, + ...outputFlags + }, + help: (parentName, config) => ` + Usage + $ ${parentName} ${config.commandName} --before= --after= + + Options + ${getFlagListOutput(config.flags, 6)} + + Examples + $ ${parentName} ${config.commandName} FakeCorp --before=aaa0aa0a-aaaa-0000-0a0a-0000000a00a0 --after=aaa1aa1a-aaaa-1111-1a1a-1111111a11a1 + ` +} + +export const cmdDiffScanGet = { + 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, + allowUnknownFlags: false + }) + + const before = String(cli.flags['before'] || '') + const after = String(cli.flags['after'] || '') + const [orgSlug = ''] = cli.input + + if (!before || !after || cli.input.length < 1) { + console.error(`${colors.bgRed(colors.white('Input error'))}: Please provide the required fields:\n + - Specify a before and after full scan ID ${!before && !after ? colors.red('(missing before and after!)') : !before ? colors.red('(missing before!)') : !after ? colors.red('(missing after!)') : colors.green('(ok)')}\n + - To get full scans IDs, you can run the command "socket scan list ". + - Org name as the first argument ${!orgSlug ? colors.red('(missing!)') : colors.green('(ok)')}\n} + `) + 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 getDiffScan( + { + outputJson: Boolean(cli.flags['json']), + outputMarkdown: Boolean(cli.flags['markdown']), + before, + after, + preview: Boolean(cli.flags['preview']), + orgSlug, + file: String(cli.flags['file'] || '') + }, + apiToken + ) +} diff --git a/src/commands/diff-scan/index.ts b/src/commands/diff-scan/cmd-diff-scan.ts similarity index 77% rename from src/commands/diff-scan/index.ts rename to src/commands/diff-scan/cmd-diff-scan.ts index b8e9da2bd..3370b39f1 100644 --- a/src/commands/diff-scan/index.ts +++ b/src/commands/diff-scan/cmd-diff-scan.ts @@ -1,16 +1,16 @@ -import { get } from './get' +import { cmdDiffScanGet } from './cmd-diff-scan-get.ts' import { meowWithSubcommands } from '../../utils/meow-with-subcommands' import type { CliSubcommand } from '../../utils/meow-with-subcommands' const description = 'Diff scans related commands' -export const diffScanCommand: CliSubcommand = { +export const cmdDiffScan: CliSubcommand = { description, async run(argv, importMeta, { parentName }) { await meowWithSubcommands( { - get + get: cmdDiffScanGet }, { argv, diff --git a/src/commands/diff-scan/get-diff-scan.ts b/src/commands/diff-scan/get-diff-scan.ts new file mode 100644 index 000000000..a4960da62 --- /dev/null +++ b/src/commands/diff-scan/get-diff-scan.ts @@ -0,0 +1,73 @@ +import fs from 'node:fs' +import util from 'node:util' + +import colors from 'yoctocolors-cjs' + +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import { handleAPIError, queryAPI } from '../../utils/api.ts' + +export async function getDiffScan( + { + after, + before, + file, + orgSlug, + outputJson + }: { + outputJson: boolean + outputMarkdown: boolean + before: string + after: string + preview: boolean + orgSlug: string + file: string + }, + apiToken: string +): Promise { + const spinnerText = 'Getting diff scan... \n' + const spinner = new Spinner({ text: spinnerText }).start() + + const response = await queryAPI( + `${orgSlug}/full-scans/diff?before=${before}&after=${after}&preview`, + apiToken + ) + const data = await response.json() + + if (!response.ok) { + const err = await handleAPIError(response.status) + spinner.error(`${colors.bgRed(colors.white(response.statusText))}: ${err}`) + return + } + + spinner.stop() + + if (file && !outputJson) { + fs.writeFile(file, JSON.stringify(data), err => { + err + ? console.error(err) + : console.log(`Data successfully written to ${file}`) + }) + return + } + + if (outputJson) { + console.log(`\n Diff scan result: \n`) + console.log( + util.inspect(data, { showHidden: false, depth: null, colors: true }) + ) + console.log( + `\n View this diff scan in the Socket dashboard: ${colors.cyan((data as any)?.['diff_report_url'])}` + ) + return + } + + console.log('Diff scan result:') + console.log(data) + console.log( + `\n 📝 To display the detailed report in the terminal, use the --json flag \n` + ) + console.log( + `\n View this diff scan in the Socket dashboard: ${colors.cyan((data as any)?.['diff_report_url'])}` + ) +} diff --git a/src/commands/diff-scan/get.ts b/src/commands/diff-scan/get.ts deleted file mode 100644 index a974daad6..000000000 --- a/src/commands/diff-scan/get.ts +++ /dev/null @@ -1,180 +0,0 @@ -import fs from 'node:fs' -import util from 'node:util' - -import meow from 'meow' -import colors from 'yoctocolors-cjs' - -import { Spinner } from '@socketsecurity/registry/lib/spinner' - -import { commonFlags, outputFlags } from '../../flags' -import { handleAPIError, queryAPI } from '../../utils/api' -import { AuthError } from '../../utils/errors' -import { getFlagListOutput } from '../../utils/output-formatting' -import { getDefaultToken } from '../../utils/sdk' - -import type { CliSubcommand } from '../../utils/meow-with-subcommands' - -export const get: CliSubcommand = { - description: 'Get a diff scan for an organization', - async run(argv, importMeta, { parentName }) { - const name = `${parentName} get` - const input = setupCommand(name, get.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 diff scan... \n' - const spinner = new Spinner({ text: spinnerText }).start() - await getDiffScan(input, spinner, apiToken) - } - } -} - -const getDiffScanFlags: { [key: string]: any } = { - before: { - type: 'string', - shortFlag: 'b', - default: '', - description: 'The full scan ID of the base scan' - }, - after: { - type: 'string', - shortFlag: 'a', - default: '', - description: 'The full scan ID of the head scan' - }, - preview: { - type: 'boolean', - shortFlag: 'p', - default: true, - description: 'A boolean flag to persist or not the diff scan result' - }, - file: { - type: 'string', - shortFlag: 'f', - default: '', - description: 'Path to a local file where the output should be saved' - } -} - -// Internal functions - -type CommandContext = { - outputJson: boolean - outputMarkdown: boolean - before: string - after: string - preview: boolean - orgSlug: string - file: string -} - -function setupCommand( - name: string, - description: string, - argv: readonly string[], - importMeta: ImportMeta -): CommandContext | undefined { - const flags: { [key: string]: any } = { - ...commonFlags, - ...getDiffScanFlags, - ...outputFlags - } - const cli = meow( - ` - Usage - $ ${name} --before= --after= - - Options - ${getFlagListOutput(flags, 6)} - - Examples - $ ${name} FakeCorp --before=aaa0aa0a-aaaa-0000-0a0a-0000000a00a0 --after=aaa1aa1a-aaaa-1111-1a1a-1111111a11a1 - `, - { - argv, - description, - importMeta, - flags - } - ) - const { after, before } = cli.flags - let showHelp = cli.flags['help'] - if (!before || !after) { - showHelp = true - console.error( - `${colors.bgRed(colors.white('Input error'))}: Please specify a before and after full scan ID. To get full scans IDs, you can run the command "socket scan list ".` - ) - } else if (cli.input.length < 1) { - showHelp = true - console.error( - `${colors.bgRed(colors.white('Input error'))}: Please provide an organization slug.` - ) - } - if (showHelp) { - cli.showHelp() - return - } - const [orgSlug = ''] = cli.input - return { - outputJson: cli.flags['json'], - outputMarkdown: cli.flags['markdown'], - before, - after, - preview: cli.flags['preview'], - orgSlug, - file: cli.flags['file'] - } -} - -async function getDiffScan( - { after, before, file, orgSlug, outputJson }: CommandContext, - spinner: Spinner, - apiToken: string -): Promise { - const response = await queryAPI( - `${orgSlug}/full-scans/diff?before=${before}&after=${after}&preview`, - apiToken - ) - const data = await response.json() - - if (!response.ok) { - const err = await handleAPIError(response.status) - spinner.error(`${colors.bgRed(colors.white(response.statusText))}: ${err}`) - return - } - - spinner.stop() - - if (file && !outputJson) { - fs.writeFile(file, JSON.stringify(data), err => { - err - ? console.error(err) - : console.log(`Data successfully written to ${file}`) - }) - return - } - - if (outputJson) { - console.log(`\n Diff scan result: \n`) - console.log( - util.inspect(data, { showHidden: false, depth: null, colors: true }) - ) - console.log( - `\n View this diff scan in the Socket dashboard: ${colors.cyan((data as any)?.['diff_report_url'])}` - ) - return - } - - console.log('Diff scan result:') - console.log(data) - console.log( - `\n 📝 To display the detailed report in the terminal, use the --json flag \n` - ) - console.log( - `\n View this diff scan in the Socket dashboard: ${colors.cyan((data as any)?.['diff_report_url'])}` - ) -} diff --git a/src/commands/fix.ts b/src/commands/fix.ts deleted file mode 100644 index 03b645ca9..000000000 --- a/src/commands/fix.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Spinner } from '@socketsecurity/registry/lib/spinner' - -import constants from '../constants' -import { shadowNpmInstall } from '../utils/npm' - -import type { CliSubcommand } from '../utils/meow-with-subcommands' - -const { SOCKET_CLI_IN_FIX_CMD, SOCKET_IPC_HANDSHAKE } = constants - -// const prev = new Set(alerts.map(a => a.key)) -// let ret: SafeNode | undefined -// /* eslint-disable no-await-in-loop */ -// while (alerts.length > 0) { -// await updateAdvisoryNodes(this, alerts) -// ret = await this[kRiskyReify](...args) -// await this.loadActual() -// await this.buildIdealTree() -// needInfoOn = getPackagesToQueryFromDiff(this.diff, { -// includeUnchanged: true -// }) -// alerts = ( -// await getPackagesAlerts(needInfoOn, { -// includeExisting: true, -// includeUnfixable: true -// }) -// ).filter(({ key }) => { -// const unseen = !prev.has(key) -// if (unseen) { -// prev.add(key) -// } -// return unseen -// }) -// } -// /* eslint-enable no-await-in-loop */ -// return ret! - -export const fixCommand: CliSubcommand = { - description: 'Fix "fixable" Socket alerts', - hidden: true, - async run() { - const spinner = new Spinner().start() - try { - await shadowNpmInstall({ - ipc: { - [SOCKET_IPC_HANDSHAKE]: { - [SOCKET_CLI_IN_FIX_CMD]: true - } - } - }) - } catch (e: any) { - console.error(e) - } finally { - spinner.stop() - } - } -} diff --git a/src/commands/fix/cmd-fix.ts b/src/commands/fix/cmd-fix.ts new file mode 100644 index 000000000..86a077efd --- /dev/null +++ b/src/commands/fix/cmd-fix.ts @@ -0,0 +1,87 @@ +import meowOrDie from 'meow' + +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import constants from '../../constants' +import { shadowNpmInstall } from '../../utils/npm' +import { getFlagListOutput } from '../../utils/output-formatting.ts' + +import type { CliCommandConfig } from '../../utils/meow-with-subcommands.ts' + +const { SOCKET_CLI_IN_FIX_CMD, SOCKET_IPC_HANDSHAKE } = constants + +const config: CliCommandConfig = { + commandName: 'fix', + description: 'Fix "fixable" Socket alerts', + hidden: true, + flags: {}, + help: (parentName, config) => ` + Usage + $ ${parentName} ${config.commandName} + + Options + ${getFlagListOutput(config.flags, 6)} + ` +} + +export const cmdFix = { + description: config.description, + hidden: config.hidden, + run +} + +async function run( + argv: readonly string[], + importMeta: ImportMeta, + { parentName }: { parentName: string } +): Promise { + meowOrDie(config.help(parentName, config), { + argv, + description: config.description, + importMeta, + flags: config.flags + }) + + // const prev = new Set(alerts.map(a => a.key)) + // let ret: SafeNode | undefined + // /* eslint-disable no-await-in-loop */ + // while (alerts.length > 0) { + // await updateAdvisoryNodes(this, alerts) + // ret = await this[kRiskyReify](...args) + // await this.loadActual() + // await this.buildIdealTree() + // needInfoOn = getPackagesToQueryFromDiff(this.diff, { + // includeUnchanged: true + // }) + // alerts = ( + // await getPackagesAlerts(needInfoOn, { + // includeExisting: true, + // includeUnfixable: true + // }) + // ).filter(({ key }) => { + // const unseen = !prev.has(key) + // if (unseen) { + // prev.add(key) + // } + // return unseen + // }) + // } + // /* eslint-enable no-await-in-loop */ + // return ret! + + const spinner = new Spinner().start() + try { + await shadowNpmInstall({ + ipc: { + [SOCKET_IPC_HANDSHAKE]: { + [SOCKET_CLI_IN_FIX_CMD]: true + } + } + }) + } catch (e: any) { + console.error(e) + spinner.error() + } finally { + spinner.stop() + } +} diff --git a/src/commands/info.ts b/src/commands/info.ts deleted file mode 100644 index b3be24772..000000000 --- a/src/commands/info.ts +++ /dev/null @@ -1,291 +0,0 @@ -import process from 'node:process' - -import meow from 'meow' -import colors from 'yoctocolors-cjs' - -import constants from '@socketsecurity/registry/lib/constants' -import { Spinner } from '@socketsecurity/registry/lib/spinner' - -import { commonFlags, outputFlags, validationFlags } from '../flags' -import { formatSeverityCount, getSeverityCount } from '../utils/alert/severity' -import { handleApiCall, handleUnsuccessfulApiResponse } from '../utils/api' -import { ColorOrMarkdown } from '../utils/color-or-markdown' -import { InputError } from '../utils/errors' -import { objectSome } from '../utils/objects' -import { getFlagListOutput } from '../utils/output-formatting' -import { getPublicToken, setupSdk } from '../utils/sdk' -import { - getSocketDevAlertUrl, - getSocketDevPackageOverviewUrl -} from '../utils/socket-url' - -import type { SocketSdkAlert } from '../utils/alert/severity' -import type { CliSubcommand } from '../utils/meow-with-subcommands' -import type { SocketSdkReturnType } from '@socketsecurity/sdk' - -const { NPM } = constants - -const description = 'Look up info regarding a package' - -export const infoCommand: CliSubcommand = { - description, - async run(argv, importMeta, { parentName }) { - const name = parentName + ' info' - - const commandContext = setupCommand(name, description, argv, importMeta) - if (commandContext) { - const spinnerText = - commandContext.pkgVersion === 'latest' - ? `Looking up data for the latest version of ${commandContext.pkgName}` - : `Looking up data for version ${commandContext.pkgVersion} of ${commandContext.pkgName}` - const spinner = new Spinner({ text: spinnerText }).start() - const packageData = await fetchPackageData( - commandContext.pkgName, - commandContext.pkgVersion, - commandContext, - spinner - ) - if (packageData) { - formatPackageDataOutput( - packageData, - { name, ...commandContext }, - spinner - ) - } - } - } -} - -// Internal functions - -interface CommandContext { - includeAllIssues: boolean - outputJson: boolean - outputMarkdown: boolean - pkgName: string - pkgVersion: string - strict: boolean -} - -function setupCommand( - name: string, - description: string, - argv: readonly string[], - importMeta: ImportMeta -): void | CommandContext { - const flags: { [key: string]: any } = { - ...commonFlags, - ...outputFlags, - ...validationFlags - } - const cli = meow( - ` - Usage - $ ${name} - - Options - ${getFlagListOutput(flags, 6)} - - Examples - $ ${name} webtorrent - $ ${name} webtorrent@1.9.1 - `, - { - argv, - description, - importMeta, - flags - } - ) - if (cli.input.length > 1) { - throw new InputError('Only one package lookup supported at once') - } - const { 0: rawPkgName = '' } = cli.input - let showHelp = cli.flags['help'] - if (!rawPkgName) { - showHelp = true - } - if (showHelp) { - cli.showHelp() - return - } - const versionSeparator = rawPkgName.lastIndexOf('@') - const pkgName = - versionSeparator < 1 ? rawPkgName : rawPkgName.slice(0, versionSeparator) - const pkgVersion = - versionSeparator < 1 ? 'latest' : rawPkgName.slice(versionSeparator + 1) - return { - includeAllIssues: cli.flags['all'], - outputJson: cli.flags['json'], - outputMarkdown: cli.flags['markdown'], - pkgName, - pkgVersion, - strict: cli.flags['strict'] - } as CommandContext -} - -interface PackageData { - data: SocketSdkReturnType<'getIssuesByNPMPackage'>['data'] - severityCount: Record - score: SocketSdkReturnType<'getScoreByNPMPackage'>['data'] -} - -async function fetchPackageData( - pkgName: string, - pkgVersion: string, - { includeAllIssues }: Pick, - spinner: Spinner -): Promise { - const socketSdk = await setupSdk(getPublicToken()) - const result = await handleApiCall( - socketSdk.getIssuesByNPMPackage(pkgName, pkgVersion), - 'looking up package' - ) - const scoreResult = await handleApiCall( - socketSdk.getScoreByNPMPackage(pkgName, pkgVersion), - 'looking up package score' - ) - - if (result.success === false) { - return handleUnsuccessfulApiResponse( - 'getIssuesByNPMPackage', - result, - spinner - ) - } - - if (scoreResult.success === false) { - return handleUnsuccessfulApiResponse( - 'getScoreByNPMPackage', - scoreResult, - spinner - ) - } - - const severityCount = getSeverityCount( - result.data, - includeAllIssues ? undefined : 'high' - ) - - return { - data: result.data, - severityCount, - score: scoreResult.data - } -} - -function formatPackageDataOutput( - { data, score, severityCount }: PackageData, - { - name, - outputJson, - outputMarkdown, - pkgName, - pkgVersion, - strict - }: CommandContext & { name: string }, - spinner: Spinner -): void { - if (outputJson) { - console.log(JSON.stringify(data, undefined, 2)) - } else { - console.log('\nPackage report card:') - const scoreResult = { - 'Supply Chain Risk': Math.floor(score.supplyChainRisk.score * 100), - Maintenance: Math.floor(score.maintenance.score * 100), - Quality: Math.floor(score.quality.score * 100), - Vulnerabilities: Math.floor(score.vulnerability.score * 100), - License: Math.floor(score.license.score * 100) - } - Object.entries(scoreResult).map(score => - console.log(`- ${score[0]}: ${formatScore(score[1])}`) - ) - console.log('\n') - if (objectSome(severityCount)) { - spinner[strict ? 'error' : 'success']( - `Package has these issues: ${formatSeverityCount(severityCount)}` - ) - formatPackageIssuesDetails(data, outputMarkdown) - } else { - spinner.success('Package has no issues') - } - - const format = new ColorOrMarkdown(!!outputMarkdown) - const url = getSocketDevPackageOverviewUrl(NPM, pkgName, pkgVersion) - - console.log('\n') - if (pkgVersion === 'latest') { - console.log( - `Detailed info on socket.dev: ${format.hyperlink(`${pkgName}`, url, { fallbackToUrl: true })}` - ) - } else { - console.log( - `Detailed info on socket.dev: ${format.hyperlink(`${pkgName} v${pkgVersion}`, url, { fallbackToUrl: true })}` - ) - } - if (!outputMarkdown) { - console.log( - colors.dim( - `\nOr rerun ${colors.italic(name)} using the ${colors.italic('--json')} flag to get full JSON output` - ) - ) - } - } - - if (strict && objectSome(severityCount)) { - process.exit(1) - } -} - -function formatPackageIssuesDetails( - packageData: SocketSdkReturnType<'getIssuesByNPMPackage'>['data'], - outputMarkdown: boolean -) { - const issueDetails = packageData.filter( - d => d.value?.severity === 'high' || d.value?.severity === 'critical' - ) - - const uniqueIssues = issueDetails.reduce( - ( - acc: { [key: string]: { count: number; label: string | undefined } }, - issue - ) => { - const { type } = issue - if (type) { - if (acc[type] === undefined) { - acc[type] = { - label: issue.value?.label, - count: 1 - } - } else { - acc[type]!.count += 1 - } - } - return acc - }, - {} - ) - - const format = new ColorOrMarkdown(!!outputMarkdown) - for (const issue of Object.keys(uniqueIssues)) { - const issueWithLink = format.hyperlink( - `${uniqueIssues[issue]?.label}`, - getSocketDevAlertUrl(issue), - { fallbackToUrl: true } - ) - if (uniqueIssues[issue]?.count === 1) { - console.log(`- ${issueWithLink}`) - } else { - console.log(`- ${issueWithLink}: ${uniqueIssues[issue]?.count}`) - } - } -} - -function formatScore(score: number): string { - if (score > 80) { - return colors.green(`${score}`) - } else if (score < 80 && score > 60) { - return colors.yellow(`${score}`) - } - return colors.red(`${score}`) -} diff --git a/src/commands/info/cmd-info.ts b/src/commands/info/cmd-info.ts new file mode 100644 index 000000000..f7c865b3a --- /dev/null +++ b/src/commands/info/cmd-info.ts @@ -0,0 +1,77 @@ +import meowOrDie from 'meow' + +import { getPackageInfo } from './get-package-info.ts' +import { commonFlags, outputFlags, validationFlags } from '../../flags' +import { InputError } from '../../utils/errors' +import { getFlagListOutput } from '../../utils/output-formatting' + +import type { CliCommandConfig } from '../../utils/meow-with-subcommands.ts' + +const config: CliCommandConfig = { + commandName: 'info', + description: 'Look up info regarding a package', + hidden: false, + flags: { + ...commonFlags, + ...outputFlags, + ...validationFlags + }, + help: (parentName, config) => ` + Usage + $ ${parentName} ${config.commandName} + + Options + ${getFlagListOutput(config.flags, 6)} + + Examples + $ ${parentName} ${config.commandName} webtorrent + $ ${parentName} ${config.commandName} webtorrent@1.9.1 + ` +} + +export const cmdInfo = { + 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 + }) + + if (cli.input.length > 1) { + throw new InputError('Only one package lookup supported at once') + } + const { 0: rawPkgName = '' } = cli.input + let showHelp = cli.flags['help'] + if (!rawPkgName) { + showHelp = true + } + if (showHelp) { + cli.showHelp() + return + } + const versionSeparator = rawPkgName.lastIndexOf('@') + const pkgName = + versionSeparator < 1 ? rawPkgName : rawPkgName.slice(0, versionSeparator) + const pkgVersion = + versionSeparator < 1 ? 'latest' : rawPkgName.slice(versionSeparator + 1) + + await getPackageInfo({ + commandName: `${parentName} ${config.commandName}`, + includeAllIssues: Boolean(cli.flags['all']), + outputJson: Boolean(cli.flags['json']), + outputMarkdown: Boolean(cli.flags['markdown']), + pkgName, + pkgVersion, + strict: Boolean(cli.flags['strict']) + }) +} diff --git a/src/commands/info/fetch-package-info.ts b/src/commands/info/fetch-package-info.ts new file mode 100644 index 000000000..e5d4b503d --- /dev/null +++ b/src/commands/info/fetch-package-info.ts @@ -0,0 +1,53 @@ +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import { PackageData } from './get-package-info.ts' +import { getSeverityCount } from '../../utils/alert/severity.ts' +import { + handleApiCall, + handleUnsuccessfulApiResponse +} from '../../utils/api.ts' +import { getPublicToken, setupSdk } from '../../utils/sdk.ts' + +export async function fetchPackageInfo( + pkgName: string, + pkgVersion: string, + includeAllIssues: boolean, + spinner: Spinner +): Promise { + const socketSdk = await setupSdk(getPublicToken()) + const result = await handleApiCall( + socketSdk.getIssuesByNPMPackage(pkgName, pkgVersion), + 'looking up package' + ) + const scoreResult = await handleApiCall( + socketSdk.getScoreByNPMPackage(pkgName, pkgVersion), + 'looking up package score' + ) + + if (result.success === false) { + return handleUnsuccessfulApiResponse( + 'getIssuesByNPMPackage', + result, + spinner + ) + } + + if (scoreResult.success === false) { + return handleUnsuccessfulApiResponse( + 'getScoreByNPMPackage', + scoreResult, + spinner + ) + } + + const severityCount = getSeverityCount( + result.data, + includeAllIssues ? undefined : 'high' + ) + + return { + data: result.data, + severityCount, + score: scoreResult.data + } +} diff --git a/src/commands/info/format-package-info.ts b/src/commands/info/format-package-info.ts new file mode 100644 index 000000000..809807dee --- /dev/null +++ b/src/commands/info/format-package-info.ts @@ -0,0 +1,142 @@ +import process from 'node:process' + +import colors from 'yoctocolors-cjs' + +import constants from '@socketsecurity/registry/lib/constants' +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import { PackageData } from './get-package-info.ts' +import { formatSeverityCount } from '../../utils/alert/severity.ts' +import { ColorOrMarkdown } from '../../utils/color-or-markdown.ts' +import { objectSome } from '../../utils/objects.ts' +import { + getSocketDevAlertUrl, + getSocketDevPackageOverviewUrl +} from '../../utils/socket-url.ts' + +import type { SocketSdkReturnType } from '@socketsecurity/sdk' + +const { NPM } = constants + +export function formatPackageInfo( + { data, score, severityCount }: PackageData, + { + name, + outputJson, + outputMarkdown, + pkgName, + pkgVersion, + strict + }: { + includeAllIssues: boolean + outputJson: boolean + outputMarkdown: boolean + pkgName: string + pkgVersion: string + strict: boolean + } & { name: string }, + spinner: Spinner +): void { + if (outputJson) { + console.log(JSON.stringify(data, undefined, 2)) + } else { + console.log('\nPackage report card:') + const scoreResult = { + 'Supply Chain Risk': Math.floor(score.supplyChainRisk.score * 100), + Maintenance: Math.floor(score.maintenance.score * 100), + Quality: Math.floor(score.quality.score * 100), + Vulnerabilities: Math.floor(score.vulnerability.score * 100), + License: Math.floor(score.license.score * 100) + } + Object.entries(scoreResult).map(score => + console.log(`- ${score[0]}: ${formatScore(score[1])}`) + ) + console.log('\n') + if (objectSome(severityCount)) { + spinner[strict ? 'error' : 'success']( + `Package has these issues: ${formatSeverityCount(severityCount)}` + ) + formatPackageIssuesDetails(data, outputMarkdown) + } else { + spinner.success('Package has no issues') + } + + const format = new ColorOrMarkdown(!!outputMarkdown) + const url = getSocketDevPackageOverviewUrl(NPM, pkgName, pkgVersion) + + console.log('\n') + if (pkgVersion === 'latest') { + console.log( + `Detailed info on socket.dev: ${format.hyperlink(`${pkgName}`, url, { fallbackToUrl: true })}` + ) + } else { + console.log( + `Detailed info on socket.dev: ${format.hyperlink(`${pkgName} v${pkgVersion}`, url, { fallbackToUrl: true })}` + ) + } + if (!outputMarkdown) { + console.log( + colors.dim( + `\nOr rerun ${colors.italic(name)} using the ${colors.italic('--json')} flag to get full JSON output` + ) + ) + } + } + + if (strict && objectSome(severityCount)) { + process.exit(1) + } +} + +function formatPackageIssuesDetails( + packageData: SocketSdkReturnType<'getIssuesByNPMPackage'>['data'], + outputMarkdown: boolean +) { + const issueDetails = packageData.filter( + d => d.value?.severity === 'high' || d.value?.severity === 'critical' + ) + + const uniqueIssues = issueDetails.reduce( + ( + acc: { [key: string]: { count: number; label: string | undefined } }, + issue + ) => { + const { type } = issue + if (type) { + if (acc[type] === undefined) { + acc[type] = { + label: issue.value?.label, + count: 1 + } + } else { + acc[type]!.count += 1 + } + } + return acc + }, + {} + ) + + const format = new ColorOrMarkdown(!!outputMarkdown) + for (const issue of Object.keys(uniqueIssues)) { + const issueWithLink = format.hyperlink( + `${uniqueIssues[issue]?.label}`, + getSocketDevAlertUrl(issue), + { fallbackToUrl: true } + ) + if (uniqueIssues[issue]?.count === 1) { + console.log(`- ${issueWithLink}`) + } else { + console.log(`- ${issueWithLink}: ${uniqueIssues[issue]?.count}`) + } + } +} + +function formatScore(score: number): string { + if (score > 80) { + return colors.green(`${score}`) + } else if (score < 80 && score > 60) { + return colors.yellow(`${score}`) + } + return colors.red(`${score}`) +} diff --git a/src/commands/info/get-package-info.ts b/src/commands/info/get-package-info.ts new file mode 100644 index 000000000..4a4d9d2f4 --- /dev/null +++ b/src/commands/info/get-package-info.ts @@ -0,0 +1,58 @@ +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import { fetchPackageInfo } from './fetch-package-info.ts' +import { formatPackageInfo } from './format-package-info.ts' + +import type { SocketSdkAlert } from '../../utils/alert/severity' +import type { SocketSdkReturnType } from '@socketsecurity/sdk' + +export interface PackageData { + data: SocketSdkReturnType<'getIssuesByNPMPackage'>['data'] + severityCount: Record + score: SocketSdkReturnType<'getScoreByNPMPackage'>['data'] +} + +export async function getPackageInfo({ + commandName, + includeAllIssues, + outputJson, + outputMarkdown, + pkgName, + pkgVersion, + strict +}: { + commandName: string + includeAllIssues: boolean + outputJson: boolean + outputMarkdown: boolean + pkgName: string + pkgVersion: string + strict: boolean +}) { + const spinnerText = + pkgVersion === 'latest' + ? `Looking up data for the latest version of ${pkgName}` + : `Looking up data for version ${pkgVersion} of ${pkgName}` + const spinner = new Spinner({ text: spinnerText }).start() + const packageData = await fetchPackageInfo( + pkgName, + pkgVersion, + includeAllIssues, + spinner + ) + if (packageData) { + formatPackageInfo( + packageData, + { + name: commandName, + includeAllIssues, + outputJson, + outputMarkdown, + pkgName, + pkgVersion, + strict + }, + spinner + ) + } +} diff --git a/src/commands/report/cmd-report-create.ts b/src/commands/report/cmd-report-create.ts new file mode 100644 index 000000000..a61440b0b --- /dev/null +++ b/src/commands/report/cmd-report-create.ts @@ -0,0 +1,120 @@ +import path from 'node:path' +import process from 'node:process' + +import meowOrDie from 'meow' + +import { createReport } from './create-report.ts' +import { getSocketConfig } from './get-socket-config.ts' +import { viewReport } from './view-report.ts' +import { commonFlags, outputFlags, validationFlags } from '../../flags' +import { ColorOrMarkdown } from '../../utils/color-or-markdown.ts' +import { getFlagListOutput } from '../../utils/output-formatting' + +import type { CliCommandConfig } from '../../utils/meow-with-subcommands' + +const config: CliCommandConfig = { + commandName: 'create', + description: 'Create a project report', + hidden: false, + flags: { + ...commonFlags, + ...outputFlags, + ...validationFlags, + dryRun: { + type: 'boolean', + default: false, + description: 'Only output what will be done without actually doing it' + }, + view: { + type: 'boolean', + shortFlag: 'v', + default: false, + description: 'Will wait for and return the created report' + } + }, + help: (parentName, config) => ` + Usage + $ ${parentName} ${config.commandName} + + 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 + $ ${parentName} ${config.commandName} . + $ ${parentName} ${config.commandName} '**/package.json' + $ ${parentName} ${config.commandName} /path/to/a/package.json /path/to/another/package.json + $ ${parentName} ${config.commandName} . --view --json + ` +} + +export const cmdReportCreate = { + 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, + allowUnknownFlags: false + }) + + // TODO: Allow setting a custom cwd and/or configFile path? + const cwd = process.cwd() + const absoluteConfigPath = path.join(cwd, 'socket.yml') + + const dryRun = Boolean(cli.flags['dryRun']) + const json = Boolean(cli.flags['json']) + const markdown = Boolean(cli.flags['markdown']) + const strict = Boolean(cli.flags['strict']) + const includeAllIssues = Boolean(cli.flags['all']) + const view = Boolean(cli.flags['view']) + + const socketConfig = await getSocketConfig(absoluteConfigPath) + + const result = await createReport(socketConfig, cli.input, { cwd, dryRun }) + + const commandName = `${parentName} ${config.commandName}` + + if (dryRun && view) { + console.log( + '[dryrun] Ignoring view flag since no report was actually generated' + ) + } + if (result?.success) { + if (view) { + const reportId = result.data.id + await viewReport(reportId, { + all: includeAllIssues, + commandName, + json, + markdown, + strict + }) + } else if (json) { + console.log(JSON.stringify(result.data, undefined, 2)) + return + } else { + const format = new ColorOrMarkdown(markdown) + console.log( + `New report: ${format.hyperlink(result.data.id, result.data.url, { fallbackToUrl: true })}` + ) + } + } +} diff --git a/src/commands/report/cmd-report-view.ts b/src/commands/report/cmd-report-view.ts new file mode 100644 index 000000000..a7e1040f9 --- /dev/null +++ b/src/commands/report/cmd-report-view.ts @@ -0,0 +1,69 @@ +import meowOrDie from 'meow' +import colors from 'yoctocolors-cjs' + +import { viewReport } from './view-report.ts' +import { commonFlags, outputFlags, validationFlags } from '../../flags' +import { getFlagListOutput } from '../../utils/output-formatting' + +import type { CliCommandConfig } from '../../utils/meow-with-subcommands' + +const config: CliCommandConfig = { + commandName: 'view', + description: 'View a project report', + hidden: false, + flags: { + ...commonFlags, + ...outputFlags, + ...validationFlags + }, + help: (parentName, config) => ` + Usage + $ ${parentName} ${config.commandName} + + Options + ${getFlagListOutput(config.flags, 6)} + + Examples + $ ${parentName} ${config.commandName} QXU8PmK7LfH608RAwfIKdbcHgwEd_ZeWJ9QEGv05FJUQ + ` +} + +export const cmdReportView = { + 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, + allowUnknownFlags: false + }) + + const [reportId, ...extraInput] = cli.input + + // Validate the input. + if (extraInput.length || !reportId) { + console.error(`${colors.bgRed(colors.white('Input error'))}: Please provide the required fields:\n + - Need at least one report ID ${!reportId ? colors.red('(missing!)') : colors.green('(ok)')}\n + - Can only handle a single report ID ${extraInput.length < 2 ? colors.red(`(received ${extraInput.length}!)`) : colors.green('(ok)')}\n + `) + cli.showHelp() + return + } + + await viewReport(reportId, { + all: Boolean(cli.flags['all']), + commandName: `${parentName} ${config.commandName}`, + json: Boolean(cli.flags['json']), + markdown: Boolean(cli.flags['markdown']), + strict: Boolean(cli.flags['strict']) + }) +} diff --git a/src/commands/report/index.ts b/src/commands/report/cmd-report.ts similarity index 67% rename from src/commands/report/index.ts rename to src/commands/report/cmd-report.ts index 8274a9635..41df4d857 100644 --- a/src/commands/report/index.ts +++ b/src/commands/report/cmd-report.ts @@ -1,18 +1,18 @@ -import { create } from './create' -import { view } from './view' +import { cmdReportCreate } from './cmd-report-create.ts' +import { cmdReportView } from './cmd-report-view.ts' import { meowWithSubcommands } from '../../utils/meow-with-subcommands' import type { CliSubcommand } from '../../utils/meow-with-subcommands' const description = '[Deprecated] Project report related commands' -export const reportCommand: CliSubcommand = { +export const cmdReport: CliSubcommand = { description, async run(argv, importMeta, { parentName }) { await meowWithSubcommands( { - create, - view + create: cmdReportCreate, + view: cmdReportView }, { argv, diff --git a/src/commands/report/create-report.ts b/src/commands/report/create-report.ts new file mode 100644 index 000000000..4246a8f53 --- /dev/null +++ b/src/commands/report/create-report.ts @@ -0,0 +1,79 @@ +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import { + handleApiCall, + handleUnsuccessfulApiResponse +} from '../../utils/api.ts' +import { debugLog } from '../../utils/debug.ts' +import { logSymbols } from '../../utils/logging.ts' +import { getPackageFiles } from '../../utils/path-resolve.ts' +import { setupSdk } from '../../utils/sdk.ts' + +import type { SocketYml } from '@socketsecurity/config' +import type { + SocketSdkResultType, + SocketSdkReturnType +} from '@socketsecurity/sdk' + +export async function createReport( + socketConfig: SocketYml | undefined, + inputPaths: Array, + { + cwd, + dryRun + }: { + cwd: string + dryRun: boolean + } +): Promise> { + const socketSdk = await setupSdk() + const supportedFiles = await socketSdk + .getReportSupportedFiles() + .then(res => { + if (!res.success) + handleUnsuccessfulApiResponse( + 'getReportSupportedFiles', + res, + new Spinner() + ) + return (res as SocketSdkReturnType<'getReportSupportedFiles'>).data + }) + .catch((cause: Error) => { + throw new Error('Failed getting supported files for report', { + cause + }) + }) + + const packagePaths = await getPackageFiles( + cwd, + inputPaths, + socketConfig, + supportedFiles + ) + + debugLog('Uploading:', packagePaths.join(`\n${logSymbols.info} Uploading: `)) + + if (dryRun) { + debugLog('[dryRun] Skipped actual upload') + return undefined + } else { + const socketSdk = await setupSdk() + const spinner = new Spinner({ + text: `Creating report with ${packagePaths.length} package files` + }).start() + + const apiCall = socketSdk.createReportFromFilePaths( + packagePaths, + cwd, + socketConfig?.issueRules + ) + const result = await handleApiCall(apiCall, 'creating report') + + if (!result.success) { + handleUnsuccessfulApiResponse('createReport', result, spinner) + return undefined + } + spinner.success() + return result + } +} diff --git a/src/commands/report/create.ts b/src/commands/report/create.ts deleted file mode 100644 index 98b4d650b..000000000 --- a/src/commands/report/create.ts +++ /dev/null @@ -1,277 +0,0 @@ -import path from 'node:path' -import process from 'node:process' - -import { betterAjvErrors } from '@apideck/better-ajv-errors' -import meow from 'meow' - -import { SocketValidationError, readSocketConfig } from '@socketsecurity/config' -import { Spinner } from '@socketsecurity/registry/lib/spinner' - -import { fetchReportData, formatReportDataOutput } from './view' -import { commonFlags, outputFlags, validationFlags } from '../../flags' -import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' -import { ColorOrMarkdown } from '../../utils/color-or-markdown' -import { debugLog } from '../../utils/debug' -import { InputError } from '../../utils/errors' -import { logSymbols } from '../../utils/logging' -import { getFlagListOutput } from '../../utils/output-formatting' -import { getPackageFiles } from '../../utils/path-resolve' -import { setupSdk } from '../../utils/sdk' - -import type { CliSubcommand } from '../../utils/meow-with-subcommands' -import type { SocketYml } from '@socketsecurity/config' -import type { SocketSdkReturnType } from '@socketsecurity/sdk' - -export const create: CliSubcommand = { - description: 'Create a project report', - async run( - argv: readonly string[], - importMeta: ImportMeta, - { parentName }: { parentName: string } - ) { - const name = `${parentName} create` - const input = await setupCommand(name, create.description, argv, importMeta) - if (input) { - const { - config, - cwd, - dryRun, - includeAllIssues, - outputJson, - outputMarkdown, - packagePaths, - strict, - view - } = input - - const result = - input && (await createReport(packagePaths, { config, cwd, dryRun })) - - if (result && view) { - const reportId = result.data.id - const reportData = - input && - (await fetchReportData(reportId, { includeAllIssues, strict })) - - if (reportData) { - formatReportDataOutput(reportData, { - includeAllIssues, - name, - outputJson, - outputMarkdown, - reportId, - strict - }) - } - } else if (result) { - formatReportCreationOutput(result.data, { outputJson, outputMarkdown }) - } - } - } -} - -// Internal functions - -type CommandContext = { - config: SocketYml | undefined - cwd: string - dryRun: boolean - includeAllIssues: boolean - outputJson: boolean - outputMarkdown: boolean - packagePaths: string[] - strict: boolean - view: boolean -} - -async function setupCommand( - name: string, - description: string, - argv: readonly string[], - importMeta: ImportMeta -): Promise { - const flags: { [key: string]: any } = { - ...commonFlags, - ...outputFlags, - ...validationFlags, - dryRun: { - type: 'boolean', - default: false, - description: 'Only output what will be done without actually doing it' - }, - view: { - type: 'boolean', - shortFlag: 'v', - default: false, - description: 'Will wait for and return the created report' - } - } - const cli = meow( - ` - Usage - $ ${name} - - 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( - { - all: 'Include all issues', - 'dry-run': 'Only output what will be done without actually doing it', - json: 'Output result as json', - markdown: 'Output result as markdown', - strict: 'Exits with an error code if any matching issues are found', - view: 'Will wait for and return the created report' - }, - 6 - )} - - Examples - $ ${name} . - $ ${name} '**/package.json' - $ ${name} /path/to/a/package.json /path/to/another/package.json - $ ${name} . --view --json - `, - { - argv, - description, - importMeta, - flags - } - ) - let showHelp = cli.flags['help'] - if (!cli.input[0]) { - showHelp = true - } - if (showHelp) { - cli.showHelp() - return - } - const { dryRun } = cli.flags - - // TODO: Allow setting a custom cwd and/or configFile path? - const cwd = process.cwd() - const absoluteConfigPath = path.join(cwd, 'socket.yml') - - const config = await readSocketConfig(absoluteConfigPath).catch( - (cause: unknown) => { - if ( - cause && - typeof cause === 'object' && - cause instanceof SocketValidationError - ) { - // Inspired by workbox-build: - // https://github.com/GoogleChrome/workbox/blob/95f97a207fd51efb3f8a653f6e3e58224183a778/packages/workbox-build/src/lib/validate-options.ts#L68-L71 - const betterErrors = betterAjvErrors({ - basePath: 'config', - data: cause.data, - errors: cause.validationErrors, - schema: cause.schema as Parameters< - typeof betterAjvErrors - >[0]['schema'] - }) - throw new InputError( - 'The socket.yml config is not valid', - betterErrors - .map( - err => - `[${err.path}] ${err.message}.${err.suggestion ? err.suggestion : ''}` - ) - .join('\n') - ) - } else { - throw new Error('Failed to read socket.yml config', { cause }) - } - } - ) - - const socketSdk = await setupSdk() - const supportedFiles = await socketSdk - .getReportSupportedFiles() - .then(res => { - if (!res.success) - handleUnsuccessfulApiResponse( - 'getReportSupportedFiles', - res, - new Spinner() - ) - return (res as SocketSdkReturnType<'getReportSupportedFiles'>).data - }) - .catch((cause: Error) => { - throw new Error('Failed getting supported files for report', { - cause - }) - }) - - const packagePaths = await getPackageFiles( - cwd, - cli.input, - config, - supportedFiles - ) - - return { - config, - cwd, - dryRun, - includeAllIssues: cli.flags['all'], - outputJson: cli.flags['json'], - outputMarkdown: cli.flags['markdown'], - packagePaths, - strict: cli.flags['strict'], - view: cli.flags['view'] - } as CommandContext -} - -async function createReport( - packagePaths: string[], - { config, cwd, dryRun }: Pick -): Promise> { - debugLog('Uploading:', packagePaths.join(`\n${logSymbols.info} Uploading: `)) - - if (dryRun) { - return - } - - const socketSdk = await setupSdk() - const spinner = new Spinner({ - text: `Creating report with ${packagePaths.length} package files` - }).start() - const apiCall = socketSdk.createReportFromFilePaths( - packagePaths, - cwd, - config?.issueRules - ) - const result = await handleApiCall(apiCall, 'creating report') - - if (result.success) { - spinner.success() - return result - } - handleUnsuccessfulApiResponse('createReport', result, spinner) - return undefined -} - -function formatReportCreationOutput( - data: SocketSdkReturnType<'createReport'>['data'], - { - outputJson, - outputMarkdown - }: Pick -): void { - if (outputJson) { - console.log(JSON.stringify(data, undefined, 2)) - return - } - const format = new ColorOrMarkdown(!!outputMarkdown) - console.log( - `New report: ${format.hyperlink(data.id, data.url, { fallbackToUrl: true })}` - ) -} diff --git a/src/commands/report/fetch-report-data.ts b/src/commands/report/fetch-report-data.ts new file mode 100644 index 000000000..cb6025a82 --- /dev/null +++ b/src/commands/report/fetch-report-data.ts @@ -0,0 +1,76 @@ +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import { + formatSeverityCount, + getSeverityCount +} from '../../utils/alert/severity.ts' +import { + handleApiCall, + handleUnsuccessfulApiResponse +} from '../../utils/api.ts' +import { setupSdk } from '../../utils/sdk.ts' + +import type { + SocketSdkResultType, + SocketSdkReturnType +} from '@socketsecurity/sdk' + +export type ReportData = SocketSdkReturnType<'getReport'>['data'] + +const MAX_TIMEOUT_RETRY = 5 +const HTTP_CODE_TIMEOUT = 524 + +export async function fetchReportData( + reportId: string, + includeAllIssues: boolean, + strict: boolean +): Promise { + const socketSdk = await setupSdk() + const spinner = new Spinner({ + text: `Fetching report with ID ${reportId} (this could take a while)` + }).start() + + let result: SocketSdkResultType<'getReport'> | undefined + for (let retry = 1; !result; ++retry) { + try { + // eslint-disable-next-line no-await-in-loop + result = await handleApiCall( + socketSdk.getReport(reportId), + 'fetching report' + ) + } catch (err) { + if ( + retry >= MAX_TIMEOUT_RETRY || + !(err instanceof Error) || + (err.cause as any)?.cause?.response?.statusCode !== HTTP_CODE_TIMEOUT + ) { + throw err + } + } + } + + if (!result.success) { + return handleUnsuccessfulApiResponse('getReport', result, spinner) + } + + // Conclude the status of the API call + + if (strict) { + if (result.data.healthy) { + spinner.success('Report result is healthy and great!') + } else { + spinner.error('Report result deemed unhealthy for project') + } + } else if (!result.data.healthy) { + const severityCount = getSeverityCount( + result.data.issues, + includeAllIssues ? undefined : 'high' + ) + const issueSummary = formatSeverityCount(severityCount) + spinner.success(`Report has these issues: ${issueSummary}`) + } else { + spinner.success('Report has no issues') + } + + return result.data +} diff --git a/src/commands/report/format-report-data.ts b/src/commands/report/format-report-data.ts new file mode 100644 index 000000000..6565b3413 --- /dev/null +++ b/src/commands/report/format-report-data.ts @@ -0,0 +1,37 @@ +import process from 'node:process' + +import colors from 'yoctocolors-cjs' + +import { ColorOrMarkdown } from '../../utils/color-or-markdown.ts' + +import type { ReportData } from './fetch-report-data.ts' + +export function formatReportDataOutput( + reportId: string, + data: ReportData, + commandName: string, + outputJson: boolean, + outputMarkdown: boolean, + strict: boolean +): void { + if (outputJson) { + console.log(JSON.stringify(data, undefined, 2)) + } else { + const format = new ColorOrMarkdown(outputMarkdown) + console.log( + '\nDetailed info on socket.dev: ' + + format.hyperlink(reportId, data.url, { fallbackToUrl: true }) + ) + if (!outputMarkdown) { + console.log( + colors.dim( + `\nOr rerun ${colors.italic(commandName)} using the ${colors.italic('--json')} flag to get full JSON output` + ) + ) + } + } + + if (strict && !data.healthy) { + process.exit(1) + } +} diff --git a/src/commands/report/get-socket-config.ts b/src/commands/report/get-socket-config.ts new file mode 100644 index 000000000..db95c0f02 --- /dev/null +++ b/src/commands/report/get-socket-config.ts @@ -0,0 +1,41 @@ +import { betterAjvErrors } from '@apideck/better-ajv-errors' + +import { SocketValidationError, readSocketConfig } from '@socketsecurity/config' + +import { InputError } from '../../utils/errors.ts' + +export async function getSocketConfig(absoluteConfigPath: string) { + const socketConfig = await readSocketConfig(absoluteConfigPath).catch( + (cause: unknown) => { + if ( + cause && + typeof cause === 'object' && + cause instanceof SocketValidationError + ) { + // Inspired by workbox-build: + // https://github.com/GoogleChrome/workbox/blob/95f97a207fd51efb3f8a653f6e3e58224183a778/packages/workbox-build/src/lib/validate-options.ts#L68-L71 + const betterErrors = betterAjvErrors({ + basePath: 'config', + data: cause.data, + errors: cause.validationErrors, + schema: cause.schema as Parameters< + typeof betterAjvErrors + >[0]['schema'] + }) + throw new InputError( + 'The socket.yml config is not valid', + betterErrors + .map( + err => + `[${err.path}] ${err.message}.${err.suggestion ? err.suggestion : ''}` + ) + .join('\n') + ) + } else { + throw new Error('Failed to read socket.yml config', { cause }) + } + } + ) + + return socketConfig +} diff --git a/src/commands/report/view-report.ts b/src/commands/report/view-report.ts new file mode 100644 index 000000000..ac6c363db --- /dev/null +++ b/src/commands/report/view-report.ts @@ -0,0 +1,31 @@ +import { fetchReportData } from './fetch-report-data.ts' +import { formatReportDataOutput } from './format-report-data.ts' + +export async function viewReport( + reportId: string, + { + all, + commandName, + json, + markdown, + strict + }: { + commandName: string + all: boolean + json: boolean + markdown: boolean + strict: boolean + } +) { + const result = await fetchReportData(reportId, all, strict) + if (result) { + formatReportDataOutput( + reportId, + result, + commandName, + json, + markdown, + strict + ) + } +} diff --git a/src/commands/report/view.ts b/src/commands/report/view.ts deleted file mode 100644 index aaf850fa8..000000000 --- a/src/commands/report/view.ts +++ /dev/null @@ -1,207 +0,0 @@ -import process from 'node:process' - -import meow from 'meow' -import colors from 'yoctocolors-cjs' - -import { Spinner } from '@socketsecurity/registry/lib/spinner' - -import { commonFlags, outputFlags, validationFlags } from '../../flags' -import { - formatSeverityCount, - getSeverityCount -} from '../../utils/alert/severity' -import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' -import { ColorOrMarkdown } from '../../utils/color-or-markdown' -import { InputError } from '../../utils/errors' -import { getFlagListOutput } from '../../utils/output-formatting' -import { setupSdk } from '../../utils/sdk' - -import type { CliSubcommand } from '../../utils/meow-with-subcommands' -import type { - SocketSdkResultType, - SocketSdkReturnType -} from '@socketsecurity/sdk' - -export const view: CliSubcommand = { - description: 'View a project report', - async run( - argv: readonly string[], - importMeta: ImportMeta, - { parentName }: { parentName: string } - ) { - const name = `${parentName} view` - const commandContext = setupCommand( - name, - view.description, - argv, - importMeta - ) - const result = commandContext - ? await fetchReportData(commandContext.reportId, commandContext) - : undefined - if (result) { - formatReportDataOutput(result, { - name, - ...((commandContext ?? {})) - }) - } - } -} - -// Internal functions - -type CommandContext = { - includeAllIssues: boolean - outputJson: boolean - outputMarkdown: boolean - reportId: string - strict: boolean -} - -function setupCommand( - name: string, - description: string, - argv: readonly string[], - importMeta: ImportMeta -): CommandContext | undefined { - const flags: { [key: string]: any } = { - ...commonFlags, - ...outputFlags, - ...validationFlags - } - const cli = meow( - ` - Usage - $ ${name} - - Options - ${getFlagListOutput(flags, 6)} - - Examples - $ ${name} QXU8PmK7LfH608RAwfIKdbcHgwEd_ZeWJ9QEGv05FJUQ - `, - { - argv, - description, - importMeta, - flags - } - ) - // Extract the input. - const [reportId, ...extraInput] = cli.input - let showHelp = cli.flags['help'] - if (reportId) { - showHelp = true - } - if (showHelp) { - cli.showHelp() - return - } - // Validate the input. - if (extraInput.length) { - throw new InputError( - `Can only handle a single report ID at a time, but got ${cli.input.length} report ID:s: ${cli.input.join(', ')}` - ) - } - return { - includeAllIssues: cli.flags['all'], - outputJson: cli.flags['json'], - outputMarkdown: cli.flags['markdown'], - reportId, - strict: cli.flags['strict'] - } as CommandContext -} -type ReportData = SocketSdkReturnType<'getReport'>['data'] - -const MAX_TIMEOUT_RETRY = 5 - -export async function fetchReportData( - reportId: string, - { - includeAllIssues, - strict - }: Pick -): Promise { - // Do the API call - const socketSdk = await setupSdk() - const spinner = new Spinner({ - text: `Fetching report with ID ${reportId} (this could take a while)` - }).start() - - let result: SocketSdkResultType<'getReport'> | undefined - for (let retry = 1; !result; ++retry) { - try { - // eslint-disable-next-line no-await-in-loop - result = await handleApiCall( - socketSdk.getReport(reportId), - 'fetching report' - ) - } catch (err) { - if ( - retry >= MAX_TIMEOUT_RETRY || - !(err instanceof Error) || - // The 524 HTTP status code indicates a timeout. - (err.cause as any)?.cause?.response?.statusCode !== 524 - ) { - throw err - } - } - } - - if (result.success === false) { - return handleUnsuccessfulApiResponse('getReport', result, spinner) - } - - // Conclude the status of the API call - - if (strict) { - if (result.data.healthy) { - spinner.success('Report result is healthy and great!') - } else { - spinner.error('Report result deemed unhealthy for project') - } - } else if (result.data.healthy === false) { - const severityCount = getSeverityCount( - result.data.issues, - includeAllIssues ? undefined : 'high' - ) - const issueSummary = formatSeverityCount(severityCount) - spinner.success(`Report has these issues: ${issueSummary}`) - } else { - spinner.success('Report has no issues') - } - - return result.data -} - -export function formatReportDataOutput( - data: ReportData, - { - name, - outputJson, - outputMarkdown, - reportId, - strict - }: { name: string } & CommandContext -): void { - if (outputJson) { - console.log(JSON.stringify(data, undefined, 2)) - } else { - const format = new ColorOrMarkdown(!!outputMarkdown) - console.log( - '\nDetailed info on socket.dev: ' + - format.hyperlink(reportId, data.url, { fallbackToUrl: true }) - ) - if (!outputMarkdown) { - console.log( - colors.dim( - `\nOr rerun ${colors.italic(name)} using the ${colors.italic('--json')} flag to get full JSON output` - ) - ) - } - } - - if (strict && data.healthy === false) { - process.exit(1) - } -} diff --git a/src/commands/repos/cmd-repos-create.ts b/src/commands/repos/cmd-repos-create.ts new file mode 100644 index 000000000..d337a193d --- /dev/null +++ b/src/commands/repos/cmd-repos-create.ts @@ -0,0 +1,111 @@ +import meowOrDie from 'meow' +import colors from 'yoctocolors-cjs' + +import { createRepo } from './create-repo.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: 'create', + description: 'Create a repository in an organization', + hidden: false, + flags: { + ...commonFlags, + ...outputFlags, + repoName: { + type: 'string', + shortFlag: 'n', + default: '', + description: 'Repository name' + }, + repoDescription: { + type: 'string', + shortFlag: 'd', + default: '', + description: 'Repository description' + }, + homepage: { + type: 'string', + shortFlag: 'h', + default: '', + description: 'Repository url' + }, + defaultBranch: { + type: 'string', + shortFlag: 'b', + default: 'main', + description: 'Repository default branch' + }, + visibility: { + type: 'string', + shortFlag: 'v', + default: 'private', + description: 'Repository visibility (Default Private)' + } + }, + help: (parentName, config) => ` + Usage + $ ${parentName} ${config.commandName} + + Options + ${getFlagListOutput(config.flags, 6)} + + Examples + $ ${parentName} ${config.commandName} FakeOrg --repoName=test-repo + ` +} + +export const cmdReposCreate = { + 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, + allowUnknownFlags: false + }) + + const repoName = cli.flags['repoName'] + const [orgSlug = ''] = cli.input + + if (!repoName || typeof repoName !== 'string' || !orgSlug) { + 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 + - Repository name using --repoName ${!repoName ? colors.red('(missing!)') : typeof repoName !== 'string' ? colors.red('(invalid!)') : colors.green('(ok)')}\n + `) + cli.showHelp() + 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 createRepo({ + outputJson: Boolean(cli.flags['json']), + outputMarkdown: Boolean(cli.flags['markdown']), + orgSlug, + repoName, + description: String(cli.flags['repoDescription'] || ''), + homepage: String(cli.flags['homepage'] || ''), + default_branch: String(cli.flags['defaultBranch'] || ''), + visibility: String(cli.flags['visibility'] || 'private'), + apiToken + }) +} diff --git a/src/commands/repos/cmd-repos-delete.ts b/src/commands/repos/cmd-repos-delete.ts new file mode 100644 index 000000000..5ec6d919e --- /dev/null +++ b/src/commands/repos/cmd-repos-delete.ts @@ -0,0 +1,63 @@ +import meowOrDie from 'meow' +import colors from 'yoctocolors-cjs' + +import { deleteRepo } from './delete-repo.ts' +import { AuthError } from '../../utils/errors' +import { getDefaultToken } from '../../utils/sdk' + +import type { CliCommandConfig } from '../../utils/meow-with-subcommands' + +const config: CliCommandConfig = { + commandName: 'delete', + description: 'Delete a repository in an organization', + hidden: false, + flags: {}, + help: (parentName, config) => ` + Usage + $ ${parentName} ${config.commandName} + + Examples + $ ${parentName} ${config.commandName} FakeOrg test-repo + ` +} + +export const cmdReposDelete = { + 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, + allowUnknownFlags: false + }) + + const [orgSlug = '', repoName = ''] = cli.input + + if (!orgSlug || !repoName) { + 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 + - Repository name as the second argument ${!repoName ? colors.red('(missing!)') : typeof repoName !== 'string' ? colors.red('(invalid!)') : colors.green('(ok)')}\n + - At least one TARGET (e.g. \`.\` or \`./package.json\` + `) + cli.showHelp() + 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 deleteRepo(orgSlug, repoName, apiToken) +} diff --git a/src/commands/repos/cmd-repos-list.ts b/src/commands/repos/cmd-repos-list.ts new file mode 100644 index 000000000..a6eb8c325 --- /dev/null +++ b/src/commands/repos/cmd-repos-list.ts @@ -0,0 +1,102 @@ +import meowOrDie from 'meow' +import colors from 'yoctocolors-cjs' + +import { listRepos } from './list-repos.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: 'list', + description: 'List repositories in an organization', + hidden: false, + flags: { + ...commonFlags, + sort: { + type: 'string', + shortFlag: 's', + default: 'created_at', + description: 'Sorting option' + }, + direction: { + type: 'string', + default: 'desc', + description: 'Direction option' + }, + perPage: { + type: 'number', + shortFlag: 'pp', + default: 30, + description: 'Number of results per page' + }, + page: { + type: 'number', + shortFlag: 'p', + default: 1, + description: 'Page number' + }, + ...outputFlags + }, + help: (parentName, config) => ` + Usage + $ ${parentName} ${config.commandName} + + Options + ${getFlagListOutput(config.flags, 6)} + + Examples + $ ${parentName} ${config.commandName} FakeOrg + ` +} + +export const cmdReposList = { + 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, + allowUnknownFlags: false + }) + + const [orgSlug = ''] = cli.input + + if (!orgSlug) { + 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 + - At least one TARGET (e.g. \`.\` or \`./package.json\` + `) + cli.showHelp() + 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 listRepos({ + apiToken, + outputJson: Boolean(cli.flags['json']), + outputMarkdown: Boolean(cli.flags['markdown']), + orgSlug, + sort: String(cli.flags['sort'] || 'created_at'), + direction: cli.flags['direction'] === 'asc' ? 'asc' : 'desc', + page: Number(cli.flags['page']) || 1, + per_page: Number(cli.flags['perPage']) || 30 + }) +} diff --git a/src/commands/repos/cmd-repos-update.ts b/src/commands/repos/cmd-repos-update.ts new file mode 100644 index 000000000..7f0bbcfb6 --- /dev/null +++ b/src/commands/repos/cmd-repos-update.ts @@ -0,0 +1,112 @@ +import meowOrDie from 'meow' +import colors from 'yoctocolors-cjs' + +import { updateRepo } from './update-repo.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: 'create', + description: 'Update a repository in an organization', + hidden: false, + flags: { + ...commonFlags, + ...outputFlags, + repoName: { + type: 'string', + shortFlag: 'n', + default: '', + description: 'Repository name' + }, + repoDescription: { + type: 'string', + shortFlag: 'd', + default: '', + description: 'Repository description' + }, + homepage: { + type: 'string', + shortFlag: 'h', + default: '', + description: 'Repository url' + }, + defaultBranch: { + type: 'string', + shortFlag: 'b', + default: 'main', + description: 'Repository default branch' + }, + visibility: { + type: 'string', + shortFlag: 'v', + default: 'private', + description: 'Repository visibility (Default Private)' + } + }, + help: (parentName, config) => ` + Usage + $ ${parentName} ${config.commandName} + + Options + ${getFlagListOutput(config.flags, 6)} + + Examples + $ ${parentName} ${config.commandName} FakeOrg + ` +} + +export const cmdReposUpdate = { + 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, + allowUnknownFlags: false + }) + + const repoName = cli.flags['repoName'] + const [orgSlug = ''] = cli.input + + if (!repoName || typeof repoName !== 'string' || !orgSlug) { + 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 + - Repository name using --repoName ${!repoName ? colors.red('(missing!)') : typeof repoName !== 'string' ? colors.red('(invalid!)') : colors.green('(ok)')}\n + - At least one TARGET (e.g. \`.\` or \`./package.json\` + `) + cli.showHelp() + 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 updateRepo({ + apiToken, + outputJson: Boolean(cli.flags['json']), + outputMarkdown: Boolean(cli.flags['markdown']), + orgSlug, + repoName, + description: String(cli.flags['repoDescription'] || ''), + homepage: String(cli.flags['homepage'] || ''), + default_branch: String(cli.flags['defaultBranch'] || ''), + visibility: String(cli.flags['visibility'] || 'private') + }) +} diff --git a/src/commands/repos/cmd-repos-view.ts b/src/commands/repos/cmd-repos-view.ts new file mode 100644 index 000000000..a96c90fab --- /dev/null +++ b/src/commands/repos/cmd-repos-view.ts @@ -0,0 +1,71 @@ +import meowOrDie from 'meow' +import colors from 'yoctocolors-cjs' + +import { viewRepo } from './view-repo.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: 'view', + description: 'View repositories in an organization', + hidden: false, + flags: { + ...commonFlags, + ...outputFlags + }, + help: (parentName, config) => ` + Usage + $ ${parentName} ${config.commandName} + + Options + ${getFlagListOutput(config.flags, 6)} + + Examples + $ ${parentName} ${config.commandName} FakeOrg + ` +} + +export const cmdReposView = { + 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, + allowUnknownFlags: false + }) + + const repoName = cli.flags['repoName'] + const [orgSlug = ''] = cli.input + + if (!repoName || typeof repoName !== 'string' || !orgSlug) { + 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 + - Repository name using --repoName ${!repoName ? colors.red('(missing!)') : typeof repoName !== 'string' ? colors.red('(invalid!)') : colors.green('(ok)')}\n + `) + cli.showHelp() + 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 viewRepo(orgSlug, repoName, apiToken) +} diff --git a/src/commands/repos/index.ts b/src/commands/repos/cmd-repos.ts similarity index 50% rename from src/commands/repos/index.ts rename to src/commands/repos/cmd-repos.ts index 54f1d965e..9dd890222 100644 --- a/src/commands/repos/index.ts +++ b/src/commands/repos/cmd-repos.ts @@ -1,24 +1,24 @@ -import { create } from './create' -import { del } from './delete' -import { list } from './list' -import { update } from './update' -import { view } from './view' +import { cmdReposCreate } from './cmd-repos-create.ts' +import { cmdReposDelete } from './cmd-repos-delete.ts' +import { cmdReposList } from './cmd-repos-list.ts' +import { cmdReposUpdate } from './cmd-repos-update.ts' +import { cmdReposView } from './cmd-repos-view.ts' import { meowWithSubcommands } from '../../utils/meow-with-subcommands' import type { CliSubcommand } from '../../utils/meow-with-subcommands' const description = 'Repositories related commands' -export const reposCommand: CliSubcommand = { +export const cmdRepos: CliSubcommand = { description, async run(argv, importMeta, { parentName }) { await meowWithSubcommands( { - create, - view, - list, - del, - update + cmdReposCreate, + cmdReposView, + cmdReposList, + cmdReposDelete, + cmdReposUpdate }, { argv, diff --git a/src/commands/repos/create-repo.ts b/src/commands/repos/create-repo.ts new file mode 100644 index 000000000..f24250987 --- /dev/null +++ b/src/commands/repos/create-repo.ts @@ -0,0 +1,53 @@ +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import { + handleApiCall, + handleUnsuccessfulApiResponse +} from '../../utils/api.ts' +import { setupSdk } from '../../utils/sdk.ts' + +export async function createRepo({ + apiToken, + default_branch, + description, + homepage, + orgSlug, + outputJson, + outputMarkdown, + repoName, + visibility +}: { + apiToken: string + outputJson: boolean + outputMarkdown: boolean + orgSlug: string + repoName: string + description: string + homepage: string + default_branch: string + visibility: string +}): Promise { + const spinnerText = 'Creating repository... \n' + const spinner = new Spinner({ text: spinnerText }).start() + + const socketSdk = await setupSdk(apiToken) + const result = await handleApiCall( + socketSdk.createOrgRepo(orgSlug, { + outputJson, + outputMarkdown, + orgSlug, + name: repoName, + description, + homepage, + default_branch, + visibility + }), + 'creating repository' + ) + + if (result.success) { + spinner.success('Repository created successfully') + } else { + handleUnsuccessfulApiResponse('createOrgRepo', result, spinner) + } +} diff --git a/src/commands/repos/create.ts b/src/commands/repos/create.ts deleted file mode 100644 index 5c63e4c18..000000000 --- a/src/commands/repos/create.ts +++ /dev/null @@ -1,155 +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 create: CliSubcommand = { - description: 'Create a repository in an organization', - async run(argv, importMeta, { parentName }) { - const name = `${parentName} create` - const input = 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 repository... \n' - const spinner = new Spinner({ text: spinnerText }).start() - await createRepo(input.orgSlug, input, spinner, apiToken) - } - } -} - -const repositoryCreationFlags: { [key: string]: any } = { - repoName: { - type: 'string', - shortFlag: 'n', - default: '', - description: 'Repository name' - }, - repoDescription: { - type: 'string', - shortFlag: 'd', - default: '', - description: 'Repository description' - }, - homepage: { - type: 'string', - shortFlag: 'h', - default: '', - description: 'Repository url' - }, - defaultBranch: { - type: 'string', - shortFlag: 'b', - default: 'main', - description: 'Repository default branch' - }, - visibility: { - type: 'string', - shortFlag: 'v', - default: 'private', - description: 'Repository visibility (Default Private)' - } -} - -// Internal functions - -type CommandContext = { - outputJson: boolean - outputMarkdown: boolean - orgSlug: string - name: string - description: string - homepage: string - default_branch: string - visibility: string -} - -function setupCommand( - name: string, - description: string, - argv: readonly string[], - importMeta: ImportMeta -): CommandContext | undefined { - const flags: { [key: string]: any } = { - ...commonFlags, - ...outputFlags, - ...repositoryCreationFlags - } - const cli = meow( - ` - Usage - $ ${name} - - Options - ${getFlagListOutput(flags, 6)} - - Examples - $ ${name} FakeOrg --repoName=test-repo - `, - { - argv, - description, - importMeta, - flags - } - ) - const { repoName } = cli.flags - const [orgSlug = ''] = cli.input - let showHelp = cli.flags['help'] - if (!orgSlug) { - showHelp = true - console.error( - `${colors.bgRed(colors.white('Input error'))}: Please provide an organization slug.` - ) - } else if (!repoName) { - showHelp = true - console.error( - `${colors.bgRed(colors.white('Input error'))}: Repository name is required.` - ) - } - if (showHelp) { - cli.showHelp() - return - } - return { - outputJson: cli.flags['json'], - outputMarkdown: cli.flags['markdown'], - orgSlug, - name: repoName, - description: cli.flags['repoDescription'], - homepage: cli.flags['homepage'], - default_branch: cli.flags['defaultBranch'], - visibility: cli.flags['visibility'] - } -} - -async function createRepo( - orgSlug: string, - input: CommandContext, - spinner: Spinner, - apiToken: string -): Promise { - const socketSdk = await setupSdk(apiToken) - const result = await handleApiCall( - socketSdk.createOrgRepo(orgSlug, input), - 'creating repository' - ) - - if (result.success) { - spinner.success('Repository created successfully') - } else { - handleUnsuccessfulApiResponse('createOrgRepo', result, spinner) - } -} diff --git a/src/commands/repos/delete-repo.ts b/src/commands/repos/delete-repo.ts new file mode 100644 index 000000000..8992e8e88 --- /dev/null +++ b/src/commands/repos/delete-repo.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 deleteRepo( + orgSlug: string, + repoName: string, + apiToken: string +): Promise { + const spinnerText = 'Deleting repository... \n' + const spinner = new Spinner({ text: spinnerText }).start() + + const socketSdk = await setupSdk(apiToken) + const result = await handleApiCall( + socketSdk.deleteOrgRepo(orgSlug, repoName), + 'deleting repository' + ) + + if (result.success) { + spinner.success('Repository deleted successfully') + } else { + handleUnsuccessfulApiResponse('deleteOrgRepo', result, spinner) + } +} diff --git a/src/commands/repos/delete.ts b/src/commands/repos/delete.ts deleted file mode 100644 index 0443638ac..000000000 --- a/src/commands/repos/delete.ts +++ /dev/null @@ -1,93 +0,0 @@ -import meow from 'meow' -import colors from 'yoctocolors-cjs' - -import { Spinner } from '@socketsecurity/registry/lib/spinner' - -import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' -import { AuthError } from '../../utils/errors' -import { getDefaultToken, setupSdk } from '../../utils/sdk' - -import type { CliSubcommand } from '../../utils/meow-with-subcommands' - -export const del: CliSubcommand = { - description: 'Delete a repository in an organization', - 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 repository... \n' - const spinner = new Spinner({ text: spinnerText }).start() - await deleteRepository(input.orgSlug, input.repoName, spinner, apiToken) - } - } -} - -// Internal functions - -type CommandContext = { - orgSlug: string - repoName: string -} - -function setupCommand( - name: string, - description: string, - argv: readonly string[], - importMeta: ImportMeta -): CommandContext | undefined { - const cli = meow( - ` - Usage - $ ${name} - - Examples - $ ${name} FakeOrg test-repo - `, - { - argv, - description, - importMeta - } - ) - const { 0: orgSlug = '', 1: repoName = '' } = cli.input - let showHelp = cli.flags['help'] - if (!orgSlug || !repoName) { - showHelp = true - console.error( - `${colors.bgRed(colors.white('Input error'))}: Please provide an organization slug and repository slug.` - ) - } - if (showHelp) { - cli.showHelp() - return - } - return { - orgSlug, - repoName - } -} - -async function deleteRepository( - orgSlug: string, - repoName: string, - spinner: Spinner, - apiToken: string -): Promise { - const socketSdk = await setupSdk(apiToken) - const result = await handleApiCall( - socketSdk.deleteOrgRepo(orgSlug, repoName), - 'deleting repository' - ) - - if (result.success) { - spinner.success('Repository deleted successfully') - } else { - handleUnsuccessfulApiResponse('deleteOrgRepo', result, spinner) - } -} diff --git a/src/commands/repos/list-repos.ts b/src/commands/repos/list-repos.ts new file mode 100644 index 000000000..fd9d90c7a --- /dev/null +++ b/src/commands/repos/list-repos.ts @@ -0,0 +1,65 @@ +// @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 listRepos({ + apiToken, + direction, + orgSlug, + outputJson, + outputMarkdown, + page, + per_page, + sort +}: { + outputJson: boolean + outputMarkdown: boolean + orgSlug: string + sort: string + direction: string + per_page: number + page: number + apiToken: string +}): Promise { + const spinnerText = 'Listing repositories... \n' + const spinner = new Spinner({ text: spinnerText }).start() + + const socketSdk = await setupSdk(apiToken) + const result = await handleApiCall( + socketSdk.getOrgRepoList(orgSlug, { + outputJson, + outputMarkdown, + orgSlug, + sort, + direction, + per_page, + page + }), + 'listing repositories' + ) + + if (!result.success) { + handleUnsuccessfulApiResponse('getOrgRepoList', result, spinner) + return + } + + const options = { + columns: [ + { field: 'id', name: colors.magenta('ID') }, + { field: 'name', name: colors.magenta('Name') }, + { field: 'visibility', name: colors.magenta('Visibility') }, + { field: 'default_branch', name: colors.magenta('Default branch') }, + { field: 'archived', name: colors.magenta('Archived') } + ] + } + + spinner.stop(chalkTable(options, result.data.results)) +} diff --git a/src/commands/repos/list.ts b/src/commands/repos/list.ts deleted file mode 100644 index 1c336124d..000000000 --- a/src/commands/repos/list.ts +++ /dev/null @@ -1,153 +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 repositories in 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 repositories... \n' - const spinner = new Spinner({ text: spinnerText }).start() - await listOrgRepos(input.orgSlug, input, spinner, apiToken) - } - } -} - -const listRepoFlags: { [key: string]: any } = { - sort: { - type: 'string', - shortFlag: 's', - default: 'created_at', - description: 'Sorting option' - }, - direction: { - type: 'string', - default: 'desc', - description: 'Direction option' - }, - perPage: { - type: 'number', - shortFlag: 'pp', - default: 30, - description: 'Number of results per page' - }, - page: { - type: 'number', - shortFlag: 'p', - default: 1, - description: 'Page number' - } -} - -// Internal functions - -type CommandContext = { - outputJson: boolean - outputMarkdown: boolean - orgSlug: string - sort: string - direction: string - per_page: number - page: number -} - -function setupCommand( - name: string, - description: string, - argv: readonly string[], - importMeta: ImportMeta -): CommandContext | undefined { - const flags: { [key: string]: any } = { - ...commonFlags, - ...listRepoFlags, - ...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 provide 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'], - page: cli.flags['page'], - per_page: cli.flags['perPage'] - } -} - -async function listOrgRepos( - orgSlug: string, - input: CommandContext, - spinner: Spinner, - apiToken: string -): Promise { - const socketSdk = await setupSdk(apiToken) - const result = await handleApiCall( - socketSdk.getOrgRepoList(orgSlug, input), - 'listing repositories' - ) - - if (!result.success) { - handleUnsuccessfulApiResponse('getOrgRepoList', result, spinner) - return - } - - const options = { - columns: [ - { field: 'id', name: colors.magenta('ID') }, - { field: 'name', name: colors.magenta('Name') }, - { field: 'visibility', name: colors.magenta('Visibility') }, - { field: 'default_branch', name: colors.magenta('Default branch') }, - { field: 'archived', name: colors.magenta('Archived') } - ] - } - - spinner.stop(chalkTable(options, result.data.results)) -} diff --git a/src/commands/repos/update-repo.ts b/src/commands/repos/update-repo.ts new file mode 100644 index 000000000..a115dbd02 --- /dev/null +++ b/src/commands/repos/update-repo.ts @@ -0,0 +1,53 @@ +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import { + handleApiCall, + handleUnsuccessfulApiResponse +} from '../../utils/api.ts' +import { setupSdk } from '../../utils/sdk.ts' + +export async function updateRepo({ + apiToken, + default_branch, + description, + homepage, + orgSlug, + outputJson, + outputMarkdown, + repoName, + visibility +}: { + apiToken: string + outputJson: boolean + outputMarkdown: boolean + orgSlug: string + repoName: string + description: string + homepage: string + default_branch: string + visibility: string +}): Promise { + const spinnerText = 'Updating repository... \n' + const spinner = new Spinner({ text: spinnerText }).start() + + const socketSdk = await setupSdk(apiToken) + const result = await handleApiCall( + socketSdk.updateOrgRepo(orgSlug, repoName, { + outputJson, + outputMarkdown, + orgSlug, + name: repoName, + description, + homepage, + default_branch, + visibility + }), + 'updating repository' + ) + + if (result.success) { + spinner.success('Repository updated successfully') + } else { + handleUnsuccessfulApiResponse('updateOrgRepo', result, spinner) + } +} diff --git a/src/commands/repos/update.ts b/src/commands/repos/update.ts deleted file mode 100644 index c0c361c9a..000000000 --- a/src/commands/repos/update.ts +++ /dev/null @@ -1,156 +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 update: CliSubcommand = { - description: 'Update a repository in an organization', - async run(argv, importMeta, { parentName }) { - const name = `${parentName} update` - const input = setupCommand(name, update.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 = 'Updating repository... \n' - const spinner = new Spinner({ text: spinnerText }).start() - await updateRepository(input.orgSlug, input, spinner, apiToken) - } - } -} - -const repositoryUpdateFlags: { [key: string]: any } = { - repoName: { - type: 'string', - shortFlag: 'n', - default: '', - description: 'Repository name' - }, - repoDescription: { - type: 'string', - shortFlag: 'd', - default: '', - description: 'Repository description' - }, - homepage: { - type: 'string', - shortFlag: 'h', - default: '', - description: 'Repository url' - }, - defaultBranch: { - type: 'string', - shortFlag: 'b', - default: 'main', - description: 'Repository default branch' - }, - visibility: { - type: 'string', - shortFlag: 'v', - default: 'private', - description: 'Repository visibility (Default Private)' - } -} - -// Internal functions - -type CommandContext = { - outputJson: boolean - outputMarkdown: boolean - orgSlug: string - name: string - description: string - homepage: string - default_branch: string - visibility: string -} - -function setupCommand( - name: string, - description: string, - argv: readonly string[], - importMeta: ImportMeta -): CommandContext | undefined { - const flags: { [key: string]: any } = { - ...commonFlags, - ...outputFlags, - ...repositoryUpdateFlags - } - - const cli = meow( - ` - Usage - $ ${name} - - Options - ${getFlagListOutput(flags, 6)} - - Examples - $ ${name} FakeOrg - `, - { - argv, - description, - importMeta, - flags - } - ) - const { repoName } = cli.flags - const [orgSlug = ''] = cli.input - let showHelp = cli.flags['help'] - if (!orgSlug) { - showHelp = true - console.error( - `${colors.bgRed(colors.white('Input error'))}: Please provide an organization slug and repository name.` - ) - } else if (!repoName) { - showHelp = true - console.error( - `${colors.bgRed(colors.white('Input error'))}: Repository name is required.` - ) - } - if (showHelp) { - cli.showHelp() - return - } - return { - outputJson: cli.flags['json'], - outputMarkdown: cli.flags['markdown'], - orgSlug, - name: repoName, - description: cli.flags['repoDescription'], - homepage: cli.flags['homepage'], - default_branch: cli.flags['defaultBranch'], - visibility: cli.flags['visibility'] - } -} - -async function updateRepository( - orgSlug: string, - input: CommandContext, - spinner: Spinner, - apiToken: string -): Promise { - const socketSdk = await setupSdk(apiToken) - const result = await handleApiCall( - socketSdk.updateOrgRepo(orgSlug, input.name, input), - 'updating repository' - ) - - if (result.success) { - spinner.success('Repository updated successfully') - } else { - handleUnsuccessfulApiResponse('updateOrgRepo', result, spinner) - } -} diff --git a/src/commands/repos/view-repo.ts b/src/commands/repos/view-repo.ts new file mode 100644 index 000000000..62f9e95b9 --- /dev/null +++ b/src/commands/repos/view-repo.ts @@ -0,0 +1,45 @@ +// @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 viewRepo( + orgSlug: string, + repoName: string, + apiToken: string +): Promise { + const spinnerText = 'Fetching repository... \n' + const spinner = new Spinner({ text: spinnerText }).start() + + const socketSdk = await setupSdk(apiToken) + const result = await handleApiCall( + socketSdk.getOrgRepo(orgSlug, repoName), + 'fetching repository' + ) + + if (!result.success) { + handleUnsuccessfulApiResponse('getOrgRepo', result, spinner) + return + } + + const options = { + columns: [ + { field: 'id', name: colors.magenta('ID') }, + { field: 'name', name: colors.magenta('Name') }, + { field: 'visibility', name: colors.magenta('Visibility') }, + { field: 'default_branch', name: colors.magenta('Default branch') }, + { field: 'homepage', name: colors.magenta('Homepage') }, + { field: 'archived', name: colors.magenta('Archived') }, + { field: 'created_at', name: colors.magenta('Created at') } + ] + } + + spinner.stop(chalkTable(options, [result.data])) +} diff --git a/src/commands/repos/view.ts b/src/commands/repos/view.ts deleted file mode 100644 index 2afdbb09e..000000000 --- a/src/commands/repos/view.ts +++ /dev/null @@ -1,127 +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 view: CliSubcommand = { - description: 'View repositories in an organization', - async run(argv, importMeta, { parentName }) { - const name = `${parentName} view` - const input = setupCommand(name, view.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 = 'Fetching repository... \n' - const spinner = new Spinner({ text: spinnerText }).start() - await viewRepository( - input.orgSlug, - input.repositoryName, - spinner, - apiToken - ) - } - } -} - -// Internal functions - -type CommandContext = { - outputJson: boolean - outputMarkdown: boolean - orgSlug: string - repositoryName: 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 - `, - { - argv, - description, - importMeta, - flags - } - ) - let showHelp = cli.flags['help'] - if (!cli.input[0]) { - showHelp = true - console.error( - `${colors.bgRed(colors.white('Input error'))}: Please provide an organization slug and repository name.` - ) - } - if (showHelp) { - cli.showHelp() - return - } - const { 0: orgSlug = '', 1: repositoryName = '' } = cli.input - return { - outputJson: cli.flags['json'], - outputMarkdown: cli.flags['markdown'], - orgSlug, - repositoryName - } -} - -async function viewRepository( - orgSlug: string, - repoName: string, - spinner: Spinner, - apiToken: string -): Promise { - const socketSdk = await setupSdk(apiToken) - const result = await handleApiCall( - socketSdk.getOrgRepo(orgSlug, repoName), - 'fetching repository' - ) - - if (!result.success) { - handleUnsuccessfulApiResponse('getOrgRepo', result, spinner) - return - } - - const options = { - columns: [ - { field: 'id', name: colors.magenta('ID') }, - { field: 'name', name: colors.magenta('Name') }, - { field: 'visibility', name: colors.magenta('Visibility') }, - { field: 'default_branch', name: colors.magenta('Default branch') }, - { field: 'homepage', name: colors.magenta('Homepage') }, - { field: 'archived', name: colors.magenta('Archived') }, - { field: 'created_at', name: colors.magenta('Created at') } - ] - } - - spinner.stop(chalkTable(options, [result.data])) -} diff --git a/src/commands/scan/cmd-create.ts b/src/commands/scan/cmd-scan-create.ts similarity index 100% rename from src/commands/scan/cmd-create.ts rename to src/commands/scan/cmd-scan-create.ts diff --git a/src/commands/scan/cmd-delete.ts b/src/commands/scan/cmd-scan-delete.ts similarity index 100% rename from src/commands/scan/cmd-delete.ts rename to src/commands/scan/cmd-scan-delete.ts diff --git a/src/commands/scan/cmd-list.ts b/src/commands/scan/cmd-scan-list.ts similarity index 100% rename from src/commands/scan/cmd-list.ts rename to src/commands/scan/cmd-scan-list.ts diff --git a/src/commands/scan/cmd-metadata.ts b/src/commands/scan/cmd-scan-metadata.ts similarity index 100% rename from src/commands/scan/cmd-metadata.ts rename to src/commands/scan/cmd-scan-metadata.ts diff --git a/src/commands/scan/cmd-stream.ts b/src/commands/scan/cmd-scan-stream.ts similarity index 100% rename from src/commands/scan/cmd-stream.ts rename to src/commands/scan/cmd-scan-stream.ts diff --git a/src/commands/scan/cmd-scan.ts b/src/commands/scan/cmd-scan.ts index b33210a5a..5262f510b 100644 --- a/src/commands/scan/cmd-scan.ts +++ b/src/commands/scan/cmd-scan.ts @@ -1,8 +1,8 @@ -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 { cmdScanCreate } from './cmd-scan-create.ts' +import { cmdScanDelete } from './cmd-scan-delete.ts' +import { cmdScanList } from './cmd-scan-list.ts' +import { cmdScanMetadata } from './cmd-scan-metadata.ts' +import { cmdScanStream } from './cmd-scan-stream.ts' import { meowWithSubcommands } from '../../utils/meow-with-subcommands' import type { CliSubcommand } from '../../utils/meow-with-subcommands' diff --git a/src/commands/types.ts b/src/commands/types.ts deleted file mode 100644 index d3e4e28fa..000000000 --- a/src/commands/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface CommandContext { - scope: string - time: number - repo: string - outputJson: boolean - file: string -} From 323305d75f399895107a232ba9bd8cfc8aabcf91 Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Tue, 18 Feb 2025 21:42:23 +0100 Subject: [PATCH 2/2] Icanhazcatlove --- src/commands/action/cmd-action.ts | 4 ++-- src/commands/analytics/cmd-analytics.ts | 4 ++-- src/commands/audit-log/cmd-audit-log.ts | 4 ++-- src/commands/cdxgen/cmd-cdxgen.ts | 4 ++-- src/commands/dependencies/cmd-dependencies.ts | 4 ++-- src/commands/diff-scan/cmd-diff-scan-get.ts | 4 ++-- src/commands/fix/cmd-fix.ts | 4 ++-- src/commands/info/cmd-info.ts | 4 ++-- src/commands/report/cmd-report-create.ts | 4 ++-- src/commands/report/cmd-report-view.ts | 4 ++-- src/commands/repos/cmd-repos-create.ts | 4 ++-- src/commands/repos/cmd-repos-delete.ts | 4 ++-- src/commands/repos/cmd-repos-list.ts | 4 ++-- src/commands/repos/cmd-repos-update.ts | 4 ++-- src/commands/repos/cmd-repos-view.ts | 4 ++-- src/commands/scan/cmd-scan-create.ts | 4 ++-- 16 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/commands/action/cmd-action.ts b/src/commands/action/cmd-action.ts index df0f85efc..a8d2d0e40 100644 --- a/src/commands/action/cmd-action.ts +++ b/src/commands/action/cmd-action.ts @@ -1,5 +1,5 @@ // https://github.com/SocketDev/socket-python-cli/blob/6d4fc56faee68d3a4764f1f80f84710635bdaf05/socketsecurity/socketcli.py -import meowOrDie from 'meow' +import meowOrExit from 'meow' import { runAction } from './run-action.ts' import { type CliCommandConfig } from '../../utils/meow-with-subcommands' @@ -47,7 +47,7 @@ async function run( importMeta: ImportMeta, { parentName }: { parentName: string } ): Promise { - const cli = meowOrDie(config.help(parentName, config), { + const cli = meowOrExit(config.help(parentName, config), { argv, description: config.description, importMeta, diff --git a/src/commands/analytics/cmd-analytics.ts b/src/commands/analytics/cmd-analytics.ts index 66d2321a5..d18488f4e 100644 --- a/src/commands/analytics/cmd-analytics.ts +++ b/src/commands/analytics/cmd-analytics.ts @@ -1,4 +1,4 @@ -import meowOrDie from 'meow' +import meowOrExit from 'meow' import colors from 'yoctocolors-cjs' import { displayAnalytics } from './display-analytics.ts' @@ -70,7 +70,7 @@ async function run( importMeta: ImportMeta, { parentName }: { parentName: string } ): Promise { - const cli = meowOrDie(config.help(parentName, config), { + const cli = meowOrExit(config.help(parentName, config), { argv, description: config.description, importMeta, diff --git a/src/commands/audit-log/cmd-audit-log.ts b/src/commands/audit-log/cmd-audit-log.ts index 63c259899..64e8b9524 100644 --- a/src/commands/audit-log/cmd-audit-log.ts +++ b/src/commands/audit-log/cmd-audit-log.ts @@ -1,4 +1,4 @@ -import meowOrDie from 'meow' +import meowOrExit from 'meow' import colors from 'yoctocolors-cjs' import { getAuditLog } from './get-audit-log.ts' @@ -58,7 +58,7 @@ async function run( importMeta: ImportMeta, { parentName }: { parentName: string } ): Promise { - const cli = meowOrDie(config.help(parentName, config), { + const cli = meowOrExit(config.help(parentName, config), { argv, description: config.description, importMeta, diff --git a/src/commands/cdxgen/cmd-cdxgen.ts b/src/commands/cdxgen/cmd-cdxgen.ts index 9f23c575c..87b2e09fa 100644 --- a/src/commands/cdxgen/cmd-cdxgen.ts +++ b/src/commands/cdxgen/cmd-cdxgen.ts @@ -1,4 +1,4 @@ -// import meowOrDie from 'meow' +// import meowOrExit from 'meow' import process from 'node:process' import yargsParse from 'yargs-parser' @@ -130,7 +130,7 @@ async function run( _importMeta: ImportMeta, { parentName: _parentName }: { parentName: string } ): Promise { - // const cli = meowOrDie(config.help(parentName, config), { + // const cli = meowOrExit(config.help(parentName, config), { // argv, // description: config.description, // importMeta, diff --git a/src/commands/dependencies/cmd-dependencies.ts b/src/commands/dependencies/cmd-dependencies.ts index 26210a5d3..e070de5b3 100644 --- a/src/commands/dependencies/cmd-dependencies.ts +++ b/src/commands/dependencies/cmd-dependencies.ts @@ -1,4 +1,4 @@ -import meowOrDie from 'meow' +import meowOrExit from 'meow' import { findDependencies } from './find-dependencies.ts' import { commonFlags, outputFlags } from '../../flags' @@ -50,7 +50,7 @@ async function run( importMeta: ImportMeta, { parentName }: { parentName: string } ): Promise { - const cli = meowOrDie(config.help(parentName, config), { + const cli = meowOrExit(config.help(parentName, config), { argv, description: config.description, importMeta, diff --git a/src/commands/diff-scan/cmd-diff-scan-get.ts b/src/commands/diff-scan/cmd-diff-scan-get.ts index db4c5754f..aac7e0bf7 100644 --- a/src/commands/diff-scan/cmd-diff-scan-get.ts +++ b/src/commands/diff-scan/cmd-diff-scan-get.ts @@ -1,4 +1,4 @@ -import meowOrDie from 'meow' +import meowOrExit from 'meow' import colors from 'yoctocolors-cjs' import { getDiffScan } from './get-diff-scan.ts' @@ -64,7 +64,7 @@ async function run( importMeta: ImportMeta, { parentName }: { parentName: string } ): Promise { - const cli = meowOrDie(config.help(parentName, config), { + const cli = meowOrExit(config.help(parentName, config), { argv, description: config.description, importMeta, diff --git a/src/commands/fix/cmd-fix.ts b/src/commands/fix/cmd-fix.ts index 86a077efd..ee70cb111 100644 --- a/src/commands/fix/cmd-fix.ts +++ b/src/commands/fix/cmd-fix.ts @@ -1,4 +1,4 @@ -import meowOrDie from 'meow' +import meowOrExit from 'meow' import { Spinner } from '@socketsecurity/registry/lib/spinner' @@ -35,7 +35,7 @@ async function run( importMeta: ImportMeta, { parentName }: { parentName: string } ): Promise { - meowOrDie(config.help(parentName, config), { + meowOrExit(config.help(parentName, config), { argv, description: config.description, importMeta, diff --git a/src/commands/info/cmd-info.ts b/src/commands/info/cmd-info.ts index f7c865b3a..da4f75660 100644 --- a/src/commands/info/cmd-info.ts +++ b/src/commands/info/cmd-info.ts @@ -1,4 +1,4 @@ -import meowOrDie from 'meow' +import meowOrExit from 'meow' import { getPackageInfo } from './get-package-info.ts' import { commonFlags, outputFlags, validationFlags } from '../../flags' @@ -40,7 +40,7 @@ async function run( importMeta: ImportMeta, { parentName }: { parentName: string } ): Promise { - const cli = meowOrDie(config.help(parentName, config), { + const cli = meowOrExit(config.help(parentName, config), { argv, description: config.description, importMeta, diff --git a/src/commands/report/cmd-report-create.ts b/src/commands/report/cmd-report-create.ts index a61440b0b..d370bfbda 100644 --- a/src/commands/report/cmd-report-create.ts +++ b/src/commands/report/cmd-report-create.ts @@ -1,7 +1,7 @@ import path from 'node:path' import process from 'node:process' -import meowOrDie from 'meow' +import meowOrExit from 'meow' import { createReport } from './create-report.ts' import { getSocketConfig } from './get-socket-config.ts' @@ -67,7 +67,7 @@ async function run( importMeta: ImportMeta, { parentName }: { parentName: string } ): Promise { - const cli = meowOrDie(config.help(parentName, config), { + const cli = meowOrExit(config.help(parentName, config), { argv, description: config.description, importMeta, diff --git a/src/commands/report/cmd-report-view.ts b/src/commands/report/cmd-report-view.ts index a7e1040f9..e4fb39ec2 100644 --- a/src/commands/report/cmd-report-view.ts +++ b/src/commands/report/cmd-report-view.ts @@ -1,4 +1,4 @@ -import meowOrDie from 'meow' +import meowOrExit from 'meow' import colors from 'yoctocolors-cjs' import { viewReport } from './view-report.ts' @@ -39,7 +39,7 @@ async function run( importMeta: ImportMeta, { parentName }: { parentName: string } ): Promise { - const cli = meowOrDie(config.help(parentName, config), { + const cli = meowOrExit(config.help(parentName, config), { argv, description: config.description, importMeta, diff --git a/src/commands/repos/cmd-repos-create.ts b/src/commands/repos/cmd-repos-create.ts index d337a193d..2859917ca 100644 --- a/src/commands/repos/cmd-repos-create.ts +++ b/src/commands/repos/cmd-repos-create.ts @@ -1,4 +1,4 @@ -import meowOrDie from 'meow' +import meowOrExit from 'meow' import colors from 'yoctocolors-cjs' import { createRepo } from './create-repo.ts' @@ -70,7 +70,7 @@ async function run( importMeta: ImportMeta, { parentName }: { parentName: string } ): Promise { - const cli = meowOrDie(config.help(parentName, config), { + const cli = meowOrExit(config.help(parentName, config), { argv, description: config.description, importMeta, diff --git a/src/commands/repos/cmd-repos-delete.ts b/src/commands/repos/cmd-repos-delete.ts index 5ec6d919e..9f4abdc57 100644 --- a/src/commands/repos/cmd-repos-delete.ts +++ b/src/commands/repos/cmd-repos-delete.ts @@ -1,4 +1,4 @@ -import meowOrDie from 'meow' +import meowOrExit from 'meow' import colors from 'yoctocolors-cjs' import { deleteRepo } from './delete-repo.ts' @@ -32,7 +32,7 @@ async function run( importMeta: ImportMeta, { parentName }: { parentName: string } ): Promise { - const cli = meowOrDie(config.help(parentName, config), { + const cli = meowOrExit(config.help(parentName, config), { argv, description: config.description, importMeta, diff --git a/src/commands/repos/cmd-repos-list.ts b/src/commands/repos/cmd-repos-list.ts index a6eb8c325..e4590a551 100644 --- a/src/commands/repos/cmd-repos-list.ts +++ b/src/commands/repos/cmd-repos-list.ts @@ -1,4 +1,4 @@ -import meowOrDie from 'meow' +import meowOrExit from 'meow' import colors from 'yoctocolors-cjs' import { listRepos } from './list-repos.ts' @@ -63,7 +63,7 @@ async function run( importMeta: ImportMeta, { parentName }: { parentName: string } ): Promise { - const cli = meowOrDie(config.help(parentName, config), { + const cli = meowOrExit(config.help(parentName, config), { argv, description: config.description, importMeta, diff --git a/src/commands/repos/cmd-repos-update.ts b/src/commands/repos/cmd-repos-update.ts index 7f0bbcfb6..aca4596d8 100644 --- a/src/commands/repos/cmd-repos-update.ts +++ b/src/commands/repos/cmd-repos-update.ts @@ -1,4 +1,4 @@ -import meowOrDie from 'meow' +import meowOrExit from 'meow' import colors from 'yoctocolors-cjs' import { updateRepo } from './update-repo.ts' @@ -70,7 +70,7 @@ async function run( importMeta: ImportMeta, { parentName }: { parentName: string } ): Promise { - const cli = meowOrDie(config.help(parentName, config), { + const cli = meowOrExit(config.help(parentName, config), { argv, description: config.description, importMeta, diff --git a/src/commands/repos/cmd-repos-view.ts b/src/commands/repos/cmd-repos-view.ts index a96c90fab..a4d8a26f7 100644 --- a/src/commands/repos/cmd-repos-view.ts +++ b/src/commands/repos/cmd-repos-view.ts @@ -1,4 +1,4 @@ -import meowOrDie from 'meow' +import meowOrExit from 'meow' import colors from 'yoctocolors-cjs' import { viewRepo } from './view-repo.ts' @@ -40,7 +40,7 @@ async function run( importMeta: ImportMeta, { parentName }: { parentName: string } ): Promise { - const cli = meowOrDie(config.help(parentName, config), { + const cli = meowOrExit(config.help(parentName, config), { argv, description: config.description, importMeta, diff --git a/src/commands/scan/cmd-scan-create.ts b/src/commands/scan/cmd-scan-create.ts index 6546d7618..606151a20 100644 --- a/src/commands/scan/cmd-scan-create.ts +++ b/src/commands/scan/cmd-scan-create.ts @@ -1,6 +1,6 @@ import process from 'node:process' -import meowOrDie from 'meow' +import meowOrExit from 'meow' import colors from 'yoctocolors-cjs' import { Spinner } from '@socketsecurity/registry/lib/spinner' @@ -106,7 +106,7 @@ async function run( importMeta: ImportMeta, { parentName }: { parentName: string } ): Promise { - const cli = meowOrDie(config.help(parentName, config), { + const cli = meowOrExit(config.help(parentName, config), { argv, description: config.description, importMeta,