mobbdev 0.0.15 → 0.0.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobbdev",
3
- "version": "0.0.15",
3
+ "version": "0.0.18",
4
4
  "description": "Automated secure code remediation tool",
5
5
  "main": "index.mjs",
6
6
  "scripts": {
@@ -17,6 +17,7 @@
17
17
  "dependencies": {
18
18
  "colors": "1.4.0",
19
19
  "configstore": "6.0.0",
20
+ "debug": "4.3.4",
20
21
  "dotenv": "16.0.3",
21
22
  "extract-zip": "2.0.1",
22
23
  "node-fetch": "3.3.1",
package/src/constants.mjs CHANGED
@@ -2,7 +2,9 @@ import path from 'node:path';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import * as dotenv from 'dotenv';
4
4
  import { z } from 'zod';
5
+ import Debug from 'debug';
5
6
 
7
+ const debug = Debug('mobbdev:constants');
6
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
9
  dotenv.config({ path: path.join(__dirname, '../.env') });
8
10
 
@@ -15,6 +17,7 @@ const envVariablesSchema = z
15
17
  .required();
16
18
 
17
19
  const envVariables = envVariablesSchema.parse(process.env);
20
+ debug('config %o', envVariables);
18
21
 
19
22
  export const WEB_LOGIN_URL = envVariables.WEB_LOGIN_URL;
20
23
  export const WEB_APP_URL = envVariables.WEB_APP_URL;
package/src/github.mjs CHANGED
@@ -4,10 +4,13 @@ import path from 'node:path';
4
4
  import { promisify } from 'node:util';
5
5
  import fetch from 'node-fetch';
6
6
  import extract from 'extract-zip';
7
+ import Debug from 'debug';
7
8
 
8
9
  const pipeline = promisify(stream.pipeline);
10
+ const debug = Debug('mobbdev:github');
9
11
 
10
12
  export async function getDefaultBranch(repoUrl) {
13
+ debug('get default branch %s', repoUrl);
11
14
  let slug = repoUrl.replace(/https?:\/\/github\.com\//i, '');
12
15
 
13
16
  if (slug.endsWith('/')) {
@@ -17,6 +20,7 @@ export async function getDefaultBranch(repoUrl) {
17
20
  if (slug.endsWith('.git')) {
18
21
  slug = slug.substring(0, slug.length - '.git'.length);
19
22
  }
23
+ debug('slug %s', slug);
20
24
 
21
25
  const response = await fetch(`https://api.github.com/repos/${slug}`, {
22
26
  method: 'GET',
@@ -27,19 +31,34 @@ export async function getDefaultBranch(repoUrl) {
27
31
  });
28
32
 
29
33
  if (!response.ok) {
34
+ debug('GH request failed %s %s', response.body, response.status);
30
35
  throw new Error(
31
36
  `Can't get default branch, make sure the repository is public: ${repoUrl}.`
32
37
  );
33
38
  }
34
39
 
35
40
  const repoInfo = await response.json();
41
+ debug('GH request ok %o', repoInfo);
36
42
 
37
43
  return repoInfo.default_branch;
38
44
  }
39
45
 
40
46
  export async function downloadRepo(repoUrl, reference, dirname) {
47
+ debug('download repo %s %s %s', repoUrl, reference, dirname);
41
48
  const zipFilePath = path.join(dirname, 'repo.zip');
42
49
  const response = await fetch(`${repoUrl}/zipball/${reference}`);
50
+
51
+ if (!response.ok) {
52
+ debug(
53
+ 'GH zipball request failed %s %s',
54
+ response.body,
55
+ response.status
56
+ );
57
+ throw new Error(
58
+ `Can't access the repository, make it is public: ${repoUrl}.`
59
+ );
60
+ }
61
+
43
62
  const fileWriterStream = fs.createWriteStream(zipFilePath);
44
63
 
45
64
  await pipeline(response.body, fileWriterStream);
@@ -49,6 +68,7 @@ export async function downloadRepo(repoUrl, reference, dirname) {
49
68
  .readdirSync(dirname, { withFileTypes: true })
50
69
  .filter((dirent) => dirent.isDirectory())
51
70
  .map((dirent) => dirent.name)[0];
71
+ debug('repo root %s', repoRoot);
52
72
 
53
73
  return path.join(dirname, repoRoot);
54
74
  }
package/src/gql.mjs CHANGED
@@ -1,22 +1,31 @@
1
1
  import fetch from 'node-fetch';
2
+ import Debug from 'debug';
2
3
  import { API_URL } from './constants.mjs';
3
4
 
5
+ const debug = Debug('mobbdev:gql');
6
+
4
7
  const ME = `
5
8
  query Me {
6
9
  user {
7
10
  id
8
11
  email
9
- userOrganizations {
12
+ }
13
+ }
14
+ `;
15
+
16
+ const GET_ORG_AND_PROJECT_ID = `
17
+ query getOrgAndProjectId {
18
+ user {
19
+ userOrganizationsAndUserOrganizationRoles {
10
20
  organization {
11
21
  id
12
- projects {
22
+ projects(order_by: {updatedAt: desc}) {
13
23
  id
14
24
  }
15
25
  }
16
26
  }
17
27
  }
18
- }
19
- `;
28
+ }`;
20
29
 
21
30
  const CREATE_COMMUNITY_USER = `
22
31
  mutation CreateCommunityUser {
@@ -59,12 +68,12 @@ mutation SubmitVulnerabilityReport($vulnerabilityReportFileName: String!, $fixRe
59
68
 
60
69
  export class GQLClient {
61
70
  constructor(token) {
71
+ debug('init with token %s', token);
62
72
  this._token = token;
63
- this._projectId = undefined;
64
- this._organizationId = undefined;
65
73
  }
66
74
 
67
75
  async _apiCall(query, variables = {}) {
76
+ debug('api call %o %s', variables, query);
68
77
  const response = await fetch(API_URL, {
69
78
  method: 'POST',
70
79
  headers: {
@@ -77,13 +86,15 @@ export class GQLClient {
77
86
  });
78
87
 
79
88
  if (!response.ok) {
89
+ debug('API request failed %s %s', response.body, response.status);
80
90
  throw new Error(`API call failed: ${response.status}`);
81
91
  }
82
92
 
83
93
  const data = await response.json();
94
+ debug('API request ok %j', data);
84
95
 
85
96
  if (data.errors) {
86
- throw new Error(`API error: ${response.errors[0].message}`);
97
+ throw new Error(`API error: ${data.errors[0].message}`);
87
98
  }
88
99
 
89
100
  if (!data.data) {
@@ -93,40 +104,35 @@ export class GQLClient {
93
104
  return data.data;
94
105
  }
95
106
 
96
- getOrganizationId() {
97
- return this._organizationId;
98
- }
99
-
100
- getProjectId() {
101
- return this._projectId;
102
- }
103
-
104
107
  async verifyToken() {
105
108
  await this.createCommunityUser();
106
109
 
107
110
  try {
108
- const userInfo = await this._apiCall(ME);
109
- const {
110
- user: [{ userOrganizations }],
111
- } = userInfo;
112
- const [
113
- {
114
- organization: { id: organizationId, projects },
115
- },
116
- ] = userOrganizations;
117
- const [{ id: projectId }] = projects;
118
- this._projectId = projectId;
119
- this._organizationId = organizationId;
111
+ await this._apiCall(ME);
120
112
  } catch (e) {
113
+ debug('verify token failed %o', e);
121
114
  return false;
122
115
  }
123
116
  return true;
124
117
  }
125
118
 
119
+ async getOrgAndProjectId() {
120
+ const data = await this._apiCall(GET_ORG_AND_PROJECT_ID);
121
+ const org =
122
+ data.user[0].userOrganizationsAndUserOrganizationRoles[0]
123
+ .organization;
124
+
125
+ return {
126
+ organizationId: org.id,
127
+ projectId: org.projects[0].id,
128
+ };
129
+ }
130
+
126
131
  async createCommunityUser() {
127
132
  try {
128
133
  await this._apiCall(CREATE_COMMUNITY_USER);
129
134
  } catch (e) {
135
+ debug('create community user failed %o', e);
130
136
  // Ignore errors
131
137
  }
132
138
  }
@@ -146,13 +152,18 @@ export class GQLClient {
146
152
  };
147
153
  }
148
154
 
149
- async submitVulnerabilityReport(fixReportId, repoUrl, reference) {
155
+ async submitVulnerabilityReport(
156
+ fixReportId,
157
+ repoUrl,
158
+ reference,
159
+ projectId
160
+ ) {
150
161
  await this._apiCall(SUBMIT_VULNERABILITY_REPORT, {
151
162
  fixReportId,
152
163
  repoUrl,
153
164
  reference,
154
165
  vulnerabilityReportFileName: 'report.json',
155
- projectId: this._projectId,
166
+ projectId,
156
167
  });
157
168
  }
158
169
  }
package/src/index.mjs CHANGED
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import semver from 'semver';
5
5
  import open from 'open';
6
6
  import Configstore from 'configstore';
7
+ import Debug from 'debug';
7
8
  import { GQLClient } from './gql.mjs';
8
9
  import { webLogin } from './web-login.mjs';
9
10
  import { downloadRepo, getDefaultBranch } from './github.mjs';
@@ -11,6 +12,7 @@ import { getSnykReport } from './snyk.mjs';
11
12
  import { uploadFile } from './upload-file.mjs';
12
13
  import { WEB_APP_URL } from './constants.mjs';
13
14
 
15
+ const debug = Debug('mobbdev:index');
14
16
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
17
  const packageJson = JSON.parse(
16
18
  fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8')
@@ -24,8 +26,11 @@ if (!semver.satisfies(process.version, packageJson.engines.node)) {
24
26
  }
25
27
 
26
28
  const config = new Configstore(packageJson.name, { token: '' });
29
+ debug('config %o', config);
27
30
 
28
31
  export async function main(dirname, repoUrl) {
32
+ debug('start %s %s', dirname, repoUrl);
33
+
29
34
  if (!repoUrl) {
30
35
  console.warn(
31
36
  'Mobb CLI usage: npx mobbdev <public GitHub repository URL>'
@@ -34,6 +39,7 @@ export async function main(dirname, repoUrl) {
34
39
  }
35
40
 
36
41
  let token = config.get('token');
42
+ debug('token %s', token);
37
43
  let gqlClient = new GQLClient(token);
38
44
 
39
45
  if (!token || !(await gqlClient.verifyToken())) {
@@ -47,10 +53,15 @@ export async function main(dirname, repoUrl) {
47
53
  console.error('Something went wrong, API token is invalid.');
48
54
  return;
49
55
  }
56
+ debug('set token %s', token);
50
57
  config.set('token', token);
51
58
  }
52
59
 
60
+ const { projectId, organizationId } = await gqlClient.getOrgAndProjectId();
61
+ debug('org id %s', organizationId);
62
+ debug('project id %s', projectId);
53
63
  const reference = await getDefaultBranch(repoUrl);
64
+ debug('default branch %s', reference);
54
65
  const uploadData = await gqlClient.uploadS3BucketInfo();
55
66
 
56
67
  const repositoryRoot = await downloadRepo(repoUrl, reference, dirname);
@@ -59,7 +70,11 @@ export async function main(dirname, repoUrl) {
59
70
  console.log(
60
71
  'We will run Snyk CLI to scan the repository for vulnerabilities. You may be redirected to the login page in the process.'
61
72
  );
62
- await getSnykReport(reportPath, repositoryRoot);
73
+
74
+ if (!(await getSnykReport(reportPath, repositoryRoot))) {
75
+ debug('snyk code is not enabled');
76
+ return;
77
+ }
63
78
 
64
79
  const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
65
80
 
@@ -72,9 +87,12 @@ export async function main(dirname, repoUrl) {
72
87
  await gqlClient.submitVulnerabilityReport(
73
88
  uploadData.fixReportId,
74
89
  repoUrl,
75
- reference
90
+ reference,
91
+ projectId
76
92
  );
77
93
 
94
+ debug('report %o', report);
95
+
78
96
  const results = ((report.runs || [])[0] || {}).results || [];
79
97
  if (results.length === 0) {
80
98
  console.log('Snyk has not found any vulnerabilities — nothing to fix.');
@@ -82,8 +100,6 @@ export async function main(dirname, repoUrl) {
82
100
  console.log(
83
101
  'You will be redirected to our report page, please wait until the analysis is finished and enjoy your fixes.'
84
102
  );
85
- const projectId = gqlClient.getProjectId();
86
- const organizationId = gqlClient.getOrganizationId();
87
103
  await open(
88
104
  `${WEB_APP_URL}/organization/${organizationId}/project/${projectId}/report/${uploadData.fixReportId}`
89
105
  );
package/src/snyk.mjs CHANGED
@@ -4,11 +4,17 @@ import readline from 'node:readline';
4
4
  import { stdout } from 'colors/lib/system/supports-colors.js';
5
5
  import open from 'open';
6
6
  import * as process from 'process';
7
+ import Debug from 'debug';
7
8
 
9
+ const debug = Debug('mobbdev:snyk');
8
10
  const require = createRequire(import.meta.url);
9
11
  const SNYK_PATH = require.resolve('snyk/bin/snyk');
10
12
 
13
+ debug('snyk executable path %s', SNYK_PATH);
14
+
11
15
  async function forkSnyk(args, display) {
16
+ debug('fork snyk with args %o %s', args, display);
17
+
12
18
  return new Promise((resolve, reject) => {
13
19
  const child = cp.fork(SNYK_PATH, args, {
14
20
  stdio: ['inherit', 'pipe', 'pipe', 'ipc'],
@@ -16,6 +22,7 @@ async function forkSnyk(args, display) {
16
22
  });
17
23
  let out = '';
18
24
  const onData = (chunk) => {
25
+ debug('chunk received from snyk std %s', chunk);
19
26
  out += chunk;
20
27
  };
21
28
 
@@ -28,9 +35,11 @@ async function forkSnyk(args, display) {
28
35
  }
29
36
 
30
37
  child.on('exit', () => {
38
+ debug('snyk exit');
31
39
  resolve(out);
32
40
  });
33
41
  child.on('error', (err) => {
42
+ debug('snyk error %o', err);
34
43
  reject(err);
35
44
  });
36
45
  });
@@ -51,9 +60,12 @@ async function question(questionString) {
51
60
  }
52
61
 
53
62
  export async function getSnykReport(reportPath, repoRoot) {
63
+ debug('get snyk report start %s %s', reportPath, repoRoot);
64
+
54
65
  const config = await forkSnyk(['config'], false);
55
66
 
56
67
  if (!config.includes('api: ')) {
68
+ debug('no token in the config %s', config);
57
69
  await forkSnyk(['auth'], true);
58
70
  }
59
71
 
@@ -67,16 +79,19 @@ export async function getSnykReport(reportPath, repoRoot) {
67
79
  'Snyk Code is not supported for org: enable in Settings > Snyk Code'
68
80
  )
69
81
  ) {
82
+ debug('snyk code is not enabled %s', out);
70
83
  const answer = await question(
71
84
  "Do you want to be taken to the relevant Snyk's online article? (Y/N)"
72
85
  );
86
+ debug('answer %s', answer);
73
87
 
74
88
  if (['y', 'yes', ''].includes(answer.toLowerCase())) {
89
+ debug('opening the browser');
75
90
  await open(
76
91
  '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'
77
92
  );
78
93
  }
79
-
80
- process.exit(0);
94
+ return false;
81
95
  }
96
+ return true;
82
97
  }
@@ -1,6 +1,13 @@
1
1
  import fetch, { FormData, fileFrom } from 'node-fetch';
2
+ import Debug from 'debug';
3
+
4
+ const debug = Debug('mobbdev:upload-file');
2
5
 
3
6
  export async function uploadFile(reportPath, url, uploadKey, uploadFields) {
7
+ debug('upload report file start %s %s', reportPath, url);
8
+ debug('upload fields %o', uploadFields);
9
+ debug('upload key %s', uploadKey);
10
+
4
11
  const form = new FormData();
5
12
 
6
13
  for (const key in uploadFields) {
@@ -16,6 +23,8 @@ export async function uploadFile(reportPath, url, uploadKey, uploadFields) {
16
23
  });
17
24
 
18
25
  if (!response.ok) {
26
+ debug('error from S3 %s %s', response.body, response.status);
19
27
  throw new Error(`Failed to upload the report: ${response.status}`);
20
28
  }
29
+ debug('upload report file done');
21
30
  }
package/src/web-login.mjs CHANGED
@@ -1,10 +1,15 @@
1
+ import Debug from 'debug';
1
2
  import { setTimeout, clearTimeout } from 'node:timers';
2
3
  import http from 'node:http';
3
- import { WEB_LOGIN_URL } from './constants.mjs';
4
4
  import querystring from 'node:querystring';
5
5
  import open from 'open';
6
+ import { WEB_LOGIN_URL } from './constants.mjs';
7
+
8
+ const debug = Debug('mobbdev:web-login');
6
9
 
7
10
  export async function webLogin() {
11
+ debug('web login start');
12
+
8
13
  let responseResolver;
9
14
  let responseRejecter;
10
15
  const responseAwaiter = new Promise((resolve, reject) => {
@@ -12,17 +17,21 @@ export async function webLogin() {
12
17
  responseRejecter = reject;
13
18
  });
14
19
  const timerHandler = setTimeout(() => {
15
- responseRejecter(new Error('No login happened in one minute.'));
16
- }, 60000);
20
+ debug('timeout happened');
21
+ responseRejecter(new Error('No login happened in three minutes.'));
22
+ }, 180000);
17
23
 
18
24
  const server = http.createServer((req, res) => {
25
+ debug('incoming request');
19
26
  let body = '';
20
27
 
21
28
  req.on('data', (chunk) => {
29
+ debug('http server get chunk %s', chunk);
22
30
  body += chunk;
23
31
  });
24
32
 
25
33
  req.on('end', () => {
34
+ debug('http server end %s', body);
26
35
  res.writeHead(301, {
27
36
  Location: `${WEB_LOGIN_URL}?done=true`,
28
37
  }).end();
@@ -30,17 +39,22 @@ export async function webLogin() {
30
39
  });
31
40
  });
32
41
 
42
+ debug('http server starting');
33
43
  const port = await new Promise((resolve) => {
34
44
  server.listen(0, '127.0.0.1', () => {
35
45
  resolve(server.address().port);
36
46
  });
37
47
  });
48
+ debug('http server started on port %d', port);
38
49
 
50
+ debug('opening the browser on %s', `${WEB_LOGIN_URL}?port=${port}`);
39
51
  await open(`${WEB_LOGIN_URL}?port=${port}`);
40
52
 
41
53
  try {
54
+ debug('waiting for http request');
42
55
  return await responseAwaiter;
43
56
  } finally {
57
+ debug('http server close');
44
58
  clearTimeout(timerHandler);
45
59
  server.close();
46
60
  }