Skip to content

Commit 606a73a

Browse files
pvdzjdalton
andauthored
Add suggestion flows to socket scan create (#345)
* Add suggestion flows to `socket scan create` * Update snapshot for scan create --------- Co-authored-by: John-David Dalton <jdalton@users.noreply.github.com>
1 parent 2db0750 commit 606a73a

File tree

7 files changed

+395
-81
lines changed

7 files changed

+395
-81
lines changed

src/commands/scan/cmd-scan-create.ts

Lines changed: 32 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,10 @@ import process from 'node:process'
22

33
import colors from 'yoctocolors-cjs'
44

5-
import { Spinner } from '@socketsecurity/registry/lib/spinner'
6-
75
import { createFullScan } from './create-full-scan'
8-
import { handleUnsuccessfulApiResponse } from '../../utils/api'
9-
import { AuthError } from '../../utils/errors'
106
import { meowOrExit } from '../../utils/meow-with-subcommands'
117
import { getFlagListOutput } from '../../utils/output-formatting'
12-
import { getPackageFilesFullScans } from '../../utils/path-resolve'
13-
import { getDefaultToken, setupSdk } from '../../utils/sdk'
8+
import { getDefaultToken } from '../../utils/sdk'
149

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

@@ -75,6 +70,12 @@ const config: CliCommandConfig = {
7570
default: false,
7671
description: 'Set as pending head'
7772
},
73+
readOnly: {
74+
type: 'boolean',
75+
default: false,
76+
description:
77+
'Similar to --dry-run except it can read from remote, stops before it would create an actual report'
78+
},
7879
tmp: {
7980
type: 'boolean',
8081
shortFlag: 't',
@@ -125,79 +126,46 @@ async function run(
125126
? String(cli.flags['cwd'])
126127
: process.cwd()
127128

128-
// Note exiting earlier to skirt a hidden auth requirement
129-
if (cli.flags['dryRun']) {
130-
return console.log('[DryRun] Bailing now')
131-
}
132-
133-
const socketSdk = await setupSdk()
134-
const supportedFiles = await socketSdk
135-
.getReportSupportedFiles()
136-
.then(res => {
137-
if (!res.success)
138-
handleUnsuccessfulApiResponse(
139-
'getReportSupportedFiles',
140-
res,
141-
new Spinner()
142-
)
143-
// TODO: verify type at runtime? Consider it trusted data and assume type?
144-
return (res as any).data
145-
})
146-
.catch((cause: Error) => {
147-
throw new Error('Failed getting supported files for report', { cause })
148-
})
149-
150-
const packagePaths = await getPackageFilesFullScans(
151-
cwd,
152-
targets,
153-
supportedFiles
154-
)
129+
let { branch: branchName, repo: repoName } = cli.flags
155130

156-
const { branch: branchName, repo: repoName } = cli.flags
131+
const apiToken = getDefaultToken()
157132

158-
if (!orgSlug || !repoName || !branchName || !packagePaths.length) {
133+
if (!apiToken && (!orgSlug || !repoName || !branchName || !targets.length)) {
134+
// Without api token we cannot recover because we can't request more info
135+
// from the server, to match and help with the current cwd/git status.
159136
// Use exit status of 2 to indicate incorrect usage, generally invalid
160137
// options or missing arguments.
161138
// https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html
162139
process.exitCode = 2
163-
console.error(`${colors.bgRed(colors.white('Input error'))}: Please provide the required fields:\n
164-
- Org name as the first argument ${!orgSlug ? colors.red('(missing!)') : colors.green('(ok)')}\n
165-
- Repository name using --repo ${!repoName ? colors.red('(missing!)') : colors.green('(ok)')}\n
166-
- Branch name using --branch ${!branchName ? colors.red('(missing!)') : colors.green('(ok)')}\n
167-
- At least one TARGET (e.g. \`.\` or \`./package.json\`) ${
168-
!packagePaths.length
169-
? colors.red(
170-
targets.length > 0
171-
? '(TARGET' +
172-
(targets.length ? 's' : '') +
173-
' contained no matching/supported files!)'
174-
: '(missing)'
175-
)
176-
: colors.green('(ok)')
177-
}\n`)
140+
console.error(`
141+
${colors.bgRed(colors.white('Input error'))}: Please provide the required fields:\n
142+
- Org name as the first argument ${!orgSlug ? colors.red('(missing!)') : colors.green('(ok)')}\n
143+
- Repository name using --repo ${!repoName ? colors.red('(missing!)') : colors.green('(ok)')}\n
144+
- Branch name using --branch ${!branchName ? colors.red('(missing!)') : colors.green('(ok)')}\n
145+
- At least one TARGET (e.g. \`.\` or \`./package.json\`) ${!targets.length ? '(missing)' : colors.green('(ok)')}\n
146+
(Additionally, no API Token was set so we cannot auto-discover these details)\n
147+
`)
178148
return
179149
}
180150

181-
const apiToken = getDefaultToken()
182-
if (!apiToken) {
183-
throw new AuthError(
184-
'User must be authenticated to run this command. To log in, run the command `socket login` and enter your API key.'
185-
)
151+
// Note exiting earlier to skirt a hidden auth requirement
152+
if (cli.flags['dryRun']) {
153+
return console.log('[DryRun] Bailing now')
186154
}
187155

188156
await createFullScan({
189-
apiToken,
190-
orgSlug,
191-
repoName: repoName as string,
192157
branchName: branchName as string,
158+
commitHash: (cli.flags['commitHash'] as string) ?? '',
193159
commitMessage: (cli.flags['commitMessage'] as string) ?? '',
160+
committers: (cli.flags['committers'] as string) ?? '',
161+
cwd,
194162
defaultBranch: Boolean(cli.flags['defaultBranch']),
163+
orgSlug,
195164
pendingHead: Boolean(cli.flags['pendingHead']),
196-
tmp: Boolean(cli.flags['tmp']),
197-
packagePaths,
198-
cwd,
199-
commitHash: (cli.flags['commitHash'] as string) ?? '',
200-
committers: (cli.flags['committers'] as string) ?? '',
201-
pullRequest: (cli.flags['pullRequest'] as number) ?? undefined
165+
pullRequest: (cli.flags['pullRequest'] as number) ?? undefined,
166+
readOnly: Boolean(cli.flags['readOnly']),
167+
repoName: repoName as string,
168+
targets,
169+
tmp: Boolean(cli.flags['tmp'])
202170
})
203171
}

src/commands/scan/create-full-scan.ts

Lines changed: 136 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import assert from 'node:assert'
12
import process from 'node:process'
23
import readline from 'node:readline/promises'
34

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

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

10+
import { suggestOrgSlug } from './suggest-org-slug.ts'
11+
import { suggestRepoSlug } from './suggest-repo-slug.ts'
12+
import { suggestBranchSlug } from './suggest_branch_slug.ts'
13+
import { suggestTarget } from './suggest_target.ts'
914
import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api'
10-
import { setupSdk } from '../../utils/sdk'
15+
import { AuthError } from '../../utils/errors.ts'
16+
import { getPackageFilesFullScans } from '../../utils/path-resolve.ts'
17+
import { getDefaultToken, setupSdk } from '../../utils/sdk'
1118

1219
export async function createFullScan({
13-
apiToken,
1420
branchName,
1521
commitHash: _commitHash,
1622
commitMessage,
1723
committers: _committers,
1824
cwd,
1925
defaultBranch,
2026
orgSlug,
21-
packagePaths,
2227
pendingHead,
2328
pullRequest: _pullRequest,
29+
readOnly,
2430
repoName,
31+
targets,
2532
tmp
2633
}: {
27-
apiToken: string
28-
orgSlug: string
29-
repoName: string
3034
branchName: string
31-
committers: string
32-
commitMessage: string
3335
commitHash: string
34-
pullRequest: number | undefined
36+
commitMessage: string
37+
committers: string
38+
cwd: string
3539
defaultBranch: boolean
40+
orgSlug: string
3641
pendingHead: boolean
42+
pullRequest: number | undefined
43+
readOnly: boolean
44+
repoName: string
45+
targets: Array<string>
3746
tmp: boolean
38-
packagePaths: string[]
39-
cwd: string
4047
}): Promise<void> {
48+
const socketSdk = await setupSdk()
49+
const supportedFiles = await socketSdk
50+
.getReportSupportedFiles()
51+
.then(res => {
52+
if (!res.success) {
53+
handleUnsuccessfulApiResponse(
54+
'getReportSupportedFiles',
55+
res,
56+
new Spinner()
57+
)
58+
assert(
59+
false,
60+
'handleUnsuccessfulApiResponse should unconditionally throw'
61+
)
62+
}
63+
64+
return res.data
65+
})
66+
.catch((cause: Error) => {
67+
throw new Error('Failed getting supported files for report', { cause })
68+
})
69+
70+
// If we updated any inputs then we should print the command line to repeat
71+
// the command without requiring user input, as a suggestion.
72+
let updatedInput = false
73+
74+
if (!targets.length) {
75+
const received = await suggestTarget()
76+
targets = received ?? []
77+
updatedInput = true
78+
}
79+
80+
const packagePaths = await getPackageFilesFullScans(
81+
cwd,
82+
targets,
83+
supportedFiles
84+
)
85+
86+
// We're going to need an api token to suggest data because those suggestions
87+
// must come from data we already know. Don't error on missing api token yet.
88+
// If the api-token is not set, ignore it for the sake of suggestions.
89+
const apiToken = getDefaultToken()
90+
91+
if (apiToken && !orgSlug) {
92+
const suggestion = await suggestOrgSlug(socketSdk)
93+
if (suggestion) orgSlug = suggestion
94+
updatedInput = true
95+
}
96+
97+
// If the current cwd is unknown and is used as a repo slug anyways, we will
98+
// first need to register the slug before we can use it.
99+
let repoDefaultBranch = ''
100+
101+
// (Don't bother asking for the rest if we didn't get an org slug above)
102+
if (apiToken && orgSlug && !repoName) {
103+
const suggestion = await suggestRepoSlug(socketSdk, orgSlug)
104+
if (suggestion) {
105+
;({ defaultBranch: repoDefaultBranch, slug: repoName } = suggestion)
106+
}
107+
updatedInput = true
108+
}
109+
110+
// (Don't bother asking for the rest if we didn't get an org/repo above)
111+
if (apiToken && orgSlug && repoName && !branchName) {
112+
const suggestion = await suggestBranchSlug(repoDefaultBranch)
113+
if (suggestion) branchName = suggestion
114+
updatedInput = true
115+
}
116+
117+
if (!orgSlug || !repoName || !branchName || !packagePaths.length) {
118+
// Use exit status of 2 to indicate incorrect usage, generally invalid
119+
// options or missing arguments.
120+
// https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html
121+
process.exitCode = 2
122+
console.error(`
123+
${colors.bgRed(colors.white('Input error'))}: Please provide the required fields:\n
124+
- Org name as the first argument ${!orgSlug ? colors.red('(missing!)') : colors.green('(ok)')}\n
125+
- Repository name using --repo ${!repoName ? colors.red('(missing!)') : colors.green('(ok)')}\n
126+
- Branch name using --branch ${!branchName ? colors.red('(missing!)') : colors.green('(ok)')}\n
127+
- At least one TARGET (e.g. \`.\` or \`./package.json\`) ${
128+
!packagePaths.length
129+
? colors.red(
130+
targets.length > 0
131+
? '(TARGET' +
132+
(targets.length ? 's' : '') +
133+
' contained no matching/supported files!)'
134+
: '(missing)'
135+
)
136+
: colors.green('(ok)')
137+
}\n
138+
${!apiToken ? 'Note: was unable to make suggestions because no API Token was found; this would make command fail regardless\n' : ''}
139+
`)
140+
return
141+
}
142+
143+
if (updatedInput) {
144+
console.log(
145+
'Note: You can invoke this command next time to skip the interactive questions:'
146+
)
147+
console.log('```')
148+
console.log(
149+
` socket scan create [other flags...] --repo ${repoName} --branch ${branchName} ${orgSlug} ${targets.join(' ')}`
150+
)
151+
console.log('```')
152+
}
153+
154+
if (!apiToken) {
155+
throw new AuthError(
156+
'User must be authenticated to run this command. To log in, run the command `socket login` and enter your API key.'
157+
)
158+
}
159+
160+
if (readOnly) {
161+
console.log('[ReadOnly] Bailing now')
162+
return
163+
}
164+
41165
const spinnerText = 'Creating a scan... \n'
42166
const spinner = new Spinner({ text: spinnerText }).start()
43167

44-
const socketSdk = await setupSdk(apiToken)
45168
const result = await handleApiCall(
46169
socketSdk.createOrgFullScan(
47170
orgSlug,
@@ -64,7 +187,7 @@ export async function createFullScan({
64187
return
65188
}
66189

67-
spinner.success('Scan created successfully')
190+
spinner.successAndStop('Scan created successfully')
68191

69192
const link = colors.underline(colors.cyan(`${result.data.html_report_url}`))
70193
console.log(`Available at: ${link}`)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { select } from '@socketsecurity/registry/lib/prompts'
2+
import { SocketSdk } from '@socketsecurity/sdk'
3+
4+
import { handleApiCall } from '../../utils/api.ts'
5+
6+
export async function suggestOrgSlug(
7+
socketSdk: SocketSdk
8+
): Promise<string | void> {
9+
const result = await handleApiCall(
10+
socketSdk.getOrganizations(),
11+
'looking up organizations'
12+
)
13+
// Ignore a failed request here. It was not the primary goal of
14+
// running this command and reporting it only leads to end-user confusion.
15+
if (result.success) {
16+
const proceed = await select<string>({
17+
message:
18+
'Missing org name; do you want to use any of these orgs for this scan?',
19+
choices: Array.from(Object.values(result.data.organizations))
20+
.map(({ name: slug }) => ({
21+
name: 'Yes [' + slug + ']',
22+
value: slug,
23+
description: `Use "${slug}" as the organization`
24+
}))
25+
.concat({
26+
name: 'No',
27+
value: '',
28+
description:
29+
'Do not use any of these organizations (will end in a no-op)'
30+
})
31+
})
32+
if (proceed) {
33+
return proceed
34+
}
35+
} else {
36+
// TODO: in verbose mode, report this error to stderr
37+
}
38+
}

0 commit comments

Comments
 (0)