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 ADDED
@@ -0,0 +1,4 @@
1
+ # production@v3
2
+ WEB_LOGIN_URL="https://app.mobb.dev/cli-login"
3
+ WEB_REPORT_URL="https://app.mobb.dev/report/"
4
+ API_URL="https://api.mobb.dev/v1/graphql"
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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../index.mjs';
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
+ }