mobbdev 0.0.20 → 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,18 +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 } = args;
10
- await scan({ repoUrl: repo, branch, scanner }, { skipPrompts: yes });
9
+ const { yes, ...restArgs } = args;
10
+ await scan(restArgs, { skipPrompts: yes });
11
11
  }
12
12
  if (command === 'analyze') {
13
- const { repo, scanFile, branch, yes } = args;
14
- await analyze(
15
- { repoUrl: repo, scanFilePath: scanFile, branch },
16
- { skipPrompts: yes }
17
- );
13
+ const { yes, ...restArgs } = args;
14
+ await analyze(restArgs, { skipPrompts: yes });
18
15
  }
19
16
  }
20
17
 
21
18
  (async () => {
22
- 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
+ }
23
29
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobbdev",
3
- "version": "0.0.20",
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,62 +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 },
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({ repoUrl, scanFilePath, branch }, { skipPrompts });
60
+ !ci && (await showWelcomeMessage(skipPrompts));
61
+ await runAnalysis({ repo, scanFile, branch, apiKey, ci }, { skipPrompts });
53
62
  }
54
63
 
55
64
  export async function scan(
56
- { repoUrl, scanner, branch },
65
+ { repo, scanner, branch, apiKey, ci },
57
66
  { skipPrompts = false } = {}
58
67
  ) {
59
- 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
+
60
76
  if (!success) {
61
- return handleScanErrorMessage({ error, repoUrl, command: 'scan' });
77
+ printRepoUrlErrorMessage({ error, repoUrl: repo, command: 'scan' });
78
+ process.exit(1);
62
79
  }
63
- await showWelcomeMessage(skipPrompts);
80
+ !ci && (await showWelcomeMessage(skipPrompts));
64
81
  scanner ??= scanner || (await choseScanner());
65
82
  if (scanner !== SCANNERS.Snyk) {
66
- console.log(
83
+ console.error(
67
84
  'Vulnerability scanning via Bugsy is available only with Snyk at the moment. Additional scanners will follow soon.'
68
85
  );
69
- return;
86
+ process.exit(1);
70
87
  }
71
- await runAnalysis({ repoUrl, scanner, branch }, { skipPrompts });
88
+ await runAnalysis({ repo, scanner, branch, apiKey }, { skipPrompts });
72
89
  }
73
90
  async function showWelcomeMessage(skipPrompts = false) {
74
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');
@@ -68,9 +68,13 @@ mutation SubmitVulnerabilityReport($vulnerabilityReportFileName: String!, $fixRe
68
68
  `;
69
69
 
70
70
  export class GQLClient {
71
- constructor(token) {
72
- debug('init with token %s', token);
71
+ constructor(args) {
72
+ const { token, apiKey } = args;
73
+ apiKey
74
+ ? debug('init with apiKey %s', apiKey)
75
+ : debug('init with token %s', token);
73
76
  this._token = token;
77
+ this._apiKey = apiKey;
74
78
  }
75
79
  async getUserInfo() {
76
80
  const { me } = await this._apiCall(ME);
@@ -79,11 +83,15 @@ export class GQLClient {
79
83
 
80
84
  async _apiCall(query, variables = {}) {
81
85
  debug('api call %o %s', variables, query);
86
+ const headers = this._apiKey
87
+ ? { 'x-mobb-key': this._apiKey }
88
+ : {
89
+ authorization: `Bearer ${this._token}`,
90
+ };
91
+ debug('headers %o', headers);
82
92
  const response = await fetch(API_URL, {
83
93
  method: 'POST',
84
- headers: {
85
- authorization: `Bearer ${this._token}`,
86
- },
94
+ headers,
87
95
  body: JSON.stringify({
88
96
  query,
89
97
  variables,
@@ -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,42 @@ 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 },
55
- { skipPrompts }
53
+ { repo, scanner, scanFile, branch, apiKey, ci },
54
+ options
56
55
  ) {
57
56
  try {
58
57
  await _scan(
59
- { dirname: tmpObj.name, repoUrl, scanner, scanFilePath, branch },
60
- { skipPrompts }
58
+ {
59
+ dirname: tmpObj.name,
60
+ repo,
61
+ scanner,
62
+ scanFile,
63
+ branch,
64
+ apiKey,
65
+ ci,
66
+ },
67
+ options
61
68
  );
62
- } catch (err) {
63
- console.error(
64
- 'Something went wrong, please try again or contact support if issue persists.'
65
- );
66
- console.error(err);
67
69
  } finally {
68
70
  tmpObj.removeCallback();
69
71
  }
70
72
  }
71
73
 
72
74
  export async function _scan(
73
- { dirname, repoUrl, scanFilePath, branch },
75
+ { dirname, repo, scanFile, branch, apiKey, ci },
74
76
  { skipPrompts = false } = {}
75
77
  ) {
76
- debug('start %s %s', dirname, repoUrl);
77
-
78
+ debug('start %s %s', dirname, repo);
79
+ const { createSpinner } = Spinner({ ci });
80
+ skipPrompts = skipPrompts || ci;
78
81
  let token = config.get('token');
79
82
  debug('token %s', token);
80
- let gqlClient = new GQLClient(token);
83
+ apiKey ?? debug('token %s', apiKey);
84
+ let gqlClient = new GQLClient(apiKey ? { apiKey } : { token });
81
85
  await handleMobbLogin();
82
86
  const userInfo = await gqlClient.getUserInfo();
83
87
  let { githubToken } = userInfo;
84
- const isRepoAvailable = await canReachRepo(repoUrl, {
88
+ const isRepoAvailable = await canReachRepo(repo, {
85
89
  token: userInfo.githubToken,
86
90
  });
87
91
  if (!isRepoAvailable) {
@@ -90,7 +94,7 @@ export async function _scan(
90
94
  }
91
95
 
92
96
  const reference =
93
- branch ?? (await getDefaultBranch(repoUrl, { token: githubToken }));
97
+ branch ?? (await getDefaultBranch(repo, { token: githubToken }));
94
98
  const { projectId, organizationId } = await gqlClient.getOrgAndProjectId();
95
99
  debug('org id %s', organizationId);
96
100
  debug('project id %s', projectId);
@@ -99,14 +103,15 @@ export async function _scan(
99
103
 
100
104
  const repositoryRoot = await downloadRepo(
101
105
  {
102
- repoUrl,
106
+ repoUrl: repo,
103
107
  reference,
104
108
  dirname,
109
+ ci,
105
110
  },
106
111
  { token: githubToken }
107
112
  );
108
113
 
109
- const reportPath = scanFilePath || (await getReportFromSnyk());
114
+ const reportPath = scanFile || (await getReportFromSnyk());
110
115
 
111
116
  const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
112
117
  await uploadFile(
@@ -119,7 +124,7 @@ export async function _scan(
119
124
  try {
120
125
  await gqlClient.submitVulnerabilityReport(
121
126
  uploadData.fixReportId,
122
- repoUrl,
127
+ repo,
123
128
  reference,
124
129
  projectId
125
130
  );
@@ -131,7 +136,7 @@ export async function _scan(
131
136
  debug('report %o', report);
132
137
 
133
138
  const results = ((report.runs || [])[0] || {}).results || [];
134
- if (results.length === 0 && !scanFilePath) {
139
+ if (results.length === 0 && !scanFile) {
135
140
  mobbSpinner.success({
136
141
  text: '🕵️‍♂️ Report did not detect any vulnerabilities — nothing to fix.',
137
142
  });
@@ -142,7 +147,6 @@ export async function _scan(
142
147
 
143
148
  await askToOpenAnalysis();
144
149
  }
145
- process.exit(0);
146
150
  async function getReportFromSnyk() {
147
151
  const reportPath = path.join(dirname, 'report.json');
148
152
 
@@ -155,24 +159,37 @@ export async function _scan(
155
159
  return reportPath;
156
160
  }
157
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);
158
165
  !skipPrompts && (await mobbAnalysisPrompt());
159
- open(
160
- `${WEB_APP_URL}/organization/${organizationId}/project/${projectId}/report/${uploadData.fixReportId}`
161
- );
162
- console.log(
163
- chalk.bgBlue(
164
- '\n\n My work here is done for now, see you soon! 🕵️‍♂️ '
165
- )
166
- );
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
+ );
167
174
  }
168
175
 
169
176
  async function handleMobbLogin() {
170
- if (token && (await gqlClient.verifyToken())) {
177
+ if (
178
+ (token && (await gqlClient.verifyToken())) ||
179
+ (apiKey && (await gqlClient.verifyToken()))
180
+ ) {
171
181
  createSpinner().start().success({
172
182
  text: '🔓 Logged in to Mobb successfully',
173
183
  });
184
+
174
185
  return;
175
186
  }
187
+ if (apiKey && !(await gqlClient.verifyToken())) {
188
+ createSpinner().start().error({
189
+ text: '🔓 Logged in to Mobb failed - check your api-key',
190
+ });
191
+ process.exit(1);
192
+ }
176
193
  const mobbLoginSpinner = createSpinner().start();
177
194
  if (!skipPrompts) {
178
195
  mobbLoginSpinner.update({ text: MOBB_LOGIN_REQUIRED_MSG });
@@ -189,13 +206,13 @@ export async function _scan(
189
206
  );
190
207
  token = loginResponse.token;
191
208
 
192
- gqlClient = new GQLClient(token);
209
+ gqlClient = new GQLClient({ token });
193
210
 
194
211
  if (!(await gqlClient.verifyToken())) {
195
212
  mobbLoginSpinner.error({
196
213
  text: 'Something went wrong, API token is invalid.',
197
214
  });
198
- process.exit(0);
215
+ process.exit(1);
199
216
  }
200
217
  debug('set token %s', token);
201
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
@@ -21,10 +21,16 @@ const yesOption = {
21
21
  };
22
22
 
23
23
  const apiKeyOption = {
24
- alias: 'api-key',
25
24
  describe: chalk.bold('Mobb authentication api-key'),
26
25
  type: 'string',
27
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
+ };
28
34
 
29
35
  export const parseArgs = (args) => {
30
36
  const yargsInstance = yargs(args);
@@ -60,7 +66,7 @@ export const parseArgs = (args) => {
60
66
  describe: chalk.bold('Select the scanner to use'),
61
67
  },
62
68
  y: yesOption,
63
- k: apiKeyOption,
69
+ ['api-key']: apiKeyOption,
64
70
  });
65
71
  },
66
72
  })
@@ -79,8 +85,10 @@ export const parseArgs = (args) => {
79
85
  ),
80
86
  },
81
87
  r: repoOption,
88
+ b: branchOption,
82
89
  y: yesOption,
83
- k: apiKeyOption,
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