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 +2 -2
- package/index.mjs +6 -0
- package/package.json +5 -1
- package/src/commands/index.mjs +32 -32
- package/src/constants.mjs +0 -2
- package/src/features/analysis/git.mjs +50 -0
- package/src/features/analysis/github.mjs +0 -5
- package/src/features/analysis/gql.mjs +18 -2
- package/src/features/analysis/index.mjs +74 -33
- package/src/features/analysis/pack.mjs +31 -0
- package/src/features/analysis/upload-file.mjs +13 -6
- package/src/utils.mjs +2 -0
- package/src/yargs.mjs +70 -19
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="
|
|
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://
|
|
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.
|
|
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"
|
package/src/commands/index.mjs
CHANGED
|
@@ -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
|
|
22
|
+
function throwRepoUrlErrorMessage({ error, repoUrl, command }) {
|
|
24
23
|
const errorMessage = error.issues[error.issues.length - 1].message;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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,
|
|
34
|
+
{ repo, scanFile, ref, apiKey, ci, commitHash, srcPath },
|
|
35
35
|
{ skipPrompts = false } = {}
|
|
36
36
|
) {
|
|
37
37
|
const { success, error } = UrlZ.safeParse(repo);
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
|
34
|
+
throw new Error(`Failed to upload the file: ${response.status}`);
|
|
28
35
|
}
|
|
29
|
-
debug('upload
|
|
36
|
+
debug('upload file done');
|
|
30
37
|
}
|
package/src/utils.mjs
CHANGED
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
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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')
|