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/dist/index.mjs ADDED
@@ -0,0 +1,2380 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __export = (target, all) => {
4
+ for (var name in all)
5
+ __defProp(target, name, { get: all[name], enumerable: true });
6
+ };
7
+ var __publicField = (obj, key, value) => {
8
+ __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
9
+ return value;
10
+ };
11
+
12
+ // src/index.ts
13
+ import { hideBin } from "yargs/helpers";
14
+
15
+ // src/args/yargs.ts
16
+ import chalk7 from "chalk";
17
+ import yargs from "yargs/yargs";
18
+
19
+ // src/args/commands/analyze.ts
20
+ import fs4 from "node:fs";
21
+
22
+ // src/constants.ts
23
+ import path from "node:path";
24
+ import { fileURLToPath } from "node:url";
25
+ import Debug from "debug";
26
+ import * as dotenv from "dotenv";
27
+ import { z } from "zod";
28
+ var debug = Debug("mobbdev:constants");
29
+ var __dirname = path.dirname(fileURLToPath(import.meta.url));
30
+ dotenv.config({ path: path.join(__dirname, "../.env") });
31
+ var SCANNERS = {
32
+ Checkmarx: "checkmarx",
33
+ Codeql: "codeql",
34
+ Fortify: "fortify",
35
+ Snyk: "snyk"
36
+ };
37
+ var envVariablesSchema = z.object({
38
+ WEB_LOGIN_URL: z.string(),
39
+ WEB_APP_URL: z.string(),
40
+ API_URL: z.string()
41
+ }).required();
42
+ var envVariables = envVariablesSchema.parse(process.env);
43
+ debug("config %o", envVariables);
44
+ var mobbAscii = `
45
+ ..
46
+ ..........
47
+ .................
48
+ ...........................
49
+ ..............................
50
+ ................................
51
+ ..................................
52
+ ....................................
53
+ .....................................
54
+ .............................................
55
+ .................................................
56
+ ............................... .................
57
+ .................................. ............
58
+ .................. ............. ..........
59
+ ......... ........ ......... ......
60
+ ............... ....
61
+ .... ..
62
+
63
+ . ...
64
+ ..............
65
+ ......................
66
+ ...........................
67
+ ................................
68
+ ......................................
69
+ ...............................
70
+ .................
71
+ `;
72
+ var WEB_LOGIN_URL = envVariables.WEB_LOGIN_URL;
73
+ var WEB_APP_URL = envVariables.WEB_APP_URL;
74
+ var API_URL = envVariables.API_URL;
75
+
76
+ // src/features/analysis/index.ts
77
+ import crypto from "node:crypto";
78
+ import fs3 from "node:fs";
79
+ import os2 from "node:os";
80
+ import path5 from "node:path";
81
+ import { pipeline } from "node:stream/promises";
82
+
83
+ // src/utils/index.ts
84
+ var utils_exports = {};
85
+ __export(utils_exports, {
86
+ CliError: () => CliError,
87
+ Spinner: () => Spinner,
88
+ getDirName: () => getDirName,
89
+ keypress: () => keypress,
90
+ sleep: () => sleep
91
+ });
92
+
93
+ // src/utils/dirname.ts
94
+ import path2 from "node:path";
95
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
96
+ function getDirName() {
97
+ return path2.dirname(fileURLToPath2(import.meta.url));
98
+ }
99
+
100
+ // src/utils/keypress.ts
101
+ import readline from "node:readline";
102
+ async function keypress() {
103
+ const rl = readline.createInterface({
104
+ input: process.stdin,
105
+ output: process.stdout
106
+ });
107
+ return new Promise((resolve) => {
108
+ rl.question("", (answer) => {
109
+ rl.close();
110
+ process.stderr.moveCursor(0, -1);
111
+ process.stderr.clearLine(1);
112
+ resolve(answer);
113
+ });
114
+ });
115
+ }
116
+
117
+ // src/utils/spinner.ts
118
+ import {
119
+ createSpinner as _createSpinner
120
+ } from "nanospinner";
121
+ var mockSpinner = {
122
+ success: () => mockSpinner,
123
+ error: () => mockSpinner,
124
+ warn: () => mockSpinner,
125
+ stop: () => mockSpinner,
126
+ start: () => mockSpinner,
127
+ update: () => mockSpinner,
128
+ reset: () => mockSpinner,
129
+ clear: () => mockSpinner,
130
+ spin: () => mockSpinner
131
+ };
132
+ function Spinner({ ci = false } = {}) {
133
+ return {
134
+ createSpinner: (text, options) => ci ? mockSpinner : _createSpinner(text, options)
135
+ };
136
+ }
137
+
138
+ // src/utils/index.ts
139
+ var sleep = (ms = 2e3) => new Promise((r) => setTimeout(r, ms));
140
+ var CliError = class extends Error {
141
+ };
142
+
143
+ // src/features/analysis/index.ts
144
+ import chalk2 from "chalk";
145
+ import Configstore from "configstore";
146
+ import Debug7 from "debug";
147
+ import extract from "extract-zip";
148
+ import fetch3 from "node-fetch";
149
+ import open2 from "open";
150
+ import semver from "semver";
151
+ import tmp from "tmp";
152
+
153
+ // src/features/analysis/git.ts
154
+ import Debug2 from "debug";
155
+ import { simpleGit } from "simple-git";
156
+ var debug2 = Debug2("mobbdev:git");
157
+ async function getGitInfo(srcDirPath) {
158
+ debug2("getting git info for %s", srcDirPath);
159
+ const git = simpleGit({
160
+ baseDir: srcDirPath,
161
+ maxConcurrentProcesses: 1,
162
+ trimmed: true
163
+ });
164
+ let repoUrl = "";
165
+ let hash = "";
166
+ let reference = "";
167
+ try {
168
+ repoUrl = (await git.getConfig("remote.origin.url")).value || "";
169
+ hash = await git.revparse(["HEAD"]) || "";
170
+ reference = await git.revparse(["--abbrev-ref", "HEAD"]) || "";
171
+ } catch (e) {
172
+ if (e instanceof Error) {
173
+ debug2("failed to run git %o", e);
174
+ if (e.message.includes(" spawn ")) {
175
+ debug2("git cli not installed");
176
+ } else if (e.message.includes(" not a git repository ")) {
177
+ debug2("folder is not a git repo");
178
+ } else {
179
+ throw e;
180
+ }
181
+ }
182
+ throw e;
183
+ }
184
+ if (repoUrl.endsWith(".git")) {
185
+ repoUrl = repoUrl.slice(0, -".git".length);
186
+ }
187
+ if (repoUrl.startsWith("git@github.com:")) {
188
+ repoUrl = repoUrl.replace("git@github.com:", "https://github.com/");
189
+ }
190
+ return {
191
+ repoUrl,
192
+ hash,
193
+ reference
194
+ };
195
+ }
196
+
197
+ // src/features/analysis/graphql/gql.ts
198
+ import Debug3 from "debug";
199
+ import { GraphQLClient } from "graphql-request";
200
+
201
+ // src/features/analysis/graphql/mutations.ts
202
+ import { gql } from "graphql-request";
203
+ var UPLOAD_S3_BUCKET_INFO = gql`
204
+ mutation uploadS3BucketInfo($fileName: String!) {
205
+ uploadS3BucketInfo(fileName: $fileName) {
206
+ status
207
+ error
208
+ reportUploadInfo: uploadInfo {
209
+ url
210
+ fixReportId
211
+ uploadFieldsJSON
212
+ uploadKey
213
+ }
214
+ repoUploadInfo {
215
+ url
216
+ fixReportId
217
+ uploadFieldsJSON
218
+ uploadKey
219
+ }
220
+ }
221
+ }
222
+ `;
223
+ var SUBMIT_VULNERABILITY_REPORT = gql`
224
+ mutation SubmitVulnerabilityReport(
225
+ $vulnerabilityReportFileName: String!
226
+ $fixReportId: String!
227
+ $repoUrl: String!
228
+ $reference: String!
229
+ $projectId: String!
230
+ $sha: String
231
+ ) {
232
+ submitVulnerabilityReport(
233
+ fixReportId: $fixReportId
234
+ repoUrl: $repoUrl
235
+ reference: $reference
236
+ sha: $sha
237
+ vulnerabilityReportFileName: $vulnerabilityReportFileName
238
+ projectId: $projectId
239
+ ) {
240
+ __typename
241
+ }
242
+ }
243
+ `;
244
+ var CREATE_COMMUNITY_USER = gql`
245
+ mutation CreateCommunityUser {
246
+ initOrganizationAndProject {
247
+ userId
248
+ projectId
249
+ organizationId
250
+ }
251
+ }
252
+ `;
253
+ var CREATE_CLI_LOGIN = gql`
254
+ mutation CreateCliLogin($publicKey: String!) {
255
+ insert_cli_login_one(object: { publicKey: $publicKey }) {
256
+ id
257
+ }
258
+ }
259
+ `;
260
+ var PERFORM_CLI_LOGIN = gql`
261
+ mutation performCliLogin($loginId: String!) {
262
+ performCliLogin(loginId: $loginId) {
263
+ status
264
+ }
265
+ }
266
+ `;
267
+
268
+ // src/features/analysis/graphql/queries.ts
269
+ import { gql as gql2 } from "graphql-request";
270
+ var ME = gql2`
271
+ query Me {
272
+ me {
273
+ id
274
+ email
275
+ githubToken
276
+ gitlabToken
277
+ }
278
+ }
279
+ `;
280
+ var GET_ORG_AND_PROJECT_ID = gql2`
281
+ query getOrgAndProjectId {
282
+ users: user {
283
+ userOrganizationsAndUserOrganizationRoles {
284
+ organization {
285
+ id
286
+ projects(order_by: { updatedAt: desc }) {
287
+ id
288
+ }
289
+ }
290
+ }
291
+ }
292
+ }
293
+ `;
294
+ var GET_ENCRYPTED_API_TOKEN = gql2`
295
+ query GetEncryptedApiToken($loginId: uuid!) {
296
+ cli_login_by_pk(id: $loginId) {
297
+ encryptedApiToken
298
+ }
299
+ }
300
+ `;
301
+
302
+ // src/features/analysis/graphql/types.ts
303
+ import { z as z2 } from "zod";
304
+ var UploadFieldsZ = z2.object({
305
+ bucket: z2.string(),
306
+ "X-Amz-Algorithm": z2.string(),
307
+ "X-Amz-Credential": z2.string(),
308
+ "X-Amz-Date": z2.string(),
309
+ Policy: z2.string(),
310
+ "X-Amz-Signature": z2.string()
311
+ });
312
+ var ReportUploadInfoZ = z2.object({
313
+ url: z2.string(),
314
+ fixReportId: z2.string(),
315
+ uploadFieldsJSON: z2.string().transform((str, ctx) => {
316
+ try {
317
+ return JSON.parse(str);
318
+ } catch (e) {
319
+ ctx.addIssue({ code: "custom", message: "Invalid JSON" });
320
+ return z2.NEVER;
321
+ }
322
+ }),
323
+ uploadKey: z2.string()
324
+ }).transform(({ uploadFieldsJSON, ...input }) => ({
325
+ ...input,
326
+ uploadFields: uploadFieldsJSON
327
+ }));
328
+ var UploadS3BucketInfoZ = z2.object({
329
+ uploadS3BucketInfo: z2.object({
330
+ status: z2.string(),
331
+ error: z2.string().nullish(),
332
+ reportUploadInfo: ReportUploadInfoZ,
333
+ repoUploadInfo: ReportUploadInfoZ
334
+ })
335
+ });
336
+ var GetOrgAndProjectIdQueryZ = z2.object({
337
+ users: z2.array(
338
+ z2.object({
339
+ userOrganizationsAndUserOrganizationRoles: z2.array(
340
+ z2.object({
341
+ organization: z2.object({
342
+ id: z2.string(),
343
+ projects: z2.array(
344
+ z2.object({
345
+ id: z2.string()
346
+ })
347
+ ).nonempty()
348
+ })
349
+ })
350
+ ).nonempty()
351
+ })
352
+ ).nonempty()
353
+ });
354
+ var CreateCliLoginZ = z2.object({
355
+ insert_cli_login_one: z2.object({
356
+ id: z2.string()
357
+ })
358
+ });
359
+ var GetEncryptedApiTokenZ = z2.object({
360
+ cli_login_by_pk: z2.object({
361
+ encryptedApiToken: z2.string().nullable()
362
+ })
363
+ });
364
+
365
+ // src/features/analysis/graphql/gql.ts
366
+ var debug3 = Debug3("mobbdev:gql");
367
+ var API_KEY_HEADER_NAME = "x-mobb-key";
368
+ var GQLClient = class {
369
+ constructor(args) {
370
+ __publicField(this, "_client");
371
+ const { apiKey } = args;
372
+ debug3(`init with apiKey ${apiKey}`);
373
+ this._client = new GraphQLClient(API_URL, {
374
+ headers: { [API_KEY_HEADER_NAME]: apiKey || "" }
375
+ });
376
+ }
377
+ async getUserInfo() {
378
+ const { me } = await this._client.request(ME);
379
+ return me;
380
+ }
381
+ async createCliLogin(variables) {
382
+ const res = CreateCliLoginZ.parse(
383
+ await this._client.request(
384
+ CREATE_CLI_LOGIN,
385
+ variables,
386
+ {
387
+ // We may have outdated API key in the config storage. Avoid using it for the login request.
388
+ [API_KEY_HEADER_NAME]: ""
389
+ }
390
+ )
391
+ );
392
+ return res.insert_cli_login_one.id;
393
+ }
394
+ async verifyToken() {
395
+ await this.createCommunityUser();
396
+ try {
397
+ await this.getUserInfo();
398
+ } catch (e) {
399
+ debug3("verify token failed %o", e);
400
+ return false;
401
+ }
402
+ return true;
403
+ }
404
+ async getOrgAndProjectId() {
405
+ const getOrgAndProjectIdResult = await this._client.request(
406
+ GET_ORG_AND_PROJECT_ID
407
+ );
408
+ const [user] = GetOrgAndProjectIdQueryZ.parse(
409
+ getOrgAndProjectIdResult
410
+ ).users;
411
+ const org = user.userOrganizationsAndUserOrganizationRoles[0].organization;
412
+ return {
413
+ organizationId: org.id,
414
+ projectId: org.projects[0].id
415
+ };
416
+ }
417
+ async getEncryptedApiToken(variables) {
418
+ const res = await this._client.request(
419
+ GET_ENCRYPTED_API_TOKEN,
420
+ variables,
421
+ {
422
+ // We may have outdated API key in the config storage. Avoid using it for the login request.
423
+ [API_KEY_HEADER_NAME]: ""
424
+ }
425
+ );
426
+ return GetEncryptedApiTokenZ.parse(res).cli_login_by_pk.encryptedApiToken;
427
+ }
428
+ async createCommunityUser() {
429
+ try {
430
+ await this._client.request(CREATE_COMMUNITY_USER);
431
+ } catch (e) {
432
+ debug3("create community user failed %o", e);
433
+ }
434
+ }
435
+ async uploadS3BucketInfo() {
436
+ const uploadS3BucketInfoResult = await this._client.request(UPLOAD_S3_BUCKET_INFO, {
437
+ fileName: "report.json"
438
+ });
439
+ return UploadS3BucketInfoZ.parse(uploadS3BucketInfoResult);
440
+ }
441
+ async submitVulnerabilityReport({
442
+ fixReportId,
443
+ repoUrl,
444
+ reference,
445
+ projectId,
446
+ sha
447
+ }) {
448
+ await this._client.request(SUBMIT_VULNERABILITY_REPORT, {
449
+ fixReportId,
450
+ repoUrl,
451
+ reference,
452
+ vulnerabilityReportFileName: "report.json",
453
+ projectId,
454
+ sha: sha || ""
455
+ });
456
+ }
457
+ };
458
+
459
+ // src/features/analysis/pack.ts
460
+ import fs from "node:fs";
461
+ import path3 from "node:path";
462
+ import AdmZip from "adm-zip";
463
+ import Debug4 from "debug";
464
+ import { globby } from "globby";
465
+ import { isBinary } from "istextorbinary";
466
+ var debug4 = Debug4("mobbdev:pack");
467
+ var MAX_FILE_SIZE = 1024 * 1024 * 5;
468
+ async function pack(srcDirPath) {
469
+ debug4("pack folder %s", srcDirPath);
470
+ const filepaths = await globby("**", {
471
+ gitignore: true,
472
+ onlyFiles: true,
473
+ cwd: srcDirPath,
474
+ followSymbolicLinks: false
475
+ });
476
+ debug4("files found %d", filepaths.length);
477
+ const zip = new AdmZip();
478
+ debug4("compressing files");
479
+ for (const filepath of filepaths) {
480
+ const absFilepath = path3.join(srcDirPath, filepath.toString());
481
+ if (fs.lstatSync(absFilepath).size > MAX_FILE_SIZE) {
482
+ debug4("ignoring %s because the size is > 5MB", filepath);
483
+ continue;
484
+ }
485
+ const data = fs.readFileSync(absFilepath);
486
+ if (isBinary(null, data)) {
487
+ debug4("ignoring %s because is seems to be a binary file", filepath);
488
+ continue;
489
+ }
490
+ zip.addFile(filepath.toString(), data);
491
+ }
492
+ debug4("get zip file buffer");
493
+ return zip.toBuffer();
494
+ }
495
+
496
+ // src/features/analysis/prompts.ts
497
+ import inquirer from "inquirer";
498
+ import { createSpinner } from "nanospinner";
499
+ var scannerChoices = [
500
+ { name: "Snyk", value: SCANNERS.Snyk },
501
+ { name: "Checkmarx", value: SCANNERS.Checkmarx },
502
+ { name: "Codeql", value: SCANNERS.Codeql },
503
+ { name: "Fortify", value: SCANNERS.Fortify }
504
+ ];
505
+ async function choseScanner() {
506
+ const { scanner } = await inquirer.prompt({
507
+ name: "scanner",
508
+ message: "Choose a scanner you wish to use to scan your code",
509
+ type: "list",
510
+ choices: scannerChoices
511
+ });
512
+ return scanner;
513
+ }
514
+ async function scmIntegrationPrompt(scmName) {
515
+ const answers = await inquirer.prompt({
516
+ name: "scmConfirm",
517
+ type: "confirm",
518
+ message: `It seems we don't have access to the repo, do you want to grant access to your ${scmName} account?`,
519
+ default: true
520
+ });
521
+ return answers.scmConfirm;
522
+ }
523
+ async function mobbAnalysisPrompt() {
524
+ const spinner = createSpinner().start();
525
+ spinner.update({ text: "Hit any key to view available fixes" });
526
+ await keypress();
527
+ return spinner.success();
528
+ }
529
+ async function snykArticlePrompt() {
530
+ const { snykArticleConfirm } = await inquirer.prompt({
531
+ name: "snykArticleConfirm",
532
+ type: "confirm",
533
+ message: "Do you want to be taken to the relevant Snyk's online article?",
534
+ default: true
535
+ });
536
+ return snykArticleConfirm;
537
+ }
538
+
539
+ // src/features/analysis/scm/gitlab.ts
540
+ import querystring from "node:querystring";
541
+ import { Gitlab } from "@gitbeaker/rest";
542
+ import { z as z5 } from "zod";
543
+
544
+ // src/features/analysis/scm/github.ts
545
+ import { RequestError } from "@octokit/request-error";
546
+ import { Octokit } from "octokit";
547
+ import { z as z3 } from "zod";
548
+ function removeTrailingSlash(str) {
549
+ return str.trim().replace(/\/+$/, "");
550
+ }
551
+ var EnvVariablesZod = z3.object({
552
+ GITHUB_API_TOKEN: z3.string().optional()
553
+ });
554
+ var { GITHUB_API_TOKEN } = EnvVariablesZod.parse(process.env);
555
+ var GetBlameDocument = `
556
+ query GetBlame(
557
+ $owner: String!
558
+ $repo: String!
559
+ $ref: String!
560
+ $path: String!
561
+ ) {
562
+ repository(name: $repo, owner: $owner) {
563
+ # branch name
564
+ object(expression: $ref) {
565
+ # cast Target to a Commit
566
+ ... on Commit {
567
+ # full repo-relative path to blame file
568
+ blame(path: $path) {
569
+ ranges {
570
+ commit {
571
+ author {
572
+ user {
573
+ name
574
+ login
575
+ }
576
+ }
577
+ authoredDate
578
+ }
579
+ startingLine
580
+ endingLine
581
+ age
582
+ }
583
+ }
584
+ }
585
+
586
+ }
587
+ }
588
+ }
589
+ `;
590
+ var githubUrlRegex = /^http[s]?:\/\/[^/\s]+\/([^/.\s]+\/[^/.\s]+)(\.git)?(\/)?$/i;
591
+ function getOktoKit(options) {
592
+ const token = options?.githubAuthToken ?? GITHUB_API_TOKEN ?? "";
593
+ return new Octokit({ auth: token });
594
+ }
595
+ async function githubValidateParams(url, accessToken) {
596
+ try {
597
+ const oktoKit = getOktoKit({ githubAuthToken: accessToken });
598
+ if (accessToken) {
599
+ await oktoKit.rest.users.getAuthenticated();
600
+ }
601
+ if (url) {
602
+ const { owner, repo } = parseOwnerAndRepo(url);
603
+ await oktoKit.rest.repos.get({ repo, owner });
604
+ }
605
+ } catch (e) {
606
+ const error = e;
607
+ const code = error.status || error.statusCode || error.response?.status || error.response?.statusCode || error.response?.code;
608
+ if (code === 401 || code === 403) {
609
+ throw new InvalidAccessTokenError(`invalid github access token`);
610
+ }
611
+ if (code === 404) {
612
+ throw new InvalidRepoUrlError(`invalid github repo Url ${url}`);
613
+ }
614
+ throw e;
615
+ }
616
+ }
617
+ async function getGithubUsername(accessToken) {
618
+ const oktoKit = getOktoKit({ githubAuthToken: accessToken });
619
+ const res = await oktoKit.rest.users.getAuthenticated();
620
+ return res.data.login;
621
+ }
622
+ async function getGithubIsUserCollaborator(username, accessToken, repoUrl) {
623
+ try {
624
+ const { owner, repo } = parseOwnerAndRepo(repoUrl);
625
+ const oktoKit = getOktoKit({ githubAuthToken: accessToken });
626
+ const res = await oktoKit.rest.repos.checkCollaborator({
627
+ owner,
628
+ repo,
629
+ username
630
+ });
631
+ if (res.status === 204) {
632
+ return true;
633
+ }
634
+ } catch (e) {
635
+ return false;
636
+ }
637
+ return false;
638
+ }
639
+ async function getGithubPullRequestStatus(accessToken, repoUrl, prNumber) {
640
+ const { owner, repo } = parseOwnerAndRepo(repoUrl);
641
+ const oktoKit = getOktoKit({ githubAuthToken: accessToken });
642
+ const res = await oktoKit.rest.pulls.get({
643
+ owner,
644
+ repo,
645
+ pull_number: prNumber
646
+ });
647
+ if (res.data.merged) {
648
+ return "merged";
649
+ }
650
+ if (res.data.draft) {
651
+ return "draft";
652
+ }
653
+ return res.data.state;
654
+ }
655
+ async function getGithubIsRemoteBranch(accessToken, repoUrl, branch) {
656
+ const { owner, repo } = parseOwnerAndRepo(repoUrl);
657
+ const oktoKit = getOktoKit({ githubAuthToken: accessToken });
658
+ try {
659
+ const res = await oktoKit.rest.repos.getBranch({
660
+ owner,
661
+ repo,
662
+ branch
663
+ });
664
+ return branch === res.data.name;
665
+ } catch (e) {
666
+ return false;
667
+ }
668
+ }
669
+ async function getGithubRepoList(accessToken) {
670
+ const oktoKit = getOktoKit({ githubAuthToken: accessToken });
671
+ try {
672
+ const githubRepos = await getRepos(oktoKit);
673
+ return githubRepos.map(
674
+ (repo) => {
675
+ const repoLanguages = [];
676
+ if (repo.language) {
677
+ repoLanguages.push(repo.language);
678
+ }
679
+ return {
680
+ repoName: repo.name,
681
+ repoUrl: repo.html_url,
682
+ repoOwner: repo.owner.login,
683
+ repoLanguages,
684
+ repoIsPublic: !repo.private,
685
+ repoUpdatedAt: repo.updated_at
686
+ };
687
+ }
688
+ );
689
+ } catch (e) {
690
+ if (e instanceof RequestError && e.status === 401) {
691
+ return [];
692
+ }
693
+ if (e instanceof RequestError && e.status === 404) {
694
+ return [];
695
+ }
696
+ throw e;
697
+ }
698
+ }
699
+ async function getGithubBranchList(accessToken, repoUrl) {
700
+ const { owner, repo } = parseOwnerAndRepo(repoUrl);
701
+ const oktoKit = getOktoKit({ githubAuthToken: accessToken });
702
+ const res = await oktoKit.rest.repos.listBranches({
703
+ owner,
704
+ repo,
705
+ per_page: 1e3,
706
+ page: 1
707
+ });
708
+ return res.data.map((branch) => branch.name);
709
+ }
710
+ async function createPullRequest(options) {
711
+ const { owner, repo } = parseOwnerAndRepo(options.repoUrl);
712
+ const oktoKit = getOktoKit({ githubAuthToken: options.accessToken });
713
+ const res = await oktoKit.rest.pulls.create({
714
+ owner,
715
+ repo,
716
+ title: options.title,
717
+ body: options.body,
718
+ head: options.sourceBranchName,
719
+ base: options.targetBranchName,
720
+ draft: false,
721
+ maintainer_can_modify: true
722
+ });
723
+ return res.data.number;
724
+ }
725
+ async function getRepos(oktoKit) {
726
+ const res = await oktoKit.request("GET /user/repos?sort=updated", {
727
+ headers: {
728
+ "X-GitHub-Api-Version": "2022-11-28",
729
+ per_page: 100
730
+ }
731
+ });
732
+ return res.data;
733
+ }
734
+ async function getGithubRepoDefaultBranch(repoUrl, options) {
735
+ const oktoKit = getOktoKit(options);
736
+ const { owner, repo } = parseOwnerAndRepo(repoUrl);
737
+ return (await oktoKit.rest.repos.get({ repo, owner })).data.default_branch;
738
+ }
739
+ async function getGithubReferenceData({ ref, gitHubUrl }, options) {
740
+ const { owner, repo } = parseOwnerAndRepo(gitHubUrl);
741
+ let res;
742
+ try {
743
+ const oktoKit = getOktoKit(options);
744
+ res = await Promise.any([
745
+ getBranch({ owner, repo, branch: ref }, oktoKit).then((result) => ({
746
+ date: result.data.commit.commit.committer?.date ? new Date(result.data.commit.commit.committer?.date) : void 0,
747
+ type: "BRANCH" /* BRANCH */,
748
+ sha: result.data.commit.sha
749
+ })),
750
+ getCommit({ commitSha: ref, repo, owner }, oktoKit).then((commit) => ({
751
+ date: new Date(commit.data.committer.date),
752
+ type: "COMMIT" /* COMMIT */,
753
+ sha: commit.data.sha
754
+ })),
755
+ getTagDate({ owner, repo, tag: ref }, oktoKit).then((data) => ({
756
+ date: new Date(data.date),
757
+ type: "TAG" /* TAG */,
758
+ sha: data.sha
759
+ }))
760
+ ]);
761
+ return res;
762
+ } catch (e) {
763
+ if (e instanceof AggregateError) {
764
+ throw new RefNotFoundError(`ref: ${ref} does not exist`);
765
+ }
766
+ throw e;
767
+ }
768
+ }
769
+ async function getBranch({ branch, owner, repo }, oktoKit) {
770
+ return oktoKit.rest.repos.getBranch({
771
+ branch,
772
+ owner,
773
+ repo
774
+ });
775
+ }
776
+ async function getTagDate({ tag, owner, repo }, oktoKit) {
777
+ const refResponse = await oktoKit.rest.git.getRef({
778
+ ref: `tags/${tag}`,
779
+ owner,
780
+ repo
781
+ });
782
+ const tagSha = refResponse.data.object.sha;
783
+ if (refResponse.data.object.type === "commit") {
784
+ const res2 = await oktoKit.rest.git.getCommit({
785
+ commit_sha: tagSha,
786
+ owner,
787
+ repo
788
+ });
789
+ return {
790
+ date: res2.data.committer.date,
791
+ sha: res2.data.sha
792
+ };
793
+ }
794
+ const res = await oktoKit.rest.git.getTag({
795
+ tag_sha: tagSha,
796
+ owner,
797
+ repo
798
+ });
799
+ return {
800
+ date: res.data.tagger.date,
801
+ sha: res.data.sha
802
+ };
803
+ }
804
+ async function getCommit({
805
+ commitSha,
806
+ owner,
807
+ repo
808
+ }, oktoKit) {
809
+ return oktoKit.rest.git.getCommit({
810
+ repo,
811
+ owner,
812
+ commit_sha: commitSha
813
+ });
814
+ }
815
+ function parseOwnerAndRepo(gitHubUrl) {
816
+ gitHubUrl = removeTrailingSlash(gitHubUrl);
817
+ if (!githubUrlRegex.test(gitHubUrl)) {
818
+ throw new InvalidUrlPatternError(`invalid github repo Url ${gitHubUrl}`);
819
+ }
820
+ const groups = gitHubUrl.split(githubUrlRegex).filter((res) => res);
821
+ const ownerAndRepo = groups[0]?.split("/");
822
+ const owner = ownerAndRepo?.at(0);
823
+ const repo = ownerAndRepo?.at(1);
824
+ if (!owner || !repo) {
825
+ throw new InvalidUrlPatternError(`invalid github repo Url ${gitHubUrl}`);
826
+ }
827
+ return { owner, repo };
828
+ }
829
+ async function queryGithubGraphql(query, variables, options) {
830
+ const token = options?.githubAuthToken ?? GITHUB_API_TOKEN ?? "";
831
+ const parameters = variables ?? {};
832
+ const authorizationHeader = {
833
+ headers: {
834
+ authorization: `bearer ${token}`
835
+ }
836
+ };
837
+ try {
838
+ const oktoKit = getOktoKit(options);
839
+ const res = await oktoKit.graphql(query, {
840
+ ...parameters,
841
+ ...authorizationHeader
842
+ });
843
+ return res;
844
+ } catch (e) {
845
+ if (e instanceof RequestError) {
846
+ return null;
847
+ }
848
+ throw e;
849
+ }
850
+ }
851
+ async function getGithubBlameRanges({ ref, gitHubUrl, path: path7 }, options) {
852
+ const { owner, repo } = parseOwnerAndRepo(gitHubUrl);
853
+ const variables = {
854
+ owner,
855
+ repo,
856
+ path: path7,
857
+ ref
858
+ };
859
+ const res = await queryGithubGraphql(
860
+ GetBlameDocument,
861
+ variables,
862
+ options
863
+ );
864
+ if (!res?.repository?.object?.blame?.ranges) {
865
+ return [];
866
+ }
867
+ return res.repository.object.blame.ranges.map((range) => ({
868
+ startingLine: range.startingLine,
869
+ endingLine: range.endingLine,
870
+ email: range.commit.author.user.email,
871
+ name: range.commit.author.user.name,
872
+ login: range.commit.author.user.login
873
+ }));
874
+ }
875
+
876
+ // src/features/analysis/scm/scmSubmit.ts
877
+ import fs2 from "node:fs/promises";
878
+ import os from "os";
879
+ import path4 from "path";
880
+ import { simpleGit as simpleGit2 } from "simple-git";
881
+ import { z as z4 } from "zod";
882
+ var isValidBranchName = async (branchName) => {
883
+ const git = simpleGit2();
884
+ try {
885
+ const res = await git.raw(["check-ref-format", "--branch", branchName]);
886
+ if (res) {
887
+ return true;
888
+ }
889
+ return false;
890
+ } catch (e) {
891
+ return false;
892
+ }
893
+ };
894
+ var SubmitFixesMessageZ = z4.object({
895
+ submitFixRequestId: z4.string().uuid(),
896
+ fixes: z4.array(
897
+ z4.object({
898
+ fixId: z4.string().uuid(),
899
+ diff: z4.string()
900
+ })
901
+ ),
902
+ branchName: z4.string(),
903
+ commitHash: z4.string(),
904
+ targetBranch: z4.string(),
905
+ repoUrl: z4.string()
906
+ });
907
+ var FixResponseArrayZ = z4.array(
908
+ z4.object({
909
+ fixId: z4.string().uuid()
910
+ })
911
+ );
912
+ var SubmitFixesResponseMessageZ = z4.object({
913
+ submitFixRequestId: z4.string().uuid(),
914
+ submitBranches: z4.array(
915
+ z4.object({
916
+ branchName: z4.string(),
917
+ fixes: FixResponseArrayZ
918
+ })
919
+ ),
920
+ error: z4.object({
921
+ type: z4.enum([
922
+ "InitialRepoAccessError",
923
+ "PushBranchError",
924
+ "UnknownError"
925
+ ]),
926
+ info: z4.object({
927
+ message: z4.string(),
928
+ pushBranchName: z4.string().optional()
929
+ })
930
+ }).optional()
931
+ });
932
+
933
+ // src/features/analysis/scm/scm.ts
934
+ function getScmLibTypeFromUrl(url) {
935
+ if (!url) {
936
+ return void 0;
937
+ }
938
+ if (url.toLowerCase().startsWith("https://gitlab.com/")) {
939
+ return "GITLAB" /* GITLAB */;
940
+ }
941
+ if (url.toLowerCase().startsWith("https://github.com/")) {
942
+ return "GITHUB" /* GITHUB */;
943
+ }
944
+ return void 0;
945
+ }
946
+ async function scmCanReachRepo({
947
+ repoUrl,
948
+ githubToken,
949
+ gitlabToken
950
+ }) {
951
+ try {
952
+ const scmLibType = getScmLibTypeFromUrl(repoUrl);
953
+ await SCMLib.init({
954
+ url: repoUrl,
955
+ accessToken: scmLibType === "GITHUB" /* GITHUB */ ? githubToken : scmLibType === "GITLAB" /* GITLAB */ ? gitlabToken : "",
956
+ scmType: scmLibType
957
+ });
958
+ return true;
959
+ } catch (e) {
960
+ return false;
961
+ }
962
+ }
963
+ var InvalidRepoUrlError = class extends Error {
964
+ constructor(m) {
965
+ super(m);
966
+ }
967
+ };
968
+ var InvalidAccessTokenError = class extends Error {
969
+ constructor(m) {
970
+ super(m);
971
+ }
972
+ };
973
+ var InvalidUrlPatternError = class extends Error {
974
+ constructor(m) {
975
+ super(m);
976
+ }
977
+ };
978
+ var RefNotFoundError = class extends Error {
979
+ constructor(m) {
980
+ super(m);
981
+ }
982
+ };
983
+ var RepoNoTokenAccessError = class extends Error {
984
+ constructor(m) {
985
+ super(m);
986
+ }
987
+ };
988
+ var SCMLib = class {
989
+ constructor(url, accessToken) {
990
+ __publicField(this, "url");
991
+ __publicField(this, "accessToken");
992
+ this.accessToken = accessToken;
993
+ this.url = url;
994
+ }
995
+ async getUrlWithCredentials() {
996
+ if (!this.url) {
997
+ console.error("no url for getUrlWithCredentials()");
998
+ throw new Error("no url");
999
+ }
1000
+ const trimmedUrl = this.url.trim().replace(/\/$/, "");
1001
+ if (!this.accessToken) {
1002
+ return trimmedUrl;
1003
+ }
1004
+ const username = await this._getUsernameForAuthUrl();
1005
+ const is_http = trimmedUrl.toLowerCase().startsWith("http://");
1006
+ const is_https = trimmedUrl.toLowerCase().startsWith("https://");
1007
+ if (is_http) {
1008
+ return `http://${username}:${this.accessToken}@${trimmedUrl.toLowerCase().replace("http://", "")}`;
1009
+ } else if (is_https) {
1010
+ return `https://${username}:${this.accessToken}@${trimmedUrl.toLowerCase().replace("https://", "")}`;
1011
+ } else {
1012
+ console.error(`invalid scm url ${trimmedUrl}`);
1013
+ throw new Error(`invalid scm url ${trimmedUrl}`);
1014
+ }
1015
+ }
1016
+ getAccessToken() {
1017
+ return this.accessToken || "";
1018
+ }
1019
+ getUrl() {
1020
+ return this.url;
1021
+ }
1022
+ getName() {
1023
+ if (!this.url) {
1024
+ return "";
1025
+ }
1026
+ return this.url.split("/").at(-1) || "";
1027
+ }
1028
+ static async getIsValidBranchName(branchName) {
1029
+ return isValidBranchName(branchName);
1030
+ }
1031
+ static async init({
1032
+ url,
1033
+ accessToken,
1034
+ scmType
1035
+ }) {
1036
+ let trimmedUrl = void 0;
1037
+ if (url) {
1038
+ trimmedUrl = url.trim().replace(/\/$/, "");
1039
+ }
1040
+ try {
1041
+ if ("GITHUB" /* GITHUB */ === scmType) {
1042
+ const scm = new GithubSCMLib(trimmedUrl, accessToken);
1043
+ await scm.validateParams();
1044
+ return scm;
1045
+ }
1046
+ if ("GITLAB" /* GITLAB */ === scmType) {
1047
+ const scm = new GitlabSCMLib(trimmedUrl, accessToken);
1048
+ await scm.validateParams();
1049
+ return scm;
1050
+ }
1051
+ } catch (e) {
1052
+ if (e instanceof InvalidRepoUrlError && url) {
1053
+ throw new RepoNoTokenAccessError("no access to repo");
1054
+ }
1055
+ }
1056
+ return new StubSCMLib(trimmedUrl);
1057
+ }
1058
+ };
1059
+ var GitlabSCMLib = class extends SCMLib {
1060
+ async createSubmitRequest(targetBranchName, sourceBranchName, title, body) {
1061
+ if (!this.accessToken || !this.url) {
1062
+ console.error("no access token or no url");
1063
+ throw new Error("no access token or no url");
1064
+ }
1065
+ return String(
1066
+ await createMergeRequest({
1067
+ title,
1068
+ body,
1069
+ targetBranchName,
1070
+ sourceBranchName,
1071
+ repoUrl: this.url,
1072
+ accessToken: this.accessToken
1073
+ })
1074
+ );
1075
+ }
1076
+ async validateParams() {
1077
+ return gitlabValidateParams({
1078
+ url: this.url,
1079
+ accessToken: this.accessToken
1080
+ });
1081
+ }
1082
+ async getRepoList() {
1083
+ if (!this.accessToken) {
1084
+ console.error("no access token");
1085
+ throw new Error("no access token");
1086
+ }
1087
+ return getGitlabRepoList(this.accessToken);
1088
+ }
1089
+ async getBranchList() {
1090
+ if (!this.accessToken || !this.url) {
1091
+ console.error("no access token or no url");
1092
+ throw new Error("no access token or no url");
1093
+ }
1094
+ return getGitlabBranchList({
1095
+ accessToken: this.accessToken,
1096
+ repoUrl: this.url
1097
+ });
1098
+ }
1099
+ getAuthHeaders() {
1100
+ if (this?.accessToken?.startsWith("glpat-")) {
1101
+ return {
1102
+ "Private-Token": this.accessToken
1103
+ };
1104
+ } else {
1105
+ return { authorization: `Bearer ${this.accessToken}` };
1106
+ }
1107
+ }
1108
+ getDownloadUrl(sha) {
1109
+ const repoName = this.url?.split("/")[-1];
1110
+ return `${this.url}/-/archive/${sha}/${repoName}-${sha}.zip`;
1111
+ }
1112
+ async _getUsernameForAuthUrl() {
1113
+ if (this?.accessToken?.startsWith("glpat-")) {
1114
+ return this.getUsername();
1115
+ } else {
1116
+ return "oauth2";
1117
+ }
1118
+ }
1119
+ async getIsRemoteBranch(branch) {
1120
+ if (!this.accessToken || !this.url) {
1121
+ console.error("no access token or no url");
1122
+ throw new Error("no access token or no url");
1123
+ }
1124
+ return getGitlabIsRemoteBranch({
1125
+ accessToken: this.accessToken,
1126
+ repoUrl: this.url,
1127
+ branch
1128
+ });
1129
+ }
1130
+ async getUserHasAccessToRepo() {
1131
+ if (!this.accessToken || !this.url) {
1132
+ console.error("no access token or no url");
1133
+ throw new Error("no access token or no url");
1134
+ }
1135
+ const username = await this.getUsername();
1136
+ return getGitlabIsUserCollaborator({
1137
+ username,
1138
+ accessToken: this.accessToken,
1139
+ repoUrl: this.url
1140
+ });
1141
+ }
1142
+ async getUsername() {
1143
+ if (!this.accessToken) {
1144
+ console.error("no access token");
1145
+ throw new Error("no access token");
1146
+ }
1147
+ return getGitlabUsername(this.accessToken);
1148
+ }
1149
+ async getSubmitRequestStatus(scmSubmitRequestId) {
1150
+ if (!this.accessToken || !this.url) {
1151
+ console.error("no access token or no url");
1152
+ throw new Error("no access token or no url");
1153
+ }
1154
+ const state = await getGitlabMergeRequestStatus({
1155
+ accessToken: this.accessToken,
1156
+ repoUrl: this.url,
1157
+ mrNumber: Number(scmSubmitRequestId)
1158
+ });
1159
+ switch (state) {
1160
+ case "merged" /* merged */:
1161
+ return "MERGED" /* MERGED */;
1162
+ case "opened" /* opened */:
1163
+ return "OPEN" /* OPEN */;
1164
+ case "closed" /* closed */:
1165
+ return "CLOSED" /* CLOSED */;
1166
+ default:
1167
+ throw new Error(`unknown state ${state}`);
1168
+ }
1169
+ }
1170
+ async getRepoBlameRanges(ref, path7) {
1171
+ if (!this.url) {
1172
+ console.error("no url");
1173
+ throw new Error("no url");
1174
+ }
1175
+ return await getGitlabBlameRanges(
1176
+ { ref, path: path7, gitlabUrl: this.url },
1177
+ {
1178
+ gitlabAuthToken: this.accessToken
1179
+ }
1180
+ );
1181
+ }
1182
+ async getReferenceData(ref) {
1183
+ if (!this.url) {
1184
+ console.error("no url");
1185
+ throw new Error("no url");
1186
+ }
1187
+ return await getGitlabReferenceData(
1188
+ { ref, gitlabUrl: this.url },
1189
+ {
1190
+ gitlabAuthToken: this.accessToken
1191
+ }
1192
+ );
1193
+ }
1194
+ async getRepoDefaultBranch() {
1195
+ if (!this.url) {
1196
+ console.error("no url");
1197
+ throw new Error("no url");
1198
+ }
1199
+ return await getGitlabRepoDefaultBranch(this.url, {
1200
+ gitlabAuthToken: this.accessToken
1201
+ });
1202
+ }
1203
+ };
1204
+ var GithubSCMLib = class extends SCMLib {
1205
+ async createSubmitRequest(targetBranchName, sourceBranchName, title, body) {
1206
+ if (!this.accessToken || !this.url) {
1207
+ console.error("no access token or no url");
1208
+ throw new Error("no access token or no url");
1209
+ }
1210
+ return String(
1211
+ await createPullRequest({
1212
+ title,
1213
+ body,
1214
+ targetBranchName,
1215
+ sourceBranchName,
1216
+ repoUrl: this.url,
1217
+ accessToken: this.accessToken
1218
+ })
1219
+ );
1220
+ }
1221
+ async validateParams() {
1222
+ return githubValidateParams(this.url, this.accessToken);
1223
+ }
1224
+ async getRepoList() {
1225
+ if (!this.accessToken) {
1226
+ console.error("no access token");
1227
+ throw new Error("no access token");
1228
+ }
1229
+ return getGithubRepoList(this.accessToken);
1230
+ }
1231
+ async getBranchList() {
1232
+ if (!this.accessToken || !this.url) {
1233
+ console.error("no access token or no url");
1234
+ throw new Error("no access token or no url");
1235
+ }
1236
+ return getGithubBranchList(this.accessToken, this.url);
1237
+ }
1238
+ getAuthHeaders() {
1239
+ if (this.accessToken) {
1240
+ return { authorization: `Bearer ${this.accessToken}` };
1241
+ }
1242
+ return {};
1243
+ }
1244
+ getDownloadUrl(sha) {
1245
+ return `${this.url}/zipball/${sha}`;
1246
+ }
1247
+ async _getUsernameForAuthUrl() {
1248
+ return this.getUsername();
1249
+ }
1250
+ async getIsRemoteBranch(branch) {
1251
+ if (!this.accessToken || !this.url) {
1252
+ console.error("no access token or no url");
1253
+ throw new Error("no access token or no url");
1254
+ }
1255
+ return getGithubIsRemoteBranch(this.accessToken, this.url, branch);
1256
+ }
1257
+ async getUserHasAccessToRepo() {
1258
+ if (!this.accessToken || !this.url) {
1259
+ console.error("no access token or no url");
1260
+ throw new Error("no access token or no url");
1261
+ }
1262
+ const username = await this.getUsername();
1263
+ return getGithubIsUserCollaborator(username, this.accessToken, this.url);
1264
+ }
1265
+ async getUsername() {
1266
+ if (!this.accessToken) {
1267
+ console.error("no access token");
1268
+ throw new Error("no access token");
1269
+ }
1270
+ return getGithubUsername(this.accessToken);
1271
+ }
1272
+ async getSubmitRequestStatus(scmSubmitRequestId) {
1273
+ if (!this.accessToken || !this.url) {
1274
+ console.error("no access token or no url");
1275
+ throw new Error("no access token or no url");
1276
+ }
1277
+ const state = await getGithubPullRequestStatus(
1278
+ this.accessToken,
1279
+ this.url,
1280
+ Number(scmSubmitRequestId)
1281
+ );
1282
+ if (state === "merged") {
1283
+ return "MERGED" /* MERGED */;
1284
+ }
1285
+ if (state === "open") {
1286
+ return "OPEN" /* OPEN */;
1287
+ }
1288
+ if (state === "draft") {
1289
+ return "DRAFT" /* DRAFT */;
1290
+ }
1291
+ if (state === "closed") {
1292
+ return "CLOSED" /* CLOSED */;
1293
+ }
1294
+ throw new Error(`unknown state ${state}`);
1295
+ }
1296
+ async getRepoBlameRanges(ref, path7) {
1297
+ if (!this.url) {
1298
+ console.error("no url");
1299
+ throw new Error("no url");
1300
+ }
1301
+ return await getGithubBlameRanges(
1302
+ { ref, path: path7, gitHubUrl: this.url },
1303
+ {
1304
+ githubAuthToken: this.accessToken
1305
+ }
1306
+ );
1307
+ }
1308
+ async getReferenceData(ref) {
1309
+ if (!this.url) {
1310
+ console.error("no url");
1311
+ throw new Error("no url");
1312
+ }
1313
+ return await getGithubReferenceData(
1314
+ { ref, gitHubUrl: this.url },
1315
+ {
1316
+ githubAuthToken: this.accessToken
1317
+ }
1318
+ );
1319
+ }
1320
+ async getRepoDefaultBranch() {
1321
+ if (!this.url) {
1322
+ console.error("no url");
1323
+ throw new Error("no url");
1324
+ }
1325
+ return await getGithubRepoDefaultBranch(this.url, {
1326
+ githubAuthToken: this.accessToken
1327
+ });
1328
+ }
1329
+ };
1330
+ var StubSCMLib = class extends SCMLib {
1331
+ async createSubmitRequest(_targetBranchName, _sourceBranchName, _title, _body) {
1332
+ console.error("createSubmitRequest() not implemented");
1333
+ throw new Error("createSubmitRequest() not implemented");
1334
+ }
1335
+ getAuthHeaders() {
1336
+ console.error("getAuthHeaders() not implemented");
1337
+ throw new Error("getAuthHeaders() not implemented");
1338
+ }
1339
+ getDownloadUrl(_sha) {
1340
+ console.error("getDownloadUrl() not implemented");
1341
+ throw new Error("getDownloadUrl() not implemented");
1342
+ }
1343
+ async _getUsernameForAuthUrl() {
1344
+ console.error("_getUsernameForAuthUrl() not implemented");
1345
+ throw new Error("_getUsernameForAuthUrl() not implemented");
1346
+ }
1347
+ async getIsRemoteBranch(_branch) {
1348
+ console.error("getIsRemoteBranch() not implemented");
1349
+ throw new Error("getIsRemoteBranch() not implemented");
1350
+ }
1351
+ async validateParams() {
1352
+ console.error("validateParams() not implemented");
1353
+ throw new Error("validateParams() not implemented");
1354
+ }
1355
+ async getRepoList() {
1356
+ console.error("getBranchList() not implemented");
1357
+ throw new Error("getBranchList() not implemented");
1358
+ }
1359
+ async getBranchList() {
1360
+ console.error("getBranchList() not implemented");
1361
+ throw new Error("getBranchList() not implemented");
1362
+ }
1363
+ async getUsername() {
1364
+ console.error("getUsername() not implemented");
1365
+ throw new Error("getUsername() not implemented");
1366
+ }
1367
+ async getSubmitRequestStatus(_scmSubmitRequestId) {
1368
+ console.error("getSubmitRequestStatus() not implemented");
1369
+ throw new Error("getSubmitRequestStatus() not implemented");
1370
+ }
1371
+ async getUserHasAccessToRepo() {
1372
+ console.error("getUserHasAccessToRepo() not implemented");
1373
+ throw new Error("getUserHasAccessToRepo() not implemented");
1374
+ }
1375
+ async getRepoBlameRanges(_ref, _path) {
1376
+ console.error("getRepoBlameRanges() not implemented");
1377
+ throw new Error("getRepoBlameRanges() not implemented");
1378
+ }
1379
+ async getReferenceData(_ref) {
1380
+ console.error("getReferenceData() not implemented");
1381
+ throw new Error("getReferenceData() not implemented");
1382
+ }
1383
+ async getRepoDefaultBranch() {
1384
+ console.error("getRepoDefaultBranch() not implemented");
1385
+ throw new Error("getRepoDefaultBranch() not implemented");
1386
+ }
1387
+ };
1388
+
1389
+ // src/features/analysis/scm/gitlab.ts
1390
+ function removeTrailingSlash2(str) {
1391
+ return str.trim().replace(/\/+$/, "");
1392
+ }
1393
+ var EnvVariablesZod2 = z5.object({
1394
+ GITLAB_API_TOKEN: z5.string().optional()
1395
+ });
1396
+ var { GITLAB_API_TOKEN } = EnvVariablesZod2.parse(process.env);
1397
+ var gitlabUrlRegex = /^http[s]?:\/\/[^/\s]+\/(([^/.\s]+[/])+)([^/.\s]+)(\.git)?(\/)?$/i;
1398
+ function getGitBeaker(options) {
1399
+ const token = options?.gitlabAuthToken ?? GITLAB_API_TOKEN ?? "";
1400
+ if (token?.startsWith("glpat-") || token === "") {
1401
+ return new Gitlab({ token });
1402
+ }
1403
+ return new Gitlab({ oauthToken: token });
1404
+ }
1405
+ async function gitlabValidateParams({
1406
+ url,
1407
+ accessToken
1408
+ }) {
1409
+ try {
1410
+ const api = getGitBeaker({ gitlabAuthToken: accessToken });
1411
+ if (accessToken) {
1412
+ await api.Users.showCurrentUser();
1413
+ }
1414
+ if (url) {
1415
+ const { projectPath } = parseOwnerAndRepo2(url);
1416
+ await api.Projects.show(projectPath);
1417
+ }
1418
+ } catch (e) {
1419
+ const error = e;
1420
+ const code = error.code || error.status || error.statusCode || error.response?.status || error.response?.statusCode || error.response?.code;
1421
+ const description = error.description || `${e}`;
1422
+ if (code === 401 || code === 403 || description.includes("401") || description.includes("403")) {
1423
+ throw new InvalidAccessTokenError(`invalid gitlab access token`);
1424
+ }
1425
+ if (code === 404 || description.includes("404") || description.includes("Not Found")) {
1426
+ throw new InvalidRepoUrlError(`invalid gitlab repo Url ${url}`);
1427
+ }
1428
+ throw e;
1429
+ }
1430
+ }
1431
+ async function getGitlabUsername(accessToken) {
1432
+ const api = getGitBeaker({ gitlabAuthToken: accessToken });
1433
+ const res = await api.Users.showCurrentUser();
1434
+ return res.username;
1435
+ }
1436
+ async function getGitlabIsUserCollaborator({
1437
+ username,
1438
+ accessToken,
1439
+ repoUrl
1440
+ }) {
1441
+ try {
1442
+ const { projectPath } = parseOwnerAndRepo2(repoUrl);
1443
+ const api = getGitBeaker({ gitlabAuthToken: accessToken });
1444
+ const res = await api.Projects.show(projectPath);
1445
+ const members = await api.ProjectMembers.all(res.id, {
1446
+ includeInherited: true
1447
+ });
1448
+ return !!members.find((member) => member.username === username);
1449
+ } catch (e) {
1450
+ return false;
1451
+ }
1452
+ }
1453
+ async function getGitlabMergeRequestStatus({
1454
+ accessToken,
1455
+ repoUrl,
1456
+ mrNumber
1457
+ }) {
1458
+ const { projectPath } = parseOwnerAndRepo2(repoUrl);
1459
+ const api = getGitBeaker({ gitlabAuthToken: accessToken });
1460
+ const res = await api.MergeRequests.show(projectPath, mrNumber);
1461
+ switch (res.state) {
1462
+ case "merged" /* merged */:
1463
+ case "opened" /* opened */:
1464
+ case "closed" /* closed */:
1465
+ return res.state;
1466
+ default:
1467
+ throw new Error(`unknown merge request state ${res.state}`);
1468
+ }
1469
+ }
1470
+ async function getGitlabIsRemoteBranch({
1471
+ accessToken,
1472
+ repoUrl,
1473
+ branch
1474
+ }) {
1475
+ const { projectPath } = parseOwnerAndRepo2(repoUrl);
1476
+ const api = getGitBeaker({ gitlabAuthToken: accessToken });
1477
+ try {
1478
+ const res = await api.Branches.show(projectPath, branch);
1479
+ return res.name === branch;
1480
+ } catch (e) {
1481
+ return false;
1482
+ }
1483
+ }
1484
+ async function getGitlabRepoList(accessToken) {
1485
+ const api = getGitBeaker({ gitlabAuthToken: accessToken });
1486
+ const res = await api.Projects.all({
1487
+ membership: true,
1488
+ //TODO: a bug in the sorting mechanism of this api call
1489
+ //disallows us to sort by updated_at in descending order
1490
+ //so we have to sort by updated_at in ascending order.
1491
+ //We can wait for the bug to be fixed or call the api
1492
+ //directly with fetch()
1493
+ sort: "asc",
1494
+ orderBy: "updated_at",
1495
+ pagination: "keyset",
1496
+ perPage: 100
1497
+ });
1498
+ return Promise.all(
1499
+ res.map(async (project) => {
1500
+ const proj = await api.Projects.show(project.id);
1501
+ const owner = proj.owner.name;
1502
+ const repoLanguages = await api.Projects.showLanguages(project.id);
1503
+ return {
1504
+ repoName: project.path,
1505
+ repoUrl: project.web_url,
1506
+ repoOwner: owner,
1507
+ repoLanguages: Object.keys(repoLanguages),
1508
+ repoIsPublic: project.visibility === "public",
1509
+ repoUpdatedAt: project.last_activity_at
1510
+ };
1511
+ })
1512
+ );
1513
+ }
1514
+ async function getGitlabBranchList({
1515
+ accessToken,
1516
+ repoUrl
1517
+ }) {
1518
+ const { projectPath } = parseOwnerAndRepo2(repoUrl);
1519
+ const api = getGitBeaker({ gitlabAuthToken: accessToken });
1520
+ try {
1521
+ const res = await api.Branches.all(projectPath, {
1522
+ perPage: 100,
1523
+ pagination: "keyset",
1524
+ orderBy: "updated_at",
1525
+ sort: "dec"
1526
+ });
1527
+ return res.map((branch) => branch.name);
1528
+ } catch (e) {
1529
+ return [];
1530
+ }
1531
+ }
1532
+ async function createMergeRequest(options) {
1533
+ const { projectPath } = parseOwnerAndRepo2(options.repoUrl);
1534
+ const api = getGitBeaker({ gitlabAuthToken: options.accessToken });
1535
+ const res = await api.MergeRequests.create(
1536
+ projectPath,
1537
+ options.sourceBranchName,
1538
+ options.targetBranchName,
1539
+ options.title,
1540
+ {
1541
+ description: options.body
1542
+ }
1543
+ );
1544
+ return res.iid;
1545
+ }
1546
+ async function getGitlabRepoDefaultBranch(repoUrl, options) {
1547
+ const api = getGitBeaker({ gitlabAuthToken: options?.gitlabAuthToken });
1548
+ const { projectPath } = parseOwnerAndRepo2(repoUrl);
1549
+ const project = await api.Projects.show(projectPath);
1550
+ if (!project.default_branch) {
1551
+ throw new Error("no default branch");
1552
+ }
1553
+ return project.default_branch;
1554
+ }
1555
+ async function getGitlabReferenceData({ ref, gitlabUrl }, options) {
1556
+ const { projectPath } = parseOwnerAndRepo2(gitlabUrl);
1557
+ const api = getGitBeaker({ gitlabAuthToken: options?.gitlabAuthToken });
1558
+ const results = await Promise.allSettled([
1559
+ (async () => {
1560
+ const res = await api.Branches.show(projectPath, ref);
1561
+ return {
1562
+ sha: res.commit.id,
1563
+ type: "BRANCH" /* BRANCH */,
1564
+ date: res.commit.committed_date ? new Date(res.commit.committed_date) : void 0
1565
+ };
1566
+ })(),
1567
+ (async () => {
1568
+ const res = await api.Commits.show(projectPath, ref);
1569
+ return {
1570
+ sha: res.id,
1571
+ type: "COMMIT" /* COMMIT */,
1572
+ date: res.committed_date ? new Date(res.committed_date) : void 0
1573
+ };
1574
+ })(),
1575
+ (async () => {
1576
+ const res = await api.Tags.show(projectPath, ref);
1577
+ return {
1578
+ sha: res.commit.id,
1579
+ type: "TAG" /* TAG */,
1580
+ date: res.commit.committed_date ? new Date(res.commit.committed_date) : void 0
1581
+ };
1582
+ })()
1583
+ ]);
1584
+ const [branchRes, commitRes, tagRes] = results;
1585
+ if (tagRes.status === "fulfilled") {
1586
+ return tagRes.value;
1587
+ }
1588
+ if (branchRes.status === "fulfilled") {
1589
+ return branchRes.value;
1590
+ }
1591
+ if (commitRes.status === "fulfilled") {
1592
+ return commitRes.value;
1593
+ }
1594
+ throw new RefNotFoundError(`ref: ${ref} does not exist`);
1595
+ }
1596
+ function parseOwnerAndRepo2(gitlabUrl) {
1597
+ gitlabUrl = removeTrailingSlash2(gitlabUrl);
1598
+ if (!gitlabUrlRegex.test(gitlabUrl)) {
1599
+ throw new InvalidUrlPatternError(`invalid gitlab repo Url ${gitlabUrl}`);
1600
+ }
1601
+ const groups = gitlabUrl.split(gitlabUrlRegex).filter((res) => res);
1602
+ const owner = groups[0]?.split("/")[0];
1603
+ const repo = groups[2];
1604
+ const projectPath = `${groups[0]}${repo}`;
1605
+ return { owner, repo, projectPath };
1606
+ }
1607
+ async function getGitlabBlameRanges({ ref, gitlabUrl, path: path7 }, options) {
1608
+ const { projectPath } = parseOwnerAndRepo2(gitlabUrl);
1609
+ const api = getGitBeaker({ gitlabAuthToken: options?.gitlabAuthToken });
1610
+ const resp = await api.RepositoryFiles.allFileBlames(projectPath, path7, ref);
1611
+ let lineNumber = 1;
1612
+ return resp.filter((range) => range.lines).map((range) => {
1613
+ const oldLineNumber = lineNumber;
1614
+ if (!range.lines) {
1615
+ throw new Error("range.lines should not be undefined");
1616
+ }
1617
+ lineNumber += range.lines.length;
1618
+ return {
1619
+ startingLine: oldLineNumber,
1620
+ endingLine: lineNumber - 1,
1621
+ login: range.commit.author_email,
1622
+ email: range.commit.author_email,
1623
+ name: range.commit.author_name
1624
+ };
1625
+ });
1626
+ }
1627
+ var GitlabAuthResultZ = z5.object({
1628
+ access_token: z5.string(),
1629
+ token_type: z5.string(),
1630
+ refresh_token: z5.string()
1631
+ });
1632
+
1633
+ // src/features/analysis/snyk.ts
1634
+ import cp from "node:child_process";
1635
+ import { createRequire } from "node:module";
1636
+ import chalk from "chalk";
1637
+ import Debug5 from "debug";
1638
+ import { createSpinner as createSpinner2 } from "nanospinner";
1639
+ import open from "open";
1640
+ import * as process2 from "process";
1641
+ import supportsColor from "supports-color";
1642
+ var { stdout: stdout2 } = supportsColor;
1643
+ var debug5 = Debug5("mobbdev:snyk");
1644
+ var require2 = createRequire(import.meta.url);
1645
+ var SNYK_PATH = require2.resolve("snyk/bin/snyk");
1646
+ 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";
1647
+ debug5("snyk executable path %s", SNYK_PATH);
1648
+ async function forkSnyk(args, { display }) {
1649
+ debug5("fork snyk with args %o %s", args, display);
1650
+ return new Promise((resolve, reject) => {
1651
+ const child = cp.fork(SNYK_PATH, args, {
1652
+ stdio: ["inherit", "pipe", "pipe", "ipc"],
1653
+ env: { FORCE_COLOR: stdout2 ? "1" : "0" }
1654
+ });
1655
+ let out = "";
1656
+ const onData = (chunk) => {
1657
+ debug5("chunk received from snyk std %s", chunk);
1658
+ out += chunk;
1659
+ };
1660
+ if (!child || !child?.stdout || !child?.stderr) {
1661
+ debug5("unable to fork snyk");
1662
+ reject(new Error("unable to fork snyk"));
1663
+ }
1664
+ child.stdout?.on("data", onData);
1665
+ child.stderr?.on("data", onData);
1666
+ if (display) {
1667
+ child.stdout?.pipe(process2.stdout);
1668
+ child.stderr?.pipe(process2.stderr);
1669
+ }
1670
+ child.on("exit", () => {
1671
+ debug5("snyk exit");
1672
+ resolve(out);
1673
+ });
1674
+ child.on("error", (err) => {
1675
+ debug5("snyk error %o", err);
1676
+ reject(err);
1677
+ });
1678
+ });
1679
+ }
1680
+ async function getSnykReport(reportPath, repoRoot, { skipPrompts = false }) {
1681
+ debug5("get snyk report start %s %s", reportPath, repoRoot);
1682
+ const config3 = await forkSnyk(["config"], { display: false });
1683
+ if (!config3.includes("api: ")) {
1684
+ const snykLoginSpinner = createSpinner2().start();
1685
+ if (!skipPrompts) {
1686
+ snykLoginSpinner.update({
1687
+ text: "\u{1F513} Login to Snyk is required, press any key to continue"
1688
+ });
1689
+ await keypress();
1690
+ }
1691
+ snykLoginSpinner.update({
1692
+ text: "\u{1F513} Waiting for Snyk login to complete"
1693
+ });
1694
+ debug5("no token in the config %s", config3);
1695
+ await forkSnyk(["auth"], { display: true });
1696
+ snykLoginSpinner.success({ text: "\u{1F513} Login to Snyk Successful" });
1697
+ }
1698
+ const snykSpinner = createSpinner2("\u{1F50D} Scanning your repo with Snyk ").start();
1699
+ const out = await forkSnyk(
1700
+ ["code", "test", `--sarif-file-output=${reportPath}`, repoRoot],
1701
+ { display: true }
1702
+ );
1703
+ if (out.includes(
1704
+ "Snyk Code is not supported for org: enable in Settings > Snyk Code"
1705
+ )) {
1706
+ debug5("snyk code is not enabled %s", out);
1707
+ snykSpinner.error({ text: "\u{1F50D} Snyk configuration needed" });
1708
+ const answer = await snykArticlePrompt();
1709
+ debug5("answer %s", answer);
1710
+ if (answer) {
1711
+ debug5("opening the browser");
1712
+ await open(SNYK_ARTICLE_URL);
1713
+ }
1714
+ console.log(
1715
+ chalk.bgBlue(
1716
+ "\nPlease enable Snyk Code in your Snyk account and try again."
1717
+ )
1718
+ );
1719
+ return false;
1720
+ }
1721
+ snykSpinner.success({ text: "\u{1F50D} Snyk code scan completed" });
1722
+ return true;
1723
+ }
1724
+
1725
+ // src/features/analysis/upload-file.ts
1726
+ import Debug6 from "debug";
1727
+ import fetch2, { File, fileFrom, FormData } from "node-fetch";
1728
+ var debug6 = Debug6("mobbdev:upload-file");
1729
+ async function uploadFile({
1730
+ file,
1731
+ url,
1732
+ uploadKey,
1733
+ uploadFields
1734
+ }) {
1735
+ debug6("upload file start %s", url);
1736
+ debug6("upload fields %o", uploadFields);
1737
+ debug6("upload key %s", uploadKey);
1738
+ const form = new FormData();
1739
+ Object.entries(uploadFields).forEach(([key, value]) => {
1740
+ form.append(key, value);
1741
+ });
1742
+ form.append("key", uploadKey);
1743
+ if (typeof file === "string") {
1744
+ debug6("upload file from path %s", file);
1745
+ form.append("file", await fileFrom(file));
1746
+ } else {
1747
+ debug6("upload file from buffer");
1748
+ form.append("file", new File([file], "file"));
1749
+ }
1750
+ const response = await fetch2(url, {
1751
+ method: "POST",
1752
+ body: form
1753
+ });
1754
+ if (!response.ok) {
1755
+ debug6("error from S3 %s %s", response.body, response.status);
1756
+ throw new Error(`Failed to upload the file: ${response.status}`);
1757
+ }
1758
+ debug6("upload file done");
1759
+ }
1760
+
1761
+ // src/features/analysis/index.ts
1762
+ var { CliError: CliError2, Spinner: Spinner2, keypress: keypress2, getDirName: getDirName2 } = utils_exports;
1763
+ var webLoginUrl = `${WEB_APP_URL}/cli-login`;
1764
+ var githubAuthUrl = `${WEB_APP_URL}/github-auth`;
1765
+ var gitlabAuthUrl = `${WEB_APP_URL}/gitlab-auth`;
1766
+ async function downloadRepo({
1767
+ repoUrl,
1768
+ authHeaders,
1769
+ downloadUrl,
1770
+ dirname,
1771
+ ci
1772
+ }) {
1773
+ const { createSpinner: createSpinner3 } = Spinner2({ ci });
1774
+ const repoSpinner = createSpinner3("\u{1F4BE} Downloading Repo").start();
1775
+ debug7("download repo %s %s %s", repoUrl, dirname);
1776
+ const zipFilePath = path5.join(dirname, "repo.zip");
1777
+ const response = await fetch3(downloadUrl, {
1778
+ method: "GET",
1779
+ headers: {
1780
+ ...authHeaders
1781
+ }
1782
+ });
1783
+ if (!response.ok) {
1784
+ debug7("SCM zipball request failed %s %s", response.body, response.status);
1785
+ repoSpinner.error({ text: "\u{1F4BE} Repo download failed" });
1786
+ throw new Error(`Can't access ${chalk2.bold(repoUrl)}`);
1787
+ }
1788
+ const fileWriterStream = fs3.createWriteStream(zipFilePath);
1789
+ if (!response.body) {
1790
+ throw new Error("Response body is empty");
1791
+ }
1792
+ await pipeline(response.body, fileWriterStream);
1793
+ await extract(zipFilePath, { dir: dirname });
1794
+ const repoRoot = fs3.readdirSync(dirname, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name)[0];
1795
+ if (!repoRoot) {
1796
+ throw new Error("Repo root not found");
1797
+ }
1798
+ debug7("repo root %s", repoRoot);
1799
+ repoSpinner.success({ text: "\u{1F4BE} Repo downloaded successfully" });
1800
+ return path5.join(dirname, repoRoot);
1801
+ }
1802
+ var LOGIN_MAX_WAIT = 10 * 60 * 1e3;
1803
+ var LOGIN_CHECK_DELAY = 5 * 1e3;
1804
+ 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, ${chalk2.bgBlue(
1805
+ "press any key to continue"
1806
+ )};`;
1807
+ var tmpObj = tmp.dirSync({
1808
+ unsafeCleanup: true
1809
+ });
1810
+ var getReportUrl = ({
1811
+ organizationId,
1812
+ projectId,
1813
+ fixReportId
1814
+ }) => `${WEB_APP_URL}/organization/${organizationId}/project/${projectId}/report/${fixReportId}`;
1815
+ var debug7 = Debug7("mobbdev:index");
1816
+ var packageJson = JSON.parse(
1817
+ fs3.readFileSync(path5.join(getDirName2(), "../package.json"), "utf8")
1818
+ );
1819
+ if (!semver.satisfies(process.version, packageJson.engines.node)) {
1820
+ throw new CliError2(
1821
+ `${packageJson.name} requires node version ${packageJson.engines.node}, but running ${process.version}.`
1822
+ );
1823
+ }
1824
+ var config2 = new Configstore(packageJson.name, { apiToken: "" });
1825
+ debug7("config %o", config2);
1826
+ async function runAnalysis(params, options) {
1827
+ try {
1828
+ await _scan(
1829
+ {
1830
+ ...params,
1831
+ dirname: tmpObj.name
1832
+ },
1833
+ options
1834
+ );
1835
+ } finally {
1836
+ tmpObj.removeCallback();
1837
+ }
1838
+ }
1839
+ async function _scan({
1840
+ dirname,
1841
+ repo,
1842
+ scanFile,
1843
+ apiKey,
1844
+ ci,
1845
+ srcPath,
1846
+ commitHash,
1847
+ ref
1848
+ }, { skipPrompts = false } = {}) {
1849
+ debug7("start %s %s", dirname, repo);
1850
+ const { createSpinner: createSpinner3 } = Spinner2({ ci });
1851
+ skipPrompts = skipPrompts || ci;
1852
+ let gqlClient = new GQLClient({
1853
+ apiKey: apiKey || config2.get("apiToken")
1854
+ });
1855
+ await handleMobbLogin();
1856
+ const { projectId, organizationId } = await gqlClient.getOrgAndProjectId();
1857
+ const {
1858
+ uploadS3BucketInfo: { repoUploadInfo, reportUploadInfo }
1859
+ } = await gqlClient.uploadS3BucketInfo();
1860
+ let reportPath = scanFile;
1861
+ if (srcPath) {
1862
+ return await uploadExistingRepo();
1863
+ }
1864
+ if (!repo) {
1865
+ throw new Error("repo is required in case srcPath is not provided");
1866
+ }
1867
+ const userInfo = await gqlClient.getUserInfo();
1868
+ const { githubToken, gitlabToken } = userInfo;
1869
+ const isRepoAvailable = await scmCanReachRepo({
1870
+ repoUrl: repo,
1871
+ githubToken,
1872
+ gitlabToken
1873
+ });
1874
+ const scmLibType = getScmLibTypeFromUrl(repo);
1875
+ const scmAuthUrl = scmLibType === "GITHUB" /* GITHUB */ ? githubAuthUrl : scmLibType === "GITLAB" /* GITLAB */ ? gitlabAuthUrl : void 0;
1876
+ let token = scmLibType === "GITHUB" /* GITHUB */ ? githubToken : scmLibType === "GITLAB" /* GITLAB */ ? gitlabToken : void 0;
1877
+ if (!isRepoAvailable) {
1878
+ if (ci || !scmLibType || !scmAuthUrl) {
1879
+ const errorMessage = scmAuthUrl ? `Cannot access repo ${repo}` : `Cannot access repo ${repo} with the provided token, please visit ${scmAuthUrl} to refresh your source control management system token`;
1880
+ throw new Error(errorMessage);
1881
+ }
1882
+ if (scmLibType && scmAuthUrl) {
1883
+ token = await handleScmIntegration(token, scmLibType, scmAuthUrl) || "";
1884
+ const isRepoAvailable2 = await scmCanReachRepo({
1885
+ repoUrl: repo,
1886
+ githubToken: token,
1887
+ gitlabToken: token
1888
+ });
1889
+ if (!isRepoAvailable2) {
1890
+ throw new Error(
1891
+ `Cannot access repo ${repo} with the provided credentials`
1892
+ );
1893
+ }
1894
+ }
1895
+ }
1896
+ const scm = await SCMLib.init({
1897
+ url: repo,
1898
+ accessToken: token,
1899
+ scmType: scmLibType
1900
+ });
1901
+ const reference = ref ?? await scm.getRepoDefaultBranch();
1902
+ const { sha } = await scm.getReferenceData(reference);
1903
+ debug7("org id %s", organizationId);
1904
+ debug7("project id %s", projectId);
1905
+ debug7("default branch %s", reference);
1906
+ const repositoryRoot = await downloadRepo({
1907
+ repoUrl: repo,
1908
+ dirname,
1909
+ ci,
1910
+ authHeaders: scm.getAuthHeaders(),
1911
+ downloadUrl: scm.getDownloadUrl(sha)
1912
+ });
1913
+ if (!reportPath) {
1914
+ reportPath = await getReportFromSnyk();
1915
+ }
1916
+ const uploadReportSpinner = createSpinner3("\u{1F4C1} Uploading Report").start();
1917
+ try {
1918
+ await uploadFile({
1919
+ file: reportPath,
1920
+ url: reportUploadInfo.url,
1921
+ uploadFields: reportUploadInfo.uploadFields,
1922
+ uploadKey: reportUploadInfo.uploadKey
1923
+ });
1924
+ } catch (e) {
1925
+ uploadReportSpinner.error({ text: "\u{1F4C1} Report upload failed" });
1926
+ throw e;
1927
+ }
1928
+ uploadReportSpinner.success({ text: "\u{1F4C1} Report uploaded successfully" });
1929
+ const mobbSpinner = createSpinner3("\u{1F575}\uFE0F\u200D\u2642\uFE0F Initiating Mobb analysis").start();
1930
+ try {
1931
+ await gqlClient.submitVulnerabilityReport({
1932
+ fixReportId: reportUploadInfo.fixReportId,
1933
+ repoUrl: repo,
1934
+ reference,
1935
+ projectId
1936
+ });
1937
+ } catch (e) {
1938
+ mobbSpinner.error({ text: "\u{1F575}\uFE0F\u200D\u2642\uFE0F Mobb analysis failed" });
1939
+ throw e;
1940
+ }
1941
+ mobbSpinner.success({
1942
+ text: "\u{1F575}\uFE0F\u200D\u2642\uFE0F Generating fixes..."
1943
+ });
1944
+ await askToOpenAnalysis();
1945
+ async function getReportFromSnyk() {
1946
+ const reportPath2 = path5.join(dirname, "report.json");
1947
+ if (!await getSnykReport(reportPath2, repositoryRoot, { skipPrompts })) {
1948
+ debug7("snyk code is not enabled");
1949
+ throw new CliError2("Snyk code is not enabled");
1950
+ }
1951
+ return reportPath2;
1952
+ }
1953
+ async function askToOpenAnalysis() {
1954
+ const reportUrl = getReportUrl({
1955
+ organizationId,
1956
+ projectId,
1957
+ fixReportId: reportUploadInfo.fixReportId
1958
+ });
1959
+ !ci && console.log("You can access the report at: \n");
1960
+ console.log(chalk2.bold(reportUrl));
1961
+ !skipPrompts && await mobbAnalysisPrompt();
1962
+ !ci && open2(reportUrl);
1963
+ !ci && console.log(
1964
+ chalk2.bgBlue("\n\n My work here is done for now, see you soon! \u{1F575}\uFE0F\u200D\u2642\uFE0F ")
1965
+ );
1966
+ }
1967
+ async function handleMobbLogin() {
1968
+ if (await gqlClient.verifyToken()) {
1969
+ createSpinner3().start().success({
1970
+ text: "\u{1F513} Logged in to Mobb successfully"
1971
+ });
1972
+ return;
1973
+ } else if (apiKey) {
1974
+ createSpinner3().start().error({
1975
+ text: "\u{1F513} Logged in to Mobb failed - check your api-key"
1976
+ });
1977
+ throw new CliError2();
1978
+ }
1979
+ const loginSpinner = createSpinner3().start();
1980
+ if (!skipPrompts) {
1981
+ loginSpinner.update({ text: MOBB_LOGIN_REQUIRED_MSG });
1982
+ await keypress2();
1983
+ }
1984
+ loginSpinner.update({
1985
+ text: "\u{1F513} Waiting for Mobb login..."
1986
+ });
1987
+ const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
1988
+ modulusLength: 2048
1989
+ });
1990
+ const loginId = await gqlClient.createCliLogin({
1991
+ publicKey: publicKey.export({ format: "pem", type: "pkcs1" }).toString()
1992
+ });
1993
+ const browserUrl = `${webLoginUrl}/${loginId}?hostname=${os2.hostname()}`;
1994
+ !ci && console.log(
1995
+ `If the page does not open automatically, kindly access it through ${browserUrl}.`
1996
+ );
1997
+ await open2(browserUrl);
1998
+ let newApiToken = null;
1999
+ for (let i = 0; i < LOGIN_MAX_WAIT / LOGIN_CHECK_DELAY; i++) {
2000
+ const encryptedApiToken = await gqlClient.getEncryptedApiToken({
2001
+ loginId
2002
+ });
2003
+ loginSpinner.spin();
2004
+ if (encryptedApiToken) {
2005
+ debug7("encrypted API token received %s", encryptedApiToken);
2006
+ newApiToken = crypto.privateDecrypt(privateKey, Buffer.from(encryptedApiToken, "base64")).toString("utf-8");
2007
+ debug7("API token decrypted");
2008
+ break;
2009
+ }
2010
+ await sleep(LOGIN_CHECK_DELAY);
2011
+ }
2012
+ if (!newApiToken) {
2013
+ loginSpinner.error({
2014
+ text: "Login timeout error"
2015
+ });
2016
+ throw new CliError2();
2017
+ }
2018
+ gqlClient = new GQLClient({ apiKey: newApiToken });
2019
+ if (await gqlClient.verifyToken()) {
2020
+ debug7("set api token %s", newApiToken);
2021
+ config2.set("apiToken", newApiToken);
2022
+ loginSpinner.success({ text: "\u{1F513} Login to Mobb successful!" });
2023
+ } else {
2024
+ loginSpinner.error({
2025
+ text: "Something went wrong, API token is invalid."
2026
+ });
2027
+ throw new CliError2();
2028
+ }
2029
+ }
2030
+ async function handleScmIntegration(oldToken, scmLibType2, scmAuthUrl2) {
2031
+ const scmName = scmLibType2 === "GITHUB" /* GITHUB */ ? "Github" : scmLibType2 === "GITLAB" /* GITLAB */ ? "Gitlab" : "";
2032
+ const addScmIntegration = skipPrompts ? true : await scmIntegrationPrompt(scmName);
2033
+ const scmSpinner = createSpinner3(
2034
+ `\u{1F517} Waiting for ${scmName} integration...`
2035
+ ).start();
2036
+ if (!addScmIntegration) {
2037
+ scmSpinner.error();
2038
+ throw Error(`Could not reach ${scmName} repo`);
2039
+ }
2040
+ console.log(
2041
+ `If the page does not open automatically, kindly access it through ${scmAuthUrl2}.`
2042
+ );
2043
+ await open2(scmAuthUrl2);
2044
+ for (let i = 0; i < LOGIN_MAX_WAIT / LOGIN_CHECK_DELAY; i++) {
2045
+ const { githubToken: githubToken2, gitlabToken: gitlabToken2 } = await gqlClient.getUserInfo();
2046
+ if (scmLibType2 === "GITHUB" /* GITHUB */ && githubToken2 !== oldToken) {
2047
+ scmSpinner.success({ text: "\u{1F517} Github integration successful!" });
2048
+ return githubToken2;
2049
+ }
2050
+ if (scmLibType2 === "GITLAB" /* GITLAB */ && gitlabToken2 !== oldToken) {
2051
+ scmSpinner.success({ text: "\u{1F517} Gitlab integration successful!" });
2052
+ return gitlabToken2;
2053
+ }
2054
+ scmSpinner.spin();
2055
+ await sleep(LOGIN_CHECK_DELAY);
2056
+ }
2057
+ scmSpinner.error({
2058
+ text: `${scmName} login timeout error`
2059
+ });
2060
+ throw new CliError2(`${scmName} login timeout`);
2061
+ }
2062
+ async function uploadExistingRepo() {
2063
+ if (!srcPath || !reportPath) {
2064
+ throw new Error("src path and reportPath is required");
2065
+ }
2066
+ const gitInfo = await getGitInfo(srcPath);
2067
+ const zippingSpinner = createSpinner3("\u{1F4E6} Zipping repo").start();
2068
+ const zipBuffer = await pack(srcPath);
2069
+ zippingSpinner.success({ text: "\u{1F4E6} Zipping repo successful!" });
2070
+ const uploadReportSpinner2 = createSpinner3("\u{1F4C1} Uploading Report").start();
2071
+ try {
2072
+ await uploadFile({
2073
+ file: reportPath,
2074
+ url: reportUploadInfo.url,
2075
+ uploadFields: reportUploadInfo.uploadFields,
2076
+ uploadKey: reportUploadInfo.uploadKey
2077
+ });
2078
+ } catch (e) {
2079
+ uploadReportSpinner2.error({ text: "\u{1F4C1} Report upload failed" });
2080
+ throw e;
2081
+ }
2082
+ uploadReportSpinner2.success({
2083
+ text: "\u{1F4C1} Uploading Report successful!"
2084
+ });
2085
+ const uploadRepoSpinner = createSpinner3("\u{1F4C1} Uploading Repo").start();
2086
+ try {
2087
+ await uploadFile({
2088
+ file: zipBuffer,
2089
+ url: repoUploadInfo.url,
2090
+ uploadFields: repoUploadInfo.uploadFields,
2091
+ uploadKey: repoUploadInfo.uploadKey
2092
+ });
2093
+ } catch (e) {
2094
+ uploadRepoSpinner.error({ text: "\u{1F4C1} Repo upload failed" });
2095
+ throw e;
2096
+ }
2097
+ uploadRepoSpinner.success({ text: "\u{1F4C1} Uploading Repo successful!" });
2098
+ const mobbSpinner2 = createSpinner3("\u{1F575}\uFE0F\u200D\u2642\uFE0F Initiating Mobb analysis").start();
2099
+ try {
2100
+ await gqlClient.submitVulnerabilityReport({
2101
+ fixReportId: reportUploadInfo.fixReportId,
2102
+ repoUrl: repo || gitInfo.repoUrl,
2103
+ reference: gitInfo.reference,
2104
+ sha: commitHash || gitInfo.hash,
2105
+ projectId
2106
+ });
2107
+ } catch (e) {
2108
+ mobbSpinner2.error({ text: "\u{1F575}\uFE0F\u200D\u2642\uFE0F Mobb analysis failed" });
2109
+ throw e;
2110
+ }
2111
+ mobbSpinner2.success({
2112
+ text: "\u{1F575}\uFE0F\u200D\u2642\uFE0F Generating fixes..."
2113
+ });
2114
+ await askToOpenAnalysis();
2115
+ }
2116
+ }
2117
+
2118
+ // src/commands/index.ts
2119
+ import chalkAnimation from "chalk-animation";
2120
+ async function analyze({ repo, f: scanFile, ref, apiKey, ci, commitHash, srcPath }, { skipPrompts = false } = {}) {
2121
+ !ci && await showWelcomeMessage(skipPrompts);
2122
+ await runAnalysis(
2123
+ {
2124
+ repo,
2125
+ scanFile,
2126
+ ref,
2127
+ apiKey,
2128
+ ci,
2129
+ commitHash,
2130
+ srcPath
2131
+ },
2132
+ { skipPrompts }
2133
+ );
2134
+ }
2135
+ async function scan(scanOptions, { skipPrompts = false } = {}) {
2136
+ const { scanner, ci } = scanOptions;
2137
+ !ci && await showWelcomeMessage(skipPrompts);
2138
+ const selectedScanner = scanner || await choseScanner();
2139
+ if (selectedScanner !== SCANNERS.Snyk) {
2140
+ throw new CliError(
2141
+ "Vulnerability scanning via Bugsy is available only with Snyk at the moment. Additional scanners will follow soon."
2142
+ );
2143
+ }
2144
+ await runAnalysis(
2145
+ { ...scanOptions, scanner: selectedScanner },
2146
+ { skipPrompts }
2147
+ );
2148
+ }
2149
+ async function showWelcomeMessage(skipPrompts = false) {
2150
+ console.log(mobbAscii);
2151
+ const welcome = chalkAnimation.rainbow("\n Welcome to Bugsy\n");
2152
+ skipPrompts ? await sleep(100) : await sleep(2e3);
2153
+ welcome.stop();
2154
+ }
2155
+
2156
+ // src/args/commands/analyze.ts
2157
+ import chalk5 from "chalk";
2158
+
2159
+ // src/args/options.ts
2160
+ import chalk3 from "chalk";
2161
+ var repoOption = {
2162
+ alias: "r",
2163
+ demandOption: true,
2164
+ type: "string",
2165
+ describe: chalk3.bold("Github / GitLab repository URL")
2166
+ };
2167
+ var yesOption = {
2168
+ alias: "yes",
2169
+ type: "boolean",
2170
+ describe: chalk3.bold("Skip prompts and use default values")
2171
+ };
2172
+ var refOption = {
2173
+ describe: chalk3.bold("reference of the repository (branch, tag, commit)"),
2174
+ type: "string",
2175
+ demandOption: false
2176
+ };
2177
+ var ciOption = {
2178
+ describe: chalk3.bold(
2179
+ "Run in CI mode, prompts and browser will not be opened"
2180
+ ),
2181
+ type: "boolean",
2182
+ default: false
2183
+ };
2184
+ var apiKeyOption = {
2185
+ type: "string",
2186
+ describe: chalk3.bold("Mobb authentication api-key")
2187
+ };
2188
+ var commitHashOption = {
2189
+ alias: "ch",
2190
+ describe: chalk3.bold("Hash of the commit"),
2191
+ type: "string"
2192
+ };
2193
+
2194
+ // src/args/validation.ts
2195
+ import chalk4 from "chalk";
2196
+ import path6 from "path";
2197
+ import { z as z6 } from "zod";
2198
+ function throwRepoUrlErrorMessage({
2199
+ error,
2200
+ repoUrl,
2201
+ command
2202
+ }) {
2203
+ const errorMessage = error.issues[error.issues.length - 1]?.message;
2204
+ const formattedErrorMessage = `
2205
+ Error: ${chalk4.bold(
2206
+ repoUrl
2207
+ )} is ${errorMessage}
2208
+ Example:
2209
+ mobbdev ${command} -r ${chalk4.bold(
2210
+ "https://github.com/WebGoat/WebGoat"
2211
+ )}`;
2212
+ throw new CliError(formattedErrorMessage);
2213
+ }
2214
+ var GIT_REPO_URL_PATTERN = /^https:\/\/(gitlab|github)\.com\/(([^/.\s]+[/])+)([^/.\s]+)(\.git)?(\/)?$/i;
2215
+ var UrlZ = z6.string({
2216
+ invalid_type_error: "is not a valid GitHub / GitLab URL"
2217
+ }).regex(GIT_REPO_URL_PATTERN, {
2218
+ message: "is not a valid GitHub / GitLab URL"
2219
+ });
2220
+ function validateRepoUrl(args) {
2221
+ const repoSafeParseResult = UrlZ.safeParse(args.repo);
2222
+ const { success } = repoSafeParseResult;
2223
+ const [command] = args._;
2224
+ if (!command) {
2225
+ throw new CliError("Command not found");
2226
+ }
2227
+ if (!success) {
2228
+ throwRepoUrlErrorMessage({
2229
+ error: repoSafeParseResult.error,
2230
+ repoUrl: args.repo,
2231
+ command
2232
+ });
2233
+ }
2234
+ }
2235
+ var supportExtensions = [".json", ".xml", ".fpr", ".sarif"];
2236
+ function validateReportFileFormat(reportFile) {
2237
+ if (!supportExtensions.includes(path6.extname(reportFile))) {
2238
+ throw new CliError(
2239
+ `
2240
+ ${chalk4.bold(
2241
+ reportFile
2242
+ )} is not a supported file extension. Supported extensions are: ${chalk4.bold(
2243
+ supportExtensions.join(", ")
2244
+ )}
2245
+ `
2246
+ );
2247
+ }
2248
+ }
2249
+
2250
+ // src/args/commands/analyze.ts
2251
+ function analyzeBuilder(yargs2) {
2252
+ return yargs2.option("f", {
2253
+ alias: "scan-file",
2254
+ demandOption: true,
2255
+ type: "string",
2256
+ describe: chalk5.bold(
2257
+ "Select the vulnerability report to analyze (Checkmarx, Snyk, Fortify, CodeQL)"
2258
+ )
2259
+ }).option("repo", repoOption).option("p", {
2260
+ alias: "src-path",
2261
+ describe: chalk5.bold(
2262
+ "Path to the repository folder with the source code"
2263
+ ),
2264
+ type: "string"
2265
+ }).option("ref", refOption).option("ch", {
2266
+ alias: "commit-hash",
2267
+ describe: chalk5.bold("Hash of the commit"),
2268
+ type: "string"
2269
+ }).option("y", yesOption).option("ci", ciOption).option("api-key", apiKeyOption).option("commit-hash", commitHashOption).example(
2270
+ "$0 analyze -r https://github.com/WebGoat/WebGoat -f <your_vulirabitliy_report_path>",
2271
+ "analyze an existing repository"
2272
+ ).help();
2273
+ }
2274
+ function validateAnalyzeOptions(argv) {
2275
+ if (!fs4.existsSync(argv.f)) {
2276
+ throw new CliError(`
2277
+ Can't access ${chalk5.bold(argv.f)}`);
2278
+ }
2279
+ if (!argv.srcPath && !argv.repo) {
2280
+ throw new CliError("You must supply either --src-path or --repo");
2281
+ }
2282
+ if (!argv.srcPath && argv.repo) {
2283
+ validateRepoUrl(argv);
2284
+ }
2285
+ if (argv.ci && !argv.apiKey) {
2286
+ throw new CliError("--ci flag requires --api-key to be provided as well");
2287
+ }
2288
+ validateReportFileFormat(argv.f);
2289
+ }
2290
+ async function analyzeHandler(args) {
2291
+ validateAnalyzeOptions(args);
2292
+ await analyze(args, { skipPrompts: args.yes });
2293
+ }
2294
+
2295
+ // src/args/commands/scan.ts
2296
+ import chalk6 from "chalk";
2297
+ function scanBuilder(args) {
2298
+ return args.coerce("scanner", (arg) => arg.toLowerCase()).option("repo", repoOption).option("ref", refOption).option("s", {
2299
+ alias: "scanner",
2300
+ choices: Object.values(SCANNERS),
2301
+ describe: chalk6.bold("Select the scanner to use")
2302
+ }).option("y", yesOption).option("ci", ciOption).option("api-key", apiKeyOption).example(
2303
+ "$0 scan -r https://github.com/WebGoat/WebGoat",
2304
+ "Scan an existing repository"
2305
+ ).help();
2306
+ }
2307
+ function validateScanOptions(argv) {
2308
+ validateRepoUrl(argv);
2309
+ if (argv.ci && !argv.apiKey) {
2310
+ throw new CliError(
2311
+ "\nError: --ci flag requires --api-key to be provided as well"
2312
+ );
2313
+ }
2314
+ }
2315
+ async function scanHandler(args) {
2316
+ validateScanOptions(args);
2317
+ await scan(args, { skipPrompts: args.yes });
2318
+ }
2319
+
2320
+ // src/args/yargs.ts
2321
+ var parseArgs = async (args) => {
2322
+ const yargsInstance = yargs(args);
2323
+ return yargsInstance.updateStrings({
2324
+ "Commands:": chalk7.yellow.underline.bold("Commands:"),
2325
+ "Options:": chalk7.yellow.underline.bold("Options:"),
2326
+ "Examples:": chalk7.yellow.underline.bold("Examples:"),
2327
+ "Show help": chalk7.bold("Show help")
2328
+ }).usage(
2329
+ `${chalk7.bold(
2330
+ "\n Bugsy - Trusted, Automatic Vulnerability Fixer \u{1F575}\uFE0F\u200D\u2642\uFE0F\n\n"
2331
+ )} ${chalk7.yellow.underline.bold("Usage:")}
2332
+ $0 ${chalk7.green(
2333
+ "<command>"
2334
+ )} ${chalk7.dim("[options]")}
2335
+ `
2336
+ ).version(false).command(
2337
+ "scan",
2338
+ chalk7.bold(
2339
+ "Scan your code for vulnerabilities, get automated fixes right away."
2340
+ ),
2341
+ scanBuilder,
2342
+ scanHandler
2343
+ ).command(
2344
+ "analyze",
2345
+ chalk7.bold(
2346
+ "Provide a vulnerability report and relevant code repository, get automated fixes right away."
2347
+ ),
2348
+ analyzeBuilder,
2349
+ analyzeHandler
2350
+ ).example(
2351
+ "$0 scan -r https://github.com/WebGoat/WebGoat",
2352
+ "Scan an existing repository"
2353
+ ).command({
2354
+ command: "*",
2355
+ handler() {
2356
+ yargsInstance.showHelp();
2357
+ }
2358
+ }).strictOptions().help("h").alias("h", "help").epilog(chalk7.bgBlue("Made with \u2764\uFE0F by Mobb")).showHelpOnFail(true).wrap(Math.min(120, yargsInstance.terminalWidth())).parse();
2359
+ };
2360
+
2361
+ // src/index.ts
2362
+ async function run() {
2363
+ return parseArgs(hideBin(process.argv));
2364
+ }
2365
+ (async () => {
2366
+ try {
2367
+ await run();
2368
+ process.exit(0);
2369
+ } catch (err) {
2370
+ if (err instanceof CliError) {
2371
+ console.error(err.message);
2372
+ process.exit(1);
2373
+ }
2374
+ console.error(
2375
+ "Something went wrong, please try again or contact support if issue persists."
2376
+ );
2377
+ console.error(err);
2378
+ process.exit(1);
2379
+ }
2380
+ })();