mobbdev 0.0.22 → 0.0.23

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/index.mjs CHANGED
@@ -6,21 +6,24 @@ async function run() {
6
6
  const args = await parseArgs(hideBin(process.argv));
7
7
  const [command] = args._;
8
8
  if (command === 'scan') {
9
- const { repo, branch, yes, scanner, apiKey } = args;
10
- await scan(
11
- { repoUrl: repo, branch, scanner, apiKey },
12
- { skipPrompts: yes }
13
- );
9
+ const { yes, ...restArgs } = args;
10
+ await scan(restArgs, { skipPrompts: yes });
14
11
  }
15
12
  if (command === 'analyze') {
16
- const { repo, scanFile, branch, yes, apiKey } = args;
17
- await analyze(
18
- { repoUrl: repo, scanFilePath: scanFile, branch, apiKey },
19
- { skipPrompts: yes }
20
- );
13
+ const { yes, ...restArgs } = args;
14
+ await analyze(restArgs, { skipPrompts: yes });
21
15
  }
22
16
  }
23
17
 
24
18
  (async () => {
25
- await run();
19
+ try {
20
+ await run();
21
+ process.exit(0);
22
+ } catch (err) {
23
+ console.error(
24
+ 'Something went wrong, please try again or contact support if issue persists.'
25
+ );
26
+ console.error(err);
27
+ process.exit(1);
28
+ }
26
29
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobbdev",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "description": "Automated secure code remediation tool",
5
5
  "main": "index.mjs",
6
6
  "scripts": {
@@ -2,9 +2,9 @@ import { z } from 'zod';
2
2
  import fs from 'node:fs';
3
3
  import chalk from 'chalk';
4
4
  import chalkAnimation from 'chalk-animation';
5
- import { choseScanner } from '../feature/analysis/prompts.mjs';
5
+ import { choseScanner } from '../features/analysis/prompts.mjs';
6
6
  import { SCANNERS, mobbAscii } from '../constants.mjs';
7
- import { runAnalysis } from '../feature/analysis/index.mjs';
7
+ import { runAnalysis } from '../features/analysis/index.mjs';
8
8
  import { sleep } from '../utils.mjs';
9
9
  import path from 'path';
10
10
 
@@ -13,65 +13,79 @@ const GITHUB_REPO_URL_PATTERN = new RegExp(
13
13
  );
14
14
 
15
15
  const UrlZ = z
16
- .string({ required_error: 'Please Enter A Github Url' })
16
+ .string({
17
+ invalid_type_error: 'is not a valid github URL',
18
+ })
17
19
  .regex(GITHUB_REPO_URL_PATTERN, {
18
- message: 'Invalid Github repository URL',
20
+ message: 'is not a valid github URL',
19
21
  });
20
22
 
21
- function handleScanErrorMessage({ error, repoUrl, command }) {
22
- console.log('\n');
23
+ function printRepoUrlErrorMessage({ error, repoUrl, command }) {
23
24
  const errorMessage = error.issues[error.issues.length - 1].message;
24
- if (repoUrl) {
25
- console.log(`Error: ${chalk.bold(repoUrl)} is ${errorMessage}`);
26
- }
27
- console.log(
28
- `Example: \n\tmobbdev ${command} ${chalk.bold(
29
- '-r https://github.com/WebGoat/WebGoat'
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'
30
29
  )}`
31
30
  );
32
31
  }
33
32
 
34
33
  export async function analyze(
35
- { repoUrl, scanFilePath, branch, apiKey },
34
+ { repo, scanFile, branch, apiKey, ci },
36
35
  { skipPrompts = false } = {}
37
36
  ) {
38
- const { success, error } = UrlZ.safeParse(repoUrl);
37
+ const { success, error } = UrlZ.safeParse(repo);
39
38
  if (!success) {
40
- return handleScanErrorMessage({ error, repoUrl, command: 'analyze' });
39
+ printRepoUrlErrorMessage({
40
+ error,
41
+ repoUrl: repo,
42
+ command: 'analyze',
43
+ });
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);
41
51
  }
42
- console.log('scanFilePath', scanFilePath);
43
- if (!fs.existsSync(scanFilePath)) {
44
- console.log(`\nCan't access ${chalk.bold(scanFilePath)}`);
45
- return;
52
+ if (!fs.existsSync(scanFile)) {
53
+ console.error(`\nCan't access ${chalk.bold(scanFile)}`);
54
+ process.exit(1);
46
55
  }
47
- if (path.extname(scanFilePath) !== '.json') {
48
- console.log(`\n${chalk.bold(scanFilePath)} is not a json file`);
49
- return;
56
+ if (path.extname(scanFile) !== '.json') {
57
+ console.error(`\n${chalk.bold(scanFile)} is not a json file`);
58
+ process.exit(1);
50
59
  }
51
- await showWelcomeMessage(skipPrompts);
52
- await runAnalysis(
53
- { repoUrl, scanFilePath, branch, apiKey },
54
- { skipPrompts }
55
- );
60
+ !ci && (await showWelcomeMessage(skipPrompts));
61
+ await runAnalysis({ repo, scanFile, branch, apiKey, ci }, { skipPrompts });
56
62
  }
57
63
 
58
64
  export async function scan(
59
- { repoUrl, scanner, branch, apiKey },
65
+ { repo, scanner, branch, apiKey, ci },
60
66
  { skipPrompts = false } = {}
61
67
  ) {
62
- const { success, error } = UrlZ.safeParse(repoUrl);
68
+ const { success, error } = UrlZ.safeParse(repo);
69
+ if (ci && !apiKey) {
70
+ console.error(
71
+ '\nError: --ci flag requires --api-key to be provided as well'
72
+ );
73
+ process.exit(1);
74
+ }
75
+
63
76
  if (!success) {
64
- return handleScanErrorMessage({ error, repoUrl, command: 'scan' });
77
+ printRepoUrlErrorMessage({ error, repoUrl: repo, command: 'scan' });
78
+ process.exit(1);
65
79
  }
66
- await showWelcomeMessage(skipPrompts);
80
+ !ci && (await showWelcomeMessage(skipPrompts));
67
81
  scanner ??= scanner || (await choseScanner());
68
82
  if (scanner !== SCANNERS.Snyk) {
69
- console.log(
83
+ console.error(
70
84
  'Vulnerability scanning via Bugsy is available only with Snyk at the moment. Additional scanners will follow soon.'
71
85
  );
72
- return;
86
+ process.exit(1);
73
87
  }
74
- await runAnalysis({ repoUrl, scanner, branch, apiKey }, { skipPrompts });
88
+ await runAnalysis({ repo, scanner, branch, apiKey }, { skipPrompts });
75
89
  }
76
90
  async function showWelcomeMessage(skipPrompts = false) {
77
91
  console.log(mobbAscii);
@@ -6,7 +6,7 @@ import { promisify } from 'node:util';
6
6
  import fetch from 'node-fetch';
7
7
  import extract from 'extract-zip';
8
8
  import Debug from 'debug';
9
- import { createSpinner } from 'nanospinner';
9
+ import { Spinner } from '../../utils.mjs';
10
10
  import { GITHUB_CLIENT_ID, WEB_APP_URL } from '../../constants.mjs';
11
11
  const pipeline = promisify(stream.pipeline);
12
12
  const debug = Debug('mobbdev:github');
@@ -69,9 +69,10 @@ export async function getDefaultBranch(repoUrl, { token } = {}) {
69
69
  }
70
70
 
71
71
  export async function downloadRepo(
72
- { repoUrl, reference, dirname },
72
+ { repoUrl, reference, dirname, ci },
73
73
  { token } = {}
74
74
  ) {
75
+ const { createSpinner } = Spinner({ ci });
75
76
  const repoSpinner = createSpinner('💾 Downloading Repo').start();
76
77
  debug('download repo %s %s %s', repoUrl, reference, dirname);
77
78
  const zipFilePath = path.join(dirname, 'repo.zip');
@@ -1,7 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import Configstore from 'configstore';
3
3
  import Debug from 'debug';
4
- import { createSpinner } from 'nanospinner';
5
4
  import fs from 'node:fs';
6
5
  import path from 'node:path';
7
6
  import { fileURLToPath } from 'node:url';
@@ -21,7 +20,7 @@ import { GQLClient } from './gql.mjs';
21
20
  import { githubIntegrationPrompt, mobbAnalysisPrompt } from './prompts.mjs';
22
21
  import { getSnykReport } from './snyk.mjs';
23
22
  import { uploadFile } from './upload-file.mjs';
24
- import { keypress } from '../../utils.mjs';
23
+ import { keypress, Spinner } from '../../utils.mjs';
25
24
 
26
25
  const webLoginUrl = `${WEB_APP_URL}/cli-login`;
27
26
  const githubSubmitUrl = `${WEB_APP_URL}/gh-callback`;
@@ -51,37 +50,34 @@ const config = new Configstore(packageJson.name, { token: '' });
51
50
  debug('config %o', config);
52
51
 
53
52
  export async function runAnalysis(
54
- { repoUrl, scanner, scanFilePath, branch, apiKey },
55
- { skipPrompts }
53
+ { repo, scanner, scanFile, branch, apiKey, ci },
54
+ options
56
55
  ) {
57
56
  try {
58
57
  await _scan(
59
58
  {
60
59
  dirname: tmpObj.name,
61
- repoUrl,
60
+ repo,
62
61
  scanner,
63
- scanFilePath,
62
+ scanFile,
64
63
  branch,
65
64
  apiKey,
65
+ ci,
66
66
  },
67
- { skipPrompts }
67
+ options
68
68
  );
69
- } catch (err) {
70
- console.error(
71
- 'Something went wrong, please try again or contact support if issue persists.'
72
- );
73
- console.error(err);
74
69
  } finally {
75
70
  tmpObj.removeCallback();
76
71
  }
77
72
  }
78
73
 
79
74
  export async function _scan(
80
- { dirname, repoUrl, scanFilePath, branch, apiKey },
75
+ { dirname, repo, scanFile, branch, apiKey, ci },
81
76
  { skipPrompts = false } = {}
82
77
  ) {
83
- debug('start %s %s', dirname, repoUrl);
84
-
78
+ debug('start %s %s', dirname, repo);
79
+ const { createSpinner } = Spinner({ ci });
80
+ skipPrompts = skipPrompts || ci;
85
81
  let token = config.get('token');
86
82
  debug('token %s', token);
87
83
  apiKey ?? debug('token %s', apiKey);
@@ -89,7 +85,7 @@ export async function _scan(
89
85
  await handleMobbLogin();
90
86
  const userInfo = await gqlClient.getUserInfo();
91
87
  let { githubToken } = userInfo;
92
- const isRepoAvailable = await canReachRepo(repoUrl, {
88
+ const isRepoAvailable = await canReachRepo(repo, {
93
89
  token: userInfo.githubToken,
94
90
  });
95
91
  if (!isRepoAvailable) {
@@ -98,7 +94,7 @@ export async function _scan(
98
94
  }
99
95
 
100
96
  const reference =
101
- branch ?? (await getDefaultBranch(repoUrl, { token: githubToken }));
97
+ branch ?? (await getDefaultBranch(repo, { token: githubToken }));
102
98
  const { projectId, organizationId } = await gqlClient.getOrgAndProjectId();
103
99
  debug('org id %s', organizationId);
104
100
  debug('project id %s', projectId);
@@ -107,14 +103,15 @@ export async function _scan(
107
103
 
108
104
  const repositoryRoot = await downloadRepo(
109
105
  {
110
- repoUrl,
106
+ repoUrl: repo,
111
107
  reference,
112
108
  dirname,
109
+ ci,
113
110
  },
114
111
  { token: githubToken }
115
112
  );
116
113
 
117
- const reportPath = scanFilePath || (await getReportFromSnyk());
114
+ const reportPath = scanFile || (await getReportFromSnyk());
118
115
 
119
116
  const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
120
117
  await uploadFile(
@@ -127,7 +124,7 @@ export async function _scan(
127
124
  try {
128
125
  await gqlClient.submitVulnerabilityReport(
129
126
  uploadData.fixReportId,
130
- repoUrl,
127
+ repo,
131
128
  reference,
132
129
  projectId
133
130
  );
@@ -139,7 +136,7 @@ export async function _scan(
139
136
  debug('report %o', report);
140
137
 
141
138
  const results = ((report.runs || [])[0] || {}).results || [];
142
- if (results.length === 0 && !scanFilePath) {
139
+ if (results.length === 0 && !scanFile) {
143
140
  mobbSpinner.success({
144
141
  text: '🕵️‍♂️ Report did not detect any vulnerabilities — nothing to fix.',
145
142
  });
@@ -150,7 +147,6 @@ export async function _scan(
150
147
 
151
148
  await askToOpenAnalysis();
152
149
  }
153
- process.exit(0);
154
150
  async function getReportFromSnyk() {
155
151
  const reportPath = path.join(dirname, 'report.json');
156
152
 
@@ -163,15 +159,18 @@ export async function _scan(
163
159
  return reportPath;
164
160
  }
165
161
  async function askToOpenAnalysis() {
162
+ const reportUrl = `${WEB_APP_URL}/organization/${organizationId}/project/${projectId}/report/${uploadData.fixReportId}`;
163
+ !ci && console.log('You can access the report at: \n');
164
+ console.log(reportUrl);
166
165
  !skipPrompts && (await mobbAnalysisPrompt());
167
- open(
168
- `${WEB_APP_URL}/organization/${organizationId}/project/${projectId}/report/${uploadData.fixReportId}`
169
- );
170
- console.log(
171
- chalk.bgBlue(
172
- '\n\n My work here is done for now, see you soon! 🕵️‍♂️ '
173
- )
174
- );
166
+
167
+ !ci && open(reportUrl);
168
+ !ci &&
169
+ console.log(
170
+ chalk.bgBlue(
171
+ '\n\n My work here is done for now, see you soon! 🕵️‍♂️ '
172
+ )
173
+ );
175
174
  }
176
175
 
177
176
  async function handleMobbLogin() {
@@ -190,7 +189,6 @@ export async function _scan(
190
189
  text: '🔓 Logged in to Mobb failed - check your api-key',
191
190
  });
192
191
  process.exit(1);
193
- return;
194
192
  }
195
193
  const mobbLoginSpinner = createSpinner().start();
196
194
  if (!skipPrompts) {
@@ -214,7 +212,7 @@ export async function _scan(
214
212
  mobbLoginSpinner.error({
215
213
  text: 'Something went wrong, API token is invalid.',
216
214
  });
217
- process.exit(0);
215
+ process.exit(1);
218
216
  }
219
217
  debug('set token %s', token);
220
218
  config.set('token', token);
package/src/utils.mjs CHANGED
@@ -1,4 +1,6 @@
1
1
  import readline from 'node:readline';
2
+ import { createSpinner as _createSpinner } from 'nanospinner';
3
+ import { PassThrough } from 'stream';
2
4
  export const sleep = (ms = 2000) => new Promise((r) => setTimeout(r, ms));
3
5
 
4
6
  export async function keypress() {
@@ -16,3 +18,13 @@ export async function keypress() {
16
18
  });
17
19
  });
18
20
  }
21
+
22
+ export function Spinner({ ci = false } = {}) {
23
+ return {
24
+ createSpinner: (text, options) =>
25
+ _createSpinner(text, {
26
+ stream: ci ? new PassThrough() : undefined,
27
+ ...options,
28
+ }),
29
+ };
30
+ }
package/src/yargs.mjs CHANGED
@@ -24,6 +24,13 @@ const apiKeyOption = {
24
24
  describe: chalk.bold('Mobb authentication api-key'),
25
25
  type: 'string',
26
26
  };
27
+ const ciOption = {
28
+ describe: chalk.bold(
29
+ 'Run in CI mode, prompts and browser will not be opened'
30
+ ),
31
+ type: 'boolean',
32
+ default: false,
33
+ };
27
34
 
28
35
  export const parseArgs = (args) => {
29
36
  const yargsInstance = yargs(args);
@@ -81,6 +88,7 @@ export const parseArgs = (args) => {
81
88
  b: branchOption,
82
89
  y: yesOption,
83
90
  ['api-key']: apiKeyOption,
91
+ ci: ciOption,
84
92
  });
85
93
  },
86
94
  })
@@ -91,6 +99,7 @@ export const parseArgs = (args) => {
91
99
  yargsInstance.showHelp();
92
100
  },
93
101
  })
102
+ .strictOptions()
94
103
  .help('h')
95
104
  .alias('h', 'help')
96
105
  .epilog(chalk.bgBlue('Made with ❤️ by Mobb'))
File without changes
File without changes
File without changes