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 +14 -8
- package/package.json +1 -1
- package/src/commands/index.mjs +48 -31
- package/src/{feature → features}/analysis/github.mjs +3 -2
- package/src/{feature → features}/analysis/gql.mjs +13 -5
- package/src/{feature → features}/analysis/index.mjs +50 -33
- package/src/utils.mjs +12 -0
- package/src/yargs.mjs +12 -3
- /package/src/{feature → features}/analysis/callback-server.mjs +0 -0
- /package/src/{feature → features}/analysis/prompts.mjs +0 -0
- /package/src/{feature → features}/analysis/snyk.mjs +0 -0
- /package/src/{feature → features}/analysis/upload-file.mjs +0 -0
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 {
|
|
10
|
-
await scan(
|
|
9
|
+
const { yes, ...restArgs } = args;
|
|
10
|
+
await scan(restArgs, { skipPrompts: yes });
|
|
11
11
|
}
|
|
12
12
|
if (command === 'analyze') {
|
|
13
|
-
const {
|
|
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
|
-
|
|
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
package/src/commands/index.mjs
CHANGED
|
@@ -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 '../
|
|
5
|
+
import { choseScanner } from '../features/analysis/prompts.mjs';
|
|
6
6
|
import { SCANNERS, mobbAscii } from '../constants.mjs';
|
|
7
|
-
import { runAnalysis } from '../
|
|
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({
|
|
16
|
+
.string({
|
|
17
|
+
invalid_type_error: 'is not a valid github URL',
|
|
18
|
+
})
|
|
17
19
|
.regex(GITHUB_REPO_URL_PATTERN, {
|
|
18
|
-
message: '
|
|
20
|
+
message: 'is not a valid github URL',
|
|
19
21
|
});
|
|
20
22
|
|
|
21
|
-
function
|
|
22
|
-
console.log('\n');
|
|
23
|
+
function printRepoUrlErrorMessage({ error, repoUrl, command }) {
|
|
23
24
|
const errorMessage = error.issues[error.issues.length - 1].message;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
{
|
|
34
|
+
{ repo, scanFile, branch, apiKey, ci },
|
|
36
35
|
{ skipPrompts = false } = {}
|
|
37
36
|
) {
|
|
38
|
-
const { success, error } = UrlZ.safeParse(
|
|
37
|
+
const { success, error } = UrlZ.safeParse(repo);
|
|
39
38
|
if (!success) {
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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(
|
|
48
|
-
console.
|
|
49
|
-
|
|
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({
|
|
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
|
-
{
|
|
65
|
+
{ repo, scanner, branch, apiKey, ci },
|
|
57
66
|
{ skipPrompts = false } = {}
|
|
58
67
|
) {
|
|
59
|
-
const { success, error } = UrlZ.safeParse(
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
86
|
+
process.exit(1);
|
|
70
87
|
}
|
|
71
|
-
await runAnalysis({
|
|
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 {
|
|
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(
|
|
72
|
-
|
|
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
|
-
{
|
|
55
|
-
|
|
53
|
+
{ repo, scanner, scanFile, branch, apiKey, ci },
|
|
54
|
+
options
|
|
56
55
|
) {
|
|
57
56
|
try {
|
|
58
57
|
await _scan(
|
|
59
|
-
{
|
|
60
|
-
|
|
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,
|
|
75
|
+
{ dirname, repo, scanFile, branch, apiKey, ci },
|
|
74
76
|
{ skipPrompts = false } = {}
|
|
75
77
|
) {
|
|
76
|
-
debug('start %s %s', dirname,
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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 && !
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|