Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 32 additions & 64 deletions src/commands/scan/cmd-scan-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,10 @@ import process from 'node:process'

import colors from 'yoctocolors-cjs'

import { Spinner } from '@socketsecurity/registry/lib/spinner'

import { createFullScan } from './create-full-scan'
import { handleUnsuccessfulApiResponse } from '../../utils/api'
import { AuthError } from '../../utils/errors'
import { meowOrExit } from '../../utils/meow-with-subcommands'
import { getFlagListOutput } from '../../utils/output-formatting'
import { getPackageFilesFullScans } from '../../utils/path-resolve'
import { getDefaultToken, setupSdk } from '../../utils/sdk'
import { getDefaultToken } from '../../utils/sdk'

import type { CliCommandConfig } from '../../utils/meow-with-subcommands'

Expand Down Expand Up @@ -75,6 +70,12 @@ const config: CliCommandConfig = {
default: false,
description: 'Set as pending head'
},
readOnly: {
type: 'boolean',
default: false,
description:
'Similar to --dry-run except it can read from remote, stops before it would create an actual report'
},
tmp: {
type: 'boolean',
shortFlag: 't',
Expand Down Expand Up @@ -125,79 +126,46 @@ async function run(
? String(cli.flags['cwd'])
: process.cwd()

// Note exiting earlier to skirt a hidden auth requirement
if (cli.flags['dryRun']) {
return console.log('[DryRun] Bailing now')
}

const socketSdk = await setupSdk()
const supportedFiles = await socketSdk
.getReportSupportedFiles()
.then(res => {
if (!res.success)
handleUnsuccessfulApiResponse(
'getReportSupportedFiles',
res,
new Spinner()
)
// TODO: verify type at runtime? Consider it trusted data and assume type?
return (res as any).data
})
.catch((cause: Error) => {
throw new Error('Failed getting supported files for report', { cause })
})

const packagePaths = await getPackageFilesFullScans(
cwd,
targets,
supportedFiles
)
let { branch: branchName, repo: repoName } = cli.flags

const { branch: branchName, repo: repoName } = cli.flags
const apiToken = getDefaultToken()

if (!orgSlug || !repoName || !branchName || !packagePaths.length) {
if (!apiToken && (!orgSlug || !repoName || !branchName || !targets.length)) {
// Without api token we cannot recover because we can't request more info
// from the server, to match and help with the current cwd/git status.
// Use exit status of 2 to indicate incorrect usage, generally invalid
// options or missing arguments.
// https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html
process.exitCode = 2
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 --repo ${!repoName ? colors.red('(missing!)') : colors.green('(ok)')}\n
- Branch name using --branch ${!branchName ? colors.red('(missing!)') : colors.green('(ok)')}\n
- At least one TARGET (e.g. \`.\` or \`./package.json\`) ${
!packagePaths.length
? colors.red(
targets.length > 0
? '(TARGET' +
(targets.length ? 's' : '') +
' contained no matching/supported files!)'
: '(missing)'
)
: colors.green('(ok)')
}\n`)
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 --repo ${!repoName ? colors.red('(missing!)') : colors.green('(ok)')}\n
- Branch name using --branch ${!branchName ? colors.red('(missing!)') : colors.green('(ok)')}\n
- At least one TARGET (e.g. \`.\` or \`./package.json\`) ${!targets.length ? '(missing)' : colors.green('(ok)')}\n
(Additionally, no API Token was set so we cannot auto-discover these details)\n
`)
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.'
)
// Note exiting earlier to skirt a hidden auth requirement
if (cli.flags['dryRun']) {
return console.log('[DryRun] Bailing now')
}

await createFullScan({
apiToken,
orgSlug,
repoName: repoName as string,
branchName: branchName as string,
commitHash: (cli.flags['commitHash'] as string) ?? '',
commitMessage: (cli.flags['commitMessage'] as string) ?? '',
committers: (cli.flags['committers'] as string) ?? '',
cwd,
defaultBranch: Boolean(cli.flags['defaultBranch']),
orgSlug,
pendingHead: Boolean(cli.flags['pendingHead']),
tmp: Boolean(cli.flags['tmp']),
packagePaths,
cwd,
commitHash: (cli.flags['commitHash'] as string) ?? '',
committers: (cli.flags['committers'] as string) ?? '',
pullRequest: (cli.flags['pullRequest'] as number) ?? undefined
pullRequest: (cli.flags['pullRequest'] as number) ?? undefined,
readOnly: Boolean(cli.flags['readOnly']),
repoName: repoName as string,
targets,
tmp: Boolean(cli.flags['tmp'])
})
}
149 changes: 136 additions & 13 deletions src/commands/scan/create-full-scan.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import assert from 'node:assert'
import process from 'node:process'
import readline from 'node:readline/promises'

Expand All @@ -6,42 +7,164 @@ import colors from 'yoctocolors-cjs'

import { Spinner } from '@socketsecurity/registry/lib/spinner'

import { suggestOrgSlug } from './suggest-org-slug.ts'
import { suggestRepoSlug } from './suggest-repo-slug.ts'
import { suggestBranchSlug } from './suggest_branch_slug.ts'
import { suggestTarget } from './suggest_target.ts'
import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api'
import { setupSdk } from '../../utils/sdk'
import { AuthError } from '../../utils/errors.ts'
import { getPackageFilesFullScans } from '../../utils/path-resolve.ts'
import { getDefaultToken, setupSdk } from '../../utils/sdk'

export async function createFullScan({
apiToken,
branchName,
commitHash: _commitHash,
commitMessage,
committers: _committers,
cwd,
defaultBranch,
orgSlug,
packagePaths,
pendingHead,
pullRequest: _pullRequest,
readOnly,
repoName,
targets,
tmp
}: {
apiToken: string
orgSlug: string
repoName: string
branchName: string
committers: string
commitMessage: string
commitHash: string
pullRequest: number | undefined
commitMessage: string
committers: string
cwd: string
defaultBranch: boolean
orgSlug: string
pendingHead: boolean
pullRequest: number | undefined
readOnly: boolean
repoName: string
targets: Array<string>
tmp: boolean
packagePaths: string[]
cwd: string
}): Promise<void> {
const socketSdk = await setupSdk()
const supportedFiles = await socketSdk
.getReportSupportedFiles()
.then(res => {
if (!res.success) {
handleUnsuccessfulApiResponse(
'getReportSupportedFiles',
res,
new Spinner()
)
assert(
false,
'handleUnsuccessfulApiResponse should unconditionally throw'
)
}

return res.data
})
.catch((cause: Error) => {
throw new Error('Failed getting supported files for report', { cause })
})

// If we updated any inputs then we should print the command line to repeat
// the command without requiring user input, as a suggestion.
let updatedInput = false

if (!targets.length) {
const received = await suggestTarget()
targets = received ?? []
updatedInput = true
}

const packagePaths = await getPackageFilesFullScans(
cwd,
targets,
supportedFiles
)

// We're going to need an api token to suggest data because those suggestions
// must come from data we already know. Don't error on missing api token yet.
// If the api-token is not set, ignore it for the sake of suggestions.
const apiToken = getDefaultToken()

if (apiToken && !orgSlug) {
const suggestion = await suggestOrgSlug(socketSdk)
if (suggestion) orgSlug = suggestion
updatedInput = true
}

// If the current cwd is unknown and is used as a repo slug anyways, we will
// first need to register the slug before we can use it.
let repoDefaultBranch = ''

// (Don't bother asking for the rest if we didn't get an org slug above)
if (apiToken && orgSlug && !repoName) {
const suggestion = await suggestRepoSlug(socketSdk, orgSlug)
if (suggestion) {
;({ defaultBranch: repoDefaultBranch, slug: repoName } = suggestion)
}
updatedInput = true
}

// (Don't bother asking for the rest if we didn't get an org/repo above)
if (apiToken && orgSlug && repoName && !branchName) {
const suggestion = await suggestBranchSlug(repoDefaultBranch)
if (suggestion) branchName = suggestion
updatedInput = true
}

if (!orgSlug || !repoName || !branchName || !packagePaths.length) {
// Use exit status of 2 to indicate incorrect usage, generally invalid
// options or missing arguments.
// https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html
process.exitCode = 2
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 --repo ${!repoName ? colors.red('(missing!)') : colors.green('(ok)')}\n
- Branch name using --branch ${!branchName ? colors.red('(missing!)') : colors.green('(ok)')}\n
- At least one TARGET (e.g. \`.\` or \`./package.json\`) ${
!packagePaths.length
? colors.red(
targets.length > 0
? '(TARGET' +
(targets.length ? 's' : '') +
' contained no matching/supported files!)'
: '(missing)'
)
: colors.green('(ok)')
}\n
${!apiToken ? 'Note: was unable to make suggestions because no API Token was found; this would make command fail regardless\n' : ''}
`)
return
}

if (updatedInput) {
console.log(
'Note: You can invoke this command next time to skip the interactive questions:'
)
console.log('```')
console.log(
` socket scan create [other flags...] --repo ${repoName} --branch ${branchName} ${orgSlug} ${targets.join(' ')}`
)
console.log('```')
}

if (!apiToken) {
throw new AuthError(
'User must be authenticated to run this command. To log in, run the command `socket login` and enter your API key.'
)
}

if (readOnly) {
console.log('[ReadOnly] Bailing now')
return
}

const spinnerText = 'Creating a scan... \n'
const spinner = new Spinner({ text: spinnerText }).start()

const socketSdk = await setupSdk(apiToken)
const result = await handleApiCall(
socketSdk.createOrgFullScan(
orgSlug,
Expand All @@ -64,7 +187,7 @@ export async function createFullScan({
return
}

spinner.success('Scan created successfully')
spinner.successAndStop('Scan created successfully')

const link = colors.underline(colors.cyan(`${result.data.html_report_url}`))
console.log(`Available at: ${link}`)
Expand Down
38 changes: 38 additions & 0 deletions src/commands/scan/suggest-org-slug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { select } from '@socketsecurity/registry/lib/prompts'
import { SocketSdk } from '@socketsecurity/sdk'

import { handleApiCall } from '../../utils/api.ts'

export async function suggestOrgSlug(
socketSdk: SocketSdk
): Promise<string | void> {
const result = await handleApiCall(
socketSdk.getOrganizations(),
'looking up organizations'
)
// Ignore a failed request here. It was not the primary goal of
// running this command and reporting it only leads to end-user confusion.
if (result.success) {
const proceed = await select<string>({
message:
'Missing org name; do you want to use any of these orgs for this scan?',
choices: Array.from(Object.values(result.data.organizations))
.map(({ name: slug }) => ({
name: 'Yes [' + slug + ']',
value: slug,
description: `Use "${slug}" as the organization`
}))
.concat({
name: 'No',
value: '',
description:
'Do not use any of these organizations (will end in a no-op)'
})
})
if (proceed) {
return proceed
}
} else {
// TODO: in verbose mode, report this error to stderr
}
}
Loading
Loading