mobbdev 0.0.1
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 +4 -0
- package/README.md +17 -0
- package/bin/cli.mjs +2 -0
- package/index.mjs +17 -0
- package/package.json +44 -0
- package/src/constants.mjs +18 -0
- package/src/github.mjs +61 -0
- package/src/gql.mjs +122 -0
- package/src/index.mjs +76 -0
- package/src/snyk.mjs +41 -0
- package/src/upload-file.mjs +19 -0
- package/src/web-login.mjs +47 -0
package/.env
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# [mobb.dev](https://www.mobb.dev)
|
|
2
|
+
|
|
3
|
+
Give your developers time back with fixes you can trust.
|
|
4
|
+
|
|
5
|
+
Turn mountains of depressing SAST findings into code fixes with Mobb's developer-first automated secure code remediation.
|
|
6
|
+
|
|
7
|
+
## Disclaimer
|
|
8
|
+
|
|
9
|
+
This is a demo version only capable of analyzing public GitHub repositories. We wrap Snyk Code CLI to produce SAST vulnerabilities report.
|
|
10
|
+
|
|
11
|
+
Only Java projects are supported at the moment.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```shell
|
|
16
|
+
npx mobb https://github.com/mobb-dev/simple-vulnerable-java-project
|
|
17
|
+
```
|
package/bin/cli.mjs
ADDED
package/index.mjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { main } from './src/index.mjs';
|
|
2
|
+
import tmp from 'tmp';
|
|
3
|
+
|
|
4
|
+
const tmpObj = tmp.dirSync({
|
|
5
|
+
unsafeCleanup: true,
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
main(tmpObj.name, process.argv.at(2))
|
|
9
|
+
.catch((err) => {
|
|
10
|
+
console.error(
|
|
11
|
+
'Something went wrong, please try again or contact support if issue persists.'
|
|
12
|
+
);
|
|
13
|
+
console.error(err);
|
|
14
|
+
})
|
|
15
|
+
.then(() => {
|
|
16
|
+
tmpObj.removeCallback();
|
|
17
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mobbdev",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Automated secure code remediation tool",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"lint": "prettier --check . && eslint **/*.mjs",
|
|
8
|
+
"lint:fix": "prettier --write . && eslint --fix **/*.mjs",
|
|
9
|
+
"test": "DOTENV_ME=${ENV_VAULT_CLI} dotenv-vault pull development .env && TOKEN=$(../../scripts/login_auth0.sh) NODE_OPTIONS=--experimental-vm-modules jest",
|
|
10
|
+
"prepack": "dotenv-vault pull production .env"
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"mobb": "bin/cli.mjs"
|
|
14
|
+
},
|
|
15
|
+
"author": "",
|
|
16
|
+
"license": "ISC",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"configstore": "6.0.0",
|
|
19
|
+
"dotenv": "16.0.3",
|
|
20
|
+
"extract-zip": "2.0.1",
|
|
21
|
+
"form-data": "4.0.0",
|
|
22
|
+
"got": "12.6.0",
|
|
23
|
+
"open": "8.4.2",
|
|
24
|
+
"snyk": "1.1118.0",
|
|
25
|
+
"tmp": "0.2.1",
|
|
26
|
+
"zod": "3.21.4"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@jest/globals": "29.5.0",
|
|
30
|
+
"eslint": "8.36.0",
|
|
31
|
+
"jest": "29.5.0",
|
|
32
|
+
"prettier": "2.8.4"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=8.5.0"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"src",
|
|
39
|
+
"index.mjs",
|
|
40
|
+
".env",
|
|
41
|
+
"README.md",
|
|
42
|
+
"package.json"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as dotenv from 'dotenv';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
dotenv.config();
|
|
5
|
+
|
|
6
|
+
const envVariablesSchema = z
|
|
7
|
+
.object({
|
|
8
|
+
WEB_LOGIN_URL: z.string(),
|
|
9
|
+
WEB_REPORT_URL: z.string(),
|
|
10
|
+
API_URL: z.string(),
|
|
11
|
+
})
|
|
12
|
+
.required();
|
|
13
|
+
|
|
14
|
+
const envVariables = envVariablesSchema.parse(process.env);
|
|
15
|
+
|
|
16
|
+
export const WEB_LOGIN_URL = envVariables.WEB_LOGIN_URL;
|
|
17
|
+
export const WEB_REPORT_URL = envVariables.WEB_REPORT_URL;
|
|
18
|
+
export const API_URL = envVariables.API_URL;
|
package/src/github.mjs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import got from 'got';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import stream from 'node:stream';
|
|
5
|
+
import extract from 'extract-zip';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
const pipeline = promisify(stream.pipeline);
|
|
9
|
+
|
|
10
|
+
export async function getDefaultBranch(repoUrl) {
|
|
11
|
+
let slug = repoUrl.replace(/https?:\/\/github\.com\//i, '');
|
|
12
|
+
|
|
13
|
+
if (slug.endsWith('/')) {
|
|
14
|
+
slug = slug.substring(0, slug.length - 1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (slug.endsWith('.git')) {
|
|
18
|
+
slug = slug.substring(0, slug.length - '.git'.length);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const repoInfo = await got(`https://api.github.com/repos/${slug}`, {
|
|
23
|
+
method: 'GET',
|
|
24
|
+
headers: {
|
|
25
|
+
Accept: 'application/vnd.github+json',
|
|
26
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
27
|
+
},
|
|
28
|
+
}).json();
|
|
29
|
+
|
|
30
|
+
return repoInfo.default_branch;
|
|
31
|
+
} catch (e) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Can't get default branch, make sure the repository is public: ${repoUrl}.`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function downloadRepo(repoUrl, reference, dirname) {
|
|
39
|
+
const zipFilePath = path.join(dirname, 'repo.zip');
|
|
40
|
+
const downloadStream = got.stream(`${repoUrl}/zipball/${reference}`);
|
|
41
|
+
const fileWriterStream = fs.createWriteStream(zipFilePath);
|
|
42
|
+
|
|
43
|
+
downloadStream.on('downloadProgress', ({ transferred, percent }) => {
|
|
44
|
+
if (transferred > 0) {
|
|
45
|
+
console.log(
|
|
46
|
+
`Progress: ${transferred} (${Math.round(percent * 100)}%) ...`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await pipeline(downloadStream, fileWriterStream);
|
|
52
|
+
await extract(zipFilePath, { dir: dirname });
|
|
53
|
+
|
|
54
|
+
const repoRoot = fs
|
|
55
|
+
.readdirSync(dirname, { withFileTypes: true })
|
|
56
|
+
.filter((dirent) => dirent.isDirectory())
|
|
57
|
+
.map((dirent) => dirent.name)
|
|
58
|
+
.at(0);
|
|
59
|
+
|
|
60
|
+
return path.join(dirname, repoRoot);
|
|
61
|
+
}
|
package/src/gql.mjs
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import got from 'got';
|
|
2
|
+
import { API_URL } from './constants.mjs';
|
|
3
|
+
|
|
4
|
+
const ME = `
|
|
5
|
+
query me {
|
|
6
|
+
me {
|
|
7
|
+
email
|
|
8
|
+
projectId
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
const CREATE_COMMUNITY_USER = `
|
|
14
|
+
mutation CreateCommunityUser {
|
|
15
|
+
createCommunityUser {
|
|
16
|
+
status
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
const UPLOAD_S3_BUCKET_INFO = `
|
|
22
|
+
mutation uploadS3BucketInfo($fileName: String!) {
|
|
23
|
+
uploadS3BucketInfo(fileName: $fileName) {
|
|
24
|
+
status
|
|
25
|
+
error
|
|
26
|
+
uploadInfo {
|
|
27
|
+
url
|
|
28
|
+
fixReportId
|
|
29
|
+
uploadFieldsJSON
|
|
30
|
+
uploadKey
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const SUBMIT_VULNERABILITY_REPORT = `
|
|
37
|
+
mutation SubmitVulnerabilityReport($vulnerabilityReportFileName: String!, $fixReportId: String!, $repoUrl: String!, $reference: String!) {
|
|
38
|
+
submitVulnerabilityReport(
|
|
39
|
+
fixReportId: $fixReportId
|
|
40
|
+
repoUrl: $repoUrl
|
|
41
|
+
reference: $reference
|
|
42
|
+
vulnerabilityReportFileName: $vulnerabilityReportFileName
|
|
43
|
+
githubAuthToken: null
|
|
44
|
+
) {
|
|
45
|
+
__typename
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
export class GQLClient {
|
|
51
|
+
#token;
|
|
52
|
+
|
|
53
|
+
constructor(token) {
|
|
54
|
+
this.#token = token;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async #apiCall(query, variables = {}) {
|
|
58
|
+
const response = await got(API_URL, {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: {
|
|
61
|
+
authorization: `Bearer ${this.#token}`,
|
|
62
|
+
},
|
|
63
|
+
body: JSON.stringify({
|
|
64
|
+
query,
|
|
65
|
+
variables,
|
|
66
|
+
}),
|
|
67
|
+
}).json();
|
|
68
|
+
|
|
69
|
+
if (response.errors) {
|
|
70
|
+
throw new Error(`API error: ${response.errors[0].message}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!response.data) {
|
|
74
|
+
throw new Error('No data returned for the API query.');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return response.data;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async verifyToken() {
|
|
81
|
+
await this.createCommunityUser();
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await this.#apiCall(ME);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async createCommunityUser() {
|
|
92
|
+
try {
|
|
93
|
+
await this.#apiCall(CREATE_COMMUNITY_USER);
|
|
94
|
+
} catch (e) {
|
|
95
|
+
// Ignore errors
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async uploadS3BucketInfo() {
|
|
100
|
+
const data = await this.#apiCall(UPLOAD_S3_BUCKET_INFO, {
|
|
101
|
+
fileName: 'report.json',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
fixReportId: data.uploadS3BucketInfo.uploadInfo.fixReportId,
|
|
106
|
+
uploadKey: data.uploadS3BucketInfo.uploadInfo.uploadKey,
|
|
107
|
+
url: data.uploadS3BucketInfo.uploadInfo.url,
|
|
108
|
+
uploadFields: JSON.parse(
|
|
109
|
+
data.uploadS3BucketInfo.uploadInfo.uploadFieldsJSON
|
|
110
|
+
),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async submitVulnerabilityReport(fixReportId, repoUrl, reference) {
|
|
115
|
+
await this.#apiCall(SUBMIT_VULNERABILITY_REPORT, {
|
|
116
|
+
fixReportId,
|
|
117
|
+
repoUrl,
|
|
118
|
+
reference,
|
|
119
|
+
vulnerabilityReportFileName: 'report.json',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import open from 'open';
|
|
5
|
+
import Configstore from 'configstore';
|
|
6
|
+
import { GQLClient } from './gql.mjs';
|
|
7
|
+
import { webLogin } from './web-login.mjs';
|
|
8
|
+
import { downloadRepo, getDefaultBranch } from './github.mjs';
|
|
9
|
+
import { getSnykReport } from './snyk.mjs';
|
|
10
|
+
import { uploadFile } from './upload-file.mjs';
|
|
11
|
+
import { WEB_REPORT_URL } from './constants.mjs';
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const packageJson = JSON.parse(
|
|
15
|
+
fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8')
|
|
16
|
+
);
|
|
17
|
+
const config = new Configstore(packageJson.name, { token: '' });
|
|
18
|
+
|
|
19
|
+
export async function main(dirname, repoUrl) {
|
|
20
|
+
if (!repoUrl) {
|
|
21
|
+
console.warn('Mobb CLI usage: npx mobb <public GitHub repository URL>');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let token = config.get('token');
|
|
26
|
+
let gqlClient = new GQLClient(token);
|
|
27
|
+
|
|
28
|
+
if (!token || !(await gqlClient.verifyToken())) {
|
|
29
|
+
console.log(
|
|
30
|
+
'You will be redirected to our login page, once the authorization is complete, return to this prompt.'
|
|
31
|
+
);
|
|
32
|
+
token = await webLogin();
|
|
33
|
+
gqlClient = new GQLClient(token);
|
|
34
|
+
|
|
35
|
+
if (!(await gqlClient.verifyToken())) {
|
|
36
|
+
console.error('Something went wrong, API token is invalid.');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
config.set('token', token);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const reference = await getDefaultBranch(repoUrl);
|
|
43
|
+
const uploadData = await gqlClient.uploadS3BucketInfo();
|
|
44
|
+
|
|
45
|
+
const repositoryRoot = await downloadRepo(repoUrl, reference, dirname);
|
|
46
|
+
const reportPath = path.join(dirname, 'report.json');
|
|
47
|
+
|
|
48
|
+
console.log(
|
|
49
|
+
'We will run Snyk CLI to scan the repository for vulnerabilities. You may be redirected to the login page in the process.'
|
|
50
|
+
);
|
|
51
|
+
await getSnykReport(reportPath, repositoryRoot);
|
|
52
|
+
|
|
53
|
+
const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
|
|
54
|
+
|
|
55
|
+
if ((report.runs?.at(0)?.results?.length ?? 0) === 0) {
|
|
56
|
+
console.log('Snyk has not found any vulnerabilities — nothing to fix.');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await uploadFile(
|
|
61
|
+
reportPath,
|
|
62
|
+
uploadData.url,
|
|
63
|
+
uploadData.uploadKey,
|
|
64
|
+
uploadData.uploadFields
|
|
65
|
+
);
|
|
66
|
+
await gqlClient.submitVulnerabilityReport(
|
|
67
|
+
uploadData.fixReportId,
|
|
68
|
+
repoUrl,
|
|
69
|
+
reference
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
console.log(
|
|
73
|
+
'You will be redirected to our report page, please wait until the analysis is finished and enjoy your fixes.'
|
|
74
|
+
);
|
|
75
|
+
await open(`${WEB_REPORT_URL}${uploadData.fixReportId}`);
|
|
76
|
+
}
|
package/src/snyk.mjs
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import cp from 'node:child_process';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const SNYK_PATH = require.resolve('snyk/bin/snyk');
|
|
6
|
+
|
|
7
|
+
async function forkSnyk(args, stdio = 'inherit') {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const child = cp.fork(SNYK_PATH, args, {
|
|
10
|
+
stdio,
|
|
11
|
+
});
|
|
12
|
+
let out = '';
|
|
13
|
+
|
|
14
|
+
if (stdio === 'pipe') {
|
|
15
|
+
child.stdout.on('data', (chunk) => {
|
|
16
|
+
out += chunk;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
child.on('exit', () => {
|
|
21
|
+
resolve(out);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
child.on('error', reject);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function getSnykReport(reportPath, repoRoot) {
|
|
29
|
+
const config = await forkSnyk(['config'], 'pipe');
|
|
30
|
+
|
|
31
|
+
if (!config.includes('api: ')) {
|
|
32
|
+
await forkSnyk(['auth']);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await forkSnyk([
|
|
36
|
+
'code',
|
|
37
|
+
'test',
|
|
38
|
+
`--sarif-file-output=${reportPath}`,
|
|
39
|
+
repoRoot,
|
|
40
|
+
]);
|
|
41
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import FormData from 'form-data';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import got from 'got';
|
|
4
|
+
|
|
5
|
+
export async function uploadFile(reportPath, url, uploadKey, uploadFields) {
|
|
6
|
+
const form = new FormData();
|
|
7
|
+
|
|
8
|
+
for (const key in uploadFields) {
|
|
9
|
+
form.append(key, uploadFields[key]);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
form.append('key', uploadKey);
|
|
13
|
+
form.append('file', fs.createReadStream(reportPath));
|
|
14
|
+
|
|
15
|
+
await got(url, {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
body: form,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { setTimeout, clearTimeout } from 'node:timers';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import { WEB_LOGIN_URL } from './constants.mjs';
|
|
4
|
+
import querystring from 'node:querystring';
|
|
5
|
+
import open from 'open';
|
|
6
|
+
|
|
7
|
+
export async function webLogin() {
|
|
8
|
+
let responseResolver;
|
|
9
|
+
let responseRejecter;
|
|
10
|
+
const responseAwaiter = new Promise((resolve, reject) => {
|
|
11
|
+
responseResolver = resolve;
|
|
12
|
+
responseRejecter = reject;
|
|
13
|
+
});
|
|
14
|
+
const timerHandler = setTimeout(() => {
|
|
15
|
+
responseRejecter(new Error('No login happened in one minute.'));
|
|
16
|
+
}, 60000);
|
|
17
|
+
|
|
18
|
+
const server = http.createServer((req, res) => {
|
|
19
|
+
let body = '';
|
|
20
|
+
|
|
21
|
+
req.on('data', (chunk) => {
|
|
22
|
+
body += chunk;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
req.on('end', () => {
|
|
26
|
+
res.writeHead(301, {
|
|
27
|
+
Location: `${WEB_LOGIN_URL}?done=true`,
|
|
28
|
+
}).end();
|
|
29
|
+
responseResolver(querystring.parse(body).token);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const port = await new Promise((resolve) => {
|
|
34
|
+
server.listen(0, '127.0.0.1', () => {
|
|
35
|
+
resolve(server.address().port);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await open(`${WEB_LOGIN_URL}?port=${port}`);
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
return await responseAwaiter;
|
|
43
|
+
} finally {
|
|
44
|
+
clearTimeout(timerHandler);
|
|
45
|
+
server.close();
|
|
46
|
+
}
|
|
47
|
+
}
|