mobbdev 0.0.28 → 0.0.30

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.
@@ -1,198 +0,0 @@
1
- import fetch from 'node-fetch';
2
- import Debug from 'debug';
3
- import { API_URL } from '../../constants.mjs';
4
-
5
- const debug = Debug('mobbdev:gql');
6
-
7
- const ME = `
8
- query Me {
9
- me {
10
- id
11
- email
12
- githubToken
13
- }
14
- }
15
- `;
16
-
17
- const GET_ORG_AND_PROJECT_ID = `
18
- query getOrgAndProjectId {
19
- user {
20
- userOrganizationsAndUserOrganizationRoles {
21
- organization {
22
- id
23
- projects(order_by: {updatedAt: desc}) {
24
- id
25
- }
26
- }
27
- }
28
- }
29
- }`;
30
-
31
- const CREATE_COMMUNITY_USER = `
32
- mutation CreateCommunityUser {
33
- initOrganizationAndProject {
34
- userId
35
- projectId
36
- organizationId
37
- }
38
- }
39
- `;
40
-
41
- const UPLOAD_S3_BUCKET_INFO = `
42
- mutation uploadS3BucketInfo($fileName: String!) {
43
- uploadS3BucketInfo(fileName: $fileName) {
44
- status
45
- error
46
- uploadInfo {
47
- url
48
- fixReportId
49
- uploadFieldsJSON
50
- uploadKey
51
- }
52
- repoUploadInfo {
53
- url
54
- fixReportId
55
- uploadFieldsJSON
56
- uploadKey
57
- }
58
- }
59
- }
60
- `;
61
-
62
- const SUBMIT_VULNERABILITY_REPORT = `
63
- mutation SubmitVulnerabilityReport($vulnerabilityReportFileName: String!, $fixReportId: String!, $repoUrl: String!, $reference: String!, $projectId: String!, $sha: String) {
64
- submitVulnerabilityReport(
65
- fixReportId: $fixReportId
66
- repoUrl: $repoUrl
67
- reference: $reference
68
- sha: $sha
69
- vulnerabilityReportFileName: $vulnerabilityReportFileName
70
- projectId: $projectId
71
- ) {
72
- __typename
73
- }
74
- }
75
- `;
76
-
77
- export class GQLClient {
78
- constructor(args) {
79
- const { token, apiKey } = args;
80
- apiKey
81
- ? debug('init with apiKey %s', apiKey)
82
- : debug('init with token %s', token);
83
- this._token = token;
84
- this._apiKey = apiKey;
85
- }
86
- async getUserInfo() {
87
- const { me } = await this._apiCall(ME);
88
- return me;
89
- }
90
-
91
- async _apiCall(query, variables = {}) {
92
- debug('api call %o %s', variables, query);
93
- const headers = this._apiKey
94
- ? { 'x-mobb-key': this._apiKey }
95
- : {
96
- authorization: `Bearer ${this._token}`,
97
- };
98
- debug('headers %o', headers);
99
- const response = await fetch(API_URL, {
100
- method: 'POST',
101
- headers,
102
- body: JSON.stringify({
103
- query,
104
- variables,
105
- }),
106
- });
107
-
108
- if (!response.ok) {
109
- debug('API request failed %s %s', response.body, response.status);
110
- throw new Error(`API call failed: ${response.status}`);
111
- }
112
-
113
- const data = await response.json();
114
- debug('API request ok %j', data);
115
-
116
- if (data.errors) {
117
- throw new Error(`API error: ${data.errors[0].message}`);
118
- }
119
-
120
- if (!data.data) {
121
- throw new Error('No data returned for the API query.');
122
- }
123
-
124
- return data.data;
125
- }
126
-
127
- async verifyToken() {
128
- await this.createCommunityUser();
129
-
130
- try {
131
- await this._apiCall(ME);
132
- } catch (e) {
133
- debug('verify token failed %o', e);
134
- return false;
135
- }
136
- return true;
137
- }
138
-
139
- async getOrgAndProjectId() {
140
- const data = await this._apiCall(GET_ORG_AND_PROJECT_ID);
141
- const org =
142
- data.user[0].userOrganizationsAndUserOrganizationRoles[0]
143
- .organization;
144
-
145
- return {
146
- organizationId: org.id,
147
- projectId: org.projects[0].id,
148
- };
149
- }
150
-
151
- async createCommunityUser() {
152
- try {
153
- await this._apiCall(CREATE_COMMUNITY_USER);
154
- } catch (e) {
155
- debug('create community user failed %o', e);
156
- // Ignore errors
157
- }
158
- }
159
-
160
- async uploadS3BucketInfo() {
161
- const data = await this._apiCall(UPLOAD_S3_BUCKET_INFO, {
162
- fileName: 'report.json',
163
- });
164
-
165
- return {
166
- fixReportId: data.uploadS3BucketInfo.uploadInfo.fixReportId,
167
- uploadKey: data.uploadS3BucketInfo.uploadInfo.uploadKey,
168
- url: data.uploadS3BucketInfo.uploadInfo.url,
169
- uploadFields: JSON.parse(
170
- data.uploadS3BucketInfo.uploadInfo.uploadFieldsJSON
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
- ),
179
- };
180
- }
181
-
182
- async submitVulnerabilityReport(
183
- fixReportId,
184
- repoUrl,
185
- reference,
186
- projectId,
187
- sha
188
- ) {
189
- await this._apiCall(SUBMIT_VULNERABILITY_REPORT, {
190
- fixReportId,
191
- repoUrl,
192
- reference,
193
- vulnerabilityReportFileName: 'report.json',
194
- projectId,
195
- sha: sha || '',
196
- });
197
- }
198
- }
@@ -1,292 +0,0 @@
1
- import chalk from 'chalk';
2
- import Configstore from 'configstore';
3
- import Debug from 'debug';
4
- import fs from 'node:fs';
5
- import path from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
- import open from 'open';
8
- import semver from 'semver';
9
- import { callbackServer } from './callback-server.mjs';
10
- import tmp from 'tmp';
11
- import { CliError } from '../../commands/index.mjs';
12
-
13
- import { WEB_APP_URL } from '../../constants.mjs';
14
- import { canReachRepo, downloadRepo, getDefaultBranch } from './github.mjs';
15
- import { GQLClient } from './gql.mjs';
16
- import { githubIntegrationPrompt, mobbAnalysisPrompt } from './prompts.mjs';
17
- import { getSnykReport } from './snyk.mjs';
18
- import { uploadFile } from './upload-file.mjs';
19
- import { keypress, Spinner } from '../../utils.mjs';
20
- import { pack } from './pack.mjs';
21
- import { getGitInfo } from './git.mjs';
22
-
23
- const webLoginUrl = `${WEB_APP_URL}/cli-login`;
24
- const githubSubmitUrl = `${WEB_APP_URL}/gh-callback`;
25
- const githubAuthUrl = `${WEB_APP_URL}/github-auth`;
26
-
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(
28
- 'press any key to continue'
29
- )};`;
30
- const tmpObj = tmp.dirSync({
31
- unsafeCleanup: true,
32
- });
33
-
34
- const debug = Debug('mobbdev:index');
35
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
36
-
37
- const packageJson = JSON.parse(
38
- fs.readFileSync(path.join(__dirname, '../../../package.json'), 'utf8')
39
- );
40
-
41
- if (!semver.satisfies(process.version, packageJson.engines.node)) {
42
- throw new CliError(
43
- `${packageJson.name} requires node version ${packageJson.engines.node}, but running ${process.version}.`
44
- );
45
- }
46
-
47
- const config = new Configstore(packageJson.name, { token: '' });
48
- debug('config %o', config);
49
-
50
- export async function runAnalysis(
51
- { repo, scanner, scanFile, apiKey, ci, commitHash, srcPath, ref },
52
- options
53
- ) {
54
- try {
55
- await _scan(
56
- {
57
- dirname: tmpObj.name,
58
- repo,
59
- scanner,
60
- scanFile,
61
- ref,
62
- apiKey,
63
- ci,
64
- srcPath,
65
- commitHash,
66
- },
67
- options
68
- );
69
- } finally {
70
- tmpObj.removeCallback();
71
- }
72
- }
73
-
74
- export async function _scan(
75
- { dirname, repo, scanFile, branch, apiKey, ci, srcPath, commitHash, ref },
76
- { skipPrompts = false } = {}
77
- ) {
78
- debug('start %s %s', dirname, repo);
79
- const { createSpinner } = Spinner({ ci });
80
- skipPrompts = skipPrompts || ci;
81
- let token = config.get('token');
82
- debug('token %s', token);
83
- apiKey ?? debug('token %s', apiKey);
84
- let gqlClient = new GQLClient(apiKey ? { apiKey } : { token });
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
-
94
- const userInfo = await gqlClient.getUserInfo();
95
- let { githubToken } = userInfo;
96
- const isRepoAvailable = await canReachRepo(repo, {
97
- token: userInfo.githubToken,
98
- });
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
- }
105
- const { token } = await handleGithubIntegration();
106
- githubToken = token;
107
- }
108
-
109
- const reference =
110
- ref ?? (await getDefaultBranch(repo, { token: githubToken }));
111
- debug('org id %s', organizationId);
112
- debug('project id %s', projectId);
113
- debug('default branch %s', reference);
114
-
115
- const repositoryRoot = await downloadRepo(
116
- {
117
- repoUrl: repo,
118
- reference,
119
- dirname,
120
- ci,
121
- },
122
- { token: githubToken }
123
- );
124
-
125
- if (!reportPath) {
126
- reportPath = await getReportFromSnyk();
127
- }
128
-
129
- const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
130
- await uploadFile(
131
- reportPath,
132
- uploadData.url,
133
- uploadData.uploadKey,
134
- uploadData.uploadFields
135
- );
136
- const mobbSpinner = createSpinner('🕵️‍♂️ Initiating Mobb analysis').start();
137
- try {
138
- await gqlClient.submitVulnerabilityReport(
139
- uploadData.fixReportId,
140
- repo,
141
- reference,
142
- projectId
143
- );
144
- } catch (e) {
145
- mobbSpinner.error({ text: '🕵️‍♂️ Mobb analysis failed' });
146
- throw e;
147
- }
148
-
149
- debug('report %o', report);
150
-
151
- const results = ((report.runs || [])[0] || {}).results || [];
152
- if (results.length === 0 && !scanFile) {
153
- mobbSpinner.success({
154
- text: '🕵️‍♂️ Report did not detect any vulnerabilities — nothing to fix.',
155
- });
156
- } else {
157
- mobbSpinner.success({
158
- text: '🕵️‍♂️ Generating fixes...',
159
- });
160
-
161
- await askToOpenAnalysis();
162
- }
163
- async function getReportFromSnyk() {
164
- const reportPath = path.join(dirname, 'report.json');
165
-
166
- if (
167
- !(await getSnykReport(reportPath, repositoryRoot, { skipPrompts }))
168
- ) {
169
- debug('snyk code is not enabled');
170
- return;
171
- }
172
- return reportPath;
173
- }
174
- async function askToOpenAnalysis() {
175
- const reportUrl = `${WEB_APP_URL}/organization/${organizationId}/project/${projectId}/report/${uploadData.fixReportId}`;
176
- !ci && console.log('You can access the report at: \n');
177
- console.log(reportUrl);
178
- !skipPrompts && (await mobbAnalysisPrompt());
179
-
180
- !ci && open(reportUrl);
181
- !ci &&
182
- console.log(
183
- chalk.bgBlue(
184
- '\n\n My work here is done for now, see you soon! 🕵️‍♂️ '
185
- )
186
- );
187
- }
188
-
189
- async function handleMobbLogin() {
190
- if (
191
- (token && (await gqlClient.verifyToken())) ||
192
- (apiKey && (await gqlClient.verifyToken()))
193
- ) {
194
- createSpinner().start().success({
195
- text: '🔓 Logged in to Mobb successfully',
196
- });
197
-
198
- return;
199
- }
200
- if (apiKey && !(await gqlClient.verifyToken())) {
201
- createSpinner().start().error({
202
- text: '🔓 Logged in to Mobb failed - check your api-key',
203
- });
204
- throw new CliError();
205
- }
206
- const mobbLoginSpinner = createSpinner().start();
207
- if (!skipPrompts) {
208
- mobbLoginSpinner.update({ text: MOBB_LOGIN_REQUIRED_MSG });
209
- await keypress();
210
- }
211
-
212
- mobbLoginSpinner.update({
213
- text: '🔓 Waiting for Mobb login...',
214
- });
215
-
216
- const loginResponse = await callbackServer(
217
- webLoginUrl,
218
- `${webLoginUrl}?done=true`
219
- );
220
- token = loginResponse.token;
221
-
222
- gqlClient = new GQLClient({ token });
223
-
224
- if (!(await gqlClient.verifyToken())) {
225
- mobbLoginSpinner.error({
226
- text: 'Something went wrong, API token is invalid.',
227
- });
228
- throw new CliError();
229
- }
230
- debug('set token %s', token);
231
- config.set('token', token);
232
- mobbLoginSpinner.success({ text: '🔓 Login to Mobb successful!' });
233
- }
234
- async function handleGithubIntegration() {
235
- const addGithubIntegration = skipPrompts
236
- ? true
237
- : await githubIntegrationPrompt();
238
-
239
- const githubSpinner = createSpinner(
240
- '🔗 Waiting for github integration...'
241
- ).start();
242
- if (!addGithubIntegration) {
243
- githubSpinner.error();
244
- throw Error('Could not reach github repo');
245
- }
246
- const result = await callbackServer(
247
- githubAuthUrl,
248
- `${githubSubmitUrl}?done=true`
249
- );
250
- githubSpinner.success({ text: '🔗 Github integration successful!' });
251
- return result;
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
- }
292
- }
@@ -1,31 +0,0 @@
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,55 +0,0 @@
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,110 +0,0 @@
1
- import cp from 'node:child_process';
2
- import { createRequire } from 'node:module';
3
- import { stdout } from 'colors/lib/system/supports-colors.js';
4
- import open from 'open';
5
- import * as process from 'process';
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';
11
-
12
- const debug = Debug('mobbdev:snyk');
13
- const require = createRequire(import.meta.url);
14
- const SNYK_PATH = require.resolve('snyk/bin/snyk');
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
-
19
- debug('snyk executable path %s', SNYK_PATH);
20
-
21
- async function forkSnyk(args, display) {
22
- debug('fork snyk with args %o %s', args, display);
23
-
24
- return new Promise((resolve, reject) => {
25
- const child = cp.fork(SNYK_PATH, args, {
26
- stdio: ['inherit', 'pipe', 'pipe', 'ipc'],
27
- env: { FORCE_COLOR: stdout.level },
28
- });
29
- let out = '';
30
- const onData = (chunk) => {
31
- debug('chunk received from snyk std %s', chunk);
32
- out += chunk;
33
- };
34
-
35
- child.stdout.on('data', onData);
36
- child.stderr.on('data', onData);
37
-
38
- if (display) {
39
- child.stdout.pipe(process.stdout);
40
- child.stderr.pipe(process.stderr);
41
- }
42
-
43
- child.on('exit', () => {
44
- debug('snyk exit');
45
- resolve(out);
46
- });
47
- child.on('error', (err) => {
48
- debug('snyk error %o', err);
49
- reject(err);
50
- });
51
- });
52
- }
53
-
54
- export async function getSnykReport(
55
- reportPath,
56
- repoRoot,
57
- { skipPrompts = false }
58
- ) {
59
- debug('get snyk report start %s %s', reportPath, repoRoot);
60
-
61
- const config = await forkSnyk(['config'], false);
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
-
74
- debug('no token in the config %s', config);
75
- await forkSnyk(['auth'], true);
76
- snykLoginSpinner.success({ text: '🔓 Login to Snyk Successful' });
77
- }
78
- const snykSpinner = createSpinner(
79
- '🔍 Scanning your repo with Snyk '
80
- ).start();
81
- const out = await forkSnyk(
82
- ['code', 'test', `--sarif-file-output=${reportPath}`, repoRoot],
83
- true
84
- );
85
-
86
- if (
87
- out.includes(
88
- 'Snyk Code is not supported for org: enable in Settings > Snyk Code'
89
- )
90
- ) {
91
- debug('snyk code is not enabled %s', out);
92
- snykSpinner.error({ text: '🔍 Snyk configuration needed' });
93
- const answer = await snykArticlePrompt();
94
- debug('answer %s', answer);
95
-
96
- if (answer) {
97
- debug('opening the browser');
98
- await open(SNYK_ARTICLE_URL);
99
- }
100
- console.log(
101
- chalk.bgBlue(
102
- '\nPlease enable Snyk Code in your Snyk account and try again.'
103
- )
104
- );
105
- return false;
106
- }
107
-
108
- snykSpinner.success({ text: '🔍 Snyk code scan completed' });
109
- return true;
110
- }