mobbdev 0.0.18 → 0.0.22
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/.env +3 -2
- package/README.md +2 -2
- package/index.mjs +24 -15
- package/package.json +6 -1
- package/src/commands/index.mjs +81 -0
- package/src/constants.mjs +38 -0
- package/src/{web-login.mjs → feature/analysis/callback-server.mjs} +6 -6
- package/src/feature/analysis/github.mjs +110 -0
- package/src/{gql.mjs → feature/analysis/gql.mjs} +20 -7
- package/src/feature/analysis/index.mjs +242 -0
- package/src/feature/analysis/prompts.mjs +55 -0
- package/src/{snyk.mjs → feature/analysis/snyk.mjs} +38 -25
- package/src/utils.mjs +18 -0
- package/src/yargs.mjs +100 -0
- package/src/github.mjs +0 -74
- package/src/index.mjs +0 -107
- /package/src/{upload-file.mjs → feature/analysis/upload-file.mjs} +0 -0
package/.env
CHANGED
package/README.md
CHANGED
|
@@ -27,11 +27,11 @@ Snyk CLI is used to produce a SAST vulnerability report.
|
|
|
27
27
|
You can use Bugsy from the command line. To evaluate and remediate a new open-source repository, you can run the following command:
|
|
28
28
|
|
|
29
29
|
```shell
|
|
30
|
-
npx mobbdev https://github.com/mobb-dev/simple-vulnerable-java-project
|
|
30
|
+
npx mobbdev scan -r https://github.com/mobb-dev/simple-vulnerable-java-project
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
Bugsy will automatically generate a fix for each supported vulnerability identified in the SAST results, present it to developers for review and commit to their code.
|
|
34
34
|
|
|
35
35
|
## Getting support
|
|
36
36
|
|
|
37
|
-
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/
|
|
37
|
+
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)
|
package/index.mjs
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { analyze, scan } from './src/commands/index.mjs';
|
|
2
|
+
import { parseArgs } from './src/yargs.mjs';
|
|
3
|
+
import { hideBin } from 'yargs/helpers';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
async function run() {
|
|
6
|
+
const args = await parseArgs(hideBin(process.argv));
|
|
7
|
+
const [command] = args._;
|
|
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
|
+
);
|
|
14
|
+
}
|
|
15
|
+
if (command === 'analyze') {
|
|
16
|
+
const { repo, scanFile, branch, yes, apiKey } = args;
|
|
17
|
+
await analyze(
|
|
18
|
+
{ repoUrl: repo, scanFilePath: scanFile, branch, apiKey },
|
|
19
|
+
{ skipPrompts: yes }
|
|
12
20
|
);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
(async () => {
|
|
25
|
+
await run();
|
|
26
|
+
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobbdev",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"description": "Automated secure code remediation tool",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"scripts": {
|
|
@@ -15,16 +15,21 @@
|
|
|
15
15
|
"author": "",
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"dependencies": {
|
|
18
|
+
"chalk": "5.3.0",
|
|
19
|
+
"chalk-animation": "2.0.3",
|
|
18
20
|
"colors": "1.4.0",
|
|
19
21
|
"configstore": "6.0.0",
|
|
20
22
|
"debug": "4.3.4",
|
|
21
23
|
"dotenv": "16.0.3",
|
|
22
24
|
"extract-zip": "2.0.1",
|
|
25
|
+
"inquirer": "9.2.7",
|
|
26
|
+
"nanospinner": "1.1.0",
|
|
23
27
|
"node-fetch": "3.3.1",
|
|
24
28
|
"open": "8.4.2",
|
|
25
29
|
"semver": "7.5.0",
|
|
26
30
|
"snyk": "1.1118.0",
|
|
27
31
|
"tmp": "0.2.1",
|
|
32
|
+
"yargs": "17.7.2",
|
|
28
33
|
"zod": "3.21.4"
|
|
29
34
|
},
|
|
30
35
|
"devDependencies": {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import chalkAnimation from 'chalk-animation';
|
|
5
|
+
import { choseScanner } from '../feature/analysis/prompts.mjs';
|
|
6
|
+
import { SCANNERS, mobbAscii } from '../constants.mjs';
|
|
7
|
+
import { runAnalysis } from '../feature/analysis/index.mjs';
|
|
8
|
+
import { sleep } from '../utils.mjs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
|
|
11
|
+
const GITHUB_REPO_URL_PATTERN = new RegExp(
|
|
12
|
+
'https://github.com/[\\w-]+/[\\w-]+'
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const UrlZ = z
|
|
16
|
+
.string({ required_error: 'Please Enter A Github Url' })
|
|
17
|
+
.regex(GITHUB_REPO_URL_PATTERN, {
|
|
18
|
+
message: 'Invalid Github repository URL',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function handleScanErrorMessage({ error, repoUrl, command }) {
|
|
22
|
+
console.log('\n');
|
|
23
|
+
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'
|
|
30
|
+
)}`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function analyze(
|
|
35
|
+
{ repoUrl, scanFilePath, branch, apiKey },
|
|
36
|
+
{ skipPrompts = false } = {}
|
|
37
|
+
) {
|
|
38
|
+
const { success, error } = UrlZ.safeParse(repoUrl);
|
|
39
|
+
if (!success) {
|
|
40
|
+
return handleScanErrorMessage({ error, repoUrl, command: 'analyze' });
|
|
41
|
+
}
|
|
42
|
+
console.log('scanFilePath', scanFilePath);
|
|
43
|
+
if (!fs.existsSync(scanFilePath)) {
|
|
44
|
+
console.log(`\nCan't access ${chalk.bold(scanFilePath)}`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (path.extname(scanFilePath) !== '.json') {
|
|
48
|
+
console.log(`\n${chalk.bold(scanFilePath)} is not a json file`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
await showWelcomeMessage(skipPrompts);
|
|
52
|
+
await runAnalysis(
|
|
53
|
+
{ repoUrl, scanFilePath, branch, apiKey },
|
|
54
|
+
{ skipPrompts }
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function scan(
|
|
59
|
+
{ repoUrl, scanner, branch, apiKey },
|
|
60
|
+
{ skipPrompts = false } = {}
|
|
61
|
+
) {
|
|
62
|
+
const { success, error } = UrlZ.safeParse(repoUrl);
|
|
63
|
+
if (!success) {
|
|
64
|
+
return handleScanErrorMessage({ error, repoUrl, command: 'scan' });
|
|
65
|
+
}
|
|
66
|
+
await showWelcomeMessage(skipPrompts);
|
|
67
|
+
scanner ??= scanner || (await choseScanner());
|
|
68
|
+
if (scanner !== SCANNERS.Snyk) {
|
|
69
|
+
console.log(
|
|
70
|
+
'Vulnerability scanning via Bugsy is available only with Snyk at the moment. Additional scanners will follow soon.'
|
|
71
|
+
);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
await runAnalysis({ repoUrl, scanner, branch, apiKey }, { skipPrompts });
|
|
75
|
+
}
|
|
76
|
+
async function showWelcomeMessage(skipPrompts = false) {
|
|
77
|
+
console.log(mobbAscii);
|
|
78
|
+
const welcome = chalkAnimation.rainbow('\n\t\t\tWelcome to Bugsy\n');
|
|
79
|
+
skipPrompts ? await sleep(100) : await sleep(2000);
|
|
80
|
+
welcome.stop();
|
|
81
|
+
}
|
package/src/constants.mjs
CHANGED
|
@@ -8,17 +8,55 @@ const debug = Debug('mobbdev:constants');
|
|
|
8
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
dotenv.config({ path: path.join(__dirname, '../.env') });
|
|
10
10
|
|
|
11
|
+
export const SCANNERS = {
|
|
12
|
+
Checkmarx: 'checkmarx',
|
|
13
|
+
Codeql: 'codeql',
|
|
14
|
+
Fortify: 'fortify',
|
|
15
|
+
Snyk: 'snyk',
|
|
16
|
+
};
|
|
17
|
+
|
|
11
18
|
const envVariablesSchema = z
|
|
12
19
|
.object({
|
|
13
20
|
WEB_LOGIN_URL: z.string(),
|
|
14
21
|
WEB_APP_URL: z.string(),
|
|
15
22
|
API_URL: z.string(),
|
|
23
|
+
GITHUB_CLIENT_ID: z.string(),
|
|
16
24
|
})
|
|
17
25
|
.required();
|
|
18
26
|
|
|
19
27
|
const envVariables = envVariablesSchema.parse(process.env);
|
|
20
28
|
debug('config %o', envVariables);
|
|
21
29
|
|
|
30
|
+
export const mobbAscii = `
|
|
31
|
+
..
|
|
32
|
+
..........
|
|
33
|
+
.................
|
|
34
|
+
...........................
|
|
35
|
+
..............................
|
|
36
|
+
................................
|
|
37
|
+
..................................
|
|
38
|
+
....................................
|
|
39
|
+
.....................................
|
|
40
|
+
.............................................
|
|
41
|
+
.................................................
|
|
42
|
+
............................... .................
|
|
43
|
+
.................................. ............
|
|
44
|
+
.................. ............. ..........
|
|
45
|
+
......... ........ ......... ......
|
|
46
|
+
............... ....
|
|
47
|
+
.... ..
|
|
48
|
+
|
|
49
|
+
. ...
|
|
50
|
+
..............
|
|
51
|
+
......................
|
|
52
|
+
...........................
|
|
53
|
+
................................
|
|
54
|
+
......................................
|
|
55
|
+
...............................
|
|
56
|
+
.................
|
|
57
|
+
`;
|
|
58
|
+
|
|
22
59
|
export const WEB_LOGIN_URL = envVariables.WEB_LOGIN_URL;
|
|
23
60
|
export const WEB_APP_URL = envVariables.WEB_APP_URL;
|
|
24
61
|
export const API_URL = envVariables.API_URL;
|
|
62
|
+
export const GITHUB_CLIENT_ID = envVariables.GITHUB_CLIENT_ID;
|
|
@@ -3,11 +3,10 @@ import { setTimeout, clearTimeout } from 'node:timers';
|
|
|
3
3
|
import http from 'node:http';
|
|
4
4
|
import querystring from 'node:querystring';
|
|
5
5
|
import open from 'open';
|
|
6
|
-
import { WEB_LOGIN_URL } from './constants.mjs';
|
|
7
6
|
|
|
8
7
|
const debug = Debug('mobbdev:web-login');
|
|
9
8
|
|
|
10
|
-
export async function
|
|
9
|
+
export async function callbackServer(url, redirectUrl) {
|
|
11
10
|
debug('web login start');
|
|
12
11
|
|
|
13
12
|
let responseResolver;
|
|
@@ -33,9 +32,10 @@ export async function webLogin() {
|
|
|
33
32
|
req.on('end', () => {
|
|
34
33
|
debug('http server end %s', body);
|
|
35
34
|
res.writeHead(301, {
|
|
36
|
-
Location:
|
|
35
|
+
Location: redirectUrl,
|
|
37
36
|
}).end();
|
|
38
|
-
|
|
37
|
+
|
|
38
|
+
responseResolver(querystring.parse(body));
|
|
39
39
|
});
|
|
40
40
|
});
|
|
41
41
|
|
|
@@ -47,8 +47,8 @@ export async function webLogin() {
|
|
|
47
47
|
});
|
|
48
48
|
debug('http server started on port %d', port);
|
|
49
49
|
|
|
50
|
-
debug('opening the browser on %s', `${
|
|
51
|
-
await open(`${
|
|
50
|
+
debug('opening the browser on %s', `${url}?port=${port}`);
|
|
51
|
+
await open(`${url}?port=${port}`);
|
|
52
52
|
|
|
53
53
|
try {
|
|
54
54
|
debug('waiting for http request');
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import stream from 'node:stream';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import fetch from 'node-fetch';
|
|
7
|
+
import extract from 'extract-zip';
|
|
8
|
+
import Debug from 'debug';
|
|
9
|
+
import { createSpinner } from 'nanospinner';
|
|
10
|
+
import { GITHUB_CLIENT_ID, WEB_APP_URL } from '../../constants.mjs';
|
|
11
|
+
const pipeline = promisify(stream.pipeline);
|
|
12
|
+
const debug = Debug('mobbdev:github');
|
|
13
|
+
|
|
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
|
+
async function getRepo(slug, { token } = {}) {
|
|
19
|
+
try {
|
|
20
|
+
return fetch(`https://api.github.com/repos/${slug}`, {
|
|
21
|
+
method: 'GET',
|
|
22
|
+
headers: {
|
|
23
|
+
Accept: 'application/vnd.github+json',
|
|
24
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
25
|
+
...(token && { Authorization: `bearer ${token}` }),
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
} catch (e) {
|
|
29
|
+
debug(`error fetching the repo ${slug}`, e);
|
|
30
|
+
throw e;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function extractSlug(repoUrl) {
|
|
35
|
+
debug('get default branch %s', repoUrl);
|
|
36
|
+
let slug = repoUrl.replace(/https?:\/\/github\.com\//i, '');
|
|
37
|
+
|
|
38
|
+
if (slug.endsWith('/')) {
|
|
39
|
+
slug = slug.substring(0, slug.length - 1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (slug.endsWith('.git')) {
|
|
43
|
+
slug = slug.substring(0, slug.length - '.git'.length);
|
|
44
|
+
}
|
|
45
|
+
debug('slug %s', slug);
|
|
46
|
+
return slug;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function canReachRepo(repoUrl, { token } = {}) {
|
|
50
|
+
const slug = extractSlug(repoUrl);
|
|
51
|
+
const response = await getRepo(slug, { token });
|
|
52
|
+
return response.ok;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function getDefaultBranch(repoUrl, { token } = {}) {
|
|
56
|
+
const slug = extractSlug(repoUrl);
|
|
57
|
+
const response = await getRepo(slug, { token });
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
debug('GH request failed %s %s', response.body, response.status);
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Can't get default branch, make sure you have access to : ${repoUrl}.`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const repoInfo = await response.json();
|
|
66
|
+
debug('GH request ok %o', repoInfo);
|
|
67
|
+
|
|
68
|
+
return repoInfo.default_branch;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function downloadRepo(
|
|
72
|
+
{ repoUrl, reference, dirname },
|
|
73
|
+
{ token } = {}
|
|
74
|
+
) {
|
|
75
|
+
const repoSpinner = createSpinner('💾 Downloading Repo').start();
|
|
76
|
+
debug('download repo %s %s %s', repoUrl, reference, dirname);
|
|
77
|
+
const zipFilePath = path.join(dirname, 'repo.zip');
|
|
78
|
+
const response = await fetch(`${repoUrl}/zipball/${reference}`, {
|
|
79
|
+
method: 'GET',
|
|
80
|
+
headers: {
|
|
81
|
+
...(token && { Authorization: `bearer ${token}` }),
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
debug(
|
|
86
|
+
'GH zipball request failed %s %s',
|
|
87
|
+
response.body,
|
|
88
|
+
response.status
|
|
89
|
+
);
|
|
90
|
+
repoSpinner.error({ text: '💾 Repo download failed' });
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Can't access the the branch ${chalk.bold(
|
|
93
|
+
reference
|
|
94
|
+
)} on ${chalk.bold(repoUrl)} make sure it exits.`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const fileWriterStream = fs.createWriteStream(zipFilePath);
|
|
99
|
+
|
|
100
|
+
await pipeline(response.body, fileWriterStream);
|
|
101
|
+
await extract(zipFilePath, { dir: dirname });
|
|
102
|
+
|
|
103
|
+
const repoRoot = fs
|
|
104
|
+
.readdirSync(dirname, { withFileTypes: true })
|
|
105
|
+
.filter((dirent) => dirent.isDirectory())
|
|
106
|
+
.map((dirent) => dirent.name)[0];
|
|
107
|
+
debug('repo root %s', repoRoot);
|
|
108
|
+
repoSpinner.success({ text: '💾 Repo downloaded successfully' });
|
|
109
|
+
return path.join(dirname, repoRoot);
|
|
110
|
+
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import fetch from 'node-fetch';
|
|
2
2
|
import Debug from 'debug';
|
|
3
|
-
import { API_URL } from '
|
|
3
|
+
import { API_URL } from '../../constants.mjs';
|
|
4
4
|
|
|
5
5
|
const debug = Debug('mobbdev:gql');
|
|
6
6
|
|
|
7
7
|
const ME = `
|
|
8
8
|
query Me {
|
|
9
|
-
|
|
9
|
+
me {
|
|
10
10
|
id
|
|
11
11
|
email
|
|
12
|
+
githubToken
|
|
12
13
|
}
|
|
13
14
|
}
|
|
14
15
|
`;
|
|
@@ -67,18 +68,30 @@ mutation SubmitVulnerabilityReport($vulnerabilityReportFileName: String!, $fixRe
|
|
|
67
68
|
`;
|
|
68
69
|
|
|
69
70
|
export class GQLClient {
|
|
70
|
-
constructor(
|
|
71
|
-
|
|
71
|
+
constructor(args) {
|
|
72
|
+
const { token, apiKey } = args;
|
|
73
|
+
apiKey
|
|
74
|
+
? debug('init with apiKey %s', apiKey)
|
|
75
|
+
: debug('init with token %s', token);
|
|
72
76
|
this._token = token;
|
|
77
|
+
this._apiKey = apiKey;
|
|
78
|
+
}
|
|
79
|
+
async getUserInfo() {
|
|
80
|
+
const { me } = await this._apiCall(ME);
|
|
81
|
+
return me;
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
async _apiCall(query, variables = {}) {
|
|
76
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);
|
|
77
92
|
const response = await fetch(API_URL, {
|
|
78
93
|
method: 'POST',
|
|
79
|
-
headers
|
|
80
|
-
authorization: `Bearer ${this._token}`,
|
|
81
|
-
},
|
|
94
|
+
headers,
|
|
82
95
|
body: JSON.stringify({
|
|
83
96
|
query,
|
|
84
97
|
variables,
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import Configstore from 'configstore';
|
|
3
|
+
import Debug from 'debug';
|
|
4
|
+
import { createSpinner } from 'nanospinner';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import open from 'open';
|
|
9
|
+
import semver from 'semver';
|
|
10
|
+
import { callbackServer } from './callback-server.mjs';
|
|
11
|
+
import tmp from 'tmp';
|
|
12
|
+
|
|
13
|
+
import { WEB_APP_URL } from '../../constants.mjs';
|
|
14
|
+
import {
|
|
15
|
+
canReachRepo,
|
|
16
|
+
downloadRepo,
|
|
17
|
+
getDefaultBranch,
|
|
18
|
+
GITHUB_OAUTH_URL,
|
|
19
|
+
} from './github.mjs';
|
|
20
|
+
import { GQLClient } from './gql.mjs';
|
|
21
|
+
import { githubIntegrationPrompt, mobbAnalysisPrompt } from './prompts.mjs';
|
|
22
|
+
import { getSnykReport } from './snyk.mjs';
|
|
23
|
+
import { uploadFile } from './upload-file.mjs';
|
|
24
|
+
import { keypress } from '../../utils.mjs';
|
|
25
|
+
|
|
26
|
+
const webLoginUrl = `${WEB_APP_URL}/cli-login`;
|
|
27
|
+
const githubSubmitUrl = `${WEB_APP_URL}/gh-callback`;
|
|
28
|
+
|
|
29
|
+
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(
|
|
30
|
+
'press any key to continue'
|
|
31
|
+
)};`;
|
|
32
|
+
const tmpObj = tmp.dirSync({
|
|
33
|
+
unsafeCleanup: true,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const debug = Debug('mobbdev:index');
|
|
37
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
|
|
39
|
+
const packageJson = JSON.parse(
|
|
40
|
+
fs.readFileSync(path.join(__dirname, '../../../package.json'), 'utf8')
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (!semver.satisfies(process.version, packageJson.engines.node)) {
|
|
44
|
+
console.error(
|
|
45
|
+
`${packageJson.name} requires node version ${packageJson.engines.node}, but running ${process.version}.`
|
|
46
|
+
);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const config = new Configstore(packageJson.name, { token: '' });
|
|
51
|
+
debug('config %o', config);
|
|
52
|
+
|
|
53
|
+
export async function runAnalysis(
|
|
54
|
+
{ repoUrl, scanner, scanFilePath, branch, apiKey },
|
|
55
|
+
{ skipPrompts }
|
|
56
|
+
) {
|
|
57
|
+
try {
|
|
58
|
+
await _scan(
|
|
59
|
+
{
|
|
60
|
+
dirname: tmpObj.name,
|
|
61
|
+
repoUrl,
|
|
62
|
+
scanner,
|
|
63
|
+
scanFilePath,
|
|
64
|
+
branch,
|
|
65
|
+
apiKey,
|
|
66
|
+
},
|
|
67
|
+
{ skipPrompts }
|
|
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
|
+
} finally {
|
|
75
|
+
tmpObj.removeCallback();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function _scan(
|
|
80
|
+
{ dirname, repoUrl, scanFilePath, branch, apiKey },
|
|
81
|
+
{ skipPrompts = false } = {}
|
|
82
|
+
) {
|
|
83
|
+
debug('start %s %s', dirname, repoUrl);
|
|
84
|
+
|
|
85
|
+
let token = config.get('token');
|
|
86
|
+
debug('token %s', token);
|
|
87
|
+
apiKey ?? debug('token %s', apiKey);
|
|
88
|
+
let gqlClient = new GQLClient(apiKey ? { apiKey } : { token });
|
|
89
|
+
await handleMobbLogin();
|
|
90
|
+
const userInfo = await gqlClient.getUserInfo();
|
|
91
|
+
let { githubToken } = userInfo;
|
|
92
|
+
const isRepoAvailable = await canReachRepo(repoUrl, {
|
|
93
|
+
token: userInfo.githubToken,
|
|
94
|
+
});
|
|
95
|
+
if (!isRepoAvailable) {
|
|
96
|
+
const { token } = await handleGithubIntegration();
|
|
97
|
+
githubToken = token;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const reference =
|
|
101
|
+
branch ?? (await getDefaultBranch(repoUrl, { token: githubToken }));
|
|
102
|
+
const { projectId, organizationId } = await gqlClient.getOrgAndProjectId();
|
|
103
|
+
debug('org id %s', organizationId);
|
|
104
|
+
debug('project id %s', projectId);
|
|
105
|
+
debug('default branch %s', reference);
|
|
106
|
+
const uploadData = await gqlClient.uploadS3BucketInfo();
|
|
107
|
+
|
|
108
|
+
const repositoryRoot = await downloadRepo(
|
|
109
|
+
{
|
|
110
|
+
repoUrl,
|
|
111
|
+
reference,
|
|
112
|
+
dirname,
|
|
113
|
+
},
|
|
114
|
+
{ token: githubToken }
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const reportPath = scanFilePath || (await getReportFromSnyk());
|
|
118
|
+
|
|
119
|
+
const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
|
|
120
|
+
await uploadFile(
|
|
121
|
+
reportPath,
|
|
122
|
+
uploadData.url,
|
|
123
|
+
uploadData.uploadKey,
|
|
124
|
+
uploadData.uploadFields
|
|
125
|
+
);
|
|
126
|
+
const mobbSpinner = createSpinner('🕵️♂️ Initiating Mobb analysis').start();
|
|
127
|
+
try {
|
|
128
|
+
await gqlClient.submitVulnerabilityReport(
|
|
129
|
+
uploadData.fixReportId,
|
|
130
|
+
repoUrl,
|
|
131
|
+
reference,
|
|
132
|
+
projectId
|
|
133
|
+
);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
mobbSpinner.error({ text: '🕵️♂️ Mobb analysis failed' });
|
|
136
|
+
throw e;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
debug('report %o', report);
|
|
140
|
+
|
|
141
|
+
const results = ((report.runs || [])[0] || {}).results || [];
|
|
142
|
+
if (results.length === 0 && !scanFilePath) {
|
|
143
|
+
mobbSpinner.success({
|
|
144
|
+
text: '🕵️♂️ Report did not detect any vulnerabilities — nothing to fix.',
|
|
145
|
+
});
|
|
146
|
+
} else {
|
|
147
|
+
mobbSpinner.success({
|
|
148
|
+
text: '🕵️♂️ Generating fixes...',
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await askToOpenAnalysis();
|
|
152
|
+
}
|
|
153
|
+
process.exit(0);
|
|
154
|
+
async function getReportFromSnyk() {
|
|
155
|
+
const reportPath = path.join(dirname, 'report.json');
|
|
156
|
+
|
|
157
|
+
if (
|
|
158
|
+
!(await getSnykReport(reportPath, repositoryRoot, { skipPrompts }))
|
|
159
|
+
) {
|
|
160
|
+
debug('snyk code is not enabled');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
return reportPath;
|
|
164
|
+
}
|
|
165
|
+
async function askToOpenAnalysis() {
|
|
166
|
+
!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
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function handleMobbLogin() {
|
|
178
|
+
if (
|
|
179
|
+
(token && (await gqlClient.verifyToken())) ||
|
|
180
|
+
(apiKey && (await gqlClient.verifyToken()))
|
|
181
|
+
) {
|
|
182
|
+
createSpinner().start().success({
|
|
183
|
+
text: '🔓 Logged in to Mobb successfully',
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (apiKey && !(await gqlClient.verifyToken())) {
|
|
189
|
+
createSpinner().start().error({
|
|
190
|
+
text: '🔓 Logged in to Mobb failed - check your api-key',
|
|
191
|
+
});
|
|
192
|
+
process.exit(1);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const mobbLoginSpinner = createSpinner().start();
|
|
196
|
+
if (!skipPrompts) {
|
|
197
|
+
mobbLoginSpinner.update({ text: MOBB_LOGIN_REQUIRED_MSG });
|
|
198
|
+
await keypress();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
mobbLoginSpinner.update({
|
|
202
|
+
text: '🔓 Waiting for Mobb login...',
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const loginResponse = await callbackServer(
|
|
206
|
+
webLoginUrl,
|
|
207
|
+
`${webLoginUrl}?done=true`
|
|
208
|
+
);
|
|
209
|
+
token = loginResponse.token;
|
|
210
|
+
|
|
211
|
+
gqlClient = new GQLClient({ token });
|
|
212
|
+
|
|
213
|
+
if (!(await gqlClient.verifyToken())) {
|
|
214
|
+
mobbLoginSpinner.error({
|
|
215
|
+
text: 'Something went wrong, API token is invalid.',
|
|
216
|
+
});
|
|
217
|
+
process.exit(0);
|
|
218
|
+
}
|
|
219
|
+
debug('set token %s', token);
|
|
220
|
+
config.set('token', token);
|
|
221
|
+
mobbLoginSpinner.success({ text: '🔓 Login to Mobb successful!' });
|
|
222
|
+
}
|
|
223
|
+
async function handleGithubIntegration() {
|
|
224
|
+
const addGithubIntegration = skipPrompts
|
|
225
|
+
? true
|
|
226
|
+
: await githubIntegrationPrompt();
|
|
227
|
+
|
|
228
|
+
const githubSpinner = createSpinner(
|
|
229
|
+
'🔗 Waiting for github integration...'
|
|
230
|
+
).start();
|
|
231
|
+
if (!addGithubIntegration) {
|
|
232
|
+
githubSpinner.error();
|
|
233
|
+
throw Error('Could not reach github repo');
|
|
234
|
+
}
|
|
235
|
+
const result = await callbackServer(
|
|
236
|
+
GITHUB_OAUTH_URL,
|
|
237
|
+
`${githubSubmitUrl}?done=true`
|
|
238
|
+
);
|
|
239
|
+
githubSpinner.success({ text: '🔗 Github integration successful!' });
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import { keypress } from '../../utils.mjs';
|
|
3
|
+
import { SCANNERS } from '../../constants.mjs';
|
|
4
|
+
import { createSpinner } from 'nanospinner';
|
|
5
|
+
|
|
6
|
+
export async function choseScanner() {
|
|
7
|
+
const { scanner } = await inquirer.prompt({
|
|
8
|
+
name: 'scanner',
|
|
9
|
+
message: 'Choose a scanner you wish to use to scan your code',
|
|
10
|
+
type: 'list',
|
|
11
|
+
choices: [
|
|
12
|
+
{ name: 'Snyk', value: SCANNERS.Snyk },
|
|
13
|
+
{ name: 'Checkmarx', value: SCANNERS.Checkmarx },
|
|
14
|
+
{ name: 'Codeql', value: SCANNERS.Codeql },
|
|
15
|
+
{ name: 'Fortify', value: SCANNERS.Fortify },
|
|
16
|
+
],
|
|
17
|
+
});
|
|
18
|
+
return scanner;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function snykLoginPrompt() {
|
|
22
|
+
const spinner = createSpinner(
|
|
23
|
+
'🔓 Login to Snyk is required, press any key to continue'
|
|
24
|
+
).start();
|
|
25
|
+
await keypress();
|
|
26
|
+
return spinner.success();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function githubIntegrationPrompt() {
|
|
30
|
+
const answers = await inquirer.prompt({
|
|
31
|
+
name: 'githubConfirm',
|
|
32
|
+
type: 'confirm',
|
|
33
|
+
message:
|
|
34
|
+
"It seems we don't have access to the repo, do you want to grant access to your github account",
|
|
35
|
+
default: true,
|
|
36
|
+
});
|
|
37
|
+
return answers.githubConfirm;
|
|
38
|
+
}
|
|
39
|
+
export async function mobbAnalysisPrompt() {
|
|
40
|
+
const spinner = createSpinner().start();
|
|
41
|
+
spinner.update({ text: 'Hit any key to view available fixes' });
|
|
42
|
+
await keypress();
|
|
43
|
+
return spinner.success();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function snykArticlePrompt() {
|
|
47
|
+
const { snykArticleConfirm } = await inquirer.prompt({
|
|
48
|
+
name: 'snykArticleConfirm',
|
|
49
|
+
type: 'confirm',
|
|
50
|
+
message:
|
|
51
|
+
"Do you want to be taken to the relevant Snyk's online article?",
|
|
52
|
+
default: true,
|
|
53
|
+
});
|
|
54
|
+
return snykArticleConfirm;
|
|
55
|
+
}
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import cp from 'node:child_process';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import readline from 'node:readline';
|
|
4
3
|
import { stdout } from 'colors/lib/system/supports-colors.js';
|
|
5
4
|
import open from 'open';
|
|
6
5
|
import * as process from 'process';
|
|
7
6
|
import Debug from 'debug';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { createSpinner } from 'nanospinner';
|
|
9
|
+
import { snykArticlePrompt } from './prompts.mjs';
|
|
10
|
+
import { keypress } from '../../utils.mjs';
|
|
8
11
|
|
|
9
12
|
const debug = Debug('mobbdev:snyk');
|
|
10
13
|
const require = createRequire(import.meta.url);
|
|
11
14
|
const SNYK_PATH = require.resolve('snyk/bin/snyk');
|
|
12
15
|
|
|
16
|
+
const SNYK_ARTICLE_URL =
|
|
17
|
+
'https://docs.snyk.io/scan-application-code/snyk-code/getting-started-with-snyk-code/activating-snyk-code-using-the-web-ui/step-1-enabling-the-snyk-code-option';
|
|
18
|
+
|
|
13
19
|
debug('snyk executable path %s', SNYK_PATH);
|
|
14
20
|
|
|
15
21
|
async function forkSnyk(args, display) {
|
|
@@ -45,30 +51,33 @@ async function forkSnyk(args, display) {
|
|
|
45
51
|
});
|
|
46
52
|
}
|
|
47
53
|
|
|
48
|
-
async function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return new Promise((resolve) => {
|
|
55
|
-
rl.question(`${questionString} `, (answer) => {
|
|
56
|
-
rl.close();
|
|
57
|
-
resolve(answer);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function getSnykReport(reportPath, repoRoot) {
|
|
54
|
+
export async function getSnykReport(
|
|
55
|
+
reportPath,
|
|
56
|
+
repoRoot,
|
|
57
|
+
{ skipPrompts = false }
|
|
58
|
+
) {
|
|
63
59
|
debug('get snyk report start %s %s', reportPath, repoRoot);
|
|
64
60
|
|
|
65
61
|
const config = await forkSnyk(['config'], false);
|
|
66
|
-
|
|
67
62
|
if (!config.includes('api: ')) {
|
|
63
|
+
const snykLoginSpinner = createSpinner().start();
|
|
64
|
+
if (!skipPrompts) {
|
|
65
|
+
snykLoginSpinner.update({
|
|
66
|
+
text: '🔓 Login to Snyk is required, press any key to continue',
|
|
67
|
+
});
|
|
68
|
+
await keypress();
|
|
69
|
+
}
|
|
70
|
+
snykLoginSpinner.update({
|
|
71
|
+
text: '🔓 Waiting for Snyk login to complete',
|
|
72
|
+
});
|
|
73
|
+
|
|
68
74
|
debug('no token in the config %s', config);
|
|
69
75
|
await forkSnyk(['auth'], true);
|
|
76
|
+
snykLoginSpinner.success({ text: '🔓 Login to Snyk Successful' });
|
|
70
77
|
}
|
|
71
|
-
|
|
78
|
+
const snykSpinner = createSpinner(
|
|
79
|
+
'🔍 Scanning your repo with Snyk '
|
|
80
|
+
).start();
|
|
72
81
|
const out = await forkSnyk(
|
|
73
82
|
['code', 'test', `--sarif-file-output=${reportPath}`, repoRoot],
|
|
74
83
|
true
|
|
@@ -80,18 +89,22 @@ export async function getSnykReport(reportPath, repoRoot) {
|
|
|
80
89
|
)
|
|
81
90
|
) {
|
|
82
91
|
debug('snyk code is not enabled %s', out);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
);
|
|
92
|
+
snykSpinner.error({ text: '🔍 Snyk configuration needed' });
|
|
93
|
+
const answer = await snykArticlePrompt();
|
|
86
94
|
debug('answer %s', answer);
|
|
87
95
|
|
|
88
|
-
if (
|
|
96
|
+
if (answer) {
|
|
89
97
|
debug('opening the browser');
|
|
90
|
-
await open(
|
|
91
|
-
'https://docs.snyk.io/scan-application-code/snyk-code/getting-started-with-snyk-code/activating-snyk-code-using-the-web-ui/step-1-enabling-the-snyk-code-option'
|
|
92
|
-
);
|
|
98
|
+
await open(SNYK_ARTICLE_URL);
|
|
93
99
|
}
|
|
100
|
+
console.log(
|
|
101
|
+
chalk.bgBlue(
|
|
102
|
+
'\nPlease enable Snyk Code in your Snyk account and try again.'
|
|
103
|
+
)
|
|
104
|
+
);
|
|
94
105
|
return false;
|
|
95
106
|
}
|
|
107
|
+
|
|
108
|
+
snykSpinner.success({ text: '🔍 Snyk code scan completed' });
|
|
96
109
|
return true;
|
|
97
110
|
}
|
package/src/utils.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
export const sleep = (ms = 2000) => new Promise((r) => setTimeout(r, ms));
|
|
3
|
+
|
|
4
|
+
export async function keypress() {
|
|
5
|
+
const rl = readline.createInterface({
|
|
6
|
+
input: process.stdin,
|
|
7
|
+
output: process.stdout,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
rl.question('', (answer) => {
|
|
12
|
+
rl.close();
|
|
13
|
+
process.stderr.moveCursor(0, -1);
|
|
14
|
+
process.stderr.clearLine(1);
|
|
15
|
+
resolve(answer);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
package/src/yargs.mjs
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import yargs from 'yargs/yargs';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
import { SCANNERS } from './constants.mjs';
|
|
5
|
+
|
|
6
|
+
const branchOption = {
|
|
7
|
+
alias: 'branch',
|
|
8
|
+
describe: chalk.bold('Branch of the repository'),
|
|
9
|
+
type: 'string',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const repoOption = {
|
|
13
|
+
alias: 'repo',
|
|
14
|
+
demandOption: true,
|
|
15
|
+
describe: chalk.bold('Github repository URL'),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const yesOption = {
|
|
19
|
+
alias: 'yes',
|
|
20
|
+
describe: chalk.bold('Skip prompts and use default values'),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const apiKeyOption = {
|
|
24
|
+
describe: chalk.bold('Mobb authentication api-key'),
|
|
25
|
+
type: 'string',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const parseArgs = (args) => {
|
|
29
|
+
const yargsInstance = yargs(args);
|
|
30
|
+
return yargsInstance
|
|
31
|
+
.updateStrings({
|
|
32
|
+
'Commands:': chalk.yellow.underline.bold('Commands:'),
|
|
33
|
+
'Options:': chalk.yellow.underline.bold('Options:'),
|
|
34
|
+
'Examples:': chalk.yellow.underline.bold('Examples:'),
|
|
35
|
+
'Show help': chalk.bold('Show help'),
|
|
36
|
+
})
|
|
37
|
+
.usage(
|
|
38
|
+
`${chalk.bold(
|
|
39
|
+
'\n Bugsy - Trusted, Automatic Vulnerability Fixer 🕵️♂️\n\n'
|
|
40
|
+
)} ${chalk.yellow.underline.bold('Usage:')} \n $0 ${chalk.green(
|
|
41
|
+
'<command>'
|
|
42
|
+
)} ${chalk.dim('[options]')}
|
|
43
|
+
`
|
|
44
|
+
)
|
|
45
|
+
.version(false)
|
|
46
|
+
.command({
|
|
47
|
+
//
|
|
48
|
+
command: 'scan',
|
|
49
|
+
describe: chalk.bold(
|
|
50
|
+
'Scan your code for vulnerabilities, get automated fixes right away.'
|
|
51
|
+
),
|
|
52
|
+
builder: (yargs) => {
|
|
53
|
+
return yargs.options({
|
|
54
|
+
r: repoOption,
|
|
55
|
+
b: branchOption,
|
|
56
|
+
s: {
|
|
57
|
+
alias: 'scanner',
|
|
58
|
+
choices: Object.values(SCANNERS),
|
|
59
|
+
describe: chalk.bold('Select the scanner to use'),
|
|
60
|
+
},
|
|
61
|
+
y: yesOption,
|
|
62
|
+
['api-key']: apiKeyOption,
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
.command({
|
|
67
|
+
command: 'analyze',
|
|
68
|
+
describe: chalk.bold(
|
|
69
|
+
'Provide a vulnerability report and relevant code repository, get automated fixes right away.'
|
|
70
|
+
),
|
|
71
|
+
builder: (yargs) => {
|
|
72
|
+
return yargs.options({
|
|
73
|
+
f: {
|
|
74
|
+
alias: 'scan-file',
|
|
75
|
+
demandOption: true,
|
|
76
|
+
describe: chalk.bold(
|
|
77
|
+
'Select the vulnerability report to analyze'
|
|
78
|
+
),
|
|
79
|
+
},
|
|
80
|
+
r: repoOption,
|
|
81
|
+
b: branchOption,
|
|
82
|
+
y: yesOption,
|
|
83
|
+
['api-key']: apiKeyOption,
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
.example('$0 scan -r https://github.com/WebGoat/WebGoat')
|
|
88
|
+
.command({
|
|
89
|
+
command: '*',
|
|
90
|
+
handler() {
|
|
91
|
+
yargsInstance.showHelp();
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
.help('h')
|
|
95
|
+
.alias('h', 'help')
|
|
96
|
+
.epilog(chalk.bgBlue('Made with ❤️ by Mobb'))
|
|
97
|
+
.showHelpOnFail(true)
|
|
98
|
+
.wrap(Math.min(120, yargsInstance.terminalWidth()))
|
|
99
|
+
.parse();
|
|
100
|
+
};
|
package/src/github.mjs
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import stream from 'node:stream';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { promisify } from 'node:util';
|
|
5
|
-
import fetch from 'node-fetch';
|
|
6
|
-
import extract from 'extract-zip';
|
|
7
|
-
import Debug from 'debug';
|
|
8
|
-
|
|
9
|
-
const pipeline = promisify(stream.pipeline);
|
|
10
|
-
const debug = Debug('mobbdev:github');
|
|
11
|
-
|
|
12
|
-
export async function getDefaultBranch(repoUrl) {
|
|
13
|
-
debug('get default branch %s', repoUrl);
|
|
14
|
-
let slug = repoUrl.replace(/https?:\/\/github\.com\//i, '');
|
|
15
|
-
|
|
16
|
-
if (slug.endsWith('/')) {
|
|
17
|
-
slug = slug.substring(0, slug.length - 1);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (slug.endsWith('.git')) {
|
|
21
|
-
slug = slug.substring(0, slug.length - '.git'.length);
|
|
22
|
-
}
|
|
23
|
-
debug('slug %s', slug);
|
|
24
|
-
|
|
25
|
-
const response = await fetch(`https://api.github.com/repos/${slug}`, {
|
|
26
|
-
method: 'GET',
|
|
27
|
-
headers: {
|
|
28
|
-
Accept: 'application/vnd.github+json',
|
|
29
|
-
'X-GitHub-Api-Version': '2022-11-28',
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
if (!response.ok) {
|
|
34
|
-
debug('GH request failed %s %s', response.body, response.status);
|
|
35
|
-
throw new Error(
|
|
36
|
-
`Can't get default branch, make sure the repository is public: ${repoUrl}.`
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const repoInfo = await response.json();
|
|
41
|
-
debug('GH request ok %o', repoInfo);
|
|
42
|
-
|
|
43
|
-
return repoInfo.default_branch;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export async function downloadRepo(repoUrl, reference, dirname) {
|
|
47
|
-
debug('download repo %s %s %s', repoUrl, reference, dirname);
|
|
48
|
-
const zipFilePath = path.join(dirname, 'repo.zip');
|
|
49
|
-
const response = await fetch(`${repoUrl}/zipball/${reference}`);
|
|
50
|
-
|
|
51
|
-
if (!response.ok) {
|
|
52
|
-
debug(
|
|
53
|
-
'GH zipball request failed %s %s',
|
|
54
|
-
response.body,
|
|
55
|
-
response.status
|
|
56
|
-
);
|
|
57
|
-
throw new Error(
|
|
58
|
-
`Can't access the repository, make it is public: ${repoUrl}.`
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const fileWriterStream = fs.createWriteStream(zipFilePath);
|
|
63
|
-
|
|
64
|
-
await pipeline(response.body, fileWriterStream);
|
|
65
|
-
await extract(zipFilePath, { dir: dirname });
|
|
66
|
-
|
|
67
|
-
const repoRoot = fs
|
|
68
|
-
.readdirSync(dirname, { withFileTypes: true })
|
|
69
|
-
.filter((dirent) => dirent.isDirectory())
|
|
70
|
-
.map((dirent) => dirent.name)[0];
|
|
71
|
-
debug('repo root %s', repoRoot);
|
|
72
|
-
|
|
73
|
-
return path.join(dirname, repoRoot);
|
|
74
|
-
}
|
package/src/index.mjs
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { fileURLToPath } from 'node:url';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import semver from 'semver';
|
|
5
|
-
import open from 'open';
|
|
6
|
-
import Configstore from 'configstore';
|
|
7
|
-
import Debug from 'debug';
|
|
8
|
-
import { GQLClient } from './gql.mjs';
|
|
9
|
-
import { webLogin } from './web-login.mjs';
|
|
10
|
-
import { downloadRepo, getDefaultBranch } from './github.mjs';
|
|
11
|
-
import { getSnykReport } from './snyk.mjs';
|
|
12
|
-
import { uploadFile } from './upload-file.mjs';
|
|
13
|
-
import { WEB_APP_URL } from './constants.mjs';
|
|
14
|
-
|
|
15
|
-
const debug = Debug('mobbdev:index');
|
|
16
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
-
const packageJson = JSON.parse(
|
|
18
|
-
fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8')
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
if (!semver.satisfies(process.version, packageJson.engines.node)) {
|
|
22
|
-
console.error(
|
|
23
|
-
`${packageJson.name} requires node version ${packageJson.engines.node}, but running ${process.version}.`
|
|
24
|
-
);
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const config = new Configstore(packageJson.name, { token: '' });
|
|
29
|
-
debug('config %o', config);
|
|
30
|
-
|
|
31
|
-
export async function main(dirname, repoUrl) {
|
|
32
|
-
debug('start %s %s', dirname, repoUrl);
|
|
33
|
-
|
|
34
|
-
if (!repoUrl) {
|
|
35
|
-
console.warn(
|
|
36
|
-
'Mobb CLI usage: npx mobbdev <public GitHub repository URL>'
|
|
37
|
-
);
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let token = config.get('token');
|
|
42
|
-
debug('token %s', token);
|
|
43
|
-
let gqlClient = new GQLClient(token);
|
|
44
|
-
|
|
45
|
-
if (!token || !(await gqlClient.verifyToken())) {
|
|
46
|
-
console.log(
|
|
47
|
-
'You will be redirected to our login page, once the authorization is complete, return to this prompt.'
|
|
48
|
-
);
|
|
49
|
-
token = await webLogin();
|
|
50
|
-
gqlClient = new GQLClient(token);
|
|
51
|
-
|
|
52
|
-
if (!(await gqlClient.verifyToken())) {
|
|
53
|
-
console.error('Something went wrong, API token is invalid.');
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
debug('set token %s', token);
|
|
57
|
-
config.set('token', token);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const { projectId, organizationId } = await gqlClient.getOrgAndProjectId();
|
|
61
|
-
debug('org id %s', organizationId);
|
|
62
|
-
debug('project id %s', projectId);
|
|
63
|
-
const reference = await getDefaultBranch(repoUrl);
|
|
64
|
-
debug('default branch %s', reference);
|
|
65
|
-
const uploadData = await gqlClient.uploadS3BucketInfo();
|
|
66
|
-
|
|
67
|
-
const repositoryRoot = await downloadRepo(repoUrl, reference, dirname);
|
|
68
|
-
const reportPath = path.join(dirname, 'report.json');
|
|
69
|
-
|
|
70
|
-
console.log(
|
|
71
|
-
'We will run Snyk CLI to scan the repository for vulnerabilities. You may be redirected to the login page in the process.'
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
if (!(await getSnykReport(reportPath, repositoryRoot))) {
|
|
75
|
-
debug('snyk code is not enabled');
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
|
|
80
|
-
|
|
81
|
-
await uploadFile(
|
|
82
|
-
reportPath,
|
|
83
|
-
uploadData.url,
|
|
84
|
-
uploadData.uploadKey,
|
|
85
|
-
uploadData.uploadFields
|
|
86
|
-
);
|
|
87
|
-
await gqlClient.submitVulnerabilityReport(
|
|
88
|
-
uploadData.fixReportId,
|
|
89
|
-
repoUrl,
|
|
90
|
-
reference,
|
|
91
|
-
projectId
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
debug('report %o', report);
|
|
95
|
-
|
|
96
|
-
const results = ((report.runs || [])[0] || {}).results || [];
|
|
97
|
-
if (results.length === 0) {
|
|
98
|
-
console.log('Snyk has not found any vulnerabilities — nothing to fix.');
|
|
99
|
-
} else {
|
|
100
|
-
console.log(
|
|
101
|
-
'You will be redirected to our report page, please wait until the analysis is finished and enjoy your fixes.'
|
|
102
|
-
);
|
|
103
|
-
await open(
|
|
104
|
-
`${WEB_APP_URL}/organization/${organizationId}/project/${projectId}/report/${uploadData.fixReportId}`
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
File without changes
|