mobbdev 0.0.62 → 0.0.63

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.
Files changed (2) hide show
  1. package/dist/index.mjs +1865 -1177
  2. package/package.json +7 -1
package/dist/index.mjs CHANGED
@@ -12,8 +12,15 @@ var __publicField = (obj, key, value) => {
12
12
  // src/index.ts
13
13
  import { hideBin } from "yargs/helpers";
14
14
 
15
+ // src/types.ts
16
+ var mobbCliCommand = {
17
+ scan: "scan",
18
+ analyze: "analyze",
19
+ review: "review"
20
+ };
21
+
15
22
  // src/args/yargs.ts
16
- import chalk8 from "chalk";
23
+ import chalk9 from "chalk";
17
24
  import yargs from "yargs/yargs";
18
25
 
19
26
  // src/args/commands/analyze.ts
@@ -35,6 +42,7 @@ var SCANNERS = {
35
42
  Fortify: "fortify",
36
43
  Snyk: "snyk"
37
44
  };
45
+ var SupportedScannersZ = z.enum([SCANNERS.Checkmarx, SCANNERS.Snyk]);
38
46
  var envVariablesSchema = z.object({
39
47
  WEB_APP_URL: z.string(),
40
48
  API_URL: z.string()
@@ -146,14 +154,16 @@ var CliError = class extends Error {
146
154
  };
147
155
 
148
156
  // src/features/analysis/index.ts
157
+ import { Octokit as Octokit3 } from "@octokit/core";
149
158
  import chalk4 from "chalk";
150
159
  import Configstore from "configstore";
151
- import Debug9 from "debug";
160
+ import Debug10 from "debug";
152
161
  import extract from "extract-zip";
153
162
  import fetch3 from "node-fetch";
154
163
  import open2 from "open";
155
164
  import semver from "semver";
156
- import tmp from "tmp";
165
+ import tmp2 from "tmp";
166
+ import { z as z8 } from "zod";
157
167
 
158
168
  // src/features/analysis/git.ts
159
169
  import Debug2 from "debug";
@@ -265,16 +275,22 @@ var SUBMIT_VULNERABILITY_REPORT = gql`
265
275
  $projectId: String!
266
276
  $sha: String
267
277
  $vulnerabilityReportFileName: String
278
+ $pullRequest: Int
268
279
  ) {
269
280
  submitVulnerabilityReport(
270
281
  fixReportId: $fixReportId
271
282
  repoUrl: $repoUrl
272
283
  reference: $reference
273
284
  sha: $sha
285
+ pullRequest: $pullRequest
274
286
  projectId: $projectId
275
287
  vulnerabilityReportFileName: $vulnerabilityReportFileName
276
288
  ) {
277
289
  __typename
290
+ ... on VulnerabilityReport {
291
+ vulnerabilityReportId
292
+ fixReportId
293
+ }
278
294
  }
279
295
  }
280
296
  `;
@@ -352,6 +368,136 @@ var GET_VULNERABILITY_REPORT_PATHS = gql2`
352
368
  }
353
369
  }
354
370
  `;
371
+ var SUBSCRIBE_TO_ANALYSIS = gql2`
372
+ subscription getAnalysis($analysisId: uuid!) {
373
+ analysis: fixReport_by_pk(id: $analysisId) {
374
+ id
375
+ state
376
+ }
377
+ }
378
+ `;
379
+ var GET_ANALYSIS = gql2`
380
+ query getAnalsyis($analysisId: uuid!) {
381
+ analysis: fixReport_by_pk(id: $analysisId) {
382
+ id
383
+ state
384
+ repo {
385
+ commitSha
386
+ pullRequest
387
+ }
388
+ fixes {
389
+ id
390
+ issueType
391
+ vulnerabilityReportIssues {
392
+ issueLanguage
393
+ state
394
+ issueType
395
+ vendorIssueId
396
+ }
397
+ }
398
+ vulnerabilityReport {
399
+ projectId
400
+ project {
401
+ organizationId
402
+ }
403
+ file {
404
+ signedFile {
405
+ url
406
+ }
407
+ }
408
+ }
409
+ }
410
+ }
411
+ `;
412
+ var GET_FIX = gql2`
413
+ query getFix($fixId: uuid!) {
414
+ fix_by_pk(id: $fixId) {
415
+ patchAndQuestions {
416
+ patch
417
+ }
418
+ }
419
+ }
420
+ `;
421
+
422
+ // src/features/analysis/graphql/subscirbe.ts
423
+ import { createClient } from "graphql-ws";
424
+ import WebSocket from "ws";
425
+ var SUBSCRIPTION_TIMEOUT_MS = 5 * 60 * 1e3;
426
+ function createWSClient(options) {
427
+ return createClient({
428
+ url: options.url,
429
+ webSocketImpl: options.websocket || WebSocket,
430
+ connectionParams: () => {
431
+ return {
432
+ headers: {
433
+ [API_KEY_HEADER_NAME]: options.apiKey
434
+ }
435
+ };
436
+ }
437
+ });
438
+ }
439
+ function subscribe(query, variables, callback, wsClientOptions) {
440
+ return new Promise((resolve, reject) => {
441
+ let timer = null;
442
+ const { timeoutInMs = SUBSCRIPTION_TIMEOUT_MS } = wsClientOptions;
443
+ const client = createWSClient({
444
+ ...wsClientOptions,
445
+ websocket: WebSocket,
446
+ url: API_URL.replace("http", "ws")
447
+ });
448
+ const unsubscribe = client.subscribe(
449
+ { query, variables },
450
+ {
451
+ next: (data) => {
452
+ function callbackResolve(data2) {
453
+ unsubscribe();
454
+ if (timer) {
455
+ clearTimeout(timer);
456
+ }
457
+ resolve(data2);
458
+ }
459
+ function callbackReject(data2) {
460
+ unsubscribe();
461
+ if (timer) {
462
+ clearTimeout(timer);
463
+ }
464
+ reject(data2);
465
+ }
466
+ if (!data.data) {
467
+ reject(
468
+ new Error(
469
+ `Broken data object from graphQL subscribe: ${JSON.stringify(
470
+ data
471
+ )} for query: ${query}`
472
+ )
473
+ );
474
+ } else {
475
+ callback(callbackResolve, callbackReject, data.data);
476
+ }
477
+ },
478
+ error: (error) => {
479
+ if (timer) {
480
+ clearTimeout(timer);
481
+ }
482
+ reject(error);
483
+ },
484
+ complete: () => {
485
+ return;
486
+ }
487
+ }
488
+ );
489
+ if (typeof timeoutInMs === "number") {
490
+ timer = setTimeout(() => {
491
+ unsubscribe();
492
+ reject(
493
+ new Error(
494
+ `Timeout expired for graphQL subscribe query: ${query} with timeout: ${timeoutInMs}`
495
+ )
496
+ );
497
+ }, timeoutInMs);
498
+ }
499
+ });
500
+ }
355
501
 
356
502
  // src/features/analysis/graphql/types.ts
357
503
  import { z as z2 } from "zod";
@@ -421,9 +567,25 @@ var DigestVulnerabilityReportZ = z2.object({
421
567
  vulnerabilityReportId: z2.string()
422
568
  })
423
569
  });
570
+ var AnalysisStateZ = z2.enum([
571
+ "Created",
572
+ "Deleted",
573
+ "Digested",
574
+ "Expired",
575
+ "Failed",
576
+ "Finished",
577
+ "Initialized",
578
+ "Requested"
579
+ ]);
424
580
  var GetFixReportZ = z2.object({
425
581
  fixReport_by_pk: z2.object({
426
- state: z2.string()
582
+ state: AnalysisStateZ
583
+ })
584
+ });
585
+ var GetFixReportSubscriptionZ = z2.object({
586
+ analysis: z2.object({
587
+ id: z2.string(),
588
+ state: AnalysisStateZ
427
589
  })
428
590
  });
429
591
  var GetVulnerabilityReportPathsZ = z2.object({
@@ -433,6 +595,55 @@ var GetVulnerabilityReportPathsZ = z2.object({
433
595
  })
434
596
  )
435
597
  });
598
+ var CreateUpdateFixReportMutationZ = z2.object({
599
+ submitVulnerabilityReport: z2.object({
600
+ __typename: z2.literal("VulnerabilityReport"),
601
+ vulnerabilityReportId: z2.string(),
602
+ fixReportId: z2.string()
603
+ })
604
+ });
605
+ var GetAnalysisQueryZ = z2.object({
606
+ analysis: z2.object({
607
+ id: z2.string(),
608
+ state: z2.string(),
609
+ repo: z2.object({
610
+ commitSha: z2.string(),
611
+ pullRequest: z2.number()
612
+ }),
613
+ fixes: z2.array(
614
+ z2.object({
615
+ id: z2.string(),
616
+ issueType: z2.string(),
617
+ vulnerabilityReportIssues: z2.array(
618
+ z2.object({
619
+ issueLanguage: z2.string(),
620
+ state: z2.string(),
621
+ issueType: z2.string(),
622
+ vendorIssueId: z2.string()
623
+ })
624
+ )
625
+ })
626
+ ),
627
+ vulnerabilityReport: z2.object({
628
+ projectId: z2.string(),
629
+ project: z2.object({
630
+ organizationId: z2.string()
631
+ }),
632
+ file: z2.object({
633
+ signedFile: z2.object({
634
+ url: z2.string()
635
+ })
636
+ })
637
+ })
638
+ })
639
+ });
640
+ var GetFixQueryZ = z2.object({
641
+ fix_by_pk: z2.object({
642
+ patchAndQuestions: z2.object({
643
+ patch: z2.string()
644
+ })
645
+ })
646
+ });
436
647
 
437
648
  // src/features/analysis/graphql/gql.ts
438
649
  var debug3 = Debug3("mobbdev:gql");
@@ -441,7 +652,9 @@ var REPORT_STATE_CHECK_DELAY = 5 * 1e3;
441
652
  var GQLClient = class {
442
653
  constructor(args) {
443
654
  __publicField(this, "_client");
655
+ __publicField(this, "_apiKey");
444
656
  const { apiKey } = args;
657
+ this._apiKey = apiKey;
445
658
  debug3(`init with apiKey ${apiKey}`);
446
659
  this._client = new GraphQLClient(API_URL, {
447
660
  headers: { [API_KEY_HEADER_NAME]: apiKey || "" },
@@ -539,22 +752,26 @@ var GQLClient = class {
539
752
  );
540
753
  return DigestVulnerabilityReportZ.parse(res).digestVulnerabilityReport;
541
754
  }
542
- async submitVulnerabilityReport({
543
- fixReportId,
544
- repoUrl,
545
- reference,
546
- projectId,
547
- sha,
548
- vulnerabilityReportFileName
549
- }) {
550
- await this._client.request(SUBMIT_VULNERABILITY_REPORT, {
755
+ async submitVulnerabilityReport(params) {
756
+ const {
757
+ fixReportId,
758
+ repoUrl,
759
+ reference,
760
+ projectId,
761
+ sha,
762
+ vulnerabilityReportFileName,
763
+ pullRequest
764
+ } = params;
765
+ const res = await this._client.request(SUBMIT_VULNERABILITY_REPORT, {
551
766
  fixReportId,
552
767
  repoUrl,
553
768
  reference,
554
769
  vulnerabilityReportFileName,
555
770
  projectId,
771
+ pullRequest,
556
772
  sha: sha || ""
557
773
  });
774
+ return CreateUpdateFixReportMutationZ.parse(res);
558
775
  }
559
776
  async getFixReportState(fixReportId) {
560
777
  const res = await this._client.request(
@@ -588,957 +805,682 @@ var GQLClient = class {
588
805
  res
589
806
  ).vulnerability_report_path.map((p) => p.path);
590
807
  }
808
+ async subscribeToAnalysis(params, callback) {
809
+ return subscribe(
810
+ SUBSCRIBE_TO_ANALYSIS,
811
+ params,
812
+ async (resolve, reject, data) => {
813
+ if (data.analysis.state === "Failed") {
814
+ reject(data);
815
+ throw new Error(`Analysis failed with id: ${data.analysis.id}`);
816
+ }
817
+ if (data.analysis?.state === "Finished") {
818
+ await callback(data.analysis.id);
819
+ resolve(data);
820
+ }
821
+ },
822
+ {
823
+ apiKey: this._apiKey
824
+ }
825
+ );
826
+ }
827
+ async getAnalysis(analysisId) {
828
+ const res = await this._client.request(GET_ANALYSIS, {
829
+ analysisId
830
+ });
831
+ return GetAnalysisQueryZ.parse(res);
832
+ }
833
+ async getFix(fixId) {
834
+ const res = await this._client.request(GET_FIX, {
835
+ fixId
836
+ });
837
+ return GetFixQueryZ.parse(res);
838
+ }
591
839
  };
592
840
 
593
- // src/features/analysis/pack.ts
594
- import fs from "node:fs";
595
- import path3 from "node:path";
596
- import AdmZip from "adm-zip";
841
+ // src/features/analysis/handle_finished_analysis.ts
597
842
  import Debug4 from "debug";
598
- import { globby } from "globby";
599
- import { isBinary } from "istextorbinary";
600
- var debug4 = Debug4("mobbdev:pack");
601
- var MAX_FILE_SIZE = 1024 * 1024 * 5;
602
- function endsWithAny(str, suffixes) {
603
- return suffixes.some(function(suffix) {
604
- return str.endsWith(suffix);
605
- });
843
+ import { z as z7 } from "zod";
844
+
845
+ // src/features/analysis/scm/gitlab.ts
846
+ import querystring from "node:querystring";
847
+ import { Gitlab } from "@gitbeaker/rest";
848
+ import { z as z5 } from "zod";
849
+
850
+ // src/features/analysis/scm/scm.ts
851
+ import { Octokit as Octokit2 } from "@octokit/core";
852
+
853
+ // src/features/analysis/scm/github/github.ts
854
+ import { RequestError } from "@octokit/request-error";
855
+ import { Octokit } from "octokit";
856
+ import { z as z3 } from "zod";
857
+
858
+ // src/features/analysis/scm/urlParser.ts
859
+ var pathnameParsingMap = {
860
+ "gitlab.com": (pathname) => {
861
+ if (pathname.length < 2)
862
+ return null;
863
+ return {
864
+ organization: pathname[0],
865
+ repoName: pathname[pathname.length - 1]
866
+ };
867
+ },
868
+ "github.com": (pathname) => {
869
+ if (pathname.length !== 2)
870
+ return null;
871
+ return {
872
+ organization: pathname[0],
873
+ repoName: pathname[1]
874
+ };
875
+ }
876
+ };
877
+ var NAME_REGEX = /[a-z0-9\-_.+]+/i;
878
+ var parseScmURL = (scmURL) => {
879
+ try {
880
+ const url = new URL(scmURL);
881
+ const hostname = url.hostname.toLowerCase();
882
+ if (!(hostname in pathnameParsingMap))
883
+ return null;
884
+ const projectPath = url.pathname.substring(1).replace(/.git$/i, "");
885
+ const repo = pathnameParsingMap[hostname](
886
+ projectPath.split("/")
887
+ );
888
+ if (!repo)
889
+ return null;
890
+ const { organization, repoName } = repo;
891
+ if (!organization || !repoName)
892
+ return null;
893
+ if (!organization.match(NAME_REGEX) || !repoName.match(NAME_REGEX))
894
+ return null;
895
+ return {
896
+ hostname: url.hostname,
897
+ organization,
898
+ projectPath,
899
+ repoName
900
+ };
901
+ } catch (e) {
902
+ return null;
903
+ }
904
+ };
905
+
906
+ // src/features/analysis/scm/github/github.ts
907
+ function removeTrailingSlash(str) {
908
+ return str.trim().replace(/\/+$/, "");
606
909
  }
607
- async function pack(srcDirPath, vulnFiles) {
608
- debug4("pack folder %s", srcDirPath);
609
- const filepaths = await globby("**", {
610
- gitignore: true,
611
- onlyFiles: true,
612
- cwd: srcDirPath,
613
- followSymbolicLinks: false
614
- });
615
- debug4("files found %d", filepaths.length);
616
- const zip = new AdmZip();
617
- debug4("compressing files");
618
- for (const filepath of filepaths) {
619
- const absFilepath = path3.join(srcDirPath, filepath.toString());
620
- if (!endsWithAny(
621
- absFilepath.toString().replaceAll(path3.win32.sep, path3.posix.sep),
622
- vulnFiles
623
- )) {
624
- debug4("ignoring %s because it is not a vulnerability file", filepath);
625
- continue;
910
+ var EnvVariablesZod = z3.object({
911
+ GITHUB_API_TOKEN: z3.string().optional()
912
+ });
913
+ var { GITHUB_API_TOKEN } = EnvVariablesZod.parse(process.env);
914
+ var GetBlameDocument = `
915
+ query GetBlame(
916
+ $owner: String!
917
+ $repo: String!
918
+ $ref: String!
919
+ $path: String!
920
+ ) {
921
+ repository(name: $repo, owner: $owner) {
922
+ # branch name
923
+ object(expression: $ref) {
924
+ # cast Target to a Commit
925
+ ... on Commit {
926
+ # full repo-relative path to blame file
927
+ blame(path: $path) {
928
+ ranges {
929
+ commit {
930
+ author {
931
+ user {
932
+ name
933
+ login
934
+ }
935
+ }
936
+ authoredDate
937
+ }
938
+ startingLine
939
+ endingLine
940
+ age
941
+ }
942
+ }
943
+ }
944
+
945
+ }
946
+ }
947
+ }
948
+ `;
949
+ function getOktoKit(options) {
950
+ const token = options?.githubAuthToken ?? GITHUB_API_TOKEN ?? "";
951
+ return new Octokit({ auth: token });
952
+ }
953
+ async function githubValidateParams(url, accessToken) {
954
+ try {
955
+ const oktoKit = getOktoKit({ githubAuthToken: accessToken });
956
+ if (accessToken) {
957
+ await oktoKit.rest.users.getAuthenticated();
626
958
  }
627
- if (fs.lstatSync(absFilepath).size > MAX_FILE_SIZE) {
628
- debug4("ignoring %s because the size is > 5MB", filepath);
629
- continue;
959
+ if (url) {
960
+ const { owner, repo } = parseOwnerAndRepo(url);
961
+ await oktoKit.rest.repos.get({ repo, owner });
630
962
  }
631
- const data = fs.readFileSync(absFilepath);
632
- if (isBinary(null, data)) {
633
- debug4("ignoring %s because is seems to be a binary file", filepath);
634
- continue;
963
+ } catch (e) {
964
+ const error = e;
965
+ const code = error.status || error.statusCode || error.response?.status || error.response?.statusCode || error.response?.code;
966
+ if (code === 401 || code === 403) {
967
+ throw new InvalidAccessTokenError(`invalid github access token`);
635
968
  }
636
- zip.addFile(filepath.toString(), data);
969
+ if (code === 404) {
970
+ throw new InvalidRepoUrlError(`invalid github repo Url ${url}`);
971
+ }
972
+ throw e;
637
973
  }
638
- debug4("get zip file buffer");
639
- return zip.toBuffer();
640
974
  }
641
-
642
- // src/features/analysis/prompts.ts
643
- import inquirer from "inquirer";
644
- import { createSpinner } from "nanospinner";
645
- var scannerChoices = [
646
- { name: "Snyk", value: SCANNERS.Snyk },
647
- { name: "Checkmarx", value: SCANNERS.Checkmarx },
648
- { name: "Codeql", value: SCANNERS.Codeql },
649
- { name: "Fortify", value: SCANNERS.Fortify }
650
- ];
651
- async function choseScanner() {
652
- const { scanner } = await inquirer.prompt({
653
- name: "scanner",
654
- message: "Choose a scanner you wish to use to scan your code",
655
- type: "list",
656
- choices: scannerChoices
657
- });
658
- return scanner;
659
- }
660
- async function tryCheckmarxConfiguarationAgain() {
661
- console.log(
662
- "\u{1F513} Oops, seems like checkmarx does not accept the current configuration"
663
- );
664
- const { confirmCheckmarxRetryConfigrations } = await inquirer.prompt({
665
- name: "confirmCheckmarxRetryConfigrations",
666
- type: "confirm",
667
- message: "Would like to try to configure them again? ",
668
- default: true
669
- });
670
- return confirmCheckmarxRetryConfigrations;
975
+ async function getGithubUsername(accessToken) {
976
+ const oktoKit = getOktoKit({ githubAuthToken: accessToken });
977
+ const res = await oktoKit.rest.users.getAuthenticated();
978
+ return res.data.login;
671
979
  }
672
- async function startCheckmarxConfigationPrompt() {
673
- const checkmarxConfigreSpinner = createSpinner(
674
- "\u{1F513} Checkmarx needs to be configured before we start, press any key to continue"
675
- ).start();
676
- await keypress();
677
- checkmarxConfigreSpinner.success();
980
+ async function getGithubIsUserCollaborator(username, accessToken, repoUrl) {
981
+ try {
982
+ const { owner, repo } = parseOwnerAndRepo(repoUrl);
983
+ const oktoKit = getOktoKit({ githubAuthToken: accessToken });
984
+ const res = await oktoKit.rest.repos.checkCollaborator({
985
+ owner,
986
+ repo,
987
+ username
988
+ });
989
+ if (res.status === 204) {
990
+ return true;
991
+ }
992
+ } catch (e) {
993
+ return false;
994
+ }
995
+ return false;
678
996
  }
679
- async function scmIntegrationPrompt(scmName) {
680
- const answers = await inquirer.prompt({
681
- name: "scmConfirm",
682
- type: "confirm",
683
- message: `It seems we don't have access to the repo, do you want to grant access to your ${scmName} account?`,
684
- default: true
997
+ async function getGithubPullRequestStatus(accessToken, repoUrl, prNumber) {
998
+ const { owner, repo } = parseOwnerAndRepo(repoUrl);
999
+ const oktoKit = getOktoKit({ githubAuthToken: accessToken });
1000
+ const res = await oktoKit.rest.pulls.get({
1001
+ owner,
1002
+ repo,
1003
+ pull_number: prNumber
685
1004
  });
686
- return answers.scmConfirm;
1005
+ if (res.data.merged) {
1006
+ return "merged";
1007
+ }
1008
+ if (res.data.draft) {
1009
+ return "draft";
1010
+ }
1011
+ return res.data.state;
687
1012
  }
688
- async function mobbAnalysisPrompt() {
689
- const spinner = createSpinner().start();
690
- spinner.update({ text: "Hit any key to view available fixes" });
691
- await keypress();
692
- return spinner.success();
1013
+ async function getGithubIsRemoteBranch(accessToken, repoUrl, branch) {
1014
+ const { owner, repo } = parseOwnerAndRepo(repoUrl);
1015
+ const oktoKit = getOktoKit({ githubAuthToken: accessToken });
1016
+ try {
1017
+ const res = await oktoKit.rest.repos.getBranch({
1018
+ owner,
1019
+ repo,
1020
+ branch
1021
+ });
1022
+ return branch === res.data.name;
1023
+ } catch (e) {
1024
+ return false;
1025
+ }
693
1026
  }
694
- async function snykArticlePrompt() {
695
- const { snykArticleConfirm } = await inquirer.prompt({
696
- name: "snykArticleConfirm",
697
- type: "confirm",
698
- message: "Do you want to be taken to the relevant Snyk's online article?",
699
- default: true
700
- });
701
- return snykArticleConfirm;
1027
+ async function getGithubRepoList(accessToken) {
1028
+ const oktoKit = getOktoKit({ githubAuthToken: accessToken });
1029
+ try {
1030
+ const githubRepos = await getRepos(oktoKit);
1031
+ return githubRepos.map(
1032
+ (repo) => {
1033
+ const repoLanguages = [];
1034
+ if (repo.language) {
1035
+ repoLanguages.push(repo.language);
1036
+ }
1037
+ return {
1038
+ repoName: repo.name,
1039
+ repoUrl: repo.html_url,
1040
+ repoOwner: repo.owner.login,
1041
+ repoLanguages,
1042
+ repoIsPublic: !repo.private,
1043
+ repoUpdatedAt: repo.updated_at
1044
+ };
1045
+ }
1046
+ );
1047
+ } catch (e) {
1048
+ if (e instanceof RequestError && e.status === 401) {
1049
+ return [];
1050
+ }
1051
+ if (e instanceof RequestError && e.status === 404) {
1052
+ return [];
1053
+ }
1054
+ throw e;
1055
+ }
702
1056
  }
703
-
704
- // src/features/analysis/scanners/checkmarx.ts
705
- import { createRequire } from "node:module";
706
-
707
- // src/post_install/constants.mjs
708
- var cxOperatingSystemSupportMessage = `Your operating system does not support checkmarx.
709
- You can see the list of supported operating systems here: https://github.com/Checkmarx/ast-cli#releases`;
710
-
711
- // src/utils/child_process.ts
712
- import cp from "node:child_process";
713
- import Debug5 from "debug";
714
- import * as process2 from "process";
715
- import supportsColor from "supports-color";
716
- var { stdout: stdout2 } = supportsColor;
717
- function createFork({ args, processPath, name }, options) {
718
- const child = cp.fork(processPath, args, {
719
- stdio: ["inherit", "pipe", "pipe", "ipc"],
720
- env: { FORCE_COLOR: stdout2 ? "1" : "0" }
1057
+ async function getGithubBranchList(accessToken, repoUrl) {
1058
+ const { owner, repo } = parseOwnerAndRepo(repoUrl);
1059
+ const oktoKit = getOktoKit({ githubAuthToken: accessToken });
1060
+ const res = await oktoKit.rest.repos.listBranches({
1061
+ owner,
1062
+ repo,
1063
+ per_page: 1e3,
1064
+ page: 1
721
1065
  });
722
- return createChildProcess({ childProcess: child, name }, options);
1066
+ return res.data.map((branch) => branch.name);
723
1067
  }
724
- function createSpwan({ args, processPath, name }, options) {
725
- const child = cp.spawn(processPath, args, {
726
- stdio: ["inherit", "pipe", "pipe", "ipc"],
727
- env: { FORCE_COLOR: stdout2 ? "1" : "0" }
1068
+ async function createPullRequest(options) {
1069
+ const { owner, repo } = parseOwnerAndRepo(options.repoUrl);
1070
+ const oktoKit = getOktoKit({ githubAuthToken: options.accessToken });
1071
+ const res = await oktoKit.rest.pulls.create({
1072
+ owner,
1073
+ repo,
1074
+ title: options.title,
1075
+ body: options.body,
1076
+ head: options.sourceBranchName,
1077
+ base: options.targetBranchName,
1078
+ draft: false,
1079
+ maintainer_can_modify: true
728
1080
  });
729
- return createChildProcess({ childProcess: child, name }, options);
1081
+ return res.data.number;
730
1082
  }
731
- function createChildProcess({ childProcess, name }, options) {
732
- const debug9 = Debug5(`mobbdev:${name}`);
733
- const { display } = options;
734
- return new Promise((resolve, reject) => {
735
- let out = "";
736
- const onData = (chunk) => {
737
- debug9(`chunk received from ${name} std ${chunk}`);
738
- out += chunk;
739
- };
740
- if (!childProcess || !childProcess?.stdout || !childProcess?.stderr) {
741
- debug9(`unable to fork ${name}`);
742
- reject(new Error(`unable to fork ${name}`));
743
- }
744
- childProcess.stdout?.on("data", onData);
745
- childProcess.stderr?.on("data", onData);
746
- if (display) {
747
- childProcess.stdout?.pipe(process2.stdout);
748
- childProcess.stderr?.pipe(process2.stderr);
1083
+ async function getRepos(oktoKit) {
1084
+ const res = await oktoKit.request("GET /user/repos?sort=updated", {
1085
+ headers: {
1086
+ "X-GitHub-Api-Version": "2022-11-28",
1087
+ per_page: 100
749
1088
  }
750
- childProcess.on("exit", (code) => {
751
- debug9(`${name} exit code ${code}`);
752
- resolve({ message: out, code });
753
- });
754
- childProcess.on("error", (err) => {
755
- debug9(`${name} error %o`, err);
756
- reject(err);
757
- });
758
1089
  });
1090
+ return res.data;
759
1091
  }
760
-
761
- // src/features/analysis/scanners/checkmarx.ts
762
- import chalk2 from "chalk";
763
- import Debug6 from "debug";
764
- import { existsSync } from "fs";
765
- import { createSpinner as createSpinner2 } from "nanospinner";
766
- import { type } from "os";
767
- import path4 from "path";
768
- var debug5 = Debug6("mobbdev:checkmarx");
769
- var require2 = createRequire(import.meta.url);
770
- var getCheckmarxPath = () => {
771
- const os3 = type();
772
- const cxFileName = os3 === "Windows_NT" ? "cx.exe" : "cx";
1092
+ async function getGithubRepoDefaultBranch(repoUrl, options) {
1093
+ const oktoKit = getOktoKit(options);
1094
+ const { owner, repo } = parseOwnerAndRepo(repoUrl);
1095
+ return (await oktoKit.rest.repos.get({ repo, owner })).data.default_branch;
1096
+ }
1097
+ async function getGithubReferenceData({ ref, gitHubUrl }, options) {
1098
+ const { owner, repo } = parseOwnerAndRepo(gitHubUrl);
1099
+ let res;
773
1100
  try {
774
- return require2.resolve(`.bin/${cxFileName}`);
1101
+ const oktoKit = getOktoKit(options);
1102
+ res = await Promise.any([
1103
+ getBranch({ owner, repo, branch: ref }, oktoKit).then((result) => ({
1104
+ date: result.data.commit.commit.committer?.date ? new Date(result.data.commit.commit.committer?.date) : void 0,
1105
+ type: "BRANCH" /* BRANCH */,
1106
+ sha: result.data.commit.sha
1107
+ })),
1108
+ getCommit({ commitSha: ref, repo, owner }, oktoKit).then((commit) => ({
1109
+ date: new Date(commit.data.committer.date),
1110
+ type: "COMMIT" /* COMMIT */,
1111
+ sha: commit.data.sha
1112
+ })),
1113
+ getTagDate({ owner, repo, tag: ref }, oktoKit).then((data) => ({
1114
+ date: new Date(data.date),
1115
+ type: "TAG" /* TAG */,
1116
+ sha: data.sha
1117
+ }))
1118
+ ]);
1119
+ return res;
775
1120
  } catch (e) {
776
- throw new CliError(cxOperatingSystemSupportMessage);
1121
+ if (e instanceof AggregateError) {
1122
+ throw new RefNotFoundError(`ref: ${ref} does not exist`);
1123
+ }
1124
+ throw e;
777
1125
  }
778
- };
779
- var getCheckmarxCommandArgs = ({
780
- repoPath,
781
- branch,
782
- fileName,
783
- filePath,
784
- projectName
785
- }) => [
786
- "--project-name",
787
- projectName,
788
- "-s",
789
- repoPath,
790
- "--branch",
791
- branch,
792
- "--scan-types",
793
- "sast",
794
- "--output-path",
795
- filePath,
796
- "--output-name",
797
- fileName,
798
- "--report-format",
799
- "json"
800
- ];
801
- var VALIDATE_COMMAND = ["auth", "validate"];
802
- var CONFIGURE_COMMAND = ["configure"];
803
- var SCAN_COMMAND = ["scan", "create"];
804
- var CHECKMARX_SUCCESS_CODE = 0;
805
- function validateCheckmarxInstallation() {
806
- existsSync(getCheckmarxPath());
807
1126
  }
808
- async function forkCheckmarx(args, { display }) {
809
- debug5("fork checkmarx with args %o %s", args.join(" "), display);
810
- return createSpwan(
811
- { args, processPath: getCheckmarxPath(), name: "checkmarx" },
812
- { display }
813
- );
1127
+ async function getBranch({ branch, owner, repo }, oktoKit) {
1128
+ return oktoKit.rest.repos.getBranch({
1129
+ branch,
1130
+ owner,
1131
+ repo
1132
+ });
814
1133
  }
815
- async function getCheckmarxReport({ reportPath, repositoryRoot, branch, projectName }, { skipPrompts = false }) {
816
- debug5("get checkmarx report start %s %s", reportPath, repositoryRoot);
817
- const { code: loginCode } = await forkCheckmarx(VALIDATE_COMMAND, {
818
- display: false
1134
+ async function getTagDate({ tag, owner, repo }, oktoKit) {
1135
+ const refResponse = await oktoKit.rest.git.getRef({
1136
+ ref: `tags/${tag}`,
1137
+ owner,
1138
+ repo
819
1139
  });
820
- if (loginCode !== CHECKMARX_SUCCESS_CODE) {
821
- if (skipPrompts) {
822
- await throwCheckmarxConfigError();
823
- }
824
- await startCheckmarxConfigationPrompt();
825
- await validateCheckamxCredentials();
1140
+ const tagSha = refResponse.data.object.sha;
1141
+ if (refResponse.data.object.type === "commit") {
1142
+ const res2 = await oktoKit.rest.git.getCommit({
1143
+ commit_sha: tagSha,
1144
+ owner,
1145
+ repo
1146
+ });
1147
+ return {
1148
+ date: res2.data.committer.date,
1149
+ sha: res2.data.sha
1150
+ };
826
1151
  }
827
- const extension = path4.extname(reportPath);
828
- const filePath = path4.dirname(reportPath);
829
- const fileName = path4.basename(reportPath, extension);
830
- const checkmarxCommandArgs = getCheckmarxCommandArgs({
831
- repoPath: repositoryRoot,
832
- branch,
833
- filePath,
834
- fileName,
835
- projectName
1152
+ const res = await oktoKit.rest.git.getTag({
1153
+ tag_sha: tagSha,
1154
+ owner,
1155
+ repo
836
1156
  });
837
- console.log("\u280B \u{1F50D} Initiating Checkmarx Scan ");
838
- const { code: scanCode } = await forkCheckmarx(
839
- [...SCAN_COMMAND, ...checkmarxCommandArgs],
840
- {
841
- display: true
842
- }
843
- );
844
- if (scanCode !== CHECKMARX_SUCCESS_CODE) {
845
- createSpinner2("\u{1F50D} Something went wrong with the checkmarx scan").start().error();
846
- throw new CliError();
847
- }
848
- await createSpinner2("\u{1F50D} Checkmarx Scan completed").start().success();
849
- return true;
850
- }
851
- async function throwCheckmarxConfigError() {
852
- await createSpinner2("\u{1F513} Checkmarx is not configued correctly").start().error();
853
- throw new CliError(
854
- `Checkmarx is not configued correctly
855
- you can configure it by using the ${chalk2.bold(
856
- "cx configure"
857
- )} command`
858
- );
1157
+ return {
1158
+ date: res.data.tagger.date,
1159
+ sha: res.data.sha
1160
+ };
859
1161
  }
860
- async function validateCheckamxCredentials() {
861
- console.log(`
862
- Here's a suggestion for checkmarx configuation:
863
- ${chalk2.bold("AST Base URI:")} https://ast.checkmarx.net
864
- ${chalk2.bold("AST Base Auth URI (IAM):")} https://iam.checkmarx.net
865
- `);
866
- await forkCheckmarx(CONFIGURE_COMMAND, { display: true });
867
- const { code: loginCode } = await forkCheckmarx(VALIDATE_COMMAND, {
868
- display: false
1162
+ async function getCommit({
1163
+ commitSha,
1164
+ owner,
1165
+ repo
1166
+ }, oktoKit) {
1167
+ return oktoKit.rest.git.getCommit({
1168
+ repo,
1169
+ owner,
1170
+ commit_sha: commitSha
869
1171
  });
870
- if (loginCode !== CHECKMARX_SUCCESS_CODE) {
871
- const tryAgain = await tryCheckmarxConfiguarationAgain();
872
- if (!tryAgain) {
873
- await throwCheckmarxConfigError();
874
- }
875
- if (await tryCheckmarxConfiguarationAgain()) {
876
- validateCheckamxCredentials();
877
- }
878
- }
879
- await createSpinner2("\u{1F513} Checkmarx configured successfully!").start().success();
880
1172
  }
881
-
882
- // src/features/analysis/scanners/snyk.ts
883
- import { createRequire as createRequire2 } from "node:module";
884
- import chalk3 from "chalk";
885
- import Debug7 from "debug";
886
- import { createSpinner as createSpinner3 } from "nanospinner";
887
- import open from "open";
888
- var debug6 = Debug7("mobbdev:snyk");
889
- var require3 = createRequire2(import.meta.url);
890
- var SNYK_PATH = require3.resolve("snyk/bin/snyk");
891
- 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";
892
- debug6("snyk executable path %s", SNYK_PATH);
893
- async function forkSnyk(args, { display }) {
894
- debug6("fork snyk with args %o %s", args, display);
895
- return createFork(
896
- { args, processPath: SNYK_PATH, name: "checkmarx" },
897
- { display }
898
- );
1173
+ function parseOwnerAndRepo(gitHubUrl) {
1174
+ gitHubUrl = removeTrailingSlash(gitHubUrl);
1175
+ const parsingResult = parseScmURL(gitHubUrl);
1176
+ if (!parsingResult || parsingResult.hostname !== "github.com") {
1177
+ throw new InvalidUrlPatternError(`invalid github repo Url ${gitHubUrl}`);
1178
+ }
1179
+ const { organization, repoName } = parsingResult;
1180
+ if (!organization || !repoName) {
1181
+ throw new InvalidUrlPatternError(`invalid github repo Url ${gitHubUrl}`);
1182
+ }
1183
+ return { owner: organization, repo: repoName };
899
1184
  }
900
- async function getSnykReport(reportPath, repoRoot, { skipPrompts = false }) {
901
- debug6("get snyk report start %s %s", reportPath, repoRoot);
902
- const config3 = await forkSnyk(["config"], { display: false });
903
- const { message: configMessage } = config3;
904
- if (!configMessage.includes("api: ")) {
905
- const snykLoginSpinner = createSpinner3().start();
906
- if (!skipPrompts) {
907
- snykLoginSpinner.update({
908
- text: "\u{1F513} Login to Snyk is required, press any key to continue"
909
- });
910
- await keypress();
1185
+ async function queryGithubGraphql(query, variables, options) {
1186
+ const token = options?.githubAuthToken ?? GITHUB_API_TOKEN ?? "";
1187
+ const parameters = variables ?? {};
1188
+ const authorizationHeader = {
1189
+ headers: {
1190
+ authorization: `bearer ${token}`
911
1191
  }
912
- snykLoginSpinner.update({
913
- text: "\u{1F513} Waiting for Snyk login to complete"
1192
+ };
1193
+ try {
1194
+ const oktoKit = getOktoKit(options);
1195
+ const res = await oktoKit.graphql(query, {
1196
+ ...parameters,
1197
+ ...authorizationHeader
914
1198
  });
915
- debug6("no token in the config %s", config3);
916
- await forkSnyk(["auth"], { display: true });
917
- snykLoginSpinner.success({ text: "\u{1F513} Login to Snyk Successful" });
1199
+ return res;
1200
+ } catch (e) {
1201
+ if (e instanceof RequestError) {
1202
+ return null;
1203
+ }
1204
+ throw e;
918
1205
  }
919
- const snykSpinner = createSpinner3("\u{1F50D} Scanning your repo with Snyk ").start();
920
- const { message: scanOutput } = await forkSnyk(
921
- ["code", "test", `--sarif-file-output=${reportPath}`, repoRoot],
922
- { display: true }
1206
+ }
1207
+ async function getGithubBlameRanges({ ref, gitHubUrl, path: path8 }, options) {
1208
+ const { owner, repo } = parseOwnerAndRepo(gitHubUrl);
1209
+ const variables = {
1210
+ owner,
1211
+ repo,
1212
+ path: path8,
1213
+ ref
1214
+ };
1215
+ const res = await queryGithubGraphql(
1216
+ GetBlameDocument,
1217
+ variables,
1218
+ options
923
1219
  );
924
- if (scanOutput.includes(
925
- "Snyk Code is not supported for org: enable in Settings > Snyk Code"
926
- )) {
927
- debug6("snyk code is not enabled %s", scanOutput);
928
- snykSpinner.error({ text: "\u{1F50D} Snyk configuration needed" });
929
- const answer = await snykArticlePrompt();
930
- debug6("answer %s", answer);
931
- if (answer) {
932
- debug6("opening the browser");
933
- await open(SNYK_ARTICLE_URL);
934
- }
935
- console.log(
936
- chalk3.bgBlue(
937
- "\nPlease enable Snyk Code in your Snyk account and try again."
938
- )
939
- );
940
- throw Error("snyk is not enbabled");
1220
+ if (!res?.repository?.object?.blame?.ranges) {
1221
+ return [];
941
1222
  }
942
- snykSpinner.success({ text: "\u{1F50D} Snyk code scan completed" });
943
- return true;
1223
+ return res.repository.object.blame.ranges.map((range) => ({
1224
+ startingLine: range.startingLine,
1225
+ endingLine: range.endingLine,
1226
+ email: range.commit.author.user.email,
1227
+ name: range.commit.author.user.name,
1228
+ login: range.commit.author.user.login
1229
+ }));
944
1230
  }
945
1231
 
946
- // src/features/analysis/scm/gitlab.ts
947
- import querystring from "node:querystring";
948
- import { Gitlab } from "@gitbeaker/rest";
949
- import { z as z5 } from "zod";
1232
+ // src/features/analysis/scm/github/consts.ts
1233
+ var POST_COMMENT_PATH = "POST /repos/{owner}/{repo}/pulls/{pull_number}/comments";
1234
+ var DELETE_COMMENT_PATH = "DELETE /repos/{owner}/{repo}/pulls/comments/{comment_id}";
1235
+ var UPDATE_COMMENT_PATH = "PATCH /repos/{owner}/{repo}/pulls/comments/{comment_id}";
1236
+ var GET_PR_COMMENTS_PATH = "GET /repos/{owner}/{repo}/pulls/comments";
950
1237
 
951
- // src/features/analysis/scm/github.ts
952
- import { RequestError } from "@octokit/request-error";
953
- import { Octokit } from "octokit";
954
- import { z as z3 } from "zod";
1238
+ // src/features/analysis/scm/github/github-v2.ts
1239
+ function postPrComment(client, params) {
1240
+ return client.request(POST_COMMENT_PATH, params);
1241
+ }
1242
+ function updatePrComment(client, params) {
1243
+ return client.request(UPDATE_COMMENT_PATH, params);
1244
+ }
1245
+ function getPrComments(client, params) {
1246
+ return client.request(GET_PR_COMMENTS_PATH, params);
1247
+ }
1248
+ function deleteComment(client, params) {
1249
+ return client.request(DELETE_COMMENT_PATH, params);
1250
+ }
955
1251
 
956
- // src/features/analysis/scm/urlParser.ts
957
- var pathnameParsingMap = {
958
- "gitlab.com": (pathname) => {
959
- if (pathname.length < 2)
960
- return null;
961
- return {
962
- organization: pathname[0],
963
- repoName: pathname[pathname.length - 1]
964
- };
965
- },
966
- "github.com": (pathname) => {
967
- if (pathname.length !== 2)
968
- return null;
969
- return {
970
- organization: pathname[0],
971
- repoName: pathname[1]
972
- };
1252
+ // src/features/analysis/scm/scmSubmit.ts
1253
+ import fs from "node:fs/promises";
1254
+ import os from "os";
1255
+ import path3 from "path";
1256
+ import { simpleGit as simpleGit2 } from "simple-git";
1257
+ import tmp from "tmp";
1258
+ import { z as z4 } from "zod";
1259
+ var isValidBranchName = async (branchName) => {
1260
+ const git = simpleGit2();
1261
+ try {
1262
+ const res = await git.raw(["check-ref-format", "--branch", branchName]);
1263
+ if (res) {
1264
+ return true;
1265
+ }
1266
+ return false;
1267
+ } catch (e) {
1268
+ return false;
973
1269
  }
974
1270
  };
975
- var NAME_REGEX = /[a-z0-9\-_.+]+/i;
976
- var parseScmURL = (scmURL) => {
977
- try {
978
- const url = new URL(scmURL);
979
- const hostname = url.hostname.toLowerCase();
980
- if (!(hostname in pathnameParsingMap))
981
- return null;
982
- const projectPath = url.pathname.substring(1).replace(/.git$/i, "");
983
- const repo = pathnameParsingMap[hostname](
984
- projectPath.split("/")
985
- );
986
- if (!repo)
987
- return null;
988
- const { organization, repoName } = repo;
989
- if (!organization || !repoName)
990
- return null;
991
- if (!organization.match(NAME_REGEX) || !repoName.match(NAME_REGEX))
992
- return null;
993
- return {
994
- hostname: url.hostname,
995
- organization,
996
- projectPath,
997
- repoName
998
- };
999
- } catch (e) {
1000
- return null;
1001
- }
1271
+ var BaseSubmitToScmMessageZ = z4.object({
1272
+ submitFixRequestId: z4.string().uuid(),
1273
+ fixes: z4.array(
1274
+ z4.object({
1275
+ fixId: z4.string().uuid(),
1276
+ diff: z4.string()
1277
+ })
1278
+ ),
1279
+ commitHash: z4.string(),
1280
+ repoUrl: z4.string()
1281
+ });
1282
+ var submitToScmMessageType = {
1283
+ commitToSameBranch: "commitToSameBranch",
1284
+ submitFixesForDifferentBranch: "submitFixesForDifferentBranch"
1002
1285
  };
1003
-
1004
- // src/features/analysis/scm/github.ts
1005
- function removeTrailingSlash(str) {
1006
- return str.trim().replace(/\/+$/, "");
1007
- }
1008
- var EnvVariablesZod = z3.object({
1009
- GITHUB_API_TOKEN: z3.string().optional()
1286
+ var CommitToSameBranchParamsZ = BaseSubmitToScmMessageZ.merge(
1287
+ z4.object({
1288
+ type: z4.literal(submitToScmMessageType.commitToSameBranch),
1289
+ branch: z4.string()
1290
+ })
1291
+ );
1292
+ var SubmitFixesToDifferentBranchParamsZ = z4.object({
1293
+ type: z4.literal(submitToScmMessageType.submitFixesForDifferentBranch),
1294
+ submitBranch: z4.string(),
1295
+ baseBranch: z4.string()
1296
+ }).merge(BaseSubmitToScmMessageZ);
1297
+ var SubmitFixesMessageZ = z4.union([
1298
+ CommitToSameBranchParamsZ,
1299
+ SubmitFixesToDifferentBranchParamsZ
1300
+ ]);
1301
+ var FixResponseArrayZ = z4.array(
1302
+ z4.object({
1303
+ fixId: z4.string().uuid()
1304
+ })
1305
+ );
1306
+ var SubmitFixesResponseMessageZ = z4.object({
1307
+ type: z4.nativeEnum(submitToScmMessageType),
1308
+ submitFixRequestId: z4.string().uuid(),
1309
+ submitBranches: z4.array(
1310
+ z4.object({
1311
+ branchName: z4.string(),
1312
+ fixes: FixResponseArrayZ
1313
+ })
1314
+ ),
1315
+ error: z4.object({
1316
+ type: z4.enum([
1317
+ "InitialRepoAccessError",
1318
+ "PushBranchError",
1319
+ "UnknownError"
1320
+ ]),
1321
+ info: z4.object({
1322
+ message: z4.string(),
1323
+ pushBranchName: z4.string().optional()
1324
+ })
1325
+ }).optional()
1010
1326
  });
1011
- var { GITHUB_API_TOKEN } = EnvVariablesZod.parse(process.env);
1012
- var GetBlameDocument = `
1013
- query GetBlame(
1014
- $owner: String!
1015
- $repo: String!
1016
- $ref: String!
1017
- $path: String!
1018
- ) {
1019
- repository(name: $repo, owner: $owner) {
1020
- # branch name
1021
- object(expression: $ref) {
1022
- # cast Target to a Commit
1023
- ... on Commit {
1024
- # full repo-relative path to blame file
1025
- blame(path: $path) {
1026
- ranges {
1027
- commit {
1028
- author {
1029
- user {
1030
- name
1031
- login
1032
- }
1033
- }
1034
- authoredDate
1035
- }
1036
- startingLine
1037
- endingLine
1038
- age
1039
- }
1040
- }
1041
- }
1042
-
1043
- }
1044
- }
1045
- }
1046
- `;
1047
- function getOktoKit(options) {
1048
- const token = options?.githubAuthToken ?? GITHUB_API_TOKEN ?? "";
1049
- return new Octokit({ auth: token });
1050
- }
1051
- async function githubValidateParams(url, accessToken) {
1052
- try {
1053
- const oktoKit = getOktoKit({ githubAuthToken: accessToken });
1054
- if (accessToken) {
1055
- await oktoKit.rest.users.getAuthenticated();
1056
- }
1057
- if (url) {
1058
- const { owner, repo } = parseOwnerAndRepo(url);
1059
- await oktoKit.rest.repos.get({ repo, owner });
1060
- }
1061
- } catch (e) {
1062
- const error = e;
1063
- const code = error.status || error.statusCode || error.response?.status || error.response?.statusCode || error.response?.code;
1064
- if (code === 401 || code === 403) {
1065
- throw new InvalidAccessTokenError(`invalid github access token`);
1066
- }
1067
- if (code === 404) {
1068
- throw new InvalidRepoUrlError(`invalid github repo Url ${url}`);
1069
- }
1070
- throw e;
1327
+ var FixesZ = z4.array(z4.object({ fixId: z4.string(), diff: z4.string() })).nonempty();
1328
+
1329
+ // src/features/analysis/scm/scm.ts
1330
+ function getScmLibTypeFromUrl(url) {
1331
+ if (!url) {
1332
+ return void 0;
1071
1333
  }
1334
+ if (url.toLowerCase().startsWith("https://gitlab.com/")) {
1335
+ return "GITLAB" /* GITLAB */;
1336
+ }
1337
+ if (url.toLowerCase().startsWith("https://github.com/")) {
1338
+ return "GITHUB" /* GITHUB */;
1339
+ }
1340
+ return void 0;
1072
1341
  }
1073
- async function getGithubUsername(accessToken) {
1074
- const oktoKit = getOktoKit({ githubAuthToken: accessToken });
1075
- const res = await oktoKit.rest.users.getAuthenticated();
1076
- return res.data.login;
1077
- }
1078
- async function getGithubIsUserCollaborator(username, accessToken, repoUrl) {
1342
+ async function scmCanReachRepo({
1343
+ repoUrl,
1344
+ githubToken,
1345
+ gitlabToken
1346
+ }) {
1079
1347
  try {
1080
- const { owner, repo } = parseOwnerAndRepo(repoUrl);
1081
- const oktoKit = getOktoKit({ githubAuthToken: accessToken });
1082
- const res = await oktoKit.rest.repos.checkCollaborator({
1083
- owner,
1084
- repo,
1085
- username
1348
+ const scmLibType = getScmLibTypeFromUrl(repoUrl);
1349
+ await SCMLib.init({
1350
+ url: repoUrl,
1351
+ accessToken: scmLibType === "GITHUB" /* GITHUB */ ? githubToken : scmLibType === "GITLAB" /* GITLAB */ ? gitlabToken : "",
1352
+ scmType: scmLibType
1086
1353
  });
1087
- if (res.status === 204) {
1088
- return true;
1089
- }
1354
+ return true;
1090
1355
  } catch (e) {
1091
1356
  return false;
1092
1357
  }
1093
- return false;
1094
1358
  }
1095
- async function getGithubPullRequestStatus(accessToken, repoUrl, prNumber) {
1096
- const { owner, repo } = parseOwnerAndRepo(repoUrl);
1097
- const oktoKit = getOktoKit({ githubAuthToken: accessToken });
1098
- const res = await oktoKit.rest.pulls.get({
1099
- owner,
1100
- repo,
1101
- pull_number: prNumber
1102
- });
1103
- if (res.data.merged) {
1104
- return "merged";
1359
+ var InvalidRepoUrlError = class extends Error {
1360
+ constructor(m) {
1361
+ super(m);
1105
1362
  }
1106
- if (res.data.draft) {
1107
- return "draft";
1363
+ };
1364
+ var InvalidAccessTokenError = class extends Error {
1365
+ constructor(m) {
1366
+ super(m);
1108
1367
  }
1109
- return res.data.state;
1110
- }
1111
- async function getGithubIsRemoteBranch(accessToken, repoUrl, branch) {
1112
- const { owner, repo } = parseOwnerAndRepo(repoUrl);
1113
- const oktoKit = getOktoKit({ githubAuthToken: accessToken });
1114
- try {
1115
- const res = await oktoKit.rest.repos.getBranch({
1116
- owner,
1117
- repo,
1118
- branch
1119
- });
1120
- return branch === res.data.name;
1121
- } catch (e) {
1122
- return false;
1368
+ };
1369
+ var InvalidUrlPatternError = class extends Error {
1370
+ constructor(m) {
1371
+ super(m);
1123
1372
  }
1124
- }
1125
- async function getGithubRepoList(accessToken) {
1126
- const oktoKit = getOktoKit({ githubAuthToken: accessToken });
1127
- try {
1128
- const githubRepos = await getRepos(oktoKit);
1129
- return githubRepos.map(
1130
- (repo) => {
1131
- const repoLanguages = [];
1132
- if (repo.language) {
1133
- repoLanguages.push(repo.language);
1134
- }
1135
- return {
1136
- repoName: repo.name,
1137
- repoUrl: repo.html_url,
1138
- repoOwner: repo.owner.login,
1139
- repoLanguages,
1140
- repoIsPublic: !repo.private,
1141
- repoUpdatedAt: repo.updated_at
1142
- };
1143
- }
1144
- );
1145
- } catch (e) {
1146
- if (e instanceof RequestError && e.status === 401) {
1147
- return [];
1373
+ };
1374
+ var RefNotFoundError = class extends Error {
1375
+ constructor(m) {
1376
+ super(m);
1377
+ }
1378
+ };
1379
+ var RepoNoTokenAccessError = class extends Error {
1380
+ constructor(m) {
1381
+ super(m);
1382
+ }
1383
+ };
1384
+ var SCMLib = class {
1385
+ constructor(url, accessToken) {
1386
+ __publicField(this, "url");
1387
+ __publicField(this, "accessToken");
1388
+ this.accessToken = accessToken;
1389
+ this.url = url;
1390
+ }
1391
+ async getUrlWithCredentials() {
1392
+ if (!this.url) {
1393
+ console.error("no url for getUrlWithCredentials()");
1394
+ throw new Error("no url");
1148
1395
  }
1149
- if (e instanceof RequestError && e.status === 404) {
1150
- return [];
1396
+ const trimmedUrl = this.url.trim().replace(/\/$/, "");
1397
+ if (!this.accessToken) {
1398
+ return trimmedUrl;
1151
1399
  }
1152
- throw e;
1153
- }
1154
- }
1155
- async function getGithubBranchList(accessToken, repoUrl) {
1156
- const { owner, repo } = parseOwnerAndRepo(repoUrl);
1157
- const oktoKit = getOktoKit({ githubAuthToken: accessToken });
1158
- const res = await oktoKit.rest.repos.listBranches({
1159
- owner,
1160
- repo,
1161
- per_page: 1e3,
1162
- page: 1
1163
- });
1164
- return res.data.map((branch) => branch.name);
1165
- }
1166
- async function createPullRequest(options) {
1167
- const { owner, repo } = parseOwnerAndRepo(options.repoUrl);
1168
- const oktoKit = getOktoKit({ githubAuthToken: options.accessToken });
1169
- const res = await oktoKit.rest.pulls.create({
1170
- owner,
1171
- repo,
1172
- title: options.title,
1173
- body: options.body,
1174
- head: options.sourceBranchName,
1175
- base: options.targetBranchName,
1176
- draft: false,
1177
- maintainer_can_modify: true
1178
- });
1179
- return res.data.number;
1180
- }
1181
- async function getRepos(oktoKit) {
1182
- const res = await oktoKit.request("GET /user/repos?sort=updated", {
1183
- headers: {
1184
- "X-GitHub-Api-Version": "2022-11-28",
1185
- per_page: 100
1186
- }
1187
- });
1188
- return res.data;
1189
- }
1190
- async function getGithubRepoDefaultBranch(repoUrl, options) {
1191
- const oktoKit = getOktoKit(options);
1192
- const { owner, repo } = parseOwnerAndRepo(repoUrl);
1193
- return (await oktoKit.rest.repos.get({ repo, owner })).data.default_branch;
1194
- }
1195
- async function getGithubReferenceData({ ref, gitHubUrl }, options) {
1196
- const { owner, repo } = parseOwnerAndRepo(gitHubUrl);
1197
- let res;
1198
- try {
1199
- const oktoKit = getOktoKit(options);
1200
- res = await Promise.any([
1201
- getBranch({ owner, repo, branch: ref }, oktoKit).then((result) => ({
1202
- date: result.data.commit.commit.committer?.date ? new Date(result.data.commit.commit.committer?.date) : void 0,
1203
- type: "BRANCH" /* BRANCH */,
1204
- sha: result.data.commit.sha
1205
- })),
1206
- getCommit({ commitSha: ref, repo, owner }, oktoKit).then((commit) => ({
1207
- date: new Date(commit.data.committer.date),
1208
- type: "COMMIT" /* COMMIT */,
1209
- sha: commit.data.sha
1210
- })),
1211
- getTagDate({ owner, repo, tag: ref }, oktoKit).then((data) => ({
1212
- date: new Date(data.date),
1213
- type: "TAG" /* TAG */,
1214
- sha: data.sha
1215
- }))
1216
- ]);
1217
- return res;
1218
- } catch (e) {
1219
- if (e instanceof AggregateError) {
1220
- throw new RefNotFoundError(`ref: ${ref} does not exist`);
1400
+ const username = await this._getUsernameForAuthUrl();
1401
+ const is_http = trimmedUrl.toLowerCase().startsWith("http://");
1402
+ const is_https = trimmedUrl.toLowerCase().startsWith("https://");
1403
+ if (is_http) {
1404
+ return `http://${username}:${this.accessToken}@${trimmedUrl.toLowerCase().replace("http://", "")}`;
1405
+ } else if (is_https) {
1406
+ return `https://${username}:${this.accessToken}@${trimmedUrl.toLowerCase().replace("https://", "")}`;
1407
+ } else {
1408
+ console.error(`invalid scm url ${trimmedUrl}`);
1409
+ throw new Error(`invalid scm url ${trimmedUrl}`);
1221
1410
  }
1222
- throw e;
1223
1411
  }
1224
- }
1225
- async function getBranch({ branch, owner, repo }, oktoKit) {
1226
- return oktoKit.rest.repos.getBranch({
1227
- branch,
1228
- owner,
1229
- repo
1230
- });
1231
- }
1232
- async function getTagDate({ tag, owner, repo }, oktoKit) {
1233
- const refResponse = await oktoKit.rest.git.getRef({
1234
- ref: `tags/${tag}`,
1235
- owner,
1236
- repo
1237
- });
1238
- const tagSha = refResponse.data.object.sha;
1239
- if (refResponse.data.object.type === "commit") {
1240
- const res2 = await oktoKit.rest.git.getCommit({
1241
- commit_sha: tagSha,
1242
- owner,
1243
- repo
1244
- });
1245
- return {
1246
- date: res2.data.committer.date,
1247
- sha: res2.data.sha
1248
- };
1412
+ getAccessToken() {
1413
+ return this.accessToken || "";
1249
1414
  }
1250
- const res = await oktoKit.rest.git.getTag({
1251
- tag_sha: tagSha,
1252
- owner,
1253
- repo
1254
- });
1255
- return {
1256
- date: res.data.tagger.date,
1257
- sha: res.data.sha
1258
- };
1259
- }
1260
- async function getCommit({
1261
- commitSha,
1262
- owner,
1263
- repo
1264
- }, oktoKit) {
1265
- return oktoKit.rest.git.getCommit({
1266
- repo,
1267
- owner,
1268
- commit_sha: commitSha
1269
- });
1270
- }
1271
- function parseOwnerAndRepo(gitHubUrl) {
1272
- gitHubUrl = removeTrailingSlash(gitHubUrl);
1273
- const parsingResult = parseScmURL(gitHubUrl);
1274
- if (!parsingResult || parsingResult.hostname !== "github.com") {
1275
- throw new InvalidUrlPatternError(`invalid github repo Url ${gitHubUrl}`);
1415
+ getUrl() {
1416
+ return this.url;
1276
1417
  }
1277
- const { organization, repoName } = parsingResult;
1278
- if (!organization || !repoName) {
1279
- throw new InvalidUrlPatternError(`invalid github repo Url ${gitHubUrl}`);
1418
+ getName() {
1419
+ if (!this.url) {
1420
+ return "";
1421
+ }
1422
+ return this.url.split("/").at(-1) || "";
1280
1423
  }
1281
- return { owner: organization, repo: repoName };
1282
- }
1283
- async function queryGithubGraphql(query, variables, options) {
1284
- const token = options?.githubAuthToken ?? GITHUB_API_TOKEN ?? "";
1285
- const parameters = variables ?? {};
1286
- const authorizationHeader = {
1287
- headers: {
1288
- authorization: `bearer ${token}`
1424
+ static async getIsValidBranchName(branchName) {
1425
+ return isValidBranchName(branchName);
1426
+ }
1427
+ static async init({
1428
+ url,
1429
+ accessToken,
1430
+ scmType
1431
+ }) {
1432
+ let trimmedUrl = void 0;
1433
+ if (url) {
1434
+ trimmedUrl = url.trim().replace(/\/$/, "");
1289
1435
  }
1290
- };
1291
- try {
1292
- const oktoKit = getOktoKit(options);
1293
- const res = await oktoKit.graphql(query, {
1294
- ...parameters,
1295
- ...authorizationHeader
1296
- });
1297
- return res;
1298
- } catch (e) {
1299
- if (e instanceof RequestError) {
1300
- return null;
1436
+ try {
1437
+ if ("GITHUB" /* GITHUB */ === scmType) {
1438
+ const scm = new GithubSCMLib(trimmedUrl, accessToken);
1439
+ await scm.validateParams();
1440
+ return scm;
1441
+ }
1442
+ if ("GITLAB" /* GITLAB */ === scmType) {
1443
+ const scm = new GitlabSCMLib(trimmedUrl, accessToken);
1444
+ await scm.validateParams();
1445
+ return scm;
1446
+ }
1447
+ } catch (e) {
1448
+ if (e instanceof InvalidRepoUrlError && url) {
1449
+ throw new RepoNoTokenAccessError("no access to repo");
1450
+ }
1301
1451
  }
1302
- throw e;
1452
+ return new StubSCMLib(trimmedUrl);
1303
1453
  }
1304
- }
1305
- async function getGithubBlameRanges({ ref, gitHubUrl, path: path8 }, options) {
1306
- const { owner, repo } = parseOwnerAndRepo(gitHubUrl);
1307
- const variables = {
1308
- owner,
1309
- repo,
1310
- path: path8,
1311
- ref
1312
- };
1313
- const res = await queryGithubGraphql(
1314
- GetBlameDocument,
1315
- variables,
1316
- options
1317
- );
1318
- if (!res?.repository?.object?.blame?.ranges) {
1319
- return [];
1454
+ };
1455
+ var GitlabSCMLib = class extends SCMLib {
1456
+ async createSubmitRequest(targetBranchName, sourceBranchName, title, body) {
1457
+ if (!this.accessToken || !this.url) {
1458
+ console.error("no access token or no url");
1459
+ throw new Error("no access token or no url");
1460
+ }
1461
+ return String(
1462
+ await createMergeRequest({
1463
+ title,
1464
+ body,
1465
+ targetBranchName,
1466
+ sourceBranchName,
1467
+ repoUrl: this.url,
1468
+ accessToken: this.accessToken
1469
+ })
1470
+ );
1320
1471
  }
1321
- return res.repository.object.blame.ranges.map((range) => ({
1322
- startingLine: range.startingLine,
1323
- endingLine: range.endingLine,
1324
- email: range.commit.author.user.email,
1325
- name: range.commit.author.user.name,
1326
- login: range.commit.author.user.login
1327
- }));
1328
- }
1329
-
1330
- // src/features/analysis/scm/scmSubmit.ts
1331
- import fs2 from "node:fs/promises";
1332
- import os from "os";
1333
- import path5 from "path";
1334
- import { simpleGit as simpleGit2 } from "simple-git";
1335
- import { z as z4 } from "zod";
1336
- var isValidBranchName = async (branchName) => {
1337
- const git = simpleGit2();
1338
- try {
1339
- const res = await git.raw(["check-ref-format", "--branch", branchName]);
1340
- if (res) {
1341
- return true;
1472
+ async validateParams() {
1473
+ return gitlabValidateParams({
1474
+ url: this.url,
1475
+ accessToken: this.accessToken
1476
+ });
1477
+ }
1478
+ async getRepoList() {
1479
+ if (!this.accessToken) {
1480
+ console.error("no access token");
1481
+ throw new Error("no access token");
1342
1482
  }
1343
- return false;
1344
- } catch (e) {
1345
- return false;
1346
- }
1347
- };
1348
- var SubmitFixesMessageZ = z4.object({
1349
- submitFixRequestId: z4.string().uuid(),
1350
- fixes: z4.array(
1351
- z4.object({
1352
- fixId: z4.string().uuid(),
1353
- diff: z4.string()
1354
- })
1355
- ),
1356
- branchName: z4.string(),
1357
- commitHash: z4.string(),
1358
- targetBranch: z4.string(),
1359
- repoUrl: z4.string()
1360
- });
1361
- var FixResponseArrayZ = z4.array(
1362
- z4.object({
1363
- fixId: z4.string().uuid()
1364
- })
1365
- );
1366
- var SubmitFixesResponseMessageZ = z4.object({
1367
- submitFixRequestId: z4.string().uuid(),
1368
- submitBranches: z4.array(
1369
- z4.object({
1370
- branchName: z4.string(),
1371
- fixes: FixResponseArrayZ
1372
- })
1373
- ),
1374
- error: z4.object({
1375
- type: z4.enum([
1376
- "InitialRepoAccessError",
1377
- "PushBranchError",
1378
- "UnknownError"
1379
- ]),
1380
- info: z4.object({
1381
- message: z4.string(),
1382
- pushBranchName: z4.string().optional()
1383
- })
1384
- }).optional()
1385
- });
1386
-
1387
- // src/features/analysis/scm/scm.ts
1388
- function getScmLibTypeFromUrl(url) {
1389
- if (!url) {
1390
- return void 0;
1391
- }
1392
- if (url.toLowerCase().startsWith("https://gitlab.com/")) {
1393
- return "GITLAB" /* GITLAB */;
1394
- }
1395
- if (url.toLowerCase().startsWith("https://github.com/")) {
1396
- return "GITHUB" /* GITHUB */;
1397
- }
1398
- return void 0;
1399
- }
1400
- async function scmCanReachRepo({
1401
- repoUrl,
1402
- githubToken,
1403
- gitlabToken
1404
- }) {
1405
- try {
1406
- const scmLibType = getScmLibTypeFromUrl(repoUrl);
1407
- await SCMLib.init({
1408
- url: repoUrl,
1409
- accessToken: scmLibType === "GITHUB" /* GITHUB */ ? githubToken : scmLibType === "GITLAB" /* GITLAB */ ? gitlabToken : "",
1410
- scmType: scmLibType
1411
- });
1412
- return true;
1413
- } catch (e) {
1414
- return false;
1415
- }
1416
- }
1417
- var InvalidRepoUrlError = class extends Error {
1418
- constructor(m) {
1419
- super(m);
1420
- }
1421
- };
1422
- var InvalidAccessTokenError = class extends Error {
1423
- constructor(m) {
1424
- super(m);
1425
- }
1426
- };
1427
- var InvalidUrlPatternError = class extends Error {
1428
- constructor(m) {
1429
- super(m);
1430
- }
1431
- };
1432
- var RefNotFoundError = class extends Error {
1433
- constructor(m) {
1434
- super(m);
1435
- }
1436
- };
1437
- var RepoNoTokenAccessError = class extends Error {
1438
- constructor(m) {
1439
- super(m);
1440
- }
1441
- };
1442
- var SCMLib = class {
1443
- constructor(url, accessToken) {
1444
- __publicField(this, "url");
1445
- __publicField(this, "accessToken");
1446
- this.accessToken = accessToken;
1447
- this.url = url;
1448
- }
1449
- async getUrlWithCredentials() {
1450
- if (!this.url) {
1451
- console.error("no url for getUrlWithCredentials()");
1452
- throw new Error("no url");
1453
- }
1454
- const trimmedUrl = this.url.trim().replace(/\/$/, "");
1455
- if (!this.accessToken) {
1456
- return trimmedUrl;
1457
- }
1458
- const username = await this._getUsernameForAuthUrl();
1459
- const is_http = trimmedUrl.toLowerCase().startsWith("http://");
1460
- const is_https = trimmedUrl.toLowerCase().startsWith("https://");
1461
- if (is_http) {
1462
- return `http://${username}:${this.accessToken}@${trimmedUrl.toLowerCase().replace("http://", "")}`;
1463
- } else if (is_https) {
1464
- return `https://${username}:${this.accessToken}@${trimmedUrl.toLowerCase().replace("https://", "")}`;
1465
- } else {
1466
- console.error(`invalid scm url ${trimmedUrl}`);
1467
- throw new Error(`invalid scm url ${trimmedUrl}`);
1468
- }
1469
- }
1470
- getAccessToken() {
1471
- return this.accessToken || "";
1472
- }
1473
- getUrl() {
1474
- return this.url;
1475
- }
1476
- getName() {
1477
- if (!this.url) {
1478
- return "";
1479
- }
1480
- return this.url.split("/").at(-1) || "";
1481
- }
1482
- static async getIsValidBranchName(branchName) {
1483
- return isValidBranchName(branchName);
1484
- }
1485
- static async init({
1486
- url,
1487
- accessToken,
1488
- scmType
1489
- }) {
1490
- let trimmedUrl = void 0;
1491
- if (url) {
1492
- trimmedUrl = url.trim().replace(/\/$/, "");
1493
- }
1494
- try {
1495
- if ("GITHUB" /* GITHUB */ === scmType) {
1496
- const scm = new GithubSCMLib(trimmedUrl, accessToken);
1497
- await scm.validateParams();
1498
- return scm;
1499
- }
1500
- if ("GITLAB" /* GITLAB */ === scmType) {
1501
- const scm = new GitlabSCMLib(trimmedUrl, accessToken);
1502
- await scm.validateParams();
1503
- return scm;
1504
- }
1505
- } catch (e) {
1506
- if (e instanceof InvalidRepoUrlError && url) {
1507
- throw new RepoNoTokenAccessError("no access to repo");
1508
- }
1509
- }
1510
- return new StubSCMLib(trimmedUrl);
1511
- }
1512
- };
1513
- var GitlabSCMLib = class extends SCMLib {
1514
- async createSubmitRequest(targetBranchName, sourceBranchName, title, body) {
1515
- if (!this.accessToken || !this.url) {
1516
- console.error("no access token or no url");
1517
- throw new Error("no access token or no url");
1518
- }
1519
- return String(
1520
- await createMergeRequest({
1521
- title,
1522
- body,
1523
- targetBranchName,
1524
- sourceBranchName,
1525
- repoUrl: this.url,
1526
- accessToken: this.accessToken
1527
- })
1528
- );
1529
- }
1530
- async validateParams() {
1531
- return gitlabValidateParams({
1532
- url: this.url,
1533
- accessToken: this.accessToken
1534
- });
1535
- }
1536
- async getRepoList() {
1537
- if (!this.accessToken) {
1538
- console.error("no access token");
1539
- throw new Error("no access token");
1540
- }
1541
- return getGitlabRepoList(this.accessToken);
1483
+ return getGitlabRepoList(this.accessToken);
1542
1484
  }
1543
1485
  async getBranchList() {
1544
1486
  if (!this.accessToken || !this.url) {
@@ -1656,6 +1598,11 @@ var GitlabSCMLib = class extends SCMLib {
1656
1598
  }
1657
1599
  };
1658
1600
  var GithubSCMLib = class extends SCMLib {
1601
+ constructor(url, accessToken) {
1602
+ super(url, accessToken);
1603
+ __publicField(this, "oktokit");
1604
+ this.oktokit = new Octokit2({ auth: accessToken });
1605
+ }
1659
1606
  async createSubmitRequest(targetBranchName, sourceBranchName, title, body) {
1660
1607
  if (!this.accessToken || !this.url) {
1661
1608
  console.error("no access token or no url");
@@ -1675,6 +1622,55 @@ var GithubSCMLib = class extends SCMLib {
1675
1622
  async validateParams() {
1676
1623
  return githubValidateParams(this.url, this.accessToken);
1677
1624
  }
1625
+ async postPrComment(params, _oktokit) {
1626
+ if (!_oktokit && !this.accessToken || !this.url) {
1627
+ throw new Error("cannot post on PR without access token or url");
1628
+ }
1629
+ const oktokit = _oktokit || this.oktokit;
1630
+ const { owner, repo } = parseOwnerAndRepo(this.url);
1631
+ return postPrComment(oktokit, {
1632
+ ...params,
1633
+ owner,
1634
+ repo
1635
+ });
1636
+ }
1637
+ async updatePrComment(params, _oktokit) {
1638
+ if (!_oktokit && !this.accessToken || !this.url) {
1639
+ throw new Error("cannot update on PR without access token or url");
1640
+ }
1641
+ const oktokit = _oktokit || this.oktokit;
1642
+ const { owner, repo } = parseOwnerAndRepo(this.url);
1643
+ return updatePrComment(oktokit, {
1644
+ ...params,
1645
+ owner,
1646
+ repo
1647
+ });
1648
+ }
1649
+ async deleteComment(params, _oktokit) {
1650
+ if (!_oktokit && !this.accessToken || !this.url) {
1651
+ throw new Error("cannot delete comment without access token or url");
1652
+ }
1653
+ const oktokit = _oktokit || this.oktokit;
1654
+ const { owner, repo } = parseOwnerAndRepo(this.url);
1655
+ return deleteComment(oktokit, {
1656
+ ...params,
1657
+ owner,
1658
+ repo
1659
+ });
1660
+ }
1661
+ async getPrComments(params, _oktokit) {
1662
+ if (!_oktokit && !this.accessToken || !this.url) {
1663
+ throw new Error("cannot get Pr Comments without access token or url");
1664
+ }
1665
+ const oktokit = _oktokit || this.oktokit;
1666
+ const { owner, repo } = parseOwnerAndRepo(this.url);
1667
+ return getPrComments(oktokit, {
1668
+ per_page: 100,
1669
+ ...params,
1670
+ owner,
1671
+ repo
1672
+ });
1673
+ }
1678
1674
  async getRepoList() {
1679
1675
  if (!this.accessToken) {
1680
1676
  console.error("no access token");
@@ -1840,259 +1836,849 @@ var StubSCMLib = class extends SCMLib {
1840
1836
  }
1841
1837
  };
1842
1838
 
1843
- // src/features/analysis/scm/gitlab.ts
1844
- function removeTrailingSlash2(str) {
1845
- return str.trim().replace(/\/+$/, "");
1839
+ // src/features/analysis/scm/gitlab.ts
1840
+ function removeTrailingSlash2(str) {
1841
+ return str.trim().replace(/\/+$/, "");
1842
+ }
1843
+ var EnvVariablesZod2 = z5.object({
1844
+ GITLAB_API_TOKEN: z5.string().optional()
1845
+ });
1846
+ var { GITLAB_API_TOKEN } = EnvVariablesZod2.parse(process.env);
1847
+ function getGitBeaker(options) {
1848
+ const token = options?.gitlabAuthToken ?? GITLAB_API_TOKEN ?? "";
1849
+ if (token?.startsWith("glpat-") || token === "") {
1850
+ return new Gitlab({ token });
1851
+ }
1852
+ return new Gitlab({ oauthToken: token });
1853
+ }
1854
+ async function gitlabValidateParams({
1855
+ url,
1856
+ accessToken
1857
+ }) {
1858
+ try {
1859
+ const api = getGitBeaker({ gitlabAuthToken: accessToken });
1860
+ if (accessToken) {
1861
+ await api.Users.showCurrentUser();
1862
+ }
1863
+ if (url) {
1864
+ const { projectPath } = parseOwnerAndRepo2(url);
1865
+ await api.Projects.show(projectPath);
1866
+ }
1867
+ } catch (e) {
1868
+ const error = e;
1869
+ const code = error.code || error.status || error.statusCode || error.response?.status || error.response?.statusCode || error.response?.code;
1870
+ const description = error.description || `${e}`;
1871
+ if (code === 401 || code === 403 || description.includes("401") || description.includes("403")) {
1872
+ throw new InvalidAccessTokenError(`invalid gitlab access token`);
1873
+ }
1874
+ if (code === 404 || description.includes("404") || description.includes("Not Found")) {
1875
+ throw new InvalidRepoUrlError(`invalid gitlab repo Url ${url}`);
1876
+ }
1877
+ throw e;
1878
+ }
1879
+ }
1880
+ async function getGitlabUsername(accessToken) {
1881
+ const api = getGitBeaker({ gitlabAuthToken: accessToken });
1882
+ const res = await api.Users.showCurrentUser();
1883
+ return res.username;
1884
+ }
1885
+ async function getGitlabIsUserCollaborator({
1886
+ username,
1887
+ accessToken,
1888
+ repoUrl
1889
+ }) {
1890
+ try {
1891
+ const { projectPath } = parseOwnerAndRepo2(repoUrl);
1892
+ const api = getGitBeaker({ gitlabAuthToken: accessToken });
1893
+ const res = await api.Projects.show(projectPath);
1894
+ const members = await api.ProjectMembers.all(res.id, {
1895
+ includeInherited: true
1896
+ });
1897
+ return !!members.find((member) => member.username === username);
1898
+ } catch (e) {
1899
+ return false;
1900
+ }
1901
+ }
1902
+ async function getGitlabMergeRequestStatus({
1903
+ accessToken,
1904
+ repoUrl,
1905
+ mrNumber
1906
+ }) {
1907
+ const { projectPath } = parseOwnerAndRepo2(repoUrl);
1908
+ const api = getGitBeaker({ gitlabAuthToken: accessToken });
1909
+ const res = await api.MergeRequests.show(projectPath, mrNumber);
1910
+ switch (res.state) {
1911
+ case "merged" /* merged */:
1912
+ case "opened" /* opened */:
1913
+ case "closed" /* closed */:
1914
+ return res.state;
1915
+ default:
1916
+ throw new Error(`unknown merge request state ${res.state}`);
1917
+ }
1918
+ }
1919
+ async function getGitlabIsRemoteBranch({
1920
+ accessToken,
1921
+ repoUrl,
1922
+ branch
1923
+ }) {
1924
+ const { projectPath } = parseOwnerAndRepo2(repoUrl);
1925
+ const api = getGitBeaker({ gitlabAuthToken: accessToken });
1926
+ try {
1927
+ const res = await api.Branches.show(projectPath, branch);
1928
+ return res.name === branch;
1929
+ } catch (e) {
1930
+ return false;
1931
+ }
1932
+ }
1933
+ async function getGitlabRepoList(accessToken) {
1934
+ const api = getGitBeaker({ gitlabAuthToken: accessToken });
1935
+ const res = await api.Projects.all({
1936
+ membership: true,
1937
+ //TODO: a bug in the sorting mechanism of this api call
1938
+ //disallows us to sort by updated_at in descending order
1939
+ //so we have to sort by updated_at in ascending order.
1940
+ //We can wait for the bug to be fixed or call the api
1941
+ //directly with fetch()
1942
+ sort: "asc",
1943
+ orderBy: "updated_at",
1944
+ perPage: 100
1945
+ });
1946
+ return Promise.all(
1947
+ res.map(async (project) => {
1948
+ const proj = await api.Projects.show(project.id);
1949
+ const owner = proj.namespace.name;
1950
+ const repoLanguages = await api.Projects.showLanguages(project.id);
1951
+ return {
1952
+ repoName: project.path,
1953
+ repoUrl: project.web_url,
1954
+ repoOwner: owner,
1955
+ repoLanguages: Object.keys(repoLanguages),
1956
+ repoIsPublic: project.visibility === "public",
1957
+ repoUpdatedAt: project.last_activity_at
1958
+ };
1959
+ })
1960
+ );
1961
+ }
1962
+ async function getGitlabBranchList({
1963
+ accessToken,
1964
+ repoUrl
1965
+ }) {
1966
+ const { projectPath } = parseOwnerAndRepo2(repoUrl);
1967
+ const api = getGitBeaker({ gitlabAuthToken: accessToken });
1968
+ try {
1969
+ const res = await api.Branches.all(projectPath, {
1970
+ perPage: 100,
1971
+ pagination: "keyset",
1972
+ orderBy: "updated_at",
1973
+ sort: "dec"
1974
+ });
1975
+ return res.map((branch) => branch.name);
1976
+ } catch (e) {
1977
+ return [];
1978
+ }
1979
+ }
1980
+ async function createMergeRequest(options) {
1981
+ const { projectPath } = parseOwnerAndRepo2(options.repoUrl);
1982
+ const api = getGitBeaker({ gitlabAuthToken: options.accessToken });
1983
+ const res = await api.MergeRequests.create(
1984
+ projectPath,
1985
+ options.sourceBranchName,
1986
+ options.targetBranchName,
1987
+ options.title,
1988
+ {
1989
+ description: options.body
1990
+ }
1991
+ );
1992
+ return res.iid;
1993
+ }
1994
+ async function getGitlabRepoDefaultBranch(repoUrl, options) {
1995
+ const api = getGitBeaker({ gitlabAuthToken: options?.gitlabAuthToken });
1996
+ const { projectPath } = parseOwnerAndRepo2(repoUrl);
1997
+ const project = await api.Projects.show(projectPath);
1998
+ if (!project.default_branch) {
1999
+ throw new Error("no default branch");
2000
+ }
2001
+ return project.default_branch;
2002
+ }
2003
+ async function getGitlabReferenceData({ ref, gitlabUrl }, options) {
2004
+ const { projectPath } = parseOwnerAndRepo2(gitlabUrl);
2005
+ const api = getGitBeaker({ gitlabAuthToken: options?.gitlabAuthToken });
2006
+ const results = await Promise.allSettled([
2007
+ (async () => {
2008
+ const res = await api.Branches.show(projectPath, ref);
2009
+ return {
2010
+ sha: res.commit.id,
2011
+ type: "BRANCH" /* BRANCH */,
2012
+ date: res.commit.committed_date ? new Date(res.commit.committed_date) : void 0
2013
+ };
2014
+ })(),
2015
+ (async () => {
2016
+ const res = await api.Commits.show(projectPath, ref);
2017
+ return {
2018
+ sha: res.id,
2019
+ type: "COMMIT" /* COMMIT */,
2020
+ date: res.committed_date ? new Date(res.committed_date) : void 0
2021
+ };
2022
+ })(),
2023
+ (async () => {
2024
+ const res = await api.Tags.show(projectPath, ref);
2025
+ return {
2026
+ sha: res.commit.id,
2027
+ type: "TAG" /* TAG */,
2028
+ date: res.commit.committed_date ? new Date(res.commit.committed_date) : void 0
2029
+ };
2030
+ })()
2031
+ ]);
2032
+ const [branchRes, commitRes, tagRes] = results;
2033
+ if (tagRes.status === "fulfilled") {
2034
+ return tagRes.value;
2035
+ }
2036
+ if (branchRes.status === "fulfilled") {
2037
+ return branchRes.value;
2038
+ }
2039
+ if (commitRes.status === "fulfilled") {
2040
+ return commitRes.value;
2041
+ }
2042
+ throw new RefNotFoundError(`ref: ${ref} does not exist`);
2043
+ }
2044
+ function parseOwnerAndRepo2(gitlabUrl) {
2045
+ gitlabUrl = removeTrailingSlash2(gitlabUrl);
2046
+ const parsingResult = parseScmURL(gitlabUrl);
2047
+ if (!parsingResult || parsingResult.hostname !== "gitlab.com") {
2048
+ throw new InvalidUrlPatternError(`invalid gitlab repo Url ${gitlabUrl}`);
2049
+ }
2050
+ const { organization, repoName, projectPath } = parsingResult;
2051
+ return { owner: organization, repo: repoName, projectPath };
2052
+ }
2053
+ async function getGitlabBlameRanges({ ref, gitlabUrl, path: path8 }, options) {
2054
+ const { projectPath } = parseOwnerAndRepo2(gitlabUrl);
2055
+ const api = getGitBeaker({ gitlabAuthToken: options?.gitlabAuthToken });
2056
+ const resp = await api.RepositoryFiles.allFileBlames(projectPath, path8, ref);
2057
+ let lineNumber = 1;
2058
+ return resp.filter((range) => range.lines).map((range) => {
2059
+ const oldLineNumber = lineNumber;
2060
+ if (!range.lines) {
2061
+ throw new Error("range.lines should not be undefined");
2062
+ }
2063
+ lineNumber += range.lines.length;
2064
+ return {
2065
+ startingLine: oldLineNumber,
2066
+ endingLine: lineNumber - 1,
2067
+ login: range.commit.author_email,
2068
+ email: range.commit.author_email,
2069
+ name: range.commit.author_name
2070
+ };
2071
+ });
2072
+ }
2073
+ var GitlabAuthResultZ = z5.object({
2074
+ access_token: z5.string(),
2075
+ token_type: z5.string(),
2076
+ refresh_token: z5.string()
2077
+ });
2078
+
2079
+ // src/features/analysis/types.ts
2080
+ import { z as z6 } from "zod";
2081
+ var VulReportLocationZ = z6.object({
2082
+ physicalLocation: z6.object({
2083
+ artifactLocation: z6.object({
2084
+ uri: z6.string(),
2085
+ uriBaseId: z6.string(),
2086
+ index: z6.number()
2087
+ }),
2088
+ region: z6.object({
2089
+ startLine: z6.number(),
2090
+ startColumn: z6.number(),
2091
+ endColumn: z6.number()
2092
+ })
2093
+ })
2094
+ });
2095
+
2096
+ // src/features/analysis/utils/get_issue_type.ts
2097
+ var getIssueType = (issueType) => {
2098
+ switch (issueType) {
2099
+ case "SQL_Injection" /* SqlInjection */:
2100
+ return "SQL Injection";
2101
+ case "CMDi_relative_path_command" /* CmDiRelativePathCommand */:
2102
+ return "Relative Path Command Injection";
2103
+ case "CMDi" /* CmDi */:
2104
+ return "Command Injection";
2105
+ case "XXE" /* Xxe */:
2106
+ return "XXE";
2107
+ case "XSS" /* Xss */:
2108
+ return "XSS";
2109
+ case "PT" /* Pt */:
2110
+ return "Path Traversal";
2111
+ case "INSECURE_RANDOMNESS" /* InsecureRandomness */:
2112
+ return "Insecure Randomness";
2113
+ case "SSRF" /* Ssrf */:
2114
+ return "Server Side Request Forgery";
2115
+ case "TYPE_CONFUSION" /* TypeConfusion */:
2116
+ return "Type Confusion";
2117
+ case "REGEX_INJECTION" /* RegexInjection */:
2118
+ return "Regular Expression Injection";
2119
+ case "INCOMPLETE_URL_SANITIZATION" /* IncompleteUrlSanitization */:
2120
+ return "Incomplete URL Sanitization";
2121
+ case "LOG_FORGING" /* LogForging */:
2122
+ return "Log Forging";
2123
+ case "MISSING_CHECK_AGAINST_NULL" /* MissingCheckAgainstNull */:
2124
+ return "Missing Check against Null";
2125
+ case "PASSWORD_IN_COMMENT" /* PasswordInComment */:
2126
+ return "Password in Comment";
2127
+ case "OVERLY_BROAD_CATCH" /* OverlyBroadCatch */:
2128
+ return "Poor Error Handling: Overly Broad Catch";
2129
+ case "USE_OF_SYSTEM_OUTPUT_STREAM" /* UseOfSystemOutputStream */:
2130
+ return "Use of System.out/System.err";
2131
+ case "DANGEROUS_FUNCTION_OVERFLOW" /* DangerousFunctionOverflow */:
2132
+ return "Use of dangerous function";
2133
+ case "DOS_STRING_BUILDER" /* DosStringBuilder */:
2134
+ return "Denial of Service: StringBuilder";
2135
+ case "OPEN_REDIRECT" /* OpenRedirect */:
2136
+ return "Open Redirect";
2137
+ case "WEAK_XML_SCHEMA_UNBOUNDED_OCCURRENCES" /* WeakXmlSchemaUnboundedOccurrences */:
2138
+ return "Weak XML Schema: Unbounded Occurrences";
2139
+ case "SYSTEM_INFORMATION_LEAK" /* SystemInformationLeak */:
2140
+ return "System Information Leak";
2141
+ case "HTTP_RESPONSE_SPLITTING" /* HttpResponseSplitting */:
2142
+ return "HTTP response splitting";
2143
+ case "HTTP_ONLY_COOKIE" /* HttpOnlyCookie */:
2144
+ return "Cookie is not HttpOnly";
2145
+ case "INSECURE_COOKIE" /* InsecureCookie */:
2146
+ return "Insecure Cookie";
2147
+ default: {
2148
+ return issueType ? issueType.replaceAll("_", " ") : "Other";
2149
+ }
2150
+ }
2151
+ };
2152
+
2153
+ // src/features/analysis/utils/index.ts
2154
+ function getFixUrl({
2155
+ fixId,
2156
+ projectId,
2157
+ organizationId,
2158
+ analysisId
2159
+ }) {
2160
+ return `${WEB_APP_URL}/organization/${organizationId}/project/${projectId}/report/${analysisId}/fix/${fixId}`;
2161
+ }
2162
+ function getCommitUrl({
2163
+ fixId,
2164
+ projectId,
2165
+ organizationId,
2166
+ analysisId,
2167
+ redirectUrl
2168
+ }) {
2169
+ return `${getFixUrl({
2170
+ fixId,
2171
+ projectId,
2172
+ organizationId,
2173
+ analysisId
2174
+ })}/commit?redirect_url=${encodeURIComponent(redirectUrl)}`;
2175
+ }
2176
+
2177
+ // src/features/analysis/handle_finished_analysis.ts
2178
+ var debug4 = Debug4("mobbdev:handle-finished-analysis");
2179
+ var MOBB_ICON_IMG = "![image](https://github.com/yhaggai/GitHub-Fixer-Demo/assets/1255845/30f566df-6544-4612-929e-2ee5e8b9d345)";
2180
+ var COMMIT_FIX_SVG = `https://felt-laptop-20190711103614-deployment.s3.us-east-1.amazonaws.com/commit-button.svg`;
2181
+ var commitFixButton = (commitUrl) => `<a href="${commitUrl}"><img src=${COMMIT_FIX_SVG}></a>`;
2182
+ function scannerToFriendlyString(scanner) {
2183
+ switch (scanner) {
2184
+ case "checkmarx":
2185
+ return "Checkmarx";
2186
+ case "codeql":
2187
+ return "CodeQL";
2188
+ case "fortify":
2189
+ return "Fortify";
2190
+ case "snyk":
2191
+ return "Snyk";
2192
+ }
2193
+ }
2194
+ async function handleFinishedAnalysis({
2195
+ analysisId,
2196
+ scm: _scm,
2197
+ gqlClient,
2198
+ githubActionOctokit,
2199
+ scanner
2200
+ }) {
2201
+ if (_scm instanceof GithubSCMLib === false) {
2202
+ return;
2203
+ }
2204
+ const scm = _scm;
2205
+ const res = await gqlClient.getAnalysis(analysisId);
2206
+ const comments = await scm.getPrComments({}, githubActionOctokit);
2207
+ await Promise.all(
2208
+ comments.data.filter((comment) => {
2209
+ return comment.body.includes("fix by Mobb is ready");
2210
+ }).map((comment) => {
2211
+ try {
2212
+ return scm.deleteComment(
2213
+ { comment_id: comment.id },
2214
+ githubActionOctokit
2215
+ );
2216
+ } catch (e) {
2217
+ debug4("delete comment failed %s", e);
2218
+ return Promise.resolve();
2219
+ }
2220
+ })
2221
+ );
2222
+ const {
2223
+ vulnerabilityReport: {
2224
+ file: {
2225
+ signedFile: { url: vulReportUrl }
2226
+ }
2227
+ },
2228
+ repo: { commitSha, pullRequest }
2229
+ } = res.analysis;
2230
+ const {
2231
+ projectId,
2232
+ project: { organizationId }
2233
+ } = res.analysis.vulnerabilityReport;
2234
+ const vulReportRes = await fetch(vulReportUrl);
2235
+ const vulReport = await vulReportRes.json();
2236
+ return Promise.all(
2237
+ res.analysis.fixes.map((fix) => {
2238
+ const [vulnerabilityReportIssue] = fix.vulnerabilityReportIssues;
2239
+ const issueIndex = parseInt(
2240
+ z7.string().parse(vulnerabilityReportIssue?.vendorIssueId)
2241
+ );
2242
+ const results = vulReport.runs[0]?.results || [];
2243
+ const ruleId = results[issueIndex]?.ruleId;
2244
+ const location = VulReportLocationZ.parse(
2245
+ results[issueIndex]?.locations[0]
2246
+ );
2247
+ const { uri: filePath } = location.physicalLocation.artifactLocation;
2248
+ const { startLine, startColumn, endColumn } = location.physicalLocation.region;
2249
+ const fixLocation = {
2250
+ filePath,
2251
+ startLine,
2252
+ startColumn,
2253
+ endColumn,
2254
+ ruleId
2255
+ };
2256
+ return {
2257
+ fix,
2258
+ fixLocation
2259
+ };
2260
+ }).map(async ({ fix, fixLocation }) => {
2261
+ const { filePath, startLine } = fixLocation;
2262
+ const getFixContent = await gqlClient.getFix(fix.id);
2263
+ const {
2264
+ fix_by_pk: {
2265
+ patchAndQuestions: { patch }
2266
+ }
2267
+ } = getFixContent;
2268
+ const commentRes = await scm.postPrComment(
2269
+ {
2270
+ body: "empty",
2271
+ pull_number: pullRequest,
2272
+ commit_id: commitSha,
2273
+ path: filePath,
2274
+ line: startLine
2275
+ },
2276
+ githubActionOctokit
2277
+ );
2278
+ const commitUrl = getCommitUrl({
2279
+ fixId: fix.id,
2280
+ projectId,
2281
+ analysisId,
2282
+ organizationId,
2283
+ redirectUrl: commentRes.data.html_url
2284
+ });
2285
+ const fixUrl = getFixUrl({
2286
+ fixId: fix.id,
2287
+ projectId,
2288
+ analysisId,
2289
+ organizationId
2290
+ });
2291
+ const scanerString = scannerToFriendlyString(scanner);
2292
+ const issueType = getIssueType(fix.issueType);
2293
+ const title = `# ${MOBB_ICON_IMG} ${issueType} fix by Mobb is ready`;
2294
+ const subTitle = `### Apply the following code change to fix ${issueType} issue detected by ${scanerString}:`;
2295
+ const diff = `\`\`\`diff
2296
+ ${patch}
2297
+ \`\`\``;
2298
+ const fixPageLink = `[Learn more and fine tune the fix](${fixUrl})`;
2299
+ await scm.updatePrComment(
2300
+ {
2301
+ body: `${title}
2302
+ ${subTitle}
2303
+ ${diff}
2304
+ ${commitFixButton(
2305
+ commitUrl
2306
+ )}
2307
+ ${fixPageLink}`,
2308
+ comment_id: commentRes.data.id
2309
+ },
2310
+ githubActionOctokit
2311
+ );
2312
+ })
2313
+ );
2314
+ }
2315
+
2316
+ // src/features/analysis/pack.ts
2317
+ import fs2 from "node:fs";
2318
+ import path4 from "node:path";
2319
+ import AdmZip from "adm-zip";
2320
+ import Debug5 from "debug";
2321
+ import { globby } from "globby";
2322
+ import { isBinary } from "istextorbinary";
2323
+ var debug5 = Debug5("mobbdev:pack");
2324
+ var MAX_FILE_SIZE = 1024 * 1024 * 5;
2325
+ function endsWithAny(str, suffixes) {
2326
+ return suffixes.some(function(suffix) {
2327
+ return str.endsWith(suffix);
2328
+ });
2329
+ }
2330
+ async function pack(srcDirPath, vulnFiles) {
2331
+ debug5("pack folder %s", srcDirPath);
2332
+ const filepaths = await globby("**", {
2333
+ gitignore: true,
2334
+ onlyFiles: true,
2335
+ cwd: srcDirPath,
2336
+ followSymbolicLinks: false
2337
+ });
2338
+ debug5("files found %d", filepaths.length);
2339
+ const zip = new AdmZip();
2340
+ debug5("compressing files");
2341
+ for (const filepath of filepaths) {
2342
+ const absFilepath = path4.join(srcDirPath, filepath.toString());
2343
+ if (!endsWithAny(
2344
+ absFilepath.toString().replaceAll(path4.win32.sep, path4.posix.sep),
2345
+ vulnFiles
2346
+ )) {
2347
+ debug5("ignoring %s because it is not a vulnerability file", filepath);
2348
+ continue;
2349
+ }
2350
+ if (fs2.lstatSync(absFilepath).size > MAX_FILE_SIZE) {
2351
+ debug5("ignoring %s because the size is > 5MB", filepath);
2352
+ continue;
2353
+ }
2354
+ const data = fs2.readFileSync(absFilepath);
2355
+ if (isBinary(null, data)) {
2356
+ debug5("ignoring %s because is seems to be a binary file", filepath);
2357
+ continue;
2358
+ }
2359
+ zip.addFile(filepath.toString(), data);
2360
+ }
2361
+ debug5("get zip file buffer");
2362
+ return zip.toBuffer();
2363
+ }
2364
+
2365
+ // src/features/analysis/prompts.ts
2366
+ import inquirer from "inquirer";
2367
+ import { createSpinner } from "nanospinner";
2368
+ var scannerChoices = [
2369
+ { name: "Snyk", value: SCANNERS.Snyk },
2370
+ { name: "Checkmarx", value: SCANNERS.Checkmarx },
2371
+ { name: "Codeql", value: SCANNERS.Codeql },
2372
+ { name: "Fortify", value: SCANNERS.Fortify }
2373
+ ];
2374
+ async function choseScanner() {
2375
+ const { scanner } = await inquirer.prompt({
2376
+ name: "scanner",
2377
+ message: "Choose a scanner you wish to use to scan your code",
2378
+ type: "list",
2379
+ choices: scannerChoices
2380
+ });
2381
+ return scanner;
2382
+ }
2383
+ async function tryCheckmarxConfiguarationAgain() {
2384
+ console.log(
2385
+ "\u{1F513} Oops, seems like checkmarx does not accept the current configuration"
2386
+ );
2387
+ const { confirmCheckmarxRetryConfigrations } = await inquirer.prompt({
2388
+ name: "confirmCheckmarxRetryConfigrations",
2389
+ type: "confirm",
2390
+ message: "Would like to try to configure them again? ",
2391
+ default: true
2392
+ });
2393
+ return confirmCheckmarxRetryConfigrations;
2394
+ }
2395
+ async function startCheckmarxConfigationPrompt() {
2396
+ const checkmarxConfigreSpinner = createSpinner(
2397
+ "\u{1F513} Checkmarx needs to be configured before we start, press any key to continue"
2398
+ ).start();
2399
+ await keypress();
2400
+ checkmarxConfigreSpinner.success();
2401
+ }
2402
+ async function scmIntegrationPrompt(scmName) {
2403
+ const answers = await inquirer.prompt({
2404
+ name: "scmConfirm",
2405
+ type: "confirm",
2406
+ message: `It seems we don't have access to the repo, do you want to grant access to your ${scmName} account?`,
2407
+ default: true
2408
+ });
2409
+ return answers.scmConfirm;
2410
+ }
2411
+ async function mobbAnalysisPrompt() {
2412
+ const spinner = createSpinner().start();
2413
+ spinner.update({ text: "Hit any key to view available fixes" });
2414
+ await keypress();
2415
+ return spinner.success();
2416
+ }
2417
+ async function snykArticlePrompt() {
2418
+ const { snykArticleConfirm } = await inquirer.prompt({
2419
+ name: "snykArticleConfirm",
2420
+ type: "confirm",
2421
+ message: "Do you want to be taken to the relevant Snyk's online article?",
2422
+ default: true
2423
+ });
2424
+ return snykArticleConfirm;
2425
+ }
2426
+
2427
+ // src/features/analysis/scanners/checkmarx.ts
2428
+ import { createRequire } from "node:module";
2429
+
2430
+ // src/post_install/constants.mjs
2431
+ var cxOperatingSystemSupportMessage = `Your operating system does not support checkmarx.
2432
+ You can see the list of supported operating systems here: https://github.com/Checkmarx/ast-cli#releases`;
2433
+
2434
+ // src/utils/child_process.ts
2435
+ import cp from "node:child_process";
2436
+ import Debug6 from "debug";
2437
+ import * as process2 from "process";
2438
+ import supportsColor from "supports-color";
2439
+ var { stdout: stdout2 } = supportsColor;
2440
+ function createFork({ args, processPath, name }, options) {
2441
+ const child = cp.fork(processPath, args, {
2442
+ stdio: ["inherit", "pipe", "pipe", "ipc"],
2443
+ env: { FORCE_COLOR: stdout2 ? "1" : "0" }
2444
+ });
2445
+ return createChildProcess({ childProcess: child, name }, options);
1846
2446
  }
1847
- var EnvVariablesZod2 = z5.object({
1848
- GITLAB_API_TOKEN: z5.string().optional()
1849
- });
1850
- var { GITLAB_API_TOKEN } = EnvVariablesZod2.parse(process.env);
1851
- function getGitBeaker(options) {
1852
- const token = options?.gitlabAuthToken ?? GITLAB_API_TOKEN ?? "";
1853
- if (token?.startsWith("glpat-") || token === "") {
1854
- return new Gitlab({ token });
1855
- }
1856
- return new Gitlab({ oauthToken: token });
2447
+ function createSpwan({ args, processPath, name }, options) {
2448
+ const child = cp.spawn(processPath, args, {
2449
+ stdio: ["inherit", "pipe", "pipe", "ipc"],
2450
+ env: { FORCE_COLOR: stdout2 ? "1" : "0" }
2451
+ });
2452
+ return createChildProcess({ childProcess: child, name }, options);
1857
2453
  }
1858
- async function gitlabValidateParams({
1859
- url,
1860
- accessToken
1861
- }) {
1862
- try {
1863
- const api = getGitBeaker({ gitlabAuthToken: accessToken });
1864
- if (accessToken) {
1865
- await api.Users.showCurrentUser();
1866
- }
1867
- if (url) {
1868
- const { projectPath } = parseOwnerAndRepo2(url);
1869
- await api.Projects.show(projectPath);
1870
- }
1871
- } catch (e) {
1872
- const error = e;
1873
- const code = error.code || error.status || error.statusCode || error.response?.status || error.response?.statusCode || error.response?.code;
1874
- const description = error.description || `${e}`;
1875
- if (code === 401 || code === 403 || description.includes("401") || description.includes("403")) {
1876
- throw new InvalidAccessTokenError(`invalid gitlab access token`);
2454
+ function createChildProcess({ childProcess, name }, options) {
2455
+ const debug10 = Debug6(`mobbdev:${name}`);
2456
+ const { display } = options;
2457
+ return new Promise((resolve, reject) => {
2458
+ let out = "";
2459
+ const onData = (chunk) => {
2460
+ debug10(`chunk received from ${name} std ${chunk}`);
2461
+ out += chunk;
2462
+ };
2463
+ if (!childProcess || !childProcess?.stdout || !childProcess?.stderr) {
2464
+ debug10(`unable to fork ${name}`);
2465
+ reject(new Error(`unable to fork ${name}`));
1877
2466
  }
1878
- if (code === 404 || description.includes("404") || description.includes("Not Found")) {
1879
- throw new InvalidRepoUrlError(`invalid gitlab repo Url ${url}`);
2467
+ childProcess.stdout?.on("data", onData);
2468
+ childProcess.stderr?.on("data", onData);
2469
+ if (display) {
2470
+ childProcess.stdout?.pipe(process2.stdout);
2471
+ childProcess.stderr?.pipe(process2.stderr);
1880
2472
  }
1881
- throw e;
1882
- }
1883
- }
1884
- async function getGitlabUsername(accessToken) {
1885
- const api = getGitBeaker({ gitlabAuthToken: accessToken });
1886
- const res = await api.Users.showCurrentUser();
1887
- return res.username;
1888
- }
1889
- async function getGitlabIsUserCollaborator({
1890
- username,
1891
- accessToken,
1892
- repoUrl
1893
- }) {
1894
- try {
1895
- const { projectPath } = parseOwnerAndRepo2(repoUrl);
1896
- const api = getGitBeaker({ gitlabAuthToken: accessToken });
1897
- const res = await api.Projects.show(projectPath);
1898
- const members = await api.ProjectMembers.all(res.id, {
1899
- includeInherited: true
2473
+ childProcess.on("exit", (code) => {
2474
+ debug10(`${name} exit code ${code}`);
2475
+ resolve({ message: out, code });
1900
2476
  });
1901
- return !!members.find((member) => member.username === username);
1902
- } catch (e) {
1903
- return false;
1904
- }
1905
- }
1906
- async function getGitlabMergeRequestStatus({
1907
- accessToken,
1908
- repoUrl,
1909
- mrNumber
1910
- }) {
1911
- const { projectPath } = parseOwnerAndRepo2(repoUrl);
1912
- const api = getGitBeaker({ gitlabAuthToken: accessToken });
1913
- const res = await api.MergeRequests.show(projectPath, mrNumber);
1914
- switch (res.state) {
1915
- case "merged" /* merged */:
1916
- case "opened" /* opened */:
1917
- case "closed" /* closed */:
1918
- return res.state;
1919
- default:
1920
- throw new Error(`unknown merge request state ${res.state}`);
1921
- }
2477
+ childProcess.on("error", (err) => {
2478
+ debug10(`${name} error %o`, err);
2479
+ reject(err);
2480
+ });
2481
+ });
1922
2482
  }
1923
- async function getGitlabIsRemoteBranch({
1924
- accessToken,
1925
- repoUrl,
1926
- branch
1927
- }) {
1928
- const { projectPath } = parseOwnerAndRepo2(repoUrl);
1929
- const api = getGitBeaker({ gitlabAuthToken: accessToken });
2483
+
2484
+ // src/features/analysis/scanners/checkmarx.ts
2485
+ import chalk2 from "chalk";
2486
+ import Debug7 from "debug";
2487
+ import { existsSync } from "fs";
2488
+ import { createSpinner as createSpinner2 } from "nanospinner";
2489
+ import { type } from "os";
2490
+ import path5 from "path";
2491
+ var debug6 = Debug7("mobbdev:checkmarx");
2492
+ var require2 = createRequire(import.meta.url);
2493
+ var getCheckmarxPath = () => {
2494
+ const os3 = type();
2495
+ const cxFileName = os3 === "Windows_NT" ? "cx.exe" : "cx";
1930
2496
  try {
1931
- const res = await api.Branches.show(projectPath, branch);
1932
- return res.name === branch;
2497
+ return require2.resolve(`.bin/${cxFileName}`);
1933
2498
  } catch (e) {
1934
- return false;
2499
+ throw new CliError(cxOperatingSystemSupportMessage);
1935
2500
  }
2501
+ };
2502
+ var getCheckmarxCommandArgs = ({
2503
+ repoPath,
2504
+ branch,
2505
+ fileName,
2506
+ filePath,
2507
+ projectName
2508
+ }) => [
2509
+ "--project-name",
2510
+ projectName,
2511
+ "-s",
2512
+ repoPath,
2513
+ "--branch",
2514
+ branch,
2515
+ "--scan-types",
2516
+ "sast",
2517
+ "--output-path",
2518
+ filePath,
2519
+ "--output-name",
2520
+ fileName,
2521
+ "--report-format",
2522
+ "json"
2523
+ ];
2524
+ var VALIDATE_COMMAND = ["auth", "validate"];
2525
+ var CONFIGURE_COMMAND = ["configure"];
2526
+ var SCAN_COMMAND = ["scan", "create"];
2527
+ var CHECKMARX_SUCCESS_CODE = 0;
2528
+ function validateCheckmarxInstallation() {
2529
+ existsSync(getCheckmarxPath());
1936
2530
  }
1937
- async function getGitlabRepoList(accessToken) {
1938
- const api = getGitBeaker({ gitlabAuthToken: accessToken });
1939
- const res = await api.Projects.all({
1940
- membership: true,
1941
- //TODO: a bug in the sorting mechanism of this api call
1942
- //disallows us to sort by updated_at in descending order
1943
- //so we have to sort by updated_at in ascending order.
1944
- //We can wait for the bug to be fixed or call the api
1945
- //directly with fetch()
1946
- sort: "asc",
1947
- orderBy: "updated_at",
1948
- perPage: 100
1949
- });
1950
- return Promise.all(
1951
- res.map(async (project) => {
1952
- const proj = await api.Projects.show(project.id);
1953
- const owner = proj.namespace.name;
1954
- const repoLanguages = await api.Projects.showLanguages(project.id);
1955
- return {
1956
- repoName: project.path,
1957
- repoUrl: project.web_url,
1958
- repoOwner: owner,
1959
- repoLanguages: Object.keys(repoLanguages),
1960
- repoIsPublic: project.visibility === "public",
1961
- repoUpdatedAt: project.last_activity_at
1962
- };
1963
- })
2531
+ async function forkCheckmarx(args, { display }) {
2532
+ debug6("fork checkmarx with args %o %s", args.join(" "), display);
2533
+ return createSpwan(
2534
+ { args, processPath: getCheckmarxPath(), name: "checkmarx" },
2535
+ { display }
1964
2536
  );
1965
2537
  }
1966
- async function getGitlabBranchList({
1967
- accessToken,
1968
- repoUrl
1969
- }) {
1970
- const { projectPath } = parseOwnerAndRepo2(repoUrl);
1971
- const api = getGitBeaker({ gitlabAuthToken: accessToken });
1972
- try {
1973
- const res = await api.Branches.all(projectPath, {
1974
- perPage: 100,
1975
- pagination: "keyset",
1976
- orderBy: "updated_at",
1977
- sort: "dec"
1978
- });
1979
- return res.map((branch) => branch.name);
1980
- } catch (e) {
1981
- return [];
2538
+ async function getCheckmarxReport({ reportPath, repositoryRoot, branch, projectName }, { skipPrompts = false }) {
2539
+ debug6("get checkmarx report start %s %s", reportPath, repositoryRoot);
2540
+ const { code: loginCode } = await forkCheckmarx(VALIDATE_COMMAND, {
2541
+ display: false
2542
+ });
2543
+ if (loginCode !== CHECKMARX_SUCCESS_CODE) {
2544
+ if (skipPrompts) {
2545
+ await throwCheckmarxConfigError();
2546
+ }
2547
+ await startCheckmarxConfigationPrompt();
2548
+ await validateCheckamxCredentials();
1982
2549
  }
1983
- }
1984
- async function createMergeRequest(options) {
1985
- const { projectPath } = parseOwnerAndRepo2(options.repoUrl);
1986
- const api = getGitBeaker({ gitlabAuthToken: options.accessToken });
1987
- const res = await api.MergeRequests.create(
1988
- projectPath,
1989
- options.sourceBranchName,
1990
- options.targetBranchName,
1991
- options.title,
2550
+ const extension = path5.extname(reportPath);
2551
+ const filePath = path5.dirname(reportPath);
2552
+ const fileName = path5.basename(reportPath, extension);
2553
+ const checkmarxCommandArgs = getCheckmarxCommandArgs({
2554
+ repoPath: repositoryRoot,
2555
+ branch,
2556
+ filePath,
2557
+ fileName,
2558
+ projectName
2559
+ });
2560
+ console.log("\u280B \u{1F50D} Initiating Checkmarx Scan ");
2561
+ const { code: scanCode } = await forkCheckmarx(
2562
+ [...SCAN_COMMAND, ...checkmarxCommandArgs],
1992
2563
  {
1993
- description: options.body
2564
+ display: true
1994
2565
  }
1995
2566
  );
1996
- return res.iid;
1997
- }
1998
- async function getGitlabRepoDefaultBranch(repoUrl, options) {
1999
- const api = getGitBeaker({ gitlabAuthToken: options?.gitlabAuthToken });
2000
- const { projectPath } = parseOwnerAndRepo2(repoUrl);
2001
- const project = await api.Projects.show(projectPath);
2002
- if (!project.default_branch) {
2003
- throw new Error("no default branch");
2567
+ if (scanCode !== CHECKMARX_SUCCESS_CODE) {
2568
+ createSpinner2("\u{1F50D} Something went wrong with the checkmarx scan").start().error();
2569
+ throw new CliError();
2004
2570
  }
2005
- return project.default_branch;
2571
+ await createSpinner2("\u{1F50D} Checkmarx Scan completed").start().success();
2572
+ return true;
2006
2573
  }
2007
- async function getGitlabReferenceData({ ref, gitlabUrl }, options) {
2008
- const { projectPath } = parseOwnerAndRepo2(gitlabUrl);
2009
- const api = getGitBeaker({ gitlabAuthToken: options?.gitlabAuthToken });
2010
- const results = await Promise.allSettled([
2011
- (async () => {
2012
- const res = await api.Branches.show(projectPath, ref);
2013
- return {
2014
- sha: res.commit.id,
2015
- type: "BRANCH" /* BRANCH */,
2016
- date: res.commit.committed_date ? new Date(res.commit.committed_date) : void 0
2017
- };
2018
- })(),
2019
- (async () => {
2020
- const res = await api.Commits.show(projectPath, ref);
2021
- return {
2022
- sha: res.id,
2023
- type: "COMMIT" /* COMMIT */,
2024
- date: res.committed_date ? new Date(res.committed_date) : void 0
2025
- };
2026
- })(),
2027
- (async () => {
2028
- const res = await api.Tags.show(projectPath, ref);
2029
- return {
2030
- sha: res.commit.id,
2031
- type: "TAG" /* TAG */,
2032
- date: res.commit.committed_date ? new Date(res.commit.committed_date) : void 0
2033
- };
2034
- })()
2035
- ]);
2036
- const [branchRes, commitRes, tagRes] = results;
2037
- if (tagRes.status === "fulfilled") {
2038
- return tagRes.value;
2039
- }
2040
- if (branchRes.status === "fulfilled") {
2041
- return branchRes.value;
2042
- }
2043
- if (commitRes.status === "fulfilled") {
2044
- return commitRes.value;
2045
- }
2046
- throw new RefNotFoundError(`ref: ${ref} does not exist`);
2574
+ async function throwCheckmarxConfigError() {
2575
+ await createSpinner2("\u{1F513} Checkmarx is not configued correctly").start().error();
2576
+ throw new CliError(
2577
+ `Checkmarx is not configued correctly
2578
+ you can configure it by using the ${chalk2.bold(
2579
+ "cx configure"
2580
+ )} command`
2581
+ );
2047
2582
  }
2048
- function parseOwnerAndRepo2(gitlabUrl) {
2049
- gitlabUrl = removeTrailingSlash2(gitlabUrl);
2050
- const parsingResult = parseScmURL(gitlabUrl);
2051
- if (!parsingResult || parsingResult.hostname !== "gitlab.com") {
2052
- throw new InvalidUrlPatternError(`invalid gitlab repo Url ${gitlabUrl}`);
2583
+ async function validateCheckamxCredentials() {
2584
+ console.log(`
2585
+ Here's a suggestion for checkmarx configuation:
2586
+ ${chalk2.bold("AST Base URI:")} https://ast.checkmarx.net
2587
+ ${chalk2.bold("AST Base Auth URI (IAM):")} https://iam.checkmarx.net
2588
+ `);
2589
+ await forkCheckmarx(CONFIGURE_COMMAND, { display: true });
2590
+ const { code: loginCode } = await forkCheckmarx(VALIDATE_COMMAND, {
2591
+ display: false
2592
+ });
2593
+ if (loginCode !== CHECKMARX_SUCCESS_CODE) {
2594
+ const tryAgain = await tryCheckmarxConfiguarationAgain();
2595
+ if (!tryAgain) {
2596
+ await throwCheckmarxConfigError();
2597
+ }
2598
+ if (await tryCheckmarxConfiguarationAgain()) {
2599
+ validateCheckamxCredentials();
2600
+ }
2053
2601
  }
2054
- const { organization, repoName, projectPath } = parsingResult;
2055
- return { owner: organization, repo: repoName, projectPath };
2602
+ await createSpinner2("\u{1F513} Checkmarx configured successfully!").start().success();
2056
2603
  }
2057
- async function getGitlabBlameRanges({ ref, gitlabUrl, path: path8 }, options) {
2058
- const { projectPath } = parseOwnerAndRepo2(gitlabUrl);
2059
- const api = getGitBeaker({ gitlabAuthToken: options?.gitlabAuthToken });
2060
- const resp = await api.RepositoryFiles.allFileBlames(projectPath, path8, ref);
2061
- let lineNumber = 1;
2062
- return resp.filter((range) => range.lines).map((range) => {
2063
- const oldLineNumber = lineNumber;
2064
- if (!range.lines) {
2065
- throw new Error("range.lines should not be undefined");
2604
+
2605
+ // src/features/analysis/scanners/snyk.ts
2606
+ import { createRequire as createRequire2 } from "node:module";
2607
+ import chalk3 from "chalk";
2608
+ import Debug8 from "debug";
2609
+ import { createSpinner as createSpinner3 } from "nanospinner";
2610
+ import open from "open";
2611
+ var debug7 = Debug8("mobbdev:snyk");
2612
+ var require3 = createRequire2(import.meta.url);
2613
+ var SNYK_PATH = require3.resolve("snyk/bin/snyk");
2614
+ 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";
2615
+ debug7("snyk executable path %s", SNYK_PATH);
2616
+ async function forkSnyk(args, { display }) {
2617
+ debug7("fork snyk with args %o %s", args, display);
2618
+ return createFork(
2619
+ { args, processPath: SNYK_PATH, name: "checkmarx" },
2620
+ { display }
2621
+ );
2622
+ }
2623
+ async function getSnykReport(reportPath, repoRoot, { skipPrompts = false }) {
2624
+ debug7("get snyk report start %s %s", reportPath, repoRoot);
2625
+ const config3 = await forkSnyk(["config"], { display: false });
2626
+ const { message: configMessage } = config3;
2627
+ if (!configMessage.includes("api: ")) {
2628
+ const snykLoginSpinner = createSpinner3().start();
2629
+ if (!skipPrompts) {
2630
+ snykLoginSpinner.update({
2631
+ text: "\u{1F513} Login to Snyk is required, press any key to continue"
2632
+ });
2633
+ await keypress();
2066
2634
  }
2067
- lineNumber += range.lines.length;
2068
- return {
2069
- startingLine: oldLineNumber,
2070
- endingLine: lineNumber - 1,
2071
- login: range.commit.author_email,
2072
- email: range.commit.author_email,
2073
- name: range.commit.author_name
2074
- };
2075
- });
2635
+ snykLoginSpinner.update({
2636
+ text: "\u{1F513} Waiting for Snyk login to complete"
2637
+ });
2638
+ debug7("no token in the config %s", config3);
2639
+ await forkSnyk(["auth"], { display: true });
2640
+ snykLoginSpinner.success({ text: "\u{1F513} Login to Snyk Successful" });
2641
+ }
2642
+ const snykSpinner = createSpinner3("\u{1F50D} Scanning your repo with Snyk ").start();
2643
+ const { message: scanOutput } = await forkSnyk(
2644
+ ["code", "test", `--sarif-file-output=${reportPath}`, repoRoot],
2645
+ { display: true }
2646
+ );
2647
+ if (scanOutput.includes(
2648
+ "Snyk Code is not supported for org: enable in Settings > Snyk Code"
2649
+ )) {
2650
+ debug7("snyk code is not enabled %s", scanOutput);
2651
+ snykSpinner.error({ text: "\u{1F50D} Snyk configuration needed" });
2652
+ const answer = await snykArticlePrompt();
2653
+ debug7("answer %s", answer);
2654
+ if (answer) {
2655
+ debug7("opening the browser");
2656
+ await open(SNYK_ARTICLE_URL);
2657
+ }
2658
+ console.log(
2659
+ chalk3.bgBlue(
2660
+ "\nPlease enable Snyk Code in your Snyk account and try again."
2661
+ )
2662
+ );
2663
+ throw Error("snyk is not enbabled");
2664
+ }
2665
+ snykSpinner.success({ text: "\u{1F50D} Snyk code scan completed" });
2666
+ return true;
2076
2667
  }
2077
- var GitlabAuthResultZ = z5.object({
2078
- access_token: z5.string(),
2079
- token_type: z5.string(),
2080
- refresh_token: z5.string()
2081
- });
2082
2668
 
2083
2669
  // src/features/analysis/upload-file.ts
2084
- import Debug8 from "debug";
2670
+ import Debug9 from "debug";
2085
2671
  import fetch2, { File, fileFrom, FormData } from "node-fetch";
2086
- var debug7 = Debug8("mobbdev:upload-file");
2672
+ var debug8 = Debug9("mobbdev:upload-file");
2087
2673
  async function uploadFile({
2088
2674
  file,
2089
2675
  url,
2090
2676
  uploadKey,
2091
2677
  uploadFields
2092
2678
  }) {
2093
- debug7("upload file start %s", url);
2094
- debug7("upload fields %o", uploadFields);
2095
- debug7("upload key %s", uploadKey);
2679
+ debug8("upload file start %s", url);
2680
+ debug8("upload fields %o", uploadFields);
2681
+ debug8("upload key %s", uploadKey);
2096
2682
  const form = new FormData();
2097
2683
  Object.entries(uploadFields).forEach(([key, value]) => {
2098
2684
  form.append(key, value);
@@ -2101,10 +2687,10 @@ async function uploadFile({
2101
2687
  form.append("key", uploadKey);
2102
2688
  }
2103
2689
  if (typeof file === "string") {
2104
- debug7("upload file from path %s", file);
2690
+ debug8("upload file from path %s", file);
2105
2691
  form.append("file", await fileFrom(file));
2106
2692
  } else {
2107
- debug7("upload file from buffer");
2693
+ debug8("upload file from buffer");
2108
2694
  form.append("file", new File([file], "file"));
2109
2695
  }
2110
2696
  const response = await fetch2(url, {
@@ -2112,10 +2698,10 @@ async function uploadFile({
2112
2698
  body: form
2113
2699
  });
2114
2700
  if (!response.ok) {
2115
- debug7("error from S3 %s %s", response.body, response.status);
2701
+ debug8("error from S3 %s %s", response.body, response.status);
2116
2702
  throw new Error(`Failed to upload the file: ${response.status}`);
2117
2703
  }
2118
- debug7("upload file done");
2704
+ debug8("upload file done");
2119
2705
  }
2120
2706
 
2121
2707
  // src/features/analysis/index.ts
@@ -2132,7 +2718,7 @@ async function downloadRepo({
2132
2718
  }) {
2133
2719
  const { createSpinner: createSpinner4 } = Spinner2({ ci });
2134
2720
  const repoSpinner = createSpinner4("\u{1F4BE} Downloading Repo").start();
2135
- debug8("download repo %s %s %s", repoUrl, dirname);
2721
+ debug9("download repo %s %s %s", repoUrl, dirname);
2136
2722
  const zipFilePath = path6.join(dirname, "repo.zip");
2137
2723
  const response = await fetch3(downloadUrl, {
2138
2724
  method: "GET",
@@ -2141,7 +2727,7 @@ async function downloadRepo({
2141
2727
  }
2142
2728
  });
2143
2729
  if (!response.ok) {
2144
- debug8("SCM zipball request failed %s %s", response.body, response.status);
2730
+ debug9("SCM zipball request failed %s %s", response.body, response.status);
2145
2731
  repoSpinner.error({ text: "\u{1F4BE} Repo download failed" });
2146
2732
  throw new Error(`Can't access ${chalk4.bold(repoUrl)}`);
2147
2733
  }
@@ -2155,7 +2741,7 @@ async function downloadRepo({
2155
2741
  if (!repoRoot) {
2156
2742
  throw new Error("Repo root not found");
2157
2743
  }
2158
- debug8("repo root %s", repoRoot);
2744
+ debug9("repo root %s", repoRoot);
2159
2745
  repoSpinner.success({ text: "\u{1F4BE} Repo downloaded successfully" });
2160
2746
  return path6.join(dirname, repoRoot);
2161
2747
  }
@@ -2164,7 +2750,7 @@ var LOGIN_CHECK_DELAY = 5 * 1e3;
2164
2750
  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, ${chalk4.bgBlue(
2165
2751
  "press any key to continue"
2166
2752
  )};`;
2167
- var tmpObj = tmp.dirSync({
2753
+ var tmpObj = tmp2.dirSync({
2168
2754
  unsafeCleanup: true
2169
2755
  });
2170
2756
  var getReportUrl = ({
@@ -2172,7 +2758,7 @@ var getReportUrl = ({
2172
2758
  projectId,
2173
2759
  fixReportId
2174
2760
  }) => `${WEB_APP_URL}/organization/${organizationId}/project/${projectId}/report/${fixReportId}`;
2175
- var debug8 = Debug9("mobbdev:index");
2761
+ var debug9 = Debug10("mobbdev:index");
2176
2762
  var packageJson = JSON.parse(
2177
2763
  fs3.readFileSync(path6.join(getDirName2(), "../package.json"), "utf8")
2178
2764
  );
@@ -2182,7 +2768,7 @@ if (!semver.satisfies(process.version, packageJson.engines.node)) {
2182
2768
  );
2183
2769
  }
2184
2770
  var config2 = new Configstore(packageJson.name, { apiToken: "" });
2185
- debug8("config %o", config2);
2771
+ debug9("config %o", config2);
2186
2772
  async function runAnalysis(params, options) {
2187
2773
  try {
2188
2774
  await _scan(
@@ -2196,20 +2782,23 @@ async function runAnalysis(params, options) {
2196
2782
  tmpObj.removeCallback();
2197
2783
  }
2198
2784
  }
2199
- async function _scan({
2200
- dirname,
2201
- repo,
2202
- scanFile,
2203
- apiKey,
2204
- ci,
2205
- srcPath,
2206
- commitHash,
2207
- ref,
2208
- scanner,
2209
- cxProjectName,
2210
- mobbProjectName
2211
- }, { skipPrompts = false } = {}) {
2212
- debug8("start %s %s", dirname, repo);
2785
+ async function _scan(params, { skipPrompts = false } = {}) {
2786
+ const {
2787
+ dirname,
2788
+ repo,
2789
+ scanFile,
2790
+ apiKey,
2791
+ ci,
2792
+ srcPath,
2793
+ commitHash,
2794
+ ref,
2795
+ scanner,
2796
+ cxProjectName,
2797
+ mobbProjectName,
2798
+ githubToken: githubActionToken,
2799
+ command
2800
+ } = params;
2801
+ debug9("start %s %s", dirname, repo);
2213
2802
  const { createSpinner: createSpinner4 } = Spinner2({ ci });
2214
2803
  skipPrompts = skipPrompts || ci;
2215
2804
  let gqlClient = new GQLClient({
@@ -2265,9 +2854,9 @@ async function _scan({
2265
2854
  });
2266
2855
  const reference = ref ?? await scm.getRepoDefaultBranch();
2267
2856
  const { sha } = await scm.getReferenceData(reference);
2268
- debug8("org id %s", organizationId);
2269
- debug8("project id %s", projectId);
2270
- debug8("default branch %s", reference);
2857
+ debug9("org id %s", organizationId);
2858
+ debug9("project id %s", projectId);
2859
+ debug9("default branch %s", reference);
2271
2860
  const repositoryRoot = await downloadRepo({
2272
2861
  repoUrl: repo,
2273
2862
  dirname,
@@ -2275,8 +2864,8 @@ async function _scan({
2275
2864
  authHeaders: scm.getAuthHeaders(),
2276
2865
  downloadUrl: scm.getDownloadUrl(sha)
2277
2866
  });
2278
- if (scanner) {
2279
- reportPath = await getReport(scanner);
2867
+ if (command === "scan") {
2868
+ reportPath = await getReport(SupportedScannersZ.parse(scanner));
2280
2869
  }
2281
2870
  if (!reportPath) {
2282
2871
  throw new Error("reportPath is null");
@@ -2295,23 +2884,43 @@ async function _scan({
2295
2884
  }
2296
2885
  uploadReportSpinner.success({ text: "\u{1F4C1} Report uploaded successfully" });
2297
2886
  const mobbSpinner = createSpinner4("\u{1F575}\uFE0F\u200D\u2642\uFE0F Initiating Mobb analysis").start();
2298
- try {
2299
- await gqlClient.submitVulnerabilityReport({
2300
- fixReportId: reportUploadInfo.fixReportId,
2301
- repoUrl: repo,
2302
- reference,
2303
- projectId,
2304
- vulnerabilityReportFileName: "report.json",
2305
- sha
2306
- });
2307
- } catch (e) {
2308
- mobbSpinner.error({ text: "\u{1F575}\uFE0F\u200D\u2642\uFE0F Mobb analysis failed" });
2309
- throw e;
2887
+ const sendReportRes = await sendReport();
2888
+ if (command === "review") {
2889
+ await gqlClient.subscribeToAnalysis(
2890
+ { analysisId: sendReportRes.submitVulnerabilityReport.fixReportId },
2891
+ (analysisId) => handleFinishedAnalysis({
2892
+ analysisId,
2893
+ gqlClient,
2894
+ scm,
2895
+ githubActionOctokit: new Octokit3({ auth: githubActionToken }),
2896
+ scanner: z8.nativeEnum(SCANNERS).parse(scanner)
2897
+ })
2898
+ );
2310
2899
  }
2311
2900
  mobbSpinner.success({
2312
2901
  text: "\u{1F575}\uFE0F\u200D\u2642\uFE0F Generating fixes..."
2313
2902
  });
2314
2903
  await askToOpenAnalysis();
2904
+ async function sendReport() {
2905
+ try {
2906
+ const sumbitRes = await gqlClient.submitVulnerabilityReport({
2907
+ fixReportId: reportUploadInfo.fixReportId,
2908
+ repoUrl: z8.string().parse(repo),
2909
+ reference,
2910
+ projectId,
2911
+ vulnerabilityReportFileName: "report.json",
2912
+ sha,
2913
+ pullRequest: params.pullRequest
2914
+ });
2915
+ if (sumbitRes.submitVulnerabilityReport.__typename !== "VulnerabilityReport") {
2916
+ throw new Error("\u{1F575}\uFE0F\u200D\u2642\uFE0F Mobb analysis failed");
2917
+ }
2918
+ return sumbitRes;
2919
+ } catch (e) {
2920
+ mobbSpinner.error({ text: "\u{1F575}\uFE0F\u200D\u2642\uFE0F Mobb analysis failed" });
2921
+ throw e;
2922
+ }
2923
+ }
2315
2924
  async function getReport(scanner2) {
2316
2925
  const reportPath2 = path6.join(dirname, "report.json");
2317
2926
  switch (scanner2) {
@@ -2342,7 +2951,6 @@ async function _scan({
2342
2951
  fixReportId: reportUploadInfo.fixReportId
2343
2952
  });
2344
2953
  !ci && console.log("You can access the analysis at: \n");
2345
- console.log(chalk4.bold(reportUrl));
2346
2954
  !skipPrompts && await mobbAnalysisPrompt();
2347
2955
  !ci && open2(reportUrl);
2348
2956
  !ci && console.log(
@@ -2387,9 +2995,9 @@ async function _scan({
2387
2995
  });
2388
2996
  loginSpinner.spin();
2389
2997
  if (encryptedApiToken) {
2390
- debug8("encrypted API token received %s", encryptedApiToken);
2998
+ debug9("encrypted API token received %s", encryptedApiToken);
2391
2999
  newApiToken = crypto.privateDecrypt(privateKey, Buffer.from(encryptedApiToken, "base64")).toString("utf-8");
2392
- debug8("API token decrypted");
3000
+ debug9("API token decrypted");
2393
3001
  break;
2394
3002
  }
2395
3003
  await sleep(LOGIN_CHECK_DELAY);
@@ -2402,7 +3010,7 @@ async function _scan({
2402
3010
  }
2403
3011
  gqlClient = new GQLClient({ apiKey: newApiToken });
2404
3012
  if (await gqlClient.verifyToken()) {
2405
- debug8("set api token %s", newApiToken);
3013
+ debug9("set api token %s", newApiToken);
2406
3014
  config2.set("apiToken", newApiToken);
2407
3015
  loginSpinner.success({ text: "\u{1F513} Login to Mobb successful!" });
2408
3016
  } else {
@@ -2526,6 +3134,35 @@ async function _scan({
2526
3134
 
2527
3135
  // src/commands/index.ts
2528
3136
  import chalkAnimation from "chalk-animation";
3137
+ async function review(params, { skipPrompts = true } = {}) {
3138
+ const {
3139
+ repo,
3140
+ f: scanFile,
3141
+ ref,
3142
+ apiKey,
3143
+ commitHash,
3144
+ mobbProjectName,
3145
+ pullRequest,
3146
+ githubToken,
3147
+ scanner
3148
+ } = params;
3149
+ await runAnalysis(
3150
+ {
3151
+ repo,
3152
+ scanFile,
3153
+ ref,
3154
+ apiKey,
3155
+ ci: true,
3156
+ commitHash,
3157
+ mobbProjectName,
3158
+ pullRequest,
3159
+ githubToken,
3160
+ scanner,
3161
+ command: "review"
3162
+ },
3163
+ { skipPrompts }
3164
+ );
3165
+ }
2529
3166
  async function analyze({
2530
3167
  repo,
2531
3168
  f: scanFile,
@@ -2546,7 +3183,8 @@ async function analyze({
2546
3183
  ci,
2547
3184
  commitHash,
2548
3185
  mobbProjectName,
2549
- srcPath
3186
+ srcPath,
3187
+ command: "analyze"
2550
3188
  },
2551
3189
  { skipPrompts }
2552
3190
  );
@@ -2565,7 +3203,7 @@ async function scan(scanOptions, { skipPrompts = false } = {}) {
2565
3203
  throw new CliError(errorMessages.missingCxProjectName);
2566
3204
  }
2567
3205
  await runAnalysis(
2568
- { ...scanOptions, scanner: selectedScanner },
3206
+ { ...scanOptions, scanner: selectedScanner, command: "scan" },
2569
3207
  { skipPrompts }
2570
3208
  );
2571
3209
  }
@@ -2631,7 +3269,7 @@ var commitHashOption = {
2631
3269
  // src/args/validation.ts
2632
3270
  import chalk6 from "chalk";
2633
3271
  import path7 from "path";
2634
- import { z as z6 } from "zod";
3272
+ import { z as z9 } from "zod";
2635
3273
  function throwRepoUrlErrorMessage({
2636
3274
  error,
2637
3275
  repoUrl,
@@ -2648,7 +3286,7 @@ Example:
2648
3286
  )}`;
2649
3287
  throw new CliError(formattedErrorMessage);
2650
3288
  }
2651
- var UrlZ = z6.string({
3289
+ var UrlZ = z9.string({
2652
3290
  invalid_type_error: "is not a valid GitHub / GitLab URL"
2653
3291
  }).refine((data) => !!parseScmURL(data), {
2654
3292
  message: "is not a valid GitHub / GitLab URL"
@@ -2728,6 +3366,49 @@ async function analyzeHandler(args) {
2728
3366
  await analyze(args, { skipPrompts: args.yes });
2729
3367
  }
2730
3368
 
3369
+ // src/args/commands/review.ts
3370
+ import fs5 from "node:fs";
3371
+ import chalk8 from "chalk";
3372
+ function reviewBuilder(yargs2) {
3373
+ return yargs2.option("f", {
3374
+ alias: "scan-file",
3375
+ demandOption: true,
3376
+ type: "string",
3377
+ describe: chalk8.bold(
3378
+ "Select the vulnerability report to analyze (Checkmarx, Snyk, Fortify, CodeQL)"
3379
+ )
3380
+ }).option("repo", { ...repoOption, demandOption: true }).option("scanner", { ...scannerOptions, demandOption: true }).option("ref", { ...refOption, demandOption: true }).option("ch", {
3381
+ alias: "commit-hash",
3382
+ describe: chalk8.bold("Hash of the commit"),
3383
+ type: "string",
3384
+ demandOption: true
3385
+ }).option("mobb-project-name", mobbProjectNameOption).option("api-key", { ...apiKeyOption, demandOption: true }).option("commit-hash", { ...commitHashOption, demandOption: true }).option("github-token", {
3386
+ describe: chalk8.bold("Github action token"),
3387
+ type: "string",
3388
+ demandOption: true
3389
+ }).option("pull-request", {
3390
+ alias: "pr",
3391
+ describe: chalk8.bold("Number of the pull request"),
3392
+ type: "number",
3393
+ demandOption: true
3394
+ }).example(
3395
+ "$0 review -r https://github.com/WebGoat/WebGoat -f <your_vulirabitliy_report_path> --ch <pr_last_commit> --pr <pr_number> --ref <pr_branch_name> --api-key <api_key>",
3396
+ "add fixes to your pr"
3397
+ ).help();
3398
+ }
3399
+ function validateReviewOptions(argv) {
3400
+ if (!fs5.existsSync(argv.f)) {
3401
+ throw new CliError(`
3402
+ Can't access ${chalk8.bold(argv.f)}`);
3403
+ }
3404
+ validateRepoUrl(argv);
3405
+ validateReportFileFormat(argv.f);
3406
+ }
3407
+ async function reviewHandler(args) {
3408
+ validateReviewOptions(args);
3409
+ await review(args, { skipPrompts: true });
3410
+ }
3411
+
2731
3412
  // src/args/commands/scan.ts
2732
3413
  function scanBuilder(args) {
2733
3414
  return args.coerce("scanner", (arg) => arg.toLowerCase()).option("repo", repoOption).option("ref", refOption).option("scanner", scannerOptions).option("mobb-project-name", mobbProjectNameOption).option("y", yesOption).option("ci", ciOption).option("api-key", apiKeyOption).option("cx-project-name", projectNameOption).example(
@@ -2756,32 +3437,39 @@ async function scanHandler(args) {
2756
3437
  var parseArgs = async (args) => {
2757
3438
  const yargsInstance = yargs(args);
2758
3439
  return yargsInstance.updateStrings({
2759
- "Commands:": chalk8.yellow.underline.bold("Commands:"),
2760
- "Options:": chalk8.yellow.underline.bold("Options:"),
2761
- "Examples:": chalk8.yellow.underline.bold("Examples:"),
2762
- "Show help": chalk8.bold("Show help")
3440
+ "Commands:": chalk9.yellow.underline.bold("Commands:"),
3441
+ "Options:": chalk9.yellow.underline.bold("Options:"),
3442
+ "Examples:": chalk9.yellow.underline.bold("Examples:"),
3443
+ "Show help": chalk9.bold("Show help")
2763
3444
  }).usage(
2764
- `${chalk8.bold(
3445
+ `${chalk9.bold(
2765
3446
  "\n Bugsy - Trusted, Automatic Vulnerability Fixer \u{1F575}\uFE0F\u200D\u2642\uFE0F\n\n"
2766
- )} ${chalk8.yellow.underline.bold("Usage:")}
2767
- $0 ${chalk8.green(
3447
+ )} ${chalk9.yellow.underline.bold("Usage:")}
3448
+ $0 ${chalk9.green(
2768
3449
  "<command>"
2769
- )} ${chalk8.dim("[options]")}
3450
+ )} ${chalk9.dim("[options]")}
2770
3451
  `
2771
3452
  ).version(false).command(
2772
- "scan",
2773
- chalk8.bold(
3453
+ mobbCliCommand.scan,
3454
+ chalk9.bold(
2774
3455
  "Scan your code for vulnerabilities, get automated fixes right away."
2775
3456
  ),
2776
3457
  scanBuilder,
2777
3458
  scanHandler
2778
3459
  ).command(
2779
- "analyze",
2780
- chalk8.bold(
3460
+ mobbCliCommand.analyze,
3461
+ chalk9.bold(
2781
3462
  "Provide a vulnerability report and relevant code repository, get automated fixes right away."
2782
3463
  ),
2783
3464
  analyzeBuilder,
2784
3465
  analyzeHandler
3466
+ ).command(
3467
+ mobbCliCommand.review,
3468
+ chalk9.bold(
3469
+ "(beta) Mobb will review your github pull requests and provide comments with fixes "
3470
+ ),
3471
+ reviewBuilder,
3472
+ reviewHandler
2785
3473
  ).example(
2786
3474
  "$0 scan -r https://github.com/WebGoat/WebGoat",
2787
3475
  "Scan an existing repository"
@@ -2790,7 +3478,7 @@ var parseArgs = async (args) => {
2790
3478
  handler() {
2791
3479
  yargsInstance.showHelp();
2792
3480
  }
2793
- }).strictOptions().help("h").alias("h", "help").epilog(chalk8.bgBlue("Made with \u2764\uFE0F by Mobb")).showHelpOnFail(true).wrap(Math.min(120, yargsInstance.terminalWidth())).parse();
3481
+ }).strictOptions().help("h").alias("h", "help").epilog(chalk9.bgBlue("Made with \u2764\uFE0F by Mobb")).showHelpOnFail(true).wrap(Math.min(120, yargsInstance.terminalWidth())).parse();
2794
3482
  };
2795
3483
 
2796
3484
  // src/index.ts