mobbdev 0.0.23 → 0.0.28
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 +15 -7
- package/index.mjs +6 -1
- package/package.json +5 -1
- package/src/commands/index.mjs +34 -28
- 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 +70 -18
- package/src/features/analysis/pack.mjs +31 -0
- package/src/features/analysis/upload-file.mjs +13 -6
- package/src/yargs.mjs +50 -18
package/README.md
CHANGED
|
@@ -1,26 +1,34 @@
|
|
|
1
1
|
# Bugsy
|
|
2
2
|
|
|
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
|
|
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="
|
|
5
|
+
<img width="1888" alt="Bugsy" src="./img/bugsy.png">
|
|
6
6
|
|
|
7
7
|
## What is [Mobb](https://www.mobb.dev)?
|
|
8
8
|
|
|
9
|
-
[Mobb](https://www.mobb.dev) is the first vendor-agnostic automatic security vulnerability remediation tool. It ingests SAST results from Checkmarx, GitHub Advanced Security, and Snyk and produces code fixes for developers to review and commit to their code.
|
|
9
|
+
[Mobb](https://www.mobb.dev) is the first vendor-agnostic automatic security vulnerability remediation tool. It ingests SAST results from Checkmarx, CodeQL (GitHub Advanced Security), OpenText Fortify, and Snyk and produces code fixes for developers to review and commit to their code.
|
|
10
10
|
|
|
11
11
|
## What does Bugsy do?
|
|
12
12
|
|
|
13
|
+
Bugsy has two modes - Analyze (the user has a pre-generated SAST report from one of the supported SAST tools) and Scan (no SAST report needed).
|
|
14
|
+
|
|
15
|
+
Scan
|
|
16
|
+
|
|
13
17
|
- Uses Snyk CLI tool to run a SAST analysis on a given open-source GitHub repo
|
|
14
18
|
- Analyzes the vulnerability report to identify issues that can be remediated automatically
|
|
15
19
|
- Produces the code fixes and redirects the user to the fix report page on the Mobb platform
|
|
16
20
|
|
|
21
|
+
Analyze
|
|
22
|
+
|
|
23
|
+
- Analyzes the vulnerability report to identify issues that can be remediated automatically
|
|
24
|
+
- Produces the code fixes and redirects the user to the fix report page on the Mobb platform
|
|
25
|
+
|
|
17
26
|
## Disclaimer
|
|
18
27
|
|
|
19
|
-
This is a community edition version that only analyzes public GitHub repositories.
|
|
28
|
+
This is a community edition version that only analyzes public GitHub repositories. Analyzing private repositories is allowed for a limited amount of time.
|
|
20
29
|
Snyk CLI is used to produce a SAST vulnerability report.
|
|
21
30
|
|
|
22
|
-
- Only Java projects are supported at the moment.
|
|
23
|
-
- Only SQLi, CMDi, XSS, XXE, and Path Traversal are currently supported.
|
|
31
|
+
- Only Java and Node.js projects are supported at the moment.
|
|
24
32
|
|
|
25
33
|
## Usage
|
|
26
34
|
|
|
@@ -34,4 +42,4 @@ Bugsy will automatically generate a fix for each supported vulnerability identif
|
|
|
34
42
|
|
|
35
43
|
## Getting support
|
|
36
44
|
|
|
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://
|
|
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,4 @@
|
|
|
1
|
-
import { analyze, scan } from './src/commands/index.mjs';
|
|
1
|
+
import { analyze, scan, CliError } from './src/commands/index.mjs';
|
|
2
2
|
import { parseArgs } from './src/yargs.mjs';
|
|
3
3
|
import { hideBin } from 'yargs/helpers';
|
|
4
4
|
|
|
@@ -20,6 +20,11 @@ async function run() {
|
|
|
20
20
|
await run();
|
|
21
21
|
process.exit(0);
|
|
22
22
|
} catch (err) {
|
|
23
|
+
if (err instanceof CliError) {
|
|
24
|
+
console.error(err.message);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
// unexpected error - print stack trace
|
|
23
28
|
console.error(
|
|
24
29
|
'Something went wrong, please try again or contact support if issue persists.'
|
|
25
30
|
);
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobbdev",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.28",
|
|
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
|
@@ -11,6 +11,7 @@ import path from 'path';
|
|
|
11
11
|
const GITHUB_REPO_URL_PATTERN = new RegExp(
|
|
12
12
|
'https://github.com/[\\w-]+/[\\w-]+'
|
|
13
13
|
);
|
|
14
|
+
export class CliError extends Error {}
|
|
14
15
|
|
|
15
16
|
const UrlZ = z
|
|
16
17
|
.string({
|
|
@@ -20,45 +21,53 @@ const UrlZ = z
|
|
|
20
21
|
message: 'is not a valid github URL',
|
|
21
22
|
});
|
|
22
23
|
|
|
23
|
-
function
|
|
24
|
+
function throwRepoUrlErrorMessage({ error, repoUrl, command }) {
|
|
24
25
|
const errorMessage = error.issues[error.issues.length - 1].message;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
)
|
|
26
|
+
const formattedErrorMessage = `\nError: ${chalk.bold(
|
|
27
|
+
repoUrl
|
|
28
|
+
)} is ${errorMessage}
|
|
29
|
+
Example: \n\tmobbdev ${command} -r ${chalk.bold(
|
|
30
|
+
'https://github.com/WebGoat/WebGoat'
|
|
31
|
+
)}`;
|
|
32
|
+
throw new CliError(formattedErrorMessage);
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export async function analyze(
|
|
34
|
-
{ repo, scanFile,
|
|
36
|
+
{ repo, scanFile, ref, apiKey, ci, commitHash, srcPath },
|
|
35
37
|
{ skipPrompts = false } = {}
|
|
36
38
|
) {
|
|
37
39
|
const { success, error } = UrlZ.safeParse(repo);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
|
|
41
|
+
if (!success && !srcPath) {
|
|
42
|
+
throwRepoUrlErrorMessage({
|
|
40
43
|
error,
|
|
41
44
|
repoUrl: repo,
|
|
42
45
|
command: 'analyze',
|
|
43
46
|
});
|
|
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
47
|
}
|
|
48
|
+
|
|
52
49
|
if (!fs.existsSync(scanFile)) {
|
|
53
|
-
|
|
54
|
-
process.exit(1);
|
|
50
|
+
throw new CliError(`\nCan't access ${chalk.bold(scanFile)}`);
|
|
55
51
|
}
|
|
52
|
+
|
|
56
53
|
if (path.extname(scanFile) !== '.json') {
|
|
57
|
-
|
|
58
|
-
process.exit(1);
|
|
54
|
+
throw new CliError(`\n${chalk.bold(scanFile)} is not a json file`);
|
|
59
55
|
}
|
|
56
|
+
|
|
60
57
|
!ci && (await showWelcomeMessage(skipPrompts));
|
|
61
|
-
|
|
58
|
+
|
|
59
|
+
await runAnalysis(
|
|
60
|
+
{
|
|
61
|
+
repo,
|
|
62
|
+
scanFile,
|
|
63
|
+
ref,
|
|
64
|
+
apiKey,
|
|
65
|
+
ci,
|
|
66
|
+
commitHash,
|
|
67
|
+
srcPath,
|
|
68
|
+
},
|
|
69
|
+
{ skipPrompts }
|
|
70
|
+
);
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
export async function scan(
|
|
@@ -67,23 +76,20 @@ export async function scan(
|
|
|
67
76
|
) {
|
|
68
77
|
const { success, error } = UrlZ.safeParse(repo);
|
|
69
78
|
if (ci && !apiKey) {
|
|
70
|
-
|
|
79
|
+
throw new CliError(
|
|
71
80
|
'\nError: --ci flag requires --api-key to be provided as well'
|
|
72
81
|
);
|
|
73
|
-
process.exit(1);
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
if (!success) {
|
|
77
|
-
|
|
78
|
-
process.exit(1);
|
|
85
|
+
throwRepoUrlErrorMessage({ error, repoUrl: repo, command: 'scan' });
|
|
79
86
|
}
|
|
80
87
|
!ci && (await showWelcomeMessage(skipPrompts));
|
|
81
88
|
scanner ??= scanner || (await choseScanner());
|
|
82
89
|
if (scanner !== SCANNERS.Snyk) {
|
|
83
|
-
|
|
90
|
+
throw new CliError(
|
|
84
91
|
'Vulnerability scanning via Bugsy is available only with Snyk at the moment. Additional scanners will follow soon.'
|
|
85
92
|
);
|
|
86
|
-
process.exit(1);
|
|
87
93
|
}
|
|
88
94
|
await runAnalysis({ repo, scanner, branch, apiKey }, { skipPrompts });
|
|
89
95
|
}
|
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
|
}
|
|
@@ -8,22 +8,21 @@ import open from 'open';
|
|
|
8
8
|
import semver from 'semver';
|
|
9
9
|
import { callbackServer } from './callback-server.mjs';
|
|
10
10
|
import tmp from 'tmp';
|
|
11
|
+
import { CliError } from '../../commands/index.mjs';
|
|
11
12
|
|
|
12
13
|
import { WEB_APP_URL } from '../../constants.mjs';
|
|
13
|
-
import {
|
|
14
|
-
canReachRepo,
|
|
15
|
-
downloadRepo,
|
|
16
|
-
getDefaultBranch,
|
|
17
|
-
GITHUB_OAUTH_URL,
|
|
18
|
-
} from './github.mjs';
|
|
14
|
+
import { canReachRepo, downloadRepo, getDefaultBranch } from './github.mjs';
|
|
19
15
|
import { GQLClient } from './gql.mjs';
|
|
20
16
|
import { githubIntegrationPrompt, mobbAnalysisPrompt } from './prompts.mjs';
|
|
21
17
|
import { getSnykReport } from './snyk.mjs';
|
|
22
18
|
import { uploadFile } from './upload-file.mjs';
|
|
23
19
|
import { keypress, Spinner } from '../../utils.mjs';
|
|
20
|
+
import { pack } from './pack.mjs';
|
|
21
|
+
import { getGitInfo } from './git.mjs';
|
|
24
22
|
|
|
25
23
|
const webLoginUrl = `${WEB_APP_URL}/cli-login`;
|
|
26
24
|
const githubSubmitUrl = `${WEB_APP_URL}/gh-callback`;
|
|
25
|
+
const githubAuthUrl = `${WEB_APP_URL}/github-auth`;
|
|
27
26
|
|
|
28
27
|
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
28
|
'press any key to continue'
|
|
@@ -40,17 +39,16 @@ const packageJson = JSON.parse(
|
|
|
40
39
|
);
|
|
41
40
|
|
|
42
41
|
if (!semver.satisfies(process.version, packageJson.engines.node)) {
|
|
43
|
-
|
|
42
|
+
throw new CliError(
|
|
44
43
|
`${packageJson.name} requires node version ${packageJson.engines.node}, but running ${process.version}.`
|
|
45
44
|
);
|
|
46
|
-
process.exit(1);
|
|
47
45
|
}
|
|
48
46
|
|
|
49
47
|
const config = new Configstore(packageJson.name, { token: '' });
|
|
50
48
|
debug('config %o', config);
|
|
51
49
|
|
|
52
50
|
export async function runAnalysis(
|
|
53
|
-
{ repo, scanner, scanFile,
|
|
51
|
+
{ repo, scanner, scanFile, apiKey, ci, commitHash, srcPath, ref },
|
|
54
52
|
options
|
|
55
53
|
) {
|
|
56
54
|
try {
|
|
@@ -60,9 +58,11 @@ export async function runAnalysis(
|
|
|
60
58
|
repo,
|
|
61
59
|
scanner,
|
|
62
60
|
scanFile,
|
|
63
|
-
|
|
61
|
+
ref,
|
|
64
62
|
apiKey,
|
|
65
63
|
ci,
|
|
64
|
+
srcPath,
|
|
65
|
+
commitHash,
|
|
66
66
|
},
|
|
67
67
|
options
|
|
68
68
|
);
|
|
@@ -72,7 +72,7 @@ export async function runAnalysis(
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
export async function _scan(
|
|
75
|
-
{ dirname, repo, scanFile, branch, apiKey, ci },
|
|
75
|
+
{ dirname, repo, scanFile, branch, apiKey, ci, srcPath, commitHash, ref },
|
|
76
76
|
{ skipPrompts = false } = {}
|
|
77
77
|
) {
|
|
78
78
|
debug('start %s %s', dirname, repo);
|
|
@@ -83,23 +83,34 @@ export async function _scan(
|
|
|
83
83
|
apiKey ?? debug('token %s', apiKey);
|
|
84
84
|
let gqlClient = new GQLClient(apiKey ? { apiKey } : { token });
|
|
85
85
|
await handleMobbLogin();
|
|
86
|
+
const { projectId, organizationId } = await gqlClient.getOrgAndProjectId();
|
|
87
|
+
const uploadData = await gqlClient.uploadS3BucketInfo();
|
|
88
|
+
let reportPath = scanFile;
|
|
89
|
+
|
|
90
|
+
if (srcPath) {
|
|
91
|
+
return await uploadExistingRepo();
|
|
92
|
+
}
|
|
93
|
+
|
|
86
94
|
const userInfo = await gqlClient.getUserInfo();
|
|
87
95
|
let { githubToken } = userInfo;
|
|
88
96
|
const isRepoAvailable = await canReachRepo(repo, {
|
|
89
97
|
token: userInfo.githubToken,
|
|
90
98
|
});
|
|
91
99
|
if (!isRepoAvailable) {
|
|
100
|
+
if (ci) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Cannot access repo ${repo} with the provided token, please visit ${githubAuthUrl} to refresh your Github token`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
92
105
|
const { token } = await handleGithubIntegration();
|
|
93
106
|
githubToken = token;
|
|
94
107
|
}
|
|
95
108
|
|
|
96
109
|
const reference =
|
|
97
|
-
|
|
98
|
-
const { projectId, organizationId } = await gqlClient.getOrgAndProjectId();
|
|
110
|
+
ref ?? (await getDefaultBranch(repo, { token: githubToken }));
|
|
99
111
|
debug('org id %s', organizationId);
|
|
100
112
|
debug('project id %s', projectId);
|
|
101
113
|
debug('default branch %s', reference);
|
|
102
|
-
const uploadData = await gqlClient.uploadS3BucketInfo();
|
|
103
114
|
|
|
104
115
|
const repositoryRoot = await downloadRepo(
|
|
105
116
|
{
|
|
@@ -111,7 +122,9 @@ export async function _scan(
|
|
|
111
122
|
{ token: githubToken }
|
|
112
123
|
);
|
|
113
124
|
|
|
114
|
-
|
|
125
|
+
if (!reportPath) {
|
|
126
|
+
reportPath = await getReportFromSnyk();
|
|
127
|
+
}
|
|
115
128
|
|
|
116
129
|
const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
|
|
117
130
|
await uploadFile(
|
|
@@ -188,7 +201,7 @@ export async function _scan(
|
|
|
188
201
|
createSpinner().start().error({
|
|
189
202
|
text: '🔓 Logged in to Mobb failed - check your api-key',
|
|
190
203
|
});
|
|
191
|
-
|
|
204
|
+
throw new CliError();
|
|
192
205
|
}
|
|
193
206
|
const mobbLoginSpinner = createSpinner().start();
|
|
194
207
|
if (!skipPrompts) {
|
|
@@ -212,7 +225,7 @@ export async function _scan(
|
|
|
212
225
|
mobbLoginSpinner.error({
|
|
213
226
|
text: 'Something went wrong, API token is invalid.',
|
|
214
227
|
});
|
|
215
|
-
|
|
228
|
+
throw new CliError();
|
|
216
229
|
}
|
|
217
230
|
debug('set token %s', token);
|
|
218
231
|
config.set('token', token);
|
|
@@ -231,10 +244,49 @@ export async function _scan(
|
|
|
231
244
|
throw Error('Could not reach github repo');
|
|
232
245
|
}
|
|
233
246
|
const result = await callbackServer(
|
|
234
|
-
|
|
247
|
+
githubAuthUrl,
|
|
235
248
|
`${githubSubmitUrl}?done=true`
|
|
236
249
|
);
|
|
237
250
|
githubSpinner.success({ text: '🔗 Github integration successful!' });
|
|
238
251
|
return result;
|
|
239
252
|
}
|
|
253
|
+
async function uploadExistingRepo() {
|
|
254
|
+
const gitInfo = await getGitInfo(srcPath);
|
|
255
|
+
const zipBuffer = await pack(srcPath);
|
|
256
|
+
|
|
257
|
+
await uploadFile(
|
|
258
|
+
reportPath,
|
|
259
|
+
uploadData.url,
|
|
260
|
+
uploadData.uploadKey,
|
|
261
|
+
uploadData.uploadFields
|
|
262
|
+
);
|
|
263
|
+
await uploadFile(
|
|
264
|
+
zipBuffer,
|
|
265
|
+
uploadData.repoUrl,
|
|
266
|
+
uploadData.repoUploadKey,
|
|
267
|
+
uploadData.repoUploadFields
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const mobbSpinner = createSpinner(
|
|
271
|
+
'🕵️♂️ Initiating Mobb analysis'
|
|
272
|
+
).start();
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
await gqlClient.submitVulnerabilityReport(
|
|
276
|
+
uploadData.fixReportId,
|
|
277
|
+
repo || gitInfo.repoUrl,
|
|
278
|
+
branch || gitInfo.reference,
|
|
279
|
+
projectId,
|
|
280
|
+
commitHash || gitInfo.hash
|
|
281
|
+
);
|
|
282
|
+
} catch (e) {
|
|
283
|
+
mobbSpinner.error({ text: '🕵️♂️ Mobb analysis failed' });
|
|
284
|
+
throw e;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
mobbSpinner.success({
|
|
288
|
+
text: '🕵️♂️ Generating fixes...',
|
|
289
|
+
});
|
|
290
|
+
await askToOpenAnalysis();
|
|
291
|
+
}
|
|
240
292
|
}
|
|
@@ -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/yargs.mjs
CHANGED
|
@@ -3,9 +3,20 @@ import chalk from 'chalk';
|
|
|
3
3
|
|
|
4
4
|
import { SCANNERS } from './constants.mjs';
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const refOption = {
|
|
7
|
+
describe: chalk.bold('reference of the repository (branch, tag, commit)'),
|
|
8
|
+
type: 'string',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const srcPathOption = {
|
|
12
|
+
alias: 'src-path',
|
|
13
|
+
describe: chalk.bold('Path to the repository folder with the source code'),
|
|
14
|
+
type: 'string',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const commitHash = {
|
|
18
|
+
alias: 'commit-hash',
|
|
19
|
+
describe: chalk.bold('Hash of the commit'),
|
|
9
20
|
type: 'string',
|
|
10
21
|
};
|
|
11
22
|
|
|
@@ -59,7 +70,7 @@ export const parseArgs = (args) => {
|
|
|
59
70
|
builder: (yargs) => {
|
|
60
71
|
return yargs.options({
|
|
61
72
|
r: repoOption,
|
|
62
|
-
|
|
73
|
+
ref: refOption,
|
|
63
74
|
s: {
|
|
64
75
|
alias: 'scanner',
|
|
65
76
|
choices: Object.values(SCANNERS),
|
|
@@ -76,20 +87,41 @@ export const parseArgs = (args) => {
|
|
|
76
87
|
'Provide a vulnerability report and relevant code repository, get automated fixes right away.'
|
|
77
88
|
),
|
|
78
89
|
builder: (yargs) => {
|
|
79
|
-
return yargs
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
return yargs
|
|
91
|
+
.options({
|
|
92
|
+
f: {
|
|
93
|
+
alias: 'scan-file',
|
|
94
|
+
demandOption: true,
|
|
95
|
+
describe: chalk.bold(
|
|
96
|
+
'Select the vulnerability report to analyze'
|
|
97
|
+
),
|
|
98
|
+
},
|
|
99
|
+
r: {
|
|
100
|
+
...repoOption,
|
|
101
|
+
demandOption: false,
|
|
102
|
+
},
|
|
103
|
+
p: srcPathOption,
|
|
104
|
+
ref: refOption,
|
|
105
|
+
ch: commitHash,
|
|
106
|
+
y: yesOption,
|
|
107
|
+
['api-key']: apiKeyOption,
|
|
108
|
+
ci: ciOption,
|
|
109
|
+
})
|
|
110
|
+
.check((argv) => {
|
|
111
|
+
if (!argv.srcPath && !argv.repo) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
'You must supply either --src-path or --repo'
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (argv.ci && !argv.apiKey) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
'--ci flag requires --api-key to be provided as well'
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return true;
|
|
124
|
+
});
|
|
93
125
|
},
|
|
94
126
|
})
|
|
95
127
|
.example('$0 scan -r https://github.com/WebGoat/WebGoat')
|