mobbdev 0.0.24 → 0.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Bugsy is a command-line interface (CLI) tool that provides automatic security vulnerability remediation for your code. It is the community edition version of [Mobb](https://www.mobb.dev), the first vendor-agnostic automated security vulnerability remediation tool. Bugsy is designed to help developers quickly identify and fix security vulnerabilities in their code.
4
4
 
5
- <img width="1888" alt="Screenshot 2023-07-13 at 5 22 11 PM" src="https://github.com/mobb-dev/autofixer/assets/96389636/f1861bfe-c024-4976-aa57-8b6c1e2f4029">
5
+ <img width="1888" alt="Bugsy" src="./img/bugsy.png">
6
6
 
7
7
  ## What is [Mobb](https://www.mobb.dev)?
8
8
 
@@ -42,4 +42,4 @@ Bugsy will automatically generate a fix for each supported vulnerability identif
42
42
 
43
43
  ## Getting support
44
44
 
45
- If you need support using Bugsy or just want to share your thoughts and learn more, you are more than welcome to join our [discord server](https://discord.gg/Jmpb5QUa)
45
+ If you need support using Bugsy or just want to share your thoughts and learn more, you are more than welcome to join our [discord server](https://bit.ly/Mobb-discord)
package/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { analyze, scan } from './src/commands/index.mjs';
2
+ import { CliError } from './src/utils.mjs';
2
3
  import { parseArgs } from './src/yargs.mjs';
3
4
  import { hideBin } from 'yargs/helpers';
4
5
 
@@ -20,6 +21,11 @@ async function run() {
20
21
  await run();
21
22
  process.exit(0);
22
23
  } catch (err) {
24
+ if (err instanceof CliError) {
25
+ console.error(err.message);
26
+ process.exit(1);
27
+ }
28
+ // unexpected error - print stack trace
23
29
  console.error(
24
30
  'Something went wrong, please try again or contact support if issue persists.'
25
31
  );
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "mobbdev",
3
- "version": "0.0.24",
3
+ "version": "0.0.29",
4
4
  "description": "Automated secure code remediation tool",
5
+ "repository": "https://github.com/mobb-dev/bugsy",
5
6
  "main": "index.mjs",
6
7
  "scripts": {
7
8
  "lint": "prettier --check . && eslint **/*.mjs",
@@ -15,6 +16,7 @@
15
16
  "author": "",
16
17
  "license": "MIT",
17
18
  "dependencies": {
19
+ "adm-zip": "0.5.10",
18
20
  "chalk": "5.3.0",
19
21
  "chalk-animation": "2.0.3",
20
22
  "colors": "1.4.0",
@@ -23,11 +25,13 @@
23
25
  "dotenv": "16.0.3",
24
26
  "extract-zip": "2.0.1",
25
27
  "inquirer": "9.2.7",
28
+ "globby": "13.2.2",
26
29
  "nanospinner": "1.1.0",
27
30
  "node-fetch": "3.3.1",
28
31
  "open": "8.4.2",
29
32
  "semver": "7.5.0",
30
33
  "snyk": "1.1118.0",
34
+ "simple-git": "3.19.1",
31
35
  "tmp": "0.2.1",
32
36
  "yargs": "17.7.2",
33
37
  "zod": "3.21.4"
@@ -5,8 +5,7 @@ import chalkAnimation from 'chalk-animation';
5
5
  import { choseScanner } from '../features/analysis/prompts.mjs';
6
6
  import { SCANNERS, mobbAscii } from '../constants.mjs';
7
7
  import { runAnalysis } from '../features/analysis/index.mjs';
8
- import { sleep } from '../utils.mjs';
9
- import path from 'path';
8
+ import { sleep, CliError } from '../utils.mjs';
10
9
 
11
10
  const GITHUB_REPO_URL_PATTERN = new RegExp(
12
11
  'https://github.com/[\\w-]+/[\\w-]+'
@@ -20,45 +19,49 @@ const UrlZ = z
20
19
  message: 'is not a valid github URL',
21
20
  });
22
21
 
23
- function printRepoUrlErrorMessage({ error, repoUrl, command }) {
22
+ function throwRepoUrlErrorMessage({ error, repoUrl, command }) {
24
23
  const errorMessage = error.issues[error.issues.length - 1].message;
25
- console.error(`\nError: ${chalk.bold(repoUrl)} is ${errorMessage}`);
26
- console.error(
27
- `Example: \n\tmobbdev ${command} -r ${chalk.bold(
28
- 'https://github.com/WebGoat/WebGoat'
29
- )}`
30
- );
24
+ const formattedErrorMessage = `\nError: ${chalk.bold(
25
+ repoUrl
26
+ )} is ${errorMessage}
27
+ Example: \n\tmobbdev ${command} -r ${chalk.bold(
28
+ 'https://github.com/WebGoat/WebGoat'
29
+ )}`;
30
+ throw new CliError(formattedErrorMessage);
31
31
  }
32
32
 
33
33
  export async function analyze(
34
- { repo, scanFile, branch, apiKey, ci },
34
+ { repo, scanFile, ref, apiKey, ci, commitHash, srcPath },
35
35
  { skipPrompts = false } = {}
36
36
  ) {
37
37
  const { success, error } = UrlZ.safeParse(repo);
38
- if (!success) {
39
- printRepoUrlErrorMessage({
38
+
39
+ if (!success && !srcPath) {
40
+ throwRepoUrlErrorMessage({
40
41
  error,
41
42
  repoUrl: repo,
42
43
  command: 'analyze',
43
44
  });
44
- process.exit(1);
45
- }
46
- if (ci && !apiKey) {
47
- console.error(
48
- '\nError: --ci flag requires --api-key to be provided as well'
49
- );
50
- process.exit(1);
51
45
  }
46
+
52
47
  if (!fs.existsSync(scanFile)) {
53
- console.error(`\nCan't access ${chalk.bold(scanFile)}`);
54
- process.exit(1);
55
- }
56
- if (path.extname(scanFile) !== '.json') {
57
- console.error(`\n${chalk.bold(scanFile)} is not a json file`);
58
- process.exit(1);
48
+ throw new CliError(`\nCan't access ${chalk.bold(scanFile)}`);
59
49
  }
50
+
60
51
  !ci && (await showWelcomeMessage(skipPrompts));
61
- await runAnalysis({ repo, scanFile, branch, apiKey, ci }, { skipPrompts });
52
+
53
+ await runAnalysis(
54
+ {
55
+ repo,
56
+ scanFile,
57
+ ref,
58
+ apiKey,
59
+ ci,
60
+ commitHash,
61
+ srcPath,
62
+ },
63
+ { skipPrompts }
64
+ );
62
65
  }
63
66
 
64
67
  export async function scan(
@@ -67,23 +70,20 @@ export async function scan(
67
70
  ) {
68
71
  const { success, error } = UrlZ.safeParse(repo);
69
72
  if (ci && !apiKey) {
70
- console.error(
73
+ throw new CliError(
71
74
  '\nError: --ci flag requires --api-key to be provided as well'
72
75
  );
73
- process.exit(1);
74
76
  }
75
77
 
76
78
  if (!success) {
77
- printRepoUrlErrorMessage({ error, repoUrl: repo, command: 'scan' });
78
- process.exit(1);
79
+ throwRepoUrlErrorMessage({ error, repoUrl: repo, command: 'scan' });
79
80
  }
80
81
  !ci && (await showWelcomeMessage(skipPrompts));
81
82
  scanner ??= scanner || (await choseScanner());
82
83
  if (scanner !== SCANNERS.Snyk) {
83
- console.error(
84
+ throw new CliError(
84
85
  'Vulnerability scanning via Bugsy is available only with Snyk at the moment. Additional scanners will follow soon.'
85
86
  );
86
- process.exit(1);
87
87
  }
88
88
  await runAnalysis({ repo, scanner, branch, apiKey }, { skipPrompts });
89
89
  }
package/src/constants.mjs CHANGED
@@ -20,7 +20,6 @@ const envVariablesSchema = z
20
20
  WEB_LOGIN_URL: z.string(),
21
21
  WEB_APP_URL: z.string(),
22
22
  API_URL: z.string(),
23
- GITHUB_CLIENT_ID: z.string(),
24
23
  })
25
24
  .required();
26
25
 
@@ -59,4 +58,3 @@ export const mobbAscii = `
59
58
  export const WEB_LOGIN_URL = envVariables.WEB_LOGIN_URL;
60
59
  export const WEB_APP_URL = envVariables.WEB_APP_URL;
61
60
  export const API_URL = envVariables.API_URL;
62
- export const GITHUB_CLIENT_ID = envVariables.GITHUB_CLIENT_ID;
@@ -0,0 +1,50 @@
1
+ import { simpleGit } from 'simple-git';
2
+ import Debug from 'debug';
3
+
4
+ const debug = Debug('mobbdev:git');
5
+
6
+ export async function getGitInfo(srcDirPath) {
7
+ debug('getting git info for %s', srcDirPath);
8
+
9
+ const git = simpleGit({
10
+ baseDir: srcDirPath,
11
+ // binary: 'git123',
12
+ maxConcurrentProcesses: 1,
13
+ trimmed: true,
14
+ });
15
+
16
+ let repoUrl = '';
17
+ let hash = '';
18
+ let reference = '';
19
+
20
+ try {
21
+ repoUrl = (await git.getConfig('remote.origin.url')).value || '';
22
+ hash = (await git.revparse(['HEAD'])) || '';
23
+ reference = (await git.revparse(['--abbrev-ref', 'HEAD'])) || '';
24
+ } catch (e) {
25
+ debug('failed to run git %o', e);
26
+ if (e.message && e.message.includes(' spawn ')) {
27
+ debug('git cli not installed');
28
+ } else if (e.message && e.message.includes(' not a git repository ')) {
29
+ debug('folder is not a git repo');
30
+ } else {
31
+ throw e;
32
+ }
33
+ }
34
+
35
+ // Normalize git URL. We may need more generic code here, but it's not very
36
+ // important at the moment.
37
+ if (repoUrl.endsWith('.git')) {
38
+ repoUrl = repoUrl.slice(0, -'.git'.length);
39
+ }
40
+
41
+ if (repoUrl.startsWith('git@github.com:')) {
42
+ repoUrl = repoUrl.replace('git@github.com:', 'https://github.com/');
43
+ }
44
+
45
+ return {
46
+ repoUrl,
47
+ hash,
48
+ reference,
49
+ };
50
+ }
@@ -7,14 +7,9 @@ import fetch from 'node-fetch';
7
7
  import extract from 'extract-zip';
8
8
  import Debug from 'debug';
9
9
  import { Spinner } from '../../utils.mjs';
10
- import { GITHUB_CLIENT_ID, WEB_APP_URL } from '../../constants.mjs';
11
10
  const pipeline = promisify(stream.pipeline);
12
11
  const debug = Debug('mobbdev:github');
13
12
 
14
- const githubTokenUrl = `${WEB_APP_URL}/gh-callback`;
15
-
16
- export const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${GITHUB_CLIENT_ID}&scope=repo&redirect_uri=${githubTokenUrl}`;
17
-
18
13
  async function getRepo(slug, { token } = {}) {
19
14
  try {
20
15
  return fetch(`https://api.github.com/repos/${slug}`, {
@@ -49,16 +49,23 @@ mutation uploadS3BucketInfo($fileName: String!) {
49
49
  uploadFieldsJSON
50
50
  uploadKey
51
51
  }
52
+ repoUploadInfo {
53
+ url
54
+ fixReportId
55
+ uploadFieldsJSON
56
+ uploadKey
57
+ }
52
58
  }
53
59
  }
54
60
  `;
55
61
 
56
62
  const SUBMIT_VULNERABILITY_REPORT = `
57
- mutation SubmitVulnerabilityReport($vulnerabilityReportFileName: String!, $fixReportId: String!, $repoUrl: String!, $reference: String!, $projectId: String!) {
63
+ mutation SubmitVulnerabilityReport($vulnerabilityReportFileName: String!, $fixReportId: String!, $repoUrl: String!, $reference: String!, $projectId: String!, $sha: String) {
58
64
  submitVulnerabilityReport(
59
65
  fixReportId: $fixReportId
60
66
  repoUrl: $repoUrl
61
67
  reference: $reference
68
+ sha: $sha
62
69
  vulnerabilityReportFileName: $vulnerabilityReportFileName
63
70
  projectId: $projectId
64
71
  ) {
@@ -162,6 +169,13 @@ export class GQLClient {
162
169
  uploadFields: JSON.parse(
163
170
  data.uploadS3BucketInfo.uploadInfo.uploadFieldsJSON
164
171
  ),
172
+
173
+ repoFixReportId: data.uploadS3BucketInfo.repoUploadInfo.fixReportId,
174
+ repoUploadKey: data.uploadS3BucketInfo.repoUploadInfo.uploadKey,
175
+ repoUrl: data.uploadS3BucketInfo.repoUploadInfo.url,
176
+ repoUploadFields: JSON.parse(
177
+ data.uploadS3BucketInfo.repoUploadInfo.uploadFieldsJSON
178
+ ),
165
179
  };
166
180
  }
167
181
 
@@ -169,7 +183,8 @@ export class GQLClient {
169
183
  fixReportId,
170
184
  repoUrl,
171
185
  reference,
172
- projectId
186
+ projectId,
187
+ sha
173
188
  ) {
174
189
  await this._apiCall(SUBMIT_VULNERABILITY_REPORT, {
175
190
  fixReportId,
@@ -177,6 +192,7 @@ export class GQLClient {
177
192
  reference,
178
193
  vulnerabilityReportFileName: 'report.json',
179
194
  projectId,
195
+ sha: sha || '',
180
196
  });
181
197
  }
182
198
  }
@@ -10,20 +10,18 @@ import { callbackServer } from './callback-server.mjs';
10
10
  import tmp from 'tmp';
11
11
 
12
12
  import { WEB_APP_URL } from '../../constants.mjs';
13
- import {
14
- canReachRepo,
15
- downloadRepo,
16
- getDefaultBranch,
17
- GITHUB_OAUTH_URL,
18
- } from './github.mjs';
13
+ import { canReachRepo, downloadRepo, getDefaultBranch } from './github.mjs';
19
14
  import { GQLClient } from './gql.mjs';
20
15
  import { githubIntegrationPrompt, mobbAnalysisPrompt } from './prompts.mjs';
21
16
  import { getSnykReport } from './snyk.mjs';
22
17
  import { uploadFile } from './upload-file.mjs';
23
- import { keypress, Spinner } from '../../utils.mjs';
18
+ import { keypress, Spinner, CliError } from '../../utils.mjs';
19
+ import { pack } from './pack.mjs';
20
+ import { getGitInfo } from './git.mjs';
24
21
 
25
22
  const webLoginUrl = `${WEB_APP_URL}/cli-login`;
26
23
  const githubSubmitUrl = `${WEB_APP_URL}/gh-callback`;
24
+ const githubAuthUrl = `${WEB_APP_URL}/github-auth`;
27
25
 
28
26
  const MOBB_LOGIN_REQUIRED_MSG = `🔓 Login to Mobb is Required, you will be redirected to our login page, once the authorization is complete return to this prompt, ${chalk.bgBlue(
29
27
  'press any key to continue'
@@ -40,17 +38,16 @@ const packageJson = JSON.parse(
40
38
  );
41
39
 
42
40
  if (!semver.satisfies(process.version, packageJson.engines.node)) {
43
- console.error(
41
+ throw new CliError(
44
42
  `${packageJson.name} requires node version ${packageJson.engines.node}, but running ${process.version}.`
45
43
  );
46
- process.exit(1);
47
44
  }
48
45
 
49
46
  const config = new Configstore(packageJson.name, { token: '' });
50
47
  debug('config %o', config);
51
48
 
52
49
  export async function runAnalysis(
53
- { repo, scanner, scanFile, branch, apiKey, ci },
50
+ { repo, scanner, scanFile, apiKey, ci, commitHash, srcPath, ref },
54
51
  options
55
52
  ) {
56
53
  try {
@@ -60,9 +57,11 @@ export async function runAnalysis(
60
57
  repo,
61
58
  scanner,
62
59
  scanFile,
63
- branch,
60
+ ref,
64
61
  apiKey,
65
62
  ci,
63
+ srcPath,
64
+ commitHash,
66
65
  },
67
66
  options
68
67
  );
@@ -72,7 +71,7 @@ export async function runAnalysis(
72
71
  }
73
72
 
74
73
  export async function _scan(
75
- { dirname, repo, scanFile, branch, apiKey, ci },
74
+ { dirname, repo, scanFile, branch, apiKey, ci, srcPath, commitHash, ref },
76
75
  { skipPrompts = false } = {}
77
76
  ) {
78
77
  debug('start %s %s', dirname, repo);
@@ -83,23 +82,34 @@ export async function _scan(
83
82
  apiKey ?? debug('token %s', apiKey);
84
83
  let gqlClient = new GQLClient(apiKey ? { apiKey } : { token });
85
84
  await handleMobbLogin();
85
+ const { projectId, organizationId } = await gqlClient.getOrgAndProjectId();
86
+ const uploadData = await gqlClient.uploadS3BucketInfo();
87
+ let reportPath = scanFile;
88
+
89
+ if (srcPath) {
90
+ return await uploadExistingRepo();
91
+ }
92
+
86
93
  const userInfo = await gqlClient.getUserInfo();
87
94
  let { githubToken } = userInfo;
88
95
  const isRepoAvailable = await canReachRepo(repo, {
89
96
  token: userInfo.githubToken,
90
97
  });
91
98
  if (!isRepoAvailable) {
99
+ if (ci) {
100
+ throw new Error(
101
+ `Cannot access repo ${repo} with the provided token, please visit ${githubAuthUrl} to refresh your Github token`
102
+ );
103
+ }
92
104
  const { token } = await handleGithubIntegration();
93
105
  githubToken = token;
94
106
  }
95
107
 
96
108
  const reference =
97
- branch ?? (await getDefaultBranch(repo, { token: githubToken }));
98
- const { projectId, organizationId } = await gqlClient.getOrgAndProjectId();
109
+ ref ?? (await getDefaultBranch(repo, { token: githubToken }));
99
110
  debug('org id %s', organizationId);
100
111
  debug('project id %s', projectId);
101
112
  debug('default branch %s', reference);
102
- const uploadData = await gqlClient.uploadS3BucketInfo();
103
113
 
104
114
  const repositoryRoot = await downloadRepo(
105
115
  {
@@ -111,9 +121,10 @@ export async function _scan(
111
121
  { token: githubToken }
112
122
  );
113
123
 
114
- const reportPath = scanFile || (await getReportFromSnyk());
124
+ if (!reportPath) {
125
+ reportPath = await getReportFromSnyk();
126
+ }
115
127
 
116
- const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
117
128
  await uploadFile(
118
129
  reportPath,
119
130
  uploadData.url,
@@ -133,20 +144,11 @@ export async function _scan(
133
144
  throw e;
134
145
  }
135
146
 
136
- debug('report %o', report);
137
-
138
- const results = ((report.runs || [])[0] || {}).results || [];
139
- if (results.length === 0 && !scanFile) {
140
- mobbSpinner.success({
141
- text: '🕵️‍♂️ Report did not detect any vulnerabilities — nothing to fix.',
142
- });
143
- } else {
144
- mobbSpinner.success({
145
- text: '🕵️‍♂️ Generating fixes...',
146
- });
147
+ mobbSpinner.success({
148
+ text: '🕵️‍♂️ Generating fixes...',
149
+ });
147
150
 
148
- await askToOpenAnalysis();
149
- }
151
+ await askToOpenAnalysis();
150
152
  async function getReportFromSnyk() {
151
153
  const reportPath = path.join(dirname, 'report.json');
152
154
 
@@ -188,7 +190,7 @@ export async function _scan(
188
190
  createSpinner().start().error({
189
191
  text: '🔓 Logged in to Mobb failed - check your api-key',
190
192
  });
191
- process.exit(1);
193
+ throw new CliError();
192
194
  }
193
195
  const mobbLoginSpinner = createSpinner().start();
194
196
  if (!skipPrompts) {
@@ -212,7 +214,7 @@ export async function _scan(
212
214
  mobbLoginSpinner.error({
213
215
  text: 'Something went wrong, API token is invalid.',
214
216
  });
215
- process.exit(1);
217
+ throw new CliError();
216
218
  }
217
219
  debug('set token %s', token);
218
220
  config.set('token', token);
@@ -231,10 +233,49 @@ export async function _scan(
231
233
  throw Error('Could not reach github repo');
232
234
  }
233
235
  const result = await callbackServer(
234
- GITHUB_OAUTH_URL,
236
+ githubAuthUrl,
235
237
  `${githubSubmitUrl}?done=true`
236
238
  );
237
239
  githubSpinner.success({ text: '🔗 Github integration successful!' });
238
240
  return result;
239
241
  }
242
+ async function uploadExistingRepo() {
243
+ const gitInfo = await getGitInfo(srcPath);
244
+ const zipBuffer = await pack(srcPath);
245
+
246
+ await uploadFile(
247
+ reportPath,
248
+ uploadData.url,
249
+ uploadData.uploadKey,
250
+ uploadData.uploadFields
251
+ );
252
+ await uploadFile(
253
+ zipBuffer,
254
+ uploadData.repoUrl,
255
+ uploadData.repoUploadKey,
256
+ uploadData.repoUploadFields
257
+ );
258
+
259
+ const mobbSpinner = createSpinner(
260
+ '🕵️‍♂️ Initiating Mobb analysis'
261
+ ).start();
262
+
263
+ try {
264
+ await gqlClient.submitVulnerabilityReport(
265
+ uploadData.fixReportId,
266
+ repo || gitInfo.repoUrl,
267
+ branch || gitInfo.reference,
268
+ projectId,
269
+ commitHash || gitInfo.hash
270
+ );
271
+ } catch (e) {
272
+ mobbSpinner.error({ text: '🕵️‍♂️ Mobb analysis failed' });
273
+ throw e;
274
+ }
275
+
276
+ mobbSpinner.success({
277
+ text: '🕵️‍♂️ Generating fixes...',
278
+ });
279
+ await askToOpenAnalysis();
280
+ }
240
281
  }
@@ -0,0 +1,31 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { globby } from 'globby';
4
+ import AdmZip from 'adm-zip';
5
+ import Debug from 'debug';
6
+
7
+ const debug = Debug('mobbdev:pack');
8
+
9
+ export async function pack(srcDirPath) {
10
+ debug('pack folder %s', srcDirPath);
11
+ const filepaths = await globby('**', {
12
+ gitignore: true,
13
+ onlyFiles: true,
14
+ cwd: srcDirPath,
15
+ followSymbolicLinks: false,
16
+ });
17
+ debug('files found %d', filepaths.length);
18
+
19
+ const zip = new AdmZip();
20
+
21
+ debug('compressing files');
22
+ for (const filepath of filepaths) {
23
+ zip.addFile(
24
+ filepath.toString(),
25
+ fs.readFileSync(path.join(srcDirPath, filepath.toString()))
26
+ );
27
+ }
28
+
29
+ debug('get zip file buffer');
30
+ return zip.toBuffer();
31
+ }
@@ -1,10 +1,11 @@
1
- import fetch, { FormData, fileFrom } from 'node-fetch';
1
+ import fetch, { FormData, fileFrom, File } from 'node-fetch';
2
2
  import Debug from 'debug';
3
3
 
4
4
  const debug = Debug('mobbdev:upload-file');
5
5
 
6
- export async function uploadFile(reportPath, url, uploadKey, uploadFields) {
7
- debug('upload report file start %s %s', reportPath, url);
6
+ // `file` can be string representing absolute path or buffer.
7
+ export async function uploadFile(file, url, uploadKey, uploadFields) {
8
+ debug('upload file start %s', url);
8
9
  debug('upload fields %o', uploadFields);
9
10
  debug('upload key %s', uploadKey);
10
11
 
@@ -15,7 +16,13 @@ export async function uploadFile(reportPath, url, uploadKey, uploadFields) {
15
16
  }
16
17
 
17
18
  form.append('key', uploadKey);
18
- form.append('file', await fileFrom(reportPath));
19
+ if (typeof file === 'string') {
20
+ debug('upload file from path %s', file);
21
+ form.append('file', await fileFrom(file));
22
+ } else {
23
+ debug('upload file from buffer');
24
+ form.append('file', new File([file], 'file'));
25
+ }
19
26
 
20
27
  const response = await fetch(url, {
21
28
  method: 'POST',
@@ -24,7 +31,7 @@ export async function uploadFile(reportPath, url, uploadKey, uploadFields) {
24
31
 
25
32
  if (!response.ok) {
26
33
  debug('error from S3 %s %s', response.body, response.status);
27
- throw new Error(`Failed to upload the report: ${response.status}`);
34
+ throw new Error(`Failed to upload the file: ${response.status}`);
28
35
  }
29
- debug('upload report file done');
36
+ debug('upload file done');
30
37
  }
package/src/utils.mjs CHANGED
@@ -28,3 +28,5 @@ export function Spinner({ ci = false } = {}) {
28
28
  }),
29
29
  };
30
30
  }
31
+
32
+ export class CliError extends Error {}
package/src/yargs.mjs CHANGED
@@ -1,11 +1,26 @@
1
1
  import yargs from 'yargs/yargs';
2
2
  import chalk from 'chalk';
3
+ import path from 'path';
3
4
 
4
5
  import { SCANNERS } from './constants.mjs';
6
+ import { CliError } from './utils.mjs';
5
7
 
6
- const branchOption = {
7
- alias: 'branch',
8
- describe: chalk.bold('Branch of the repository'),
8
+ const supportExtensions = ['.json', '.xml', '.fpr', '.sarif'];
9
+
10
+ const refOption = {
11
+ describe: chalk.bold('reference of the repository (branch, tag, commit)'),
12
+ type: 'string',
13
+ };
14
+
15
+ const srcPathOption = {
16
+ alias: 'src-path',
17
+ describe: chalk.bold('Path to the repository folder with the source code'),
18
+ type: 'string',
19
+ };
20
+
21
+ const commitHash = {
22
+ alias: 'commit-hash',
23
+ describe: chalk.bold('Hash of the commit'),
9
24
  type: 'string',
10
25
  };
11
26
 
@@ -59,10 +74,12 @@ export const parseArgs = (args) => {
59
74
  builder: (yargs) => {
60
75
  return yargs.options({
61
76
  r: repoOption,
62
- b: branchOption,
77
+ ref: refOption,
63
78
  s: {
64
79
  alias: 'scanner',
65
- choices: Object.values(SCANNERS),
80
+ choices: Object.values(SCANNERS).map((scanner) =>
81
+ scanner.toLowerCase()
82
+ ),
66
83
  describe: chalk.bold('Select the scanner to use'),
67
84
  },
68
85
  y: yesOption,
@@ -76,20 +93,54 @@ export const parseArgs = (args) => {
76
93
  'Provide a vulnerability report and relevant code repository, get automated fixes right away.'
77
94
  ),
78
95
  builder: (yargs) => {
79
- return yargs.options({
80
- f: {
81
- alias: 'scan-file',
82
- demandOption: true,
83
- describe: chalk.bold(
84
- 'Select the vulnerability report to analyze'
85
- ),
86
- },
87
- r: repoOption,
88
- b: branchOption,
89
- y: yesOption,
90
- ['api-key']: apiKeyOption,
91
- ci: ciOption,
92
- });
96
+ return yargs
97
+ .options({
98
+ f: {
99
+ alias: 'scan-file',
100
+ demandOption: true,
101
+ describe: chalk.bold(
102
+ 'Select the vulnerability report to analyze (Checkmarx, Snyk, Fortify, CodeQL)'
103
+ ),
104
+ },
105
+ r: {
106
+ ...repoOption,
107
+ demandOption: false,
108
+ },
109
+ p: srcPathOption,
110
+ ref: refOption,
111
+ ch: commitHash,
112
+ y: yesOption,
113
+ ['api-key']: apiKeyOption,
114
+ ci: ciOption,
115
+ })
116
+ .check((argv) => {
117
+ if (!argv.srcPath && !argv.repo) {
118
+ throw new CliError(
119
+ 'You must supply either --src-path or --repo'
120
+ );
121
+ }
122
+
123
+ if (argv.ci && !argv.apiKey) {
124
+ throw new CliError(
125
+ '--ci flag requires --api-key to be provided as well'
126
+ );
127
+ }
128
+ if (
129
+ !supportExtensions.includes(
130
+ path.extname(argv.f).toLowerCase()
131
+ )
132
+ ) {
133
+ throw new CliError(
134
+ `\n${chalk.bold(
135
+ argv.f
136
+ )} is not a supported file extension. Supported extensions are: ${chalk.bold(
137
+ supportExtensions.join(', ')
138
+ )}\n`
139
+ );
140
+ }
141
+
142
+ return true;
143
+ });
93
144
  },
94
145
  })
95
146
  .example('$0 scan -r https://github.com/WebGoat/WebGoat')