mobbdev 0.0.9 → 0.0.11

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 CHANGED
@@ -1,4 +1,5 @@
1
- # production@v3
1
+ # production@v5
2
2
  WEB_LOGIN_URL="https://app.mobb.dev/cli-login"
3
3
  WEB_REPORT_URL="https://app.mobb.dev/report/"
4
- API_URL="https://api.mobb.dev/v1/graphql"
4
+ API_URL="https://api.mobb.dev/v1/graphql"
5
+ WEB_APP_URL="https://app.mobb.dev"
package/index.mjs CHANGED
@@ -5,7 +5,7 @@ const tmpObj = tmp.dirSync({
5
5
  unsafeCleanup: true,
6
6
  });
7
7
 
8
- main(tmpObj.name, process.argv.at(2))
8
+ main(tmpObj.name, process.argv[2])
9
9
  .catch((err) => {
10
10
  console.error(
11
11
  'Something went wrong, please try again or contact support if issue persists.'
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "mobbdev",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "Automated secure code remediation tool",
5
- "main": "index.js",
5
+ "main": "index.mjs",
6
6
  "scripts": {
7
7
  "lint": "prettier --check . && eslint **/*.mjs",
8
8
  "lint:fix": "prettier --write . && eslint --fix **/*.mjs",
@@ -19,9 +19,9 @@
19
19
  "configstore": "6.0.0",
20
20
  "dotenv": "16.0.3",
21
21
  "extract-zip": "2.0.1",
22
- "form-data": "4.0.0",
23
- "got": "12.6.0",
22
+ "node-fetch": "3.3.1",
24
23
  "open": "8.4.2",
24
+ "semver": "7.5.0",
25
25
  "snyk": "1.1118.0",
26
26
  "tmp": "0.2.1",
27
27
  "zod": "3.21.4"
@@ -33,12 +33,12 @@
33
33
  "prettier": "2.8.4"
34
34
  },
35
35
  "engines": {
36
- "node": ">=8.5.0"
36
+ "node": ">=12.20.0"
37
37
  },
38
+ "type": "module",
38
39
  "files": [
39
40
  "bin",
40
41
  "src",
41
- "index.mjs",
42
42
  ".env",
43
43
  "README.md",
44
44
  "LICENSE",
package/src/constants.mjs CHANGED
@@ -9,7 +9,7 @@ dotenv.config({ path: path.join(__dirname, '../.env') });
9
9
  const envVariablesSchema = z
10
10
  .object({
11
11
  WEB_LOGIN_URL: z.string(),
12
- WEB_REPORT_URL: z.string(),
12
+ WEB_APP_URL: z.string(),
13
13
  API_URL: z.string(),
14
14
  })
15
15
  .required();
@@ -17,5 +17,5 @@ const envVariablesSchema = z
17
17
  const envVariables = envVariablesSchema.parse(process.env);
18
18
 
19
19
  export const WEB_LOGIN_URL = envVariables.WEB_LOGIN_URL;
20
- export const WEB_REPORT_URL = envVariables.WEB_REPORT_URL;
20
+ export const WEB_APP_URL = envVariables.WEB_APP_URL;
21
21
  export const API_URL = envVariables.API_URL;
package/src/github.mjs CHANGED
@@ -1,9 +1,9 @@
1
- import got from 'got';
2
1
  import fs from 'node:fs';
3
- import { promisify } from 'node:util';
4
2
  import stream from 'node:stream';
5
- import extract from 'extract-zip';
6
3
  import path from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import fetch from 'node-fetch';
6
+ import extract from 'extract-zip';
7
7
 
8
8
  const pipeline = promisify(stream.pipeline);
9
9
 
@@ -18,44 +18,37 @@ export async function getDefaultBranch(repoUrl) {
18
18
  slug = slug.substring(0, slug.length - '.git'.length);
19
19
  }
20
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) {
21
+ const response = await fetch(`https://api.github.com/repos/${slug}`, {
22
+ method: 'GET',
23
+ headers: {
24
+ Accept: 'application/vnd.github+json',
25
+ 'X-GitHub-Api-Version': '2022-11-28',
26
+ },
27
+ });
28
+
29
+ if (!response.ok) {
32
30
  throw new Error(
33
31
  `Can't get default branch, make sure the repository is public: ${repoUrl}.`
34
32
  );
35
33
  }
34
+
35
+ const repoInfo = await response.json();
36
+
37
+ return repoInfo.default_branch;
36
38
  }
37
39
 
38
40
  export async function downloadRepo(repoUrl, reference, dirname) {
39
41
  const zipFilePath = path.join(dirname, 'repo.zip');
40
- const downloadStream = got.stream(`${repoUrl}/zipball/${reference}`);
42
+ const response = await fetch(`${repoUrl}/zipball/${reference}`);
41
43
  const fileWriterStream = fs.createWriteStream(zipFilePath);
42
44
 
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);
45
+ await pipeline(response.body, fileWriterStream);
52
46
  await extract(zipFilePath, { dir: dirname });
53
47
 
54
48
  const repoRoot = fs
55
49
  .readdirSync(dirname, { withFileTypes: true })
56
50
  .filter((dirent) => dirent.isDirectory())
57
- .map((dirent) => dirent.name)
58
- .at(0);
51
+ .map((dirent) => dirent.name)[0];
59
52
 
60
53
  return path.join(dirname, repoRoot);
61
54
  }
package/src/gql.mjs CHANGED
@@ -1,19 +1,29 @@
1
- import got from 'got';
1
+ import fetch from 'node-fetch';
2
2
  import { API_URL } from './constants.mjs';
3
3
 
4
4
  const ME = `
5
- query me {
6
- me {
5
+ query Me {
6
+ user {
7
+ id
7
8
  email
8
- projectId
9
+ userOrganizations {
10
+ organization {
11
+ id
12
+ projects {
13
+ id
14
+ }
15
+ }
16
+ }
9
17
  }
10
18
  }
11
19
  `;
12
20
 
13
21
  const CREATE_COMMUNITY_USER = `
14
22
  mutation CreateCommunityUser {
15
- createCommunityUser {
16
- status
23
+ initOrganizationAndProject {
24
+ userId
25
+ projectId
26
+ organizationId
17
27
  }
18
28
  }
19
29
  `;
@@ -34,13 +44,13 @@ mutation uploadS3BucketInfo($fileName: String!) {
34
44
  `;
35
45
 
36
46
  const SUBMIT_VULNERABILITY_REPORT = `
37
- mutation SubmitVulnerabilityReport($vulnerabilityReportFileName: String!, $fixReportId: String!, $repoUrl: String!, $reference: String!) {
47
+ mutation SubmitVulnerabilityReport($vulnerabilityReportFileName: String!, $fixReportId: String!, $repoUrl: String!, $reference: String!, $projectId: String!) {
38
48
  submitVulnerabilityReport(
39
49
  fixReportId: $fixReportId
40
50
  repoUrl: $repoUrl
41
51
  reference: $reference
42
52
  vulnerabilityReportFileName: $vulnerabilityReportFileName
43
- githubAuthToken: null
53
+ projectId: $projectId
44
54
  ) {
45
55
  __typename
46
56
  }
@@ -48,40 +58,65 @@ mutation SubmitVulnerabilityReport($vulnerabilityReportFileName: String!, $fixRe
48
58
  `;
49
59
 
50
60
  export class GQLClient {
51
- #token;
52
-
53
61
  constructor(token) {
54
- this.#token = token;
62
+ this._token = token;
63
+ this._projectId = undefined;
64
+ this._organizationId = undefined;
55
65
  }
56
66
 
57
- async #apiCall(query, variables = {}) {
58
- const response = await got(API_URL, {
67
+ async _apiCall(query, variables = {}) {
68
+ const response = await fetch(API_URL, {
59
69
  method: 'POST',
60
70
  headers: {
61
- authorization: `Bearer ${this.#token}`,
71
+ authorization: `Bearer ${this._token}`,
62
72
  },
63
73
  body: JSON.stringify({
64
74
  query,
65
75
  variables,
66
76
  }),
67
- }).json();
77
+ });
68
78
 
69
- if (response.errors) {
79
+ if (!response.ok) {
80
+ throw new Error(`API call failed: ${response.status}`);
81
+ }
82
+
83
+ const data = await response.json();
84
+
85
+ if (data.errors) {
70
86
  throw new Error(`API error: ${response.errors[0].message}`);
71
87
  }
72
88
 
73
- if (!response.data) {
89
+ if (!data.data) {
74
90
  throw new Error('No data returned for the API query.');
75
91
  }
76
92
 
77
- return response.data;
93
+ return data.data;
94
+ }
95
+
96
+ getOrganizationId() {
97
+ return this._organizationId;
98
+ }
99
+
100
+ getProjectId() {
101
+ return this._projectId;
78
102
  }
79
103
 
80
104
  async verifyToken() {
81
105
  await this.createCommunityUser();
82
106
 
83
107
  try {
84
- await this.#apiCall(ME);
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;
85
120
  } catch (e) {
86
121
  return false;
87
122
  }
@@ -90,14 +125,14 @@ export class GQLClient {
90
125
 
91
126
  async createCommunityUser() {
92
127
  try {
93
- await this.#apiCall(CREATE_COMMUNITY_USER);
128
+ await this._apiCall(CREATE_COMMUNITY_USER);
94
129
  } catch (e) {
95
130
  // Ignore errors
96
131
  }
97
132
  }
98
133
 
99
134
  async uploadS3BucketInfo() {
100
- const data = await this.#apiCall(UPLOAD_S3_BUCKET_INFO, {
135
+ const data = await this._apiCall(UPLOAD_S3_BUCKET_INFO, {
101
136
  fileName: 'report.json',
102
137
  });
103
138
 
@@ -112,11 +147,12 @@ export class GQLClient {
112
147
  }
113
148
 
114
149
  async submitVulnerabilityReport(fixReportId, repoUrl, reference) {
115
- await this.#apiCall(SUBMIT_VULNERABILITY_REPORT, {
150
+ await this._apiCall(SUBMIT_VULNERABILITY_REPORT, {
116
151
  fixReportId,
117
152
  repoUrl,
118
153
  reference,
119
154
  vulnerabilityReportFileName: 'report.json',
155
+ projectId: this._projectId,
120
156
  });
121
157
  }
122
158
  }
package/src/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { fileURLToPath } from 'node:url';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ import semver from 'semver';
4
5
  import open from 'open';
5
6
  import Configstore from 'configstore';
6
7
  import { GQLClient } from './gql.mjs';
@@ -8,12 +9,20 @@ import { webLogin } from './web-login.mjs';
8
9
  import { downloadRepo, getDefaultBranch } from './github.mjs';
9
10
  import { getSnykReport } from './snyk.mjs';
10
11
  import { uploadFile } from './upload-file.mjs';
11
- import { WEB_REPORT_URL } from './constants.mjs';
12
+ import { WEB_APP_URL } from './constants.mjs';
12
13
 
13
14
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
15
  const packageJson = JSON.parse(
15
16
  fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8')
16
17
  );
18
+
19
+ if (!semver.satisfies(process.version, packageJson.engines.node)) {
20
+ console.error(
21
+ `${packageJson.name} requires node version ${packageJson.engines.node}, but running ${process.version}.`
22
+ );
23
+ process.exit(1);
24
+ }
25
+
17
26
  const config = new Configstore(packageJson.name, { token: '' });
18
27
 
19
28
  export async function main(dirname, repoUrl) {
@@ -66,12 +75,17 @@ export async function main(dirname, repoUrl) {
66
75
  reference
67
76
  );
68
77
 
69
- if ((report.runs?.at(0)?.results?.length ?? 0) === 0) {
78
+ const results = ((report.runs || [])[0] || {}).results || [];
79
+ if (results.length === 0) {
70
80
  console.log('Snyk has not found any vulnerabilities — nothing to fix.');
71
81
  } else {
72
82
  console.log(
73
83
  'You will be redirected to our report page, please wait until the analysis is finished and enjoy your fixes.'
74
84
  );
75
- await open(`${WEB_REPORT_URL}${uploadData.fixReportId}`);
85
+ const projectId = gqlClient.getProjectId();
86
+ const organizationId = gqlClient.getOrganizationId();
87
+ await open(
88
+ `${WEB_APP_URL}/organization/${organizationId}/project/${projectId}/report/${uploadData.fixReportId}`
89
+ );
76
90
  }
77
91
  }
@@ -1,6 +1,4 @@
1
- import FormData from 'form-data';
2
- import fs from 'node:fs';
3
- import got from 'got';
1
+ import fetch, { FormData, fileFrom } from 'node-fetch';
4
2
 
5
3
  export async function uploadFile(reportPath, url, uploadKey, uploadFields) {
6
4
  const form = new FormData();
@@ -10,10 +8,14 @@ export async function uploadFile(reportPath, url, uploadKey, uploadFields) {
10
8
  }
11
9
 
12
10
  form.append('key', uploadKey);
13
- form.append('file', fs.createReadStream(reportPath));
11
+ form.append('file', await fileFrom(reportPath));
14
12
 
15
- await got(url, {
13
+ const response = await fetch(url, {
16
14
  method: 'POST',
17
15
  body: form,
18
16
  });
17
+
18
+ if (!response.ok) {
19
+ throw new Error(`Failed to upload the report: ${response.status}`);
20
+ }
19
21
  }