mobbdev 0.0.36 → 0.0.38
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/bin/cli.mjs +1 -1
- package/dist/index.mjs +2380 -0
- package/package.json +5 -2
- package/dist/index.js +0 -1324
package/dist/index.js
DELETED
|
@@ -1,1324 +0,0 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __export = (target, all) => {
|
|
3
|
-
for (var name in all)
|
|
4
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
// src/index.ts
|
|
8
|
-
import { hideBin } from "yargs/helpers";
|
|
9
|
-
|
|
10
|
-
// src/args/yargs.ts
|
|
11
|
-
import chalk8 from "chalk";
|
|
12
|
-
import yargs from "yargs/yargs";
|
|
13
|
-
|
|
14
|
-
// src/args/commands/analyze.ts
|
|
15
|
-
import fs4 from "node:fs";
|
|
16
|
-
|
|
17
|
-
// src/constants.ts
|
|
18
|
-
import path from "node:path";
|
|
19
|
-
import { fileURLToPath } from "node:url";
|
|
20
|
-
import Debug from "debug";
|
|
21
|
-
import * as dotenv from "dotenv";
|
|
22
|
-
import { z } from "zod";
|
|
23
|
-
var debug = Debug("mobbdev:constants");
|
|
24
|
-
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
-
dotenv.config({ path: path.join(__dirname, "../.env") });
|
|
26
|
-
var SCANNERS = {
|
|
27
|
-
Checkmarx: "checkmarx",
|
|
28
|
-
Codeql: "codeql",
|
|
29
|
-
Fortify: "fortify",
|
|
30
|
-
Snyk: "snyk"
|
|
31
|
-
};
|
|
32
|
-
var envVariablesSchema = z.object({
|
|
33
|
-
WEB_LOGIN_URL: z.string(),
|
|
34
|
-
WEB_APP_URL: z.string(),
|
|
35
|
-
API_URL: z.string()
|
|
36
|
-
}).required();
|
|
37
|
-
var envVariables = envVariablesSchema.parse(process.env);
|
|
38
|
-
debug("config %o", envVariables);
|
|
39
|
-
var mobbAscii = `
|
|
40
|
-
..
|
|
41
|
-
..........
|
|
42
|
-
.................
|
|
43
|
-
...........................
|
|
44
|
-
..............................
|
|
45
|
-
................................
|
|
46
|
-
..................................
|
|
47
|
-
....................................
|
|
48
|
-
.....................................
|
|
49
|
-
.............................................
|
|
50
|
-
.................................................
|
|
51
|
-
............................... .................
|
|
52
|
-
.................................. ............
|
|
53
|
-
.................. ............. ..........
|
|
54
|
-
......... ........ ......... ......
|
|
55
|
-
............... ....
|
|
56
|
-
.... ..
|
|
57
|
-
|
|
58
|
-
. ...
|
|
59
|
-
..............
|
|
60
|
-
......................
|
|
61
|
-
...........................
|
|
62
|
-
................................
|
|
63
|
-
......................................
|
|
64
|
-
...............................
|
|
65
|
-
.................
|
|
66
|
-
`;
|
|
67
|
-
var WEB_LOGIN_URL = envVariables.WEB_LOGIN_URL;
|
|
68
|
-
var WEB_APP_URL = envVariables.WEB_APP_URL;
|
|
69
|
-
var API_URL = envVariables.API_URL;
|
|
70
|
-
|
|
71
|
-
// src/features/analysis/index.ts
|
|
72
|
-
import crypto from "node:crypto";
|
|
73
|
-
import fs3 from "node:fs";
|
|
74
|
-
import os from "node:os";
|
|
75
|
-
import path5 from "node:path";
|
|
76
|
-
|
|
77
|
-
// src/utils/index.ts
|
|
78
|
-
var utils_exports = {};
|
|
79
|
-
__export(utils_exports, {
|
|
80
|
-
CliError: () => CliError,
|
|
81
|
-
Spinner: () => Spinner,
|
|
82
|
-
getDirName: () => getDirName,
|
|
83
|
-
keypress: () => keypress,
|
|
84
|
-
sleep: () => sleep
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// src/utils/dirname.ts
|
|
88
|
-
import path2 from "node:path";
|
|
89
|
-
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
90
|
-
function getDirName() {
|
|
91
|
-
return path2.dirname(fileURLToPath2(import.meta.url));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// src/utils/keypress.ts
|
|
95
|
-
import readline from "node:readline";
|
|
96
|
-
async function keypress() {
|
|
97
|
-
const rl = readline.createInterface({
|
|
98
|
-
input: process.stdin,
|
|
99
|
-
output: process.stdout
|
|
100
|
-
});
|
|
101
|
-
return new Promise((resolve) => {
|
|
102
|
-
rl.question("", (answer) => {
|
|
103
|
-
rl.close();
|
|
104
|
-
process.stderr.moveCursor(0, -1);
|
|
105
|
-
process.stderr.clearLine(1);
|
|
106
|
-
resolve(answer);
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// src/utils/spinner.ts
|
|
112
|
-
import {
|
|
113
|
-
createSpinner as _createSpinner
|
|
114
|
-
} from "nanospinner";
|
|
115
|
-
var mockSpinner = {
|
|
116
|
-
success: () => mockSpinner,
|
|
117
|
-
error: () => mockSpinner,
|
|
118
|
-
warn: () => mockSpinner,
|
|
119
|
-
stop: () => mockSpinner,
|
|
120
|
-
start: () => mockSpinner,
|
|
121
|
-
update: () => mockSpinner,
|
|
122
|
-
reset: () => mockSpinner,
|
|
123
|
-
clear: () => mockSpinner,
|
|
124
|
-
spin: () => mockSpinner
|
|
125
|
-
};
|
|
126
|
-
function Spinner({ ci = false } = {}) {
|
|
127
|
-
return {
|
|
128
|
-
createSpinner: (text, options) => ci ? mockSpinner : _createSpinner(text, options)
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// src/utils/index.ts
|
|
133
|
-
var sleep = (ms = 2e3) => new Promise((r) => setTimeout(r, ms));
|
|
134
|
-
var CliError = class extends Error {
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
// src/features/analysis/index.ts
|
|
138
|
-
import chalk3 from "chalk";
|
|
139
|
-
import Configstore from "configstore";
|
|
140
|
-
import Debug8 from "debug";
|
|
141
|
-
import open2 from "open";
|
|
142
|
-
import semver from "semver";
|
|
143
|
-
import tmp from "tmp";
|
|
144
|
-
|
|
145
|
-
// src/features/analysis/git.ts
|
|
146
|
-
import Debug2 from "debug";
|
|
147
|
-
import { simpleGit } from "simple-git";
|
|
148
|
-
var debug2 = Debug2("mobbdev:git");
|
|
149
|
-
async function getGitInfo(srcDirPath) {
|
|
150
|
-
debug2("getting git info for %s", srcDirPath);
|
|
151
|
-
const git = simpleGit({
|
|
152
|
-
baseDir: srcDirPath,
|
|
153
|
-
maxConcurrentProcesses: 1,
|
|
154
|
-
trimmed: true
|
|
155
|
-
});
|
|
156
|
-
let repoUrl = "";
|
|
157
|
-
let hash = "";
|
|
158
|
-
let reference = "";
|
|
159
|
-
try {
|
|
160
|
-
repoUrl = (await git.getConfig("remote.origin.url")).value || "";
|
|
161
|
-
hash = await git.revparse(["HEAD"]) || "";
|
|
162
|
-
reference = await git.revparse(["--abbrev-ref", "HEAD"]) || "";
|
|
163
|
-
} catch (e) {
|
|
164
|
-
if (e instanceof Error) {
|
|
165
|
-
debug2("failed to run git %o", e);
|
|
166
|
-
if (e.message.includes(" spawn ")) {
|
|
167
|
-
debug2("git cli not installed");
|
|
168
|
-
} else if (e.message.includes(" not a git repository ")) {
|
|
169
|
-
debug2("folder is not a git repo");
|
|
170
|
-
} else {
|
|
171
|
-
throw e;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
throw e;
|
|
175
|
-
}
|
|
176
|
-
if (repoUrl.endsWith(".git")) {
|
|
177
|
-
repoUrl = repoUrl.slice(0, -".git".length);
|
|
178
|
-
}
|
|
179
|
-
if (repoUrl.startsWith("git@github.com:")) {
|
|
180
|
-
repoUrl = repoUrl.replace("git@github.com:", "https://github.com/");
|
|
181
|
-
}
|
|
182
|
-
return {
|
|
183
|
-
repoUrl,
|
|
184
|
-
hash,
|
|
185
|
-
reference
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// src/features/analysis/github/github.ts
|
|
190
|
-
import fs from "node:fs";
|
|
191
|
-
import path3 from "node:path";
|
|
192
|
-
import stream from "node:stream";
|
|
193
|
-
import { promisify } from "node:util";
|
|
194
|
-
import { RequestError } from "@octokit/request-error";
|
|
195
|
-
import chalk from "chalk";
|
|
196
|
-
import Debug3 from "debug";
|
|
197
|
-
import extract from "extract-zip";
|
|
198
|
-
import fetch from "node-fetch";
|
|
199
|
-
import { Octokit } from "octokit";
|
|
200
|
-
var pipeline = promisify(stream.pipeline);
|
|
201
|
-
var debug3 = Debug3("mobbdev:github");
|
|
202
|
-
async function _getRepo({ owner, repo }, { token } = {}) {
|
|
203
|
-
const octokit = new Octokit({ auth: token });
|
|
204
|
-
return octokit.rest.repos.get({
|
|
205
|
-
owner,
|
|
206
|
-
repo
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
function extractSlug(repoUrl) {
|
|
210
|
-
debug3("get default branch %s", repoUrl);
|
|
211
|
-
let slug = repoUrl.replace(/https?:\/\/github\.com\//i, "");
|
|
212
|
-
if (slug.endsWith("/")) {
|
|
213
|
-
slug = slug.substring(0, slug.length - 1);
|
|
214
|
-
}
|
|
215
|
-
if (slug.endsWith(".git")) {
|
|
216
|
-
slug = slug.substring(0, slug.length - ".git".length);
|
|
217
|
-
}
|
|
218
|
-
debug3("slug %s", slug);
|
|
219
|
-
return slug;
|
|
220
|
-
}
|
|
221
|
-
function parseRepoUrl(repoUrl) {
|
|
222
|
-
const slug = extractSlug(repoUrl);
|
|
223
|
-
const [owner, repo] = slug.split("/");
|
|
224
|
-
if (!owner || !repo) {
|
|
225
|
-
throw new Error(`Error parsing repo url ${repoUrl}}`);
|
|
226
|
-
}
|
|
227
|
-
return { owner, repo };
|
|
228
|
-
}
|
|
229
|
-
async function canReachRepo(repoUrl, { token } = {}) {
|
|
230
|
-
const repoAndOnwer = parseRepoUrl(repoUrl);
|
|
231
|
-
try {
|
|
232
|
-
const res = await _getRepo(repoAndOnwer, { token });
|
|
233
|
-
return res;
|
|
234
|
-
} catch (e) {
|
|
235
|
-
if (e instanceof RequestError) {
|
|
236
|
-
return false;
|
|
237
|
-
}
|
|
238
|
-
throw e;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
async function getRepo(repoUrl, { token } = {}) {
|
|
242
|
-
const repoAndOnwer = parseRepoUrl(repoUrl);
|
|
243
|
-
try {
|
|
244
|
-
const res = await _getRepo(repoAndOnwer, { token });
|
|
245
|
-
return res;
|
|
246
|
-
} catch (e) {
|
|
247
|
-
if (e instanceof RequestError) {
|
|
248
|
-
debug3("GH request failed %s %s", e.message, e.status);
|
|
249
|
-
throw new CliError(
|
|
250
|
-
`Can't get repository, make sure you have access to : ${repoUrl}.`
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
throw e;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
async function downloadRepo({ repoUrl, reference, dirname, ci }, { token } = {}) {
|
|
257
|
-
const { createSpinner: createSpinner3 } = Spinner({ ci });
|
|
258
|
-
const repoSpinner = createSpinner3("\u{1F4BE} Downloading Repo").start();
|
|
259
|
-
debug3("download repo %s %s %s", repoUrl, reference, dirname);
|
|
260
|
-
const zipFilePath = path3.join(dirname, "repo.zip");
|
|
261
|
-
const response = await fetch(`${repoUrl}/zipball/${reference}`, {
|
|
262
|
-
method: "GET",
|
|
263
|
-
headers: {
|
|
264
|
-
...token && { Authorization: `bearer ${token}` }
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
if (!response.ok) {
|
|
268
|
-
debug3("GH zipball request failed %s %s", response.body, response.status);
|
|
269
|
-
repoSpinner.error({ text: "\u{1F4BE} Repo download failed" });
|
|
270
|
-
throw new Error(
|
|
271
|
-
`Can't access the the branch ${chalk.bold(reference)} on ${chalk.bold(
|
|
272
|
-
repoUrl
|
|
273
|
-
)} make sure it exits.`
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
const fileWriterStream = fs.createWriteStream(zipFilePath);
|
|
277
|
-
if (!response.body) {
|
|
278
|
-
throw new Error("Response body is empty");
|
|
279
|
-
}
|
|
280
|
-
await pipeline(response.body, fileWriterStream);
|
|
281
|
-
await extract(zipFilePath, { dir: dirname });
|
|
282
|
-
const repoRoot = fs.readdirSync(dirname, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name)[0];
|
|
283
|
-
if (!repoRoot) {
|
|
284
|
-
throw new Error("Repo root not found");
|
|
285
|
-
}
|
|
286
|
-
debug3("repo root %s", repoRoot);
|
|
287
|
-
repoSpinner.success({ text: "\u{1F4BE} Repo downloaded successfully" });
|
|
288
|
-
return path3.join(dirname, repoRoot);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// src/features/analysis/graphql/gql.ts
|
|
292
|
-
import Debug4 from "debug";
|
|
293
|
-
import { GraphQLClient } from "graphql-request";
|
|
294
|
-
|
|
295
|
-
// src/features/analysis/graphql/mutations.ts
|
|
296
|
-
import { gql } from "graphql-request";
|
|
297
|
-
var UPLOAD_S3_BUCKET_INFO = gql`
|
|
298
|
-
mutation uploadS3BucketInfo($fileName: String!) {
|
|
299
|
-
uploadS3BucketInfo(fileName: $fileName) {
|
|
300
|
-
status
|
|
301
|
-
error
|
|
302
|
-
reportUploadInfo: uploadInfo {
|
|
303
|
-
url
|
|
304
|
-
fixReportId
|
|
305
|
-
uploadFieldsJSON
|
|
306
|
-
uploadKey
|
|
307
|
-
}
|
|
308
|
-
repoUploadInfo {
|
|
309
|
-
url
|
|
310
|
-
fixReportId
|
|
311
|
-
uploadFieldsJSON
|
|
312
|
-
uploadKey
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
`;
|
|
317
|
-
var SUBMIT_VULNERABILITY_REPORT = gql`
|
|
318
|
-
mutation SubmitVulnerabilityReport(
|
|
319
|
-
$vulnerabilityReportFileName: String!
|
|
320
|
-
$fixReportId: String!
|
|
321
|
-
$repoUrl: String!
|
|
322
|
-
$reference: String!
|
|
323
|
-
$projectId: String!
|
|
324
|
-
$sha: String
|
|
325
|
-
) {
|
|
326
|
-
submitVulnerabilityReport(
|
|
327
|
-
fixReportId: $fixReportId
|
|
328
|
-
repoUrl: $repoUrl
|
|
329
|
-
reference: $reference
|
|
330
|
-
sha: $sha
|
|
331
|
-
vulnerabilityReportFileName: $vulnerabilityReportFileName
|
|
332
|
-
projectId: $projectId
|
|
333
|
-
) {
|
|
334
|
-
__typename
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
`;
|
|
338
|
-
var CREATE_COMMUNITY_USER = gql`
|
|
339
|
-
mutation CreateCommunityUser {
|
|
340
|
-
initOrganizationAndProject {
|
|
341
|
-
userId
|
|
342
|
-
projectId
|
|
343
|
-
organizationId
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
`;
|
|
347
|
-
var CREATE_CLI_LOGIN = gql`
|
|
348
|
-
mutation CreateCliLogin($publicKey: String!) {
|
|
349
|
-
insert_cli_login_one(object: { publicKey: $publicKey }) {
|
|
350
|
-
id
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
`;
|
|
354
|
-
var PERFORM_CLI_LOGIN = gql`
|
|
355
|
-
mutation performCliLogin($loginId: String!) {
|
|
356
|
-
performCliLogin(loginId: $loginId) {
|
|
357
|
-
status
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
`;
|
|
361
|
-
|
|
362
|
-
// src/features/analysis/graphql/queries.ts
|
|
363
|
-
import { gql as gql2 } from "graphql-request";
|
|
364
|
-
var ME = gql2`
|
|
365
|
-
query Me {
|
|
366
|
-
me {
|
|
367
|
-
id
|
|
368
|
-
email
|
|
369
|
-
githubToken
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
`;
|
|
373
|
-
var GET_ORG_AND_PROJECT_ID = gql2`
|
|
374
|
-
query getOrgAndProjectId {
|
|
375
|
-
users: user {
|
|
376
|
-
userOrganizationsAndUserOrganizationRoles {
|
|
377
|
-
organization {
|
|
378
|
-
id
|
|
379
|
-
projects(order_by: { updatedAt: desc }) {
|
|
380
|
-
id
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
`;
|
|
387
|
-
var GET_ENCRYPTED_API_TOKEN = gql2`
|
|
388
|
-
query GetEncryptedApiToken($loginId: uuid!) {
|
|
389
|
-
cli_login_by_pk(id: $loginId) {
|
|
390
|
-
encryptedApiToken
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
`;
|
|
394
|
-
|
|
395
|
-
// src/features/analysis/graphql/types.ts
|
|
396
|
-
import { z as z2 } from "zod";
|
|
397
|
-
var UploadFieldsZ = z2.object({
|
|
398
|
-
bucket: z2.string(),
|
|
399
|
-
"X-Amz-Algorithm": z2.string(),
|
|
400
|
-
"X-Amz-Credential": z2.string(),
|
|
401
|
-
"X-Amz-Date": z2.string(),
|
|
402
|
-
Policy: z2.string(),
|
|
403
|
-
"X-Amz-Signature": z2.string()
|
|
404
|
-
});
|
|
405
|
-
var ReportUploadInfoZ = z2.object({
|
|
406
|
-
url: z2.string(),
|
|
407
|
-
fixReportId: z2.string(),
|
|
408
|
-
uploadFieldsJSON: z2.string().transform((str, ctx) => {
|
|
409
|
-
try {
|
|
410
|
-
return JSON.parse(str);
|
|
411
|
-
} catch (e) {
|
|
412
|
-
ctx.addIssue({ code: "custom", message: "Invalid JSON" });
|
|
413
|
-
return z2.NEVER;
|
|
414
|
-
}
|
|
415
|
-
}),
|
|
416
|
-
uploadKey: z2.string()
|
|
417
|
-
}).transform(({ uploadFieldsJSON, ...input }) => ({
|
|
418
|
-
...input,
|
|
419
|
-
uploadFields: uploadFieldsJSON
|
|
420
|
-
}));
|
|
421
|
-
var UploadS3BucketInfoZ = z2.object({
|
|
422
|
-
uploadS3BucketInfo: z2.object({
|
|
423
|
-
status: z2.string(),
|
|
424
|
-
error: z2.string().nullish(),
|
|
425
|
-
reportUploadInfo: ReportUploadInfoZ,
|
|
426
|
-
repoUploadInfo: ReportUploadInfoZ
|
|
427
|
-
})
|
|
428
|
-
});
|
|
429
|
-
var GetOrgAndProjectIdQueryZ = z2.object({
|
|
430
|
-
users: z2.array(
|
|
431
|
-
z2.object({
|
|
432
|
-
userOrganizationsAndUserOrganizationRoles: z2.array(
|
|
433
|
-
z2.object({
|
|
434
|
-
organization: z2.object({
|
|
435
|
-
id: z2.string(),
|
|
436
|
-
projects: z2.array(
|
|
437
|
-
z2.object({
|
|
438
|
-
id: z2.string()
|
|
439
|
-
})
|
|
440
|
-
).nonempty()
|
|
441
|
-
})
|
|
442
|
-
})
|
|
443
|
-
).nonempty()
|
|
444
|
-
})
|
|
445
|
-
).nonempty()
|
|
446
|
-
});
|
|
447
|
-
var CreateCliLoginZ = z2.object({
|
|
448
|
-
insert_cli_login_one: z2.object({
|
|
449
|
-
id: z2.string()
|
|
450
|
-
})
|
|
451
|
-
});
|
|
452
|
-
var GetEncryptedApiTokenZ = z2.object({
|
|
453
|
-
cli_login_by_pk: z2.object({
|
|
454
|
-
encryptedApiToken: z2.string().nullable()
|
|
455
|
-
})
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
// src/features/analysis/graphql/gql.ts
|
|
459
|
-
var debug4 = Debug4("mobbdev:gql");
|
|
460
|
-
var API_KEY_HEADER_NAME = "x-mobb-key";
|
|
461
|
-
var GQLClient = class {
|
|
462
|
-
constructor(args) {
|
|
463
|
-
const { apiKey } = args;
|
|
464
|
-
debug4(`init with apiKey ${apiKey}`);
|
|
465
|
-
this._client = new GraphQLClient(API_URL, {
|
|
466
|
-
headers: { [API_KEY_HEADER_NAME]: apiKey || "" }
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
async getUserInfo() {
|
|
470
|
-
const { me } = await this._client.request(ME);
|
|
471
|
-
return me;
|
|
472
|
-
}
|
|
473
|
-
async createCliLogin(variables) {
|
|
474
|
-
const res = CreateCliLoginZ.parse(
|
|
475
|
-
await this._client.request(
|
|
476
|
-
CREATE_CLI_LOGIN,
|
|
477
|
-
variables,
|
|
478
|
-
{
|
|
479
|
-
// We may have outdated API key in the config storage. Avoid using it for the login request.
|
|
480
|
-
[API_KEY_HEADER_NAME]: ""
|
|
481
|
-
}
|
|
482
|
-
)
|
|
483
|
-
);
|
|
484
|
-
return res.insert_cli_login_one.id;
|
|
485
|
-
}
|
|
486
|
-
async verifyToken() {
|
|
487
|
-
await this.createCommunityUser();
|
|
488
|
-
try {
|
|
489
|
-
await this.getUserInfo();
|
|
490
|
-
} catch (e) {
|
|
491
|
-
debug4("verify token failed %o", e);
|
|
492
|
-
return false;
|
|
493
|
-
}
|
|
494
|
-
return true;
|
|
495
|
-
}
|
|
496
|
-
async getOrgAndProjectId() {
|
|
497
|
-
const getOrgAndProjectIdResult = await this._client.request(
|
|
498
|
-
GET_ORG_AND_PROJECT_ID
|
|
499
|
-
);
|
|
500
|
-
const [user] = GetOrgAndProjectIdQueryZ.parse(
|
|
501
|
-
getOrgAndProjectIdResult
|
|
502
|
-
).users;
|
|
503
|
-
const org = user.userOrganizationsAndUserOrganizationRoles[0].organization;
|
|
504
|
-
return {
|
|
505
|
-
organizationId: org.id,
|
|
506
|
-
projectId: org.projects[0].id
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
async getEncryptedApiToken(variables) {
|
|
510
|
-
const res = await this._client.request(
|
|
511
|
-
GET_ENCRYPTED_API_TOKEN,
|
|
512
|
-
variables,
|
|
513
|
-
{
|
|
514
|
-
// We may have outdated API key in the config storage. Avoid using it for the login request.
|
|
515
|
-
[API_KEY_HEADER_NAME]: ""
|
|
516
|
-
}
|
|
517
|
-
);
|
|
518
|
-
return GetEncryptedApiTokenZ.parse(res).cli_login_by_pk.encryptedApiToken;
|
|
519
|
-
}
|
|
520
|
-
async createCommunityUser() {
|
|
521
|
-
try {
|
|
522
|
-
await this._client.request(CREATE_COMMUNITY_USER);
|
|
523
|
-
} catch (e) {
|
|
524
|
-
debug4("create community user failed %o", e);
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
async uploadS3BucketInfo() {
|
|
528
|
-
const uploadS3BucketInfoResult = await this._client.request(UPLOAD_S3_BUCKET_INFO, {
|
|
529
|
-
fileName: "report.json"
|
|
530
|
-
});
|
|
531
|
-
return UploadS3BucketInfoZ.parse(uploadS3BucketInfoResult);
|
|
532
|
-
}
|
|
533
|
-
async submitVulnerabilityReport({
|
|
534
|
-
fixReportId,
|
|
535
|
-
repoUrl,
|
|
536
|
-
reference,
|
|
537
|
-
projectId,
|
|
538
|
-
sha
|
|
539
|
-
}) {
|
|
540
|
-
await this._client.request(SUBMIT_VULNERABILITY_REPORT, {
|
|
541
|
-
fixReportId,
|
|
542
|
-
repoUrl,
|
|
543
|
-
reference,
|
|
544
|
-
vulnerabilityReportFileName: "report.json",
|
|
545
|
-
projectId,
|
|
546
|
-
sha: sha || ""
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
};
|
|
550
|
-
|
|
551
|
-
// src/features/analysis/pack.ts
|
|
552
|
-
import fs2 from "node:fs";
|
|
553
|
-
import path4 from "node:path";
|
|
554
|
-
import AdmZip from "adm-zip";
|
|
555
|
-
import Debug5 from "debug";
|
|
556
|
-
import { globby } from "globby";
|
|
557
|
-
import { isBinary } from "istextorbinary";
|
|
558
|
-
var debug5 = Debug5("mobbdev:pack");
|
|
559
|
-
var MAX_FILE_SIZE = 1024 * 1024 * 5;
|
|
560
|
-
async function pack(srcDirPath) {
|
|
561
|
-
debug5("pack folder %s", srcDirPath);
|
|
562
|
-
const filepaths = await globby("**", {
|
|
563
|
-
gitignore: true,
|
|
564
|
-
onlyFiles: true,
|
|
565
|
-
cwd: srcDirPath,
|
|
566
|
-
followSymbolicLinks: false
|
|
567
|
-
});
|
|
568
|
-
debug5("files found %d", filepaths.length);
|
|
569
|
-
const zip = new AdmZip();
|
|
570
|
-
debug5("compressing files");
|
|
571
|
-
for (const filepath of filepaths) {
|
|
572
|
-
const absFilepath = path4.join(srcDirPath, filepath.toString());
|
|
573
|
-
if (fs2.lstatSync(absFilepath).size > MAX_FILE_SIZE) {
|
|
574
|
-
debug5("ignoring %s because the size is > 5MB", filepath);
|
|
575
|
-
continue;
|
|
576
|
-
}
|
|
577
|
-
const data = fs2.readFileSync(absFilepath);
|
|
578
|
-
if (isBinary(null, data)) {
|
|
579
|
-
debug5("ignoring %s because is seems to be a binary file", filepath);
|
|
580
|
-
continue;
|
|
581
|
-
}
|
|
582
|
-
zip.addFile(filepath.toString(), data);
|
|
583
|
-
}
|
|
584
|
-
debug5("get zip file buffer");
|
|
585
|
-
return zip.toBuffer();
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// src/features/analysis/prompts.ts
|
|
589
|
-
import inquirer from "inquirer";
|
|
590
|
-
import { createSpinner } from "nanospinner";
|
|
591
|
-
var scannerChoices = [
|
|
592
|
-
{ name: "Snyk", value: SCANNERS.Snyk },
|
|
593
|
-
{ name: "Checkmarx", value: SCANNERS.Checkmarx },
|
|
594
|
-
{ name: "Codeql", value: SCANNERS.Codeql },
|
|
595
|
-
{ name: "Fortify", value: SCANNERS.Fortify }
|
|
596
|
-
];
|
|
597
|
-
async function choseScanner() {
|
|
598
|
-
const { scanner } = await inquirer.prompt({
|
|
599
|
-
name: "scanner",
|
|
600
|
-
message: "Choose a scanner you wish to use to scan your code",
|
|
601
|
-
type: "list",
|
|
602
|
-
choices: scannerChoices
|
|
603
|
-
});
|
|
604
|
-
return scanner;
|
|
605
|
-
}
|
|
606
|
-
async function githubIntegrationPrompt() {
|
|
607
|
-
const answers = await inquirer.prompt({
|
|
608
|
-
name: "githubConfirm",
|
|
609
|
-
type: "confirm",
|
|
610
|
-
message: "It seems we don't have access to the repo, do you want to grant access to your github account",
|
|
611
|
-
default: true
|
|
612
|
-
});
|
|
613
|
-
return answers.githubConfirm;
|
|
614
|
-
}
|
|
615
|
-
async function mobbAnalysisPrompt() {
|
|
616
|
-
const spinner = createSpinner().start();
|
|
617
|
-
spinner.update({ text: "Hit any key to view available fixes" });
|
|
618
|
-
await keypress();
|
|
619
|
-
return spinner.success();
|
|
620
|
-
}
|
|
621
|
-
async function snykArticlePrompt() {
|
|
622
|
-
const { snykArticleConfirm } = await inquirer.prompt({
|
|
623
|
-
name: "snykArticleConfirm",
|
|
624
|
-
type: "confirm",
|
|
625
|
-
message: "Do you want to be taken to the relevant Snyk's online article?",
|
|
626
|
-
default: true
|
|
627
|
-
});
|
|
628
|
-
return snykArticleConfirm;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
// src/features/analysis/snyk.ts
|
|
632
|
-
import cp from "node:child_process";
|
|
633
|
-
import { createRequire } from "node:module";
|
|
634
|
-
import chalk2 from "chalk";
|
|
635
|
-
import Debug6 from "debug";
|
|
636
|
-
import { createSpinner as createSpinner2 } from "nanospinner";
|
|
637
|
-
import open from "open";
|
|
638
|
-
import * as process2 from "process";
|
|
639
|
-
import supportsColor from "supports-color";
|
|
640
|
-
var { stdout: stdout2 } = supportsColor;
|
|
641
|
-
var debug6 = Debug6("mobbdev:snyk");
|
|
642
|
-
var require2 = createRequire(import.meta.url);
|
|
643
|
-
var SNYK_PATH = require2.resolve("snyk/bin/snyk");
|
|
644
|
-
var SNYK_ARTICLE_URL = "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";
|
|
645
|
-
debug6("snyk executable path %s", SNYK_PATH);
|
|
646
|
-
async function forkSnyk(args, { display }) {
|
|
647
|
-
debug6("fork snyk with args %o %s", args, display);
|
|
648
|
-
return new Promise((resolve, reject) => {
|
|
649
|
-
const child = cp.fork(SNYK_PATH, args, {
|
|
650
|
-
stdio: ["inherit", "pipe", "pipe", "ipc"],
|
|
651
|
-
env: { FORCE_COLOR: stdout2 ? "1" : "0" }
|
|
652
|
-
});
|
|
653
|
-
let out = "";
|
|
654
|
-
const onData = (chunk) => {
|
|
655
|
-
debug6("chunk received from snyk std %s", chunk);
|
|
656
|
-
out += chunk;
|
|
657
|
-
};
|
|
658
|
-
if (!child || !child?.stdout || !child?.stderr) {
|
|
659
|
-
debug6("unable to fork snyk");
|
|
660
|
-
reject(new Error("unable to fork snyk"));
|
|
661
|
-
}
|
|
662
|
-
child.stdout?.on("data", onData);
|
|
663
|
-
child.stderr?.on("data", onData);
|
|
664
|
-
if (display) {
|
|
665
|
-
child.stdout?.pipe(process2.stdout);
|
|
666
|
-
child.stderr?.pipe(process2.stderr);
|
|
667
|
-
}
|
|
668
|
-
child.on("exit", () => {
|
|
669
|
-
debug6("snyk exit");
|
|
670
|
-
resolve(out);
|
|
671
|
-
});
|
|
672
|
-
child.on("error", (err) => {
|
|
673
|
-
debug6("snyk error %o", err);
|
|
674
|
-
reject(err);
|
|
675
|
-
});
|
|
676
|
-
});
|
|
677
|
-
}
|
|
678
|
-
async function getSnykReport(reportPath, repoRoot, { skipPrompts = false }) {
|
|
679
|
-
debug6("get snyk report start %s %s", reportPath, repoRoot);
|
|
680
|
-
const config3 = await forkSnyk(["config"], { display: false });
|
|
681
|
-
if (!config3.includes("api: ")) {
|
|
682
|
-
const snykLoginSpinner = createSpinner2().start();
|
|
683
|
-
if (!skipPrompts) {
|
|
684
|
-
snykLoginSpinner.update({
|
|
685
|
-
text: "\u{1F513} Login to Snyk is required, press any key to continue"
|
|
686
|
-
});
|
|
687
|
-
await keypress();
|
|
688
|
-
}
|
|
689
|
-
snykLoginSpinner.update({
|
|
690
|
-
text: "\u{1F513} Waiting for Snyk login to complete"
|
|
691
|
-
});
|
|
692
|
-
debug6("no token in the config %s", config3);
|
|
693
|
-
await forkSnyk(["auth"], { display: true });
|
|
694
|
-
snykLoginSpinner.success({ text: "\u{1F513} Login to Snyk Successful" });
|
|
695
|
-
}
|
|
696
|
-
const snykSpinner = createSpinner2("\u{1F50D} Scanning your repo with Snyk ").start();
|
|
697
|
-
const out = await forkSnyk(
|
|
698
|
-
["code", "test", `--sarif-file-output=${reportPath}`, repoRoot],
|
|
699
|
-
{ display: true }
|
|
700
|
-
);
|
|
701
|
-
if (out.includes(
|
|
702
|
-
"Snyk Code is not supported for org: enable in Settings > Snyk Code"
|
|
703
|
-
)) {
|
|
704
|
-
debug6("snyk code is not enabled %s", out);
|
|
705
|
-
snykSpinner.error({ text: "\u{1F50D} Snyk configuration needed" });
|
|
706
|
-
const answer = await snykArticlePrompt();
|
|
707
|
-
debug6("answer %s", answer);
|
|
708
|
-
if (answer) {
|
|
709
|
-
debug6("opening the browser");
|
|
710
|
-
await open(SNYK_ARTICLE_URL);
|
|
711
|
-
}
|
|
712
|
-
console.log(
|
|
713
|
-
chalk2.bgBlue(
|
|
714
|
-
"\nPlease enable Snyk Code in your Snyk account and try again."
|
|
715
|
-
)
|
|
716
|
-
);
|
|
717
|
-
return false;
|
|
718
|
-
}
|
|
719
|
-
snykSpinner.success({ text: "\u{1F50D} Snyk code scan completed" });
|
|
720
|
-
return true;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
// src/features/analysis/upload-file.ts
|
|
724
|
-
import Debug7 from "debug";
|
|
725
|
-
import fetch2, { File, fileFrom, FormData } from "node-fetch";
|
|
726
|
-
var debug7 = Debug7("mobbdev:upload-file");
|
|
727
|
-
async function uploadFile({
|
|
728
|
-
file,
|
|
729
|
-
url,
|
|
730
|
-
uploadKey,
|
|
731
|
-
uploadFields
|
|
732
|
-
}) {
|
|
733
|
-
debug7("upload file start %s", url);
|
|
734
|
-
debug7("upload fields %o", uploadFields);
|
|
735
|
-
debug7("upload key %s", uploadKey);
|
|
736
|
-
const form = new FormData();
|
|
737
|
-
Object.entries(uploadFields).forEach(([key, value]) => {
|
|
738
|
-
form.append(key, value);
|
|
739
|
-
});
|
|
740
|
-
form.append("key", uploadKey);
|
|
741
|
-
if (typeof file === "string") {
|
|
742
|
-
debug7("upload file from path %s", file);
|
|
743
|
-
form.append("file", await fileFrom(file));
|
|
744
|
-
} else {
|
|
745
|
-
debug7("upload file from buffer");
|
|
746
|
-
form.append("file", new File([file], "file"));
|
|
747
|
-
}
|
|
748
|
-
const response = await fetch2(url, {
|
|
749
|
-
method: "POST",
|
|
750
|
-
body: form
|
|
751
|
-
});
|
|
752
|
-
if (!response.ok) {
|
|
753
|
-
debug7("error from S3 %s %s", response.body, response.status);
|
|
754
|
-
throw new Error(`Failed to upload the file: ${response.status}`);
|
|
755
|
-
}
|
|
756
|
-
debug7("upload file done");
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// src/features/analysis/index.ts
|
|
760
|
-
var { CliError: CliError2, Spinner: Spinner2, keypress: keypress2, getDirName: getDirName2 } = utils_exports;
|
|
761
|
-
var webLoginUrl = `${WEB_APP_URL}/cli-login`;
|
|
762
|
-
var githubAuthUrl = `${WEB_APP_URL}/github-auth`;
|
|
763
|
-
var LOGIN_MAX_WAIT = 10 * 60 * 1e3;
|
|
764
|
-
var LOGIN_CHECK_DELAY = 5 * 1e3;
|
|
765
|
-
var MOBB_LOGIN_REQUIRED_MSG = `\u{1F513} Login to Mobb is Required, you will be redirected to our login page, once the authorization is complete return to this prompt, ${chalk3.bgBlue(
|
|
766
|
-
"press any key to continue"
|
|
767
|
-
)};`;
|
|
768
|
-
var tmpObj = tmp.dirSync({
|
|
769
|
-
unsafeCleanup: true
|
|
770
|
-
});
|
|
771
|
-
var getReportUrl = ({
|
|
772
|
-
organizationId,
|
|
773
|
-
projectId,
|
|
774
|
-
fixReportId
|
|
775
|
-
}) => `${WEB_APP_URL}/organization/${organizationId}/project/${projectId}/report/${fixReportId}`;
|
|
776
|
-
var debug8 = Debug8("mobbdev:index");
|
|
777
|
-
var packageJson = JSON.parse(
|
|
778
|
-
fs3.readFileSync(path5.join(getDirName2(), "../package.json"), "utf8")
|
|
779
|
-
);
|
|
780
|
-
if (!semver.satisfies(process.version, packageJson.engines.node)) {
|
|
781
|
-
throw new CliError2(
|
|
782
|
-
`${packageJson.name} requires node version ${packageJson.engines.node}, but running ${process.version}.`
|
|
783
|
-
);
|
|
784
|
-
}
|
|
785
|
-
var config2 = new Configstore(packageJson.name, { apiToken: "" });
|
|
786
|
-
debug8("config %o", config2);
|
|
787
|
-
async function runAnalysis(params, options) {
|
|
788
|
-
try {
|
|
789
|
-
await _scan(
|
|
790
|
-
{
|
|
791
|
-
...params,
|
|
792
|
-
dirname: tmpObj.name
|
|
793
|
-
},
|
|
794
|
-
options
|
|
795
|
-
);
|
|
796
|
-
} finally {
|
|
797
|
-
tmpObj.removeCallback();
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
async function _scan({
|
|
801
|
-
dirname,
|
|
802
|
-
repo,
|
|
803
|
-
scanFile,
|
|
804
|
-
apiKey,
|
|
805
|
-
ci,
|
|
806
|
-
srcPath,
|
|
807
|
-
commitHash,
|
|
808
|
-
ref
|
|
809
|
-
}, { skipPrompts = false } = {}) {
|
|
810
|
-
debug8("start %s %s", dirname, repo);
|
|
811
|
-
const { createSpinner: createSpinner3 } = Spinner2({ ci });
|
|
812
|
-
skipPrompts = skipPrompts || ci;
|
|
813
|
-
let gqlClient = new GQLClient({
|
|
814
|
-
apiKey: apiKey || config2.get("apiToken")
|
|
815
|
-
});
|
|
816
|
-
await handleMobbLogin();
|
|
817
|
-
const { projectId, organizationId } = await gqlClient.getOrgAndProjectId();
|
|
818
|
-
const {
|
|
819
|
-
uploadS3BucketInfo: { repoUploadInfo, reportUploadInfo }
|
|
820
|
-
} = await gqlClient.uploadS3BucketInfo();
|
|
821
|
-
let reportPath = scanFile;
|
|
822
|
-
if (srcPath) {
|
|
823
|
-
return await uploadExistingRepo();
|
|
824
|
-
}
|
|
825
|
-
if (!repo) {
|
|
826
|
-
throw new Error("repo is required in case srcPath is not provided");
|
|
827
|
-
}
|
|
828
|
-
const userInfo = await gqlClient.getUserInfo();
|
|
829
|
-
let { githubToken } = userInfo;
|
|
830
|
-
const isRepoAvailable = await canReachRepo(repo, {
|
|
831
|
-
token: githubToken
|
|
832
|
-
});
|
|
833
|
-
if (!isRepoAvailable) {
|
|
834
|
-
if (ci) {
|
|
835
|
-
throw new Error(
|
|
836
|
-
`Cannot access repo ${repo} with the provided token, please visit ${githubAuthUrl} to refresh your Github token`
|
|
837
|
-
);
|
|
838
|
-
}
|
|
839
|
-
githubToken = await handleGithubIntegration(githubToken);
|
|
840
|
-
const isRepoAvailable2 = await canReachRepo(repo, {
|
|
841
|
-
token: githubToken
|
|
842
|
-
});
|
|
843
|
-
if (!isRepoAvailable2) {
|
|
844
|
-
throw new Error(
|
|
845
|
-
`Cannot access repo ${repo} with the provided credentials`
|
|
846
|
-
);
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
const reference = ref ?? (await getRepo(repo, { token: githubToken })).data.default_branch;
|
|
850
|
-
debug8("org id %s", organizationId);
|
|
851
|
-
debug8("project id %s", projectId);
|
|
852
|
-
debug8("default branch %s", reference);
|
|
853
|
-
const repositoryRoot = await downloadRepo(
|
|
854
|
-
{
|
|
855
|
-
repoUrl: repo,
|
|
856
|
-
reference,
|
|
857
|
-
dirname,
|
|
858
|
-
ci
|
|
859
|
-
},
|
|
860
|
-
{ token: githubToken }
|
|
861
|
-
);
|
|
862
|
-
if (!reportPath) {
|
|
863
|
-
reportPath = await getReportFromSnyk();
|
|
864
|
-
}
|
|
865
|
-
const uploadReportSpinner = createSpinner3("\u{1F4C1} Uploading Report").start();
|
|
866
|
-
try {
|
|
867
|
-
await uploadFile({
|
|
868
|
-
file: reportPath,
|
|
869
|
-
url: reportUploadInfo.url,
|
|
870
|
-
uploadFields: reportUploadInfo.uploadFields,
|
|
871
|
-
uploadKey: reportUploadInfo.uploadKey
|
|
872
|
-
});
|
|
873
|
-
} catch (e) {
|
|
874
|
-
uploadReportSpinner.error({ text: "\u{1F4C1} Report upload failed" });
|
|
875
|
-
throw e;
|
|
876
|
-
}
|
|
877
|
-
uploadReportSpinner.success({ text: "\u{1F4C1} Report uploaded successfully" });
|
|
878
|
-
const mobbSpinner = createSpinner3("\u{1F575}\uFE0F\u200D\u2642\uFE0F Initiating Mobb analysis").start();
|
|
879
|
-
try {
|
|
880
|
-
await gqlClient.submitVulnerabilityReport({
|
|
881
|
-
fixReportId: reportUploadInfo.fixReportId,
|
|
882
|
-
repoUrl: repo,
|
|
883
|
-
reference,
|
|
884
|
-
projectId
|
|
885
|
-
});
|
|
886
|
-
} catch (e) {
|
|
887
|
-
mobbSpinner.error({ text: "\u{1F575}\uFE0F\u200D\u2642\uFE0F Mobb analysis failed" });
|
|
888
|
-
throw e;
|
|
889
|
-
}
|
|
890
|
-
mobbSpinner.success({
|
|
891
|
-
text: "\u{1F575}\uFE0F\u200D\u2642\uFE0F Generating fixes..."
|
|
892
|
-
});
|
|
893
|
-
await askToOpenAnalysis();
|
|
894
|
-
async function getReportFromSnyk() {
|
|
895
|
-
const reportPath2 = path5.join(dirname, "report.json");
|
|
896
|
-
if (!await getSnykReport(reportPath2, repositoryRoot, { skipPrompts })) {
|
|
897
|
-
debug8("snyk code is not enabled");
|
|
898
|
-
throw new CliError2("Snyk code is not enabled");
|
|
899
|
-
}
|
|
900
|
-
return reportPath2;
|
|
901
|
-
}
|
|
902
|
-
async function askToOpenAnalysis() {
|
|
903
|
-
const reportUrl = getReportUrl({
|
|
904
|
-
organizationId,
|
|
905
|
-
projectId,
|
|
906
|
-
fixReportId: reportUploadInfo.fixReportId
|
|
907
|
-
});
|
|
908
|
-
!ci && console.log("You can access the report at: \n");
|
|
909
|
-
console.log(chalk3.bold(reportUrl));
|
|
910
|
-
!skipPrompts && await mobbAnalysisPrompt();
|
|
911
|
-
!ci && open2(reportUrl);
|
|
912
|
-
!ci && console.log(
|
|
913
|
-
chalk3.bgBlue("\n\n My work here is done for now, see you soon! \u{1F575}\uFE0F\u200D\u2642\uFE0F ")
|
|
914
|
-
);
|
|
915
|
-
}
|
|
916
|
-
async function handleMobbLogin() {
|
|
917
|
-
if (await gqlClient.verifyToken()) {
|
|
918
|
-
createSpinner3().start().success({
|
|
919
|
-
text: "\u{1F513} Logged in to Mobb successfully"
|
|
920
|
-
});
|
|
921
|
-
return;
|
|
922
|
-
} else if (apiKey) {
|
|
923
|
-
createSpinner3().start().error({
|
|
924
|
-
text: "\u{1F513} Logged in to Mobb failed - check your api-key"
|
|
925
|
-
});
|
|
926
|
-
throw new CliError2();
|
|
927
|
-
}
|
|
928
|
-
const loginSpinner = createSpinner3().start();
|
|
929
|
-
if (!skipPrompts) {
|
|
930
|
-
loginSpinner.update({ text: MOBB_LOGIN_REQUIRED_MSG });
|
|
931
|
-
await keypress2();
|
|
932
|
-
}
|
|
933
|
-
loginSpinner.update({
|
|
934
|
-
text: "\u{1F513} Waiting for Mobb login..."
|
|
935
|
-
});
|
|
936
|
-
const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
|
|
937
|
-
modulusLength: 2048
|
|
938
|
-
});
|
|
939
|
-
const loginId = await gqlClient.createCliLogin({
|
|
940
|
-
publicKey: publicKey.export({ format: "pem", type: "pkcs1" }).toString()
|
|
941
|
-
});
|
|
942
|
-
const browserUrl = `${webLoginUrl}/${loginId}?hostname=${os.hostname()}`;
|
|
943
|
-
!ci && console.log(
|
|
944
|
-
`If the page does not open automatically, kindly access it through ${browserUrl}.`
|
|
945
|
-
);
|
|
946
|
-
await open2(browserUrl);
|
|
947
|
-
let newApiToken = null;
|
|
948
|
-
for (let i = 0; i < LOGIN_MAX_WAIT / LOGIN_CHECK_DELAY; i++) {
|
|
949
|
-
const encryptedApiToken = await gqlClient.getEncryptedApiToken({
|
|
950
|
-
loginId
|
|
951
|
-
});
|
|
952
|
-
loginSpinner.spin();
|
|
953
|
-
if (encryptedApiToken) {
|
|
954
|
-
debug8("encrypted API token received %s", encryptedApiToken);
|
|
955
|
-
newApiToken = crypto.privateDecrypt(privateKey, Buffer.from(encryptedApiToken, "base64")).toString("utf-8");
|
|
956
|
-
debug8("API token decrypted");
|
|
957
|
-
break;
|
|
958
|
-
}
|
|
959
|
-
await sleep(LOGIN_CHECK_DELAY);
|
|
960
|
-
}
|
|
961
|
-
if (!newApiToken) {
|
|
962
|
-
loginSpinner.error({
|
|
963
|
-
text: "Login timeout error"
|
|
964
|
-
});
|
|
965
|
-
throw new CliError2();
|
|
966
|
-
}
|
|
967
|
-
gqlClient = new GQLClient({ apiKey: newApiToken });
|
|
968
|
-
if (await gqlClient.verifyToken()) {
|
|
969
|
-
debug8("set api token %s", newApiToken);
|
|
970
|
-
config2.set("apiToken", newApiToken);
|
|
971
|
-
loginSpinner.success({ text: "\u{1F513} Login to Mobb successful!" });
|
|
972
|
-
} else {
|
|
973
|
-
loginSpinner.error({
|
|
974
|
-
text: "Something went wrong, API token is invalid."
|
|
975
|
-
});
|
|
976
|
-
throw new CliError2();
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
async function handleGithubIntegration(oldToken) {
|
|
980
|
-
const addGithubIntegration = skipPrompts ? true : await githubIntegrationPrompt();
|
|
981
|
-
const githubSpinner = createSpinner3(
|
|
982
|
-
"\u{1F517} Waiting for github integration..."
|
|
983
|
-
).start();
|
|
984
|
-
if (!addGithubIntegration) {
|
|
985
|
-
githubSpinner.error();
|
|
986
|
-
throw Error("Could not reach github repo");
|
|
987
|
-
}
|
|
988
|
-
console.log(
|
|
989
|
-
`If the page does not open automatically, kindly access it through ${githubAuthUrl}.`
|
|
990
|
-
);
|
|
991
|
-
await open2(githubAuthUrl);
|
|
992
|
-
for (let i = 0; i < LOGIN_MAX_WAIT / LOGIN_CHECK_DELAY; i++) {
|
|
993
|
-
const { githubToken: githubToken2 } = await gqlClient.getUserInfo();
|
|
994
|
-
if (githubToken2 && githubToken2 !== oldToken) {
|
|
995
|
-
githubSpinner.success({ text: "\u{1F517} Github integration successful!" });
|
|
996
|
-
return githubToken2;
|
|
997
|
-
}
|
|
998
|
-
githubSpinner.spin();
|
|
999
|
-
await sleep(LOGIN_CHECK_DELAY);
|
|
1000
|
-
}
|
|
1001
|
-
githubSpinner.error({
|
|
1002
|
-
text: "Github login timeout error"
|
|
1003
|
-
});
|
|
1004
|
-
throw new CliError2("Github login timeout");
|
|
1005
|
-
}
|
|
1006
|
-
async function uploadExistingRepo() {
|
|
1007
|
-
if (!srcPath || !reportPath) {
|
|
1008
|
-
throw new Error("src path and reportPath is required");
|
|
1009
|
-
}
|
|
1010
|
-
const gitInfo = await getGitInfo(srcPath);
|
|
1011
|
-
const zippingSpinner = createSpinner3("\u{1F4E6} Zipping repo").start();
|
|
1012
|
-
const zipBuffer = await pack(srcPath);
|
|
1013
|
-
zippingSpinner.success({ text: "\u{1F4E6} Zipping repo successful!" });
|
|
1014
|
-
const uploadReportSpinner2 = createSpinner3("\u{1F4C1} Uploading Report").start();
|
|
1015
|
-
try {
|
|
1016
|
-
await uploadFile({
|
|
1017
|
-
file: reportPath,
|
|
1018
|
-
url: reportUploadInfo.url,
|
|
1019
|
-
uploadFields: reportUploadInfo.uploadFields,
|
|
1020
|
-
uploadKey: reportUploadInfo.uploadKey
|
|
1021
|
-
});
|
|
1022
|
-
} catch (e) {
|
|
1023
|
-
uploadReportSpinner2.error({ text: "\u{1F4C1} Report upload failed" });
|
|
1024
|
-
throw e;
|
|
1025
|
-
}
|
|
1026
|
-
uploadReportSpinner2.success({
|
|
1027
|
-
text: "\u{1F4C1} Uploading Report successful!"
|
|
1028
|
-
});
|
|
1029
|
-
const uploadRepoSpinner = createSpinner3("\u{1F4C1} Uploading Repo").start();
|
|
1030
|
-
try {
|
|
1031
|
-
await uploadFile({
|
|
1032
|
-
file: zipBuffer,
|
|
1033
|
-
url: repoUploadInfo.url,
|
|
1034
|
-
uploadFields: repoUploadInfo.uploadFields,
|
|
1035
|
-
uploadKey: repoUploadInfo.uploadKey
|
|
1036
|
-
});
|
|
1037
|
-
} catch (e) {
|
|
1038
|
-
uploadRepoSpinner.error({ text: "\u{1F4C1} Repo upload failed" });
|
|
1039
|
-
throw e;
|
|
1040
|
-
}
|
|
1041
|
-
uploadRepoSpinner.success({ text: "\u{1F4C1} Uploading Repo successful!" });
|
|
1042
|
-
const mobbSpinner2 = createSpinner3("\u{1F575}\uFE0F\u200D\u2642\uFE0F Initiating Mobb analysis").start();
|
|
1043
|
-
try {
|
|
1044
|
-
await gqlClient.submitVulnerabilityReport({
|
|
1045
|
-
fixReportId: reportUploadInfo.fixReportId,
|
|
1046
|
-
repoUrl: repo || gitInfo.repoUrl,
|
|
1047
|
-
reference: gitInfo.reference,
|
|
1048
|
-
sha: commitHash || gitInfo.hash,
|
|
1049
|
-
projectId
|
|
1050
|
-
});
|
|
1051
|
-
} catch (e) {
|
|
1052
|
-
mobbSpinner2.error({ text: "\u{1F575}\uFE0F\u200D\u2642\uFE0F Mobb analysis failed" });
|
|
1053
|
-
throw e;
|
|
1054
|
-
}
|
|
1055
|
-
mobbSpinner2.success({
|
|
1056
|
-
text: "\u{1F575}\uFE0F\u200D\u2642\uFE0F Generating fixes..."
|
|
1057
|
-
});
|
|
1058
|
-
await askToOpenAnalysis();
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
// src/commands/index.ts
|
|
1063
|
-
import chalkAnimation from "chalk-animation";
|
|
1064
|
-
async function analyze({ repo, f: scanFile, ref, apiKey, ci, commitHash, srcPath }, { skipPrompts = false } = {}) {
|
|
1065
|
-
!ci && await showWelcomeMessage(skipPrompts);
|
|
1066
|
-
await runAnalysis(
|
|
1067
|
-
{
|
|
1068
|
-
repo,
|
|
1069
|
-
scanFile,
|
|
1070
|
-
ref,
|
|
1071
|
-
apiKey,
|
|
1072
|
-
ci,
|
|
1073
|
-
commitHash,
|
|
1074
|
-
srcPath
|
|
1075
|
-
},
|
|
1076
|
-
{ skipPrompts }
|
|
1077
|
-
);
|
|
1078
|
-
}
|
|
1079
|
-
async function scan(scanOptions, { skipPrompts = false } = {}) {
|
|
1080
|
-
const { scanner, ci } = scanOptions;
|
|
1081
|
-
!ci && await showWelcomeMessage(skipPrompts);
|
|
1082
|
-
const selectedScanner = scanner || await choseScanner();
|
|
1083
|
-
if (selectedScanner !== SCANNERS.Snyk) {
|
|
1084
|
-
throw new CliError(
|
|
1085
|
-
"Vulnerability scanning via Bugsy is available only with Snyk at the moment. Additional scanners will follow soon."
|
|
1086
|
-
);
|
|
1087
|
-
}
|
|
1088
|
-
await runAnalysis(
|
|
1089
|
-
{ ...scanOptions, scanner: selectedScanner },
|
|
1090
|
-
{ skipPrompts }
|
|
1091
|
-
);
|
|
1092
|
-
}
|
|
1093
|
-
async function showWelcomeMessage(skipPrompts = false) {
|
|
1094
|
-
console.log(mobbAscii);
|
|
1095
|
-
const welcome = chalkAnimation.rainbow("\n Welcome to Bugsy\n");
|
|
1096
|
-
skipPrompts ? await sleep(100) : await sleep(2e3);
|
|
1097
|
-
welcome.stop();
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
// src/args/commands/analyze.ts
|
|
1101
|
-
import chalk6 from "chalk";
|
|
1102
|
-
|
|
1103
|
-
// src/args/options.ts
|
|
1104
|
-
import chalk4 from "chalk";
|
|
1105
|
-
var repoOption = {
|
|
1106
|
-
alias: "r",
|
|
1107
|
-
demandOption: true,
|
|
1108
|
-
type: "string",
|
|
1109
|
-
describe: chalk4.bold("Github repository URL")
|
|
1110
|
-
};
|
|
1111
|
-
var yesOption = {
|
|
1112
|
-
alias: "yes",
|
|
1113
|
-
type: "boolean",
|
|
1114
|
-
describe: chalk4.bold("Skip prompts and use default values")
|
|
1115
|
-
};
|
|
1116
|
-
var refOption = {
|
|
1117
|
-
describe: chalk4.bold("reference of the repository (branch, tag, commit)"),
|
|
1118
|
-
type: "string",
|
|
1119
|
-
demandOption: false
|
|
1120
|
-
};
|
|
1121
|
-
var ciOption = {
|
|
1122
|
-
describe: chalk4.bold(
|
|
1123
|
-
"Run in CI mode, prompts and browser will not be opened"
|
|
1124
|
-
),
|
|
1125
|
-
type: "boolean",
|
|
1126
|
-
default: false
|
|
1127
|
-
};
|
|
1128
|
-
var apiKeyOption = {
|
|
1129
|
-
type: "string",
|
|
1130
|
-
describe: chalk4.bold("Mobb authentication api-key")
|
|
1131
|
-
};
|
|
1132
|
-
var commitHashOption = {
|
|
1133
|
-
alias: "ch",
|
|
1134
|
-
describe: chalk4.bold("Hash of the commit"),
|
|
1135
|
-
type: "string"
|
|
1136
|
-
};
|
|
1137
|
-
|
|
1138
|
-
// src/args/validation.ts
|
|
1139
|
-
import chalk5 from "chalk";
|
|
1140
|
-
import path6 from "path";
|
|
1141
|
-
import { z as z3 } from "zod";
|
|
1142
|
-
function throwRepoUrlErrorMessage({
|
|
1143
|
-
error,
|
|
1144
|
-
repoUrl,
|
|
1145
|
-
command
|
|
1146
|
-
}) {
|
|
1147
|
-
const errorMessage = error.issues[error.issues.length - 1]?.message;
|
|
1148
|
-
const formattedErrorMessage = `
|
|
1149
|
-
Error: ${chalk5.bold(
|
|
1150
|
-
repoUrl
|
|
1151
|
-
)} is ${errorMessage}
|
|
1152
|
-
Example:
|
|
1153
|
-
mobbdev ${command} -r ${chalk5.bold(
|
|
1154
|
-
"https://github.com/WebGoat/WebGoat"
|
|
1155
|
-
)}`;
|
|
1156
|
-
throw new CliError(formattedErrorMessage);
|
|
1157
|
-
}
|
|
1158
|
-
var GITHUB_REPO_URL_PATTERN = new RegExp("https://github.com/[\\w-]+/[\\w-]+");
|
|
1159
|
-
var UrlZ = z3.string({
|
|
1160
|
-
invalid_type_error: "is not a valid github URL"
|
|
1161
|
-
}).regex(GITHUB_REPO_URL_PATTERN, {
|
|
1162
|
-
message: "is not a valid github URL"
|
|
1163
|
-
});
|
|
1164
|
-
function validateRepoUrl(args) {
|
|
1165
|
-
const repoSafeParseResult = UrlZ.safeParse(args.repo);
|
|
1166
|
-
const { success } = repoSafeParseResult;
|
|
1167
|
-
const [command] = args._;
|
|
1168
|
-
if (!command) {
|
|
1169
|
-
throw new CliError("Command not found");
|
|
1170
|
-
}
|
|
1171
|
-
if (!success) {
|
|
1172
|
-
throwRepoUrlErrorMessage({
|
|
1173
|
-
error: repoSafeParseResult.error,
|
|
1174
|
-
repoUrl: args.repo,
|
|
1175
|
-
command
|
|
1176
|
-
});
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
var supportExtensions = [".json", ".xml", ".fpr", ".sarif"];
|
|
1180
|
-
function validateReportFileFormat(reportFile) {
|
|
1181
|
-
if (!supportExtensions.includes(path6.extname(reportFile))) {
|
|
1182
|
-
throw new CliError(
|
|
1183
|
-
`
|
|
1184
|
-
${chalk5.bold(
|
|
1185
|
-
reportFile
|
|
1186
|
-
)} is not a supported file extension. Supported extensions are: ${chalk5.bold(
|
|
1187
|
-
supportExtensions.join(", ")
|
|
1188
|
-
)}
|
|
1189
|
-
`
|
|
1190
|
-
);
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
// src/args/commands/analyze.ts
|
|
1195
|
-
function analyzeBuilder(yargs2) {
|
|
1196
|
-
return yargs2.option("f", {
|
|
1197
|
-
alias: "scan-file",
|
|
1198
|
-
demandOption: true,
|
|
1199
|
-
type: "string",
|
|
1200
|
-
describe: chalk6.bold(
|
|
1201
|
-
"Select the vulnerability report to analyze (Checkmarx, Snyk, Fortify, CodeQL)"
|
|
1202
|
-
)
|
|
1203
|
-
}).option("repo", repoOption).option("p", {
|
|
1204
|
-
alias: "src-path",
|
|
1205
|
-
describe: chalk6.bold(
|
|
1206
|
-
"Path to the repository folder with the source code"
|
|
1207
|
-
),
|
|
1208
|
-
type: "string"
|
|
1209
|
-
}).option("ref", refOption).option("ch", {
|
|
1210
|
-
alias: "commit-hash",
|
|
1211
|
-
describe: chalk6.bold("Hash of the commit"),
|
|
1212
|
-
type: "string"
|
|
1213
|
-
}).option("y", yesOption).option("ci", ciOption).option("api-key", apiKeyOption).option("commit-hash", commitHashOption).example(
|
|
1214
|
-
"$0 analyze -r https://github.com/WebGoat/WebGoat -f <your_vulirabitliy_report_path>",
|
|
1215
|
-
"analyze an existing repository"
|
|
1216
|
-
).help();
|
|
1217
|
-
}
|
|
1218
|
-
function validateAnalyzeOptions(argv) {
|
|
1219
|
-
if (!fs4.existsSync(argv.f)) {
|
|
1220
|
-
throw new CliError(`
|
|
1221
|
-
Can't access ${chalk6.bold(argv.f)}`);
|
|
1222
|
-
}
|
|
1223
|
-
if (!argv.srcPath && !argv.repo) {
|
|
1224
|
-
throw new CliError("You must supply either --src-path or --repo");
|
|
1225
|
-
}
|
|
1226
|
-
if (!argv.srcPath && argv.repo) {
|
|
1227
|
-
validateRepoUrl(argv);
|
|
1228
|
-
}
|
|
1229
|
-
if (argv.ci && !argv.apiKey) {
|
|
1230
|
-
throw new CliError("--ci flag requires --api-key to be provided as well");
|
|
1231
|
-
}
|
|
1232
|
-
validateReportFileFormat(argv.f);
|
|
1233
|
-
}
|
|
1234
|
-
async function analyzeHandler(args) {
|
|
1235
|
-
validateAnalyzeOptions(args);
|
|
1236
|
-
await analyze(args, { skipPrompts: args.yes });
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
// src/args/commands/scan.ts
|
|
1240
|
-
import chalk7 from "chalk";
|
|
1241
|
-
function scanBuilder(args) {
|
|
1242
|
-
return args.coerce("scanner", (arg) => arg.toLowerCase()).option("repo", repoOption).option("ref", refOption).option("s", {
|
|
1243
|
-
alias: "scanner",
|
|
1244
|
-
choices: Object.values(SCANNERS),
|
|
1245
|
-
describe: chalk7.bold("Select the scanner to use")
|
|
1246
|
-
}).option("y", yesOption).option("ci", ciOption).option("api-key", apiKeyOption).example(
|
|
1247
|
-
"$0 scan -r https://github.com/WebGoat/WebGoat",
|
|
1248
|
-
"Scan an existing repository"
|
|
1249
|
-
).help();
|
|
1250
|
-
}
|
|
1251
|
-
function validateScanOptions(argv) {
|
|
1252
|
-
validateRepoUrl(argv);
|
|
1253
|
-
if (argv.ci && !argv.apiKey) {
|
|
1254
|
-
throw new CliError(
|
|
1255
|
-
"\nError: --ci flag requires --api-key to be provided as well"
|
|
1256
|
-
);
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
async function scanHandler(args) {
|
|
1260
|
-
validateScanOptions(args);
|
|
1261
|
-
await scan(args, { skipPrompts: args.yes });
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
// src/args/yargs.ts
|
|
1265
|
-
var parseArgs = async (args) => {
|
|
1266
|
-
const yargsInstance = yargs(args);
|
|
1267
|
-
return yargsInstance.updateStrings({
|
|
1268
|
-
"Commands:": chalk8.yellow.underline.bold("Commands:"),
|
|
1269
|
-
"Options:": chalk8.yellow.underline.bold("Options:"),
|
|
1270
|
-
"Examples:": chalk8.yellow.underline.bold("Examples:"),
|
|
1271
|
-
"Show help": chalk8.bold("Show help")
|
|
1272
|
-
}).usage(
|
|
1273
|
-
`${chalk8.bold(
|
|
1274
|
-
"\n Bugsy - Trusted, Automatic Vulnerability Fixer \u{1F575}\uFE0F\u200D\u2642\uFE0F\n\n"
|
|
1275
|
-
)} ${chalk8.yellow.underline.bold("Usage:")}
|
|
1276
|
-
$0 ${chalk8.green(
|
|
1277
|
-
"<command>"
|
|
1278
|
-
)} ${chalk8.dim("[options]")}
|
|
1279
|
-
`
|
|
1280
|
-
).version(false).command(
|
|
1281
|
-
"scan",
|
|
1282
|
-
chalk8.bold(
|
|
1283
|
-
"Scan your code for vulnerabilities, get automated fixes right away."
|
|
1284
|
-
),
|
|
1285
|
-
scanBuilder,
|
|
1286
|
-
scanHandler
|
|
1287
|
-
).command(
|
|
1288
|
-
"analyze",
|
|
1289
|
-
chalk8.bold(
|
|
1290
|
-
"Provide a vulnerability report and relevant code repository, get automated fixes right away."
|
|
1291
|
-
),
|
|
1292
|
-
analyzeBuilder,
|
|
1293
|
-
analyzeHandler
|
|
1294
|
-
).example(
|
|
1295
|
-
"$0 scan -r https://github.com/WebGoat/WebGoat",
|
|
1296
|
-
"Scan an existing repository"
|
|
1297
|
-
).command({
|
|
1298
|
-
command: "*",
|
|
1299
|
-
handler() {
|
|
1300
|
-
yargsInstance.showHelp();
|
|
1301
|
-
}
|
|
1302
|
-
}).strictOptions().help("h").alias("h", "help").epilog(chalk8.bgBlue("Made with \u2764\uFE0F by Mobb")).showHelpOnFail(true).wrap(Math.min(120, yargsInstance.terminalWidth())).parse();
|
|
1303
|
-
};
|
|
1304
|
-
|
|
1305
|
-
// src/index.ts
|
|
1306
|
-
async function run() {
|
|
1307
|
-
return parseArgs(hideBin(process.argv));
|
|
1308
|
-
}
|
|
1309
|
-
(async () => {
|
|
1310
|
-
try {
|
|
1311
|
-
await run();
|
|
1312
|
-
process.exit(0);
|
|
1313
|
-
} catch (err) {
|
|
1314
|
-
if (err instanceof CliError) {
|
|
1315
|
-
console.error(err.message);
|
|
1316
|
-
process.exit(1);
|
|
1317
|
-
}
|
|
1318
|
-
console.error(
|
|
1319
|
-
"Something went wrong, please try again or contact support if issue persists."
|
|
1320
|
-
);
|
|
1321
|
-
console.error(err);
|
|
1322
|
-
process.exit(1);
|
|
1323
|
-
}
|
|
1324
|
-
})();
|