mobbdev 1.0.106 → 1.0.108
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +923 -396
- package/package.json +8 -8
package/dist/index.mjs
CHANGED
|
@@ -540,6 +540,9 @@ var FixDetailsFragmentDoc = `
|
|
|
540
540
|
vulnerability_report_issue_tag_value
|
|
541
541
|
}
|
|
542
542
|
}
|
|
543
|
+
sharedState {
|
|
544
|
+
id
|
|
545
|
+
}
|
|
543
546
|
patchAndQuestions {
|
|
544
547
|
__typename
|
|
545
548
|
... on FixData {
|
|
@@ -1062,19 +1065,33 @@ var AutoPrAnalysisDocument = `
|
|
|
1062
1065
|
var GetLatestReportByRepoUrlDocument = `
|
|
1063
1066
|
query GetLatestReportByRepoUrl($repoUrl: String!, $filters: fix_bool_exp = {}, $limit: Int!, $offset: Int!) {
|
|
1064
1067
|
fixReport(
|
|
1065
|
-
where: {repo: {originalUrl: {_eq: $repoUrl}}}
|
|
1068
|
+
where: {_and: [{repo: {originalUrl: {_eq: $repoUrl}}}, {state: {_eq: Finished}}]}
|
|
1066
1069
|
order_by: {createdOn: desc}
|
|
1067
1070
|
limit: 1
|
|
1068
1071
|
) {
|
|
1069
1072
|
...FixReportSummaryFields
|
|
1070
1073
|
}
|
|
1074
|
+
expiredReport: fixReport(
|
|
1075
|
+
where: {_and: [{repo: {originalUrl: {_eq: $repoUrl}}}, {state: {_eq: Expired}}]}
|
|
1076
|
+
order_by: {createdOn: desc}
|
|
1077
|
+
limit: 1
|
|
1078
|
+
) {
|
|
1079
|
+
id
|
|
1080
|
+
expirationOn
|
|
1081
|
+
}
|
|
1071
1082
|
}
|
|
1072
1083
|
${FixReportSummaryFieldsFragmentDoc}`;
|
|
1073
1084
|
var GetReportFixesDocument = `
|
|
1074
1085
|
query GetReportFixes($reportId: uuid!, $filters: fix_bool_exp = {}, $limit: Int!, $offset: Int!) {
|
|
1075
|
-
fixReport(where: {id: {_eq: $reportId}}) {
|
|
1086
|
+
fixReport(where: {_and: [{id: {_eq: $reportId}}, {state: {_eq: Finished}}]}) {
|
|
1076
1087
|
...FixReportSummaryFields
|
|
1077
1088
|
}
|
|
1089
|
+
expiredReport: fixReport(
|
|
1090
|
+
where: {_and: [{id: {_eq: $reportId}}, {state: {_eq: Expired}}]}
|
|
1091
|
+
) {
|
|
1092
|
+
id
|
|
1093
|
+
expirationOn
|
|
1094
|
+
}
|
|
1078
1095
|
}
|
|
1079
1096
|
${FixReportSummaryFieldsFragmentDoc}`;
|
|
1080
1097
|
var defaultWrapper = (action, _operationName, _operationType, _variables) => action();
|
|
@@ -4430,9 +4447,15 @@ import { z as z15 } from "zod";
|
|
|
4430
4447
|
var EnvVariablesZod = z15.object({
|
|
4431
4448
|
GITLAB_API_TOKEN: z15.string().optional(),
|
|
4432
4449
|
GITHUB_API_TOKEN: z15.string().optional(),
|
|
4433
|
-
GIT_PROXY_HOST: z15.string().optional().default("http://tinyproxy:8888")
|
|
4450
|
+
GIT_PROXY_HOST: z15.string().optional().default("http://tinyproxy:8888"),
|
|
4451
|
+
MAX_UPLOAD_FILE_SIZE_MB: z15.coerce.number().gt(0).default(5)
|
|
4434
4452
|
});
|
|
4435
|
-
var {
|
|
4453
|
+
var {
|
|
4454
|
+
GITLAB_API_TOKEN,
|
|
4455
|
+
GITHUB_API_TOKEN,
|
|
4456
|
+
GIT_PROXY_HOST,
|
|
4457
|
+
MAX_UPLOAD_FILE_SIZE_MB
|
|
4458
|
+
} = EnvVariablesZod.parse(process.env);
|
|
4436
4459
|
|
|
4437
4460
|
// src/features/analysis/scm/ado/validation.ts
|
|
4438
4461
|
import { z as z16 } from "zod";
|
|
@@ -5062,6 +5085,18 @@ import { setTimeout as setTimeout2 } from "timers/promises";
|
|
|
5062
5085
|
import * as path2 from "path";
|
|
5063
5086
|
import { simpleGit } from "simple-git";
|
|
5064
5087
|
|
|
5088
|
+
// src/mcp/core/configs.ts
|
|
5089
|
+
var MCP_DEFAULT_API_URL = "https://api.mobb.ai/v1/graphql";
|
|
5090
|
+
var MCP_API_KEY_HEADER_NAME = "x-mobb-key";
|
|
5091
|
+
var MCP_LOGIN_MAX_WAIT = 10 * 60 * 1e3;
|
|
5092
|
+
var MCP_LOGIN_CHECK_DELAY = 1 * 1e3;
|
|
5093
|
+
var MCP_VUL_REPORT_DIGEST_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
5094
|
+
var MCP_MAX_FILE_SIZE = MAX_UPLOAD_FILE_SIZE_MB * 1024 * 1024;
|
|
5095
|
+
var MCP_PERIODIC_CHECK_INTERVAL = 15 * 60 * 1e3;
|
|
5096
|
+
var MCP_DEFAULT_MAX_FILES_TO_SCAN = 10;
|
|
5097
|
+
var MCP_REPORT_ID_EXPIRATION_MS = 2 * 60 * 60 * 1e3;
|
|
5098
|
+
var MCP_TOOLS_BROWSER_COOLDOWN_MS = 24 * 60 * 60 * 1e3;
|
|
5099
|
+
|
|
5065
5100
|
// src/features/analysis/scm/FileUtils.ts
|
|
5066
5101
|
import fs2 from "fs";
|
|
5067
5102
|
import { isBinary } from "istextorbinary";
|
|
@@ -5069,6 +5104,9 @@ import path from "path";
|
|
|
5069
5104
|
var EXCLUDED_FILE_PATTERNS = [
|
|
5070
5105
|
// ... (copy the full array from FilePacking.ts)
|
|
5071
5106
|
".json",
|
|
5107
|
+
".snap",
|
|
5108
|
+
".env.vault",
|
|
5109
|
+
".env",
|
|
5072
5110
|
".yaml",
|
|
5073
5111
|
".yml",
|
|
5074
5112
|
".toml",
|
|
@@ -5220,16 +5258,24 @@ var FileUtils = class {
|
|
|
5220
5258
|
}
|
|
5221
5259
|
static shouldPackFile(filepath, maxFileSize = 1024 * 1024 * 5) {
|
|
5222
5260
|
const absoluteFilepath = path.resolve(filepath);
|
|
5223
|
-
if (this.isExcludedFileType(filepath))
|
|
5224
|
-
|
|
5225
|
-
|
|
5261
|
+
if (this.isExcludedFileType(filepath)) {
|
|
5262
|
+
return false;
|
|
5263
|
+
}
|
|
5264
|
+
if (!fs2.existsSync(absoluteFilepath)) {
|
|
5265
|
+
return false;
|
|
5266
|
+
}
|
|
5267
|
+
if (fs2.lstatSync(absoluteFilepath).size > maxFileSize) {
|
|
5268
|
+
return false;
|
|
5269
|
+
}
|
|
5226
5270
|
let data;
|
|
5227
5271
|
try {
|
|
5228
5272
|
data = fs2.readFileSync(absoluteFilepath);
|
|
5229
5273
|
} catch {
|
|
5230
5274
|
return false;
|
|
5231
5275
|
}
|
|
5232
|
-
if (isBinary(null, data))
|
|
5276
|
+
if (isBinary(null, data)) {
|
|
5277
|
+
return false;
|
|
5278
|
+
}
|
|
5233
5279
|
return true;
|
|
5234
5280
|
}
|
|
5235
5281
|
static getAllFiles(dir, rootDir) {
|
|
@@ -5239,7 +5285,7 @@ var FileUtils = class {
|
|
|
5239
5285
|
if (relativeDepth > 20) {
|
|
5240
5286
|
return [];
|
|
5241
5287
|
}
|
|
5242
|
-
if (results.length >
|
|
5288
|
+
if (results.length > 1e3) {
|
|
5243
5289
|
return [];
|
|
5244
5290
|
}
|
|
5245
5291
|
try {
|
|
@@ -5270,10 +5316,14 @@ var FileUtils = class {
|
|
|
5270
5316
|
}
|
|
5271
5317
|
return results;
|
|
5272
5318
|
}
|
|
5273
|
-
static getLastChangedFiles(
|
|
5319
|
+
static getLastChangedFiles({
|
|
5320
|
+
dir,
|
|
5321
|
+
maxFileSize,
|
|
5322
|
+
maxFiles = MCP_DEFAULT_MAX_FILES_TO_SCAN
|
|
5323
|
+
}) {
|
|
5274
5324
|
if (!fs2.existsSync(dir) || !fs2.lstatSync(dir).isDirectory()) return [];
|
|
5275
5325
|
const files = this.getAllFiles(dir);
|
|
5276
|
-
return files.filter((file) => this.shouldPackFile(file.fullPath, maxFileSize)).sort((a, b) => b.time - a.time).slice(0,
|
|
5326
|
+
return files.filter((file) => this.shouldPackFile(file.fullPath, maxFileSize)).sort((a, b) => b.time - a.time).slice(0, maxFiles).map((file) => file.relativePath);
|
|
5277
5327
|
}
|
|
5278
5328
|
};
|
|
5279
5329
|
|
|
@@ -5322,7 +5372,10 @@ var GitService = class {
|
|
|
5322
5372
|
gitRoot,
|
|
5323
5373
|
this.repositoryPath
|
|
5324
5374
|
);
|
|
5325
|
-
const
|
|
5375
|
+
const deletedFiles = status.files.filter((file) => file.index === "D" || file.working_dir === "D").map((file) => file.path);
|
|
5376
|
+
const files = status.files.filter((file) => {
|
|
5377
|
+
return !(file.index === "D" || file.working_dir === "D");
|
|
5378
|
+
}).map((file) => {
|
|
5326
5379
|
const gitRelativePath = file.path;
|
|
5327
5380
|
if (relativePathFromGitRoot === "") {
|
|
5328
5381
|
return gitRelativePath;
|
|
@@ -5339,11 +5392,13 @@ var GitService = class {
|
|
|
5339
5392
|
fileCount: files.length,
|
|
5340
5393
|
files: files.slice(0, 10),
|
|
5341
5394
|
// Log first 10 files to avoid spam
|
|
5395
|
+
deletedFileCount: deletedFiles.length,
|
|
5396
|
+
deletedFiles: deletedFiles.slice(0, 10),
|
|
5342
5397
|
gitRoot,
|
|
5343
5398
|
workingDir: this.repositoryPath,
|
|
5344
5399
|
relativePathFromGitRoot
|
|
5345
5400
|
});
|
|
5346
|
-
return { files, status };
|
|
5401
|
+
return { files, deletedFiles, status };
|
|
5347
5402
|
} catch (error) {
|
|
5348
5403
|
const errorMessage = `Failed to get git status: ${error.message}`;
|
|
5349
5404
|
this.log(errorMessage, "error", { error });
|
|
@@ -5468,11 +5523,13 @@ var GitService = class {
|
|
|
5468
5523
|
}
|
|
5469
5524
|
}
|
|
5470
5525
|
/**
|
|
5471
|
-
* Gets the
|
|
5526
|
+
* Gets the maxFiles most recently changed files based on commit history
|
|
5472
5527
|
*/
|
|
5473
|
-
async getRecentlyChangedFiles(
|
|
5528
|
+
async getRecentlyChangedFiles({
|
|
5529
|
+
maxFiles = MCP_DEFAULT_MAX_FILES_TO_SCAN
|
|
5530
|
+
}) {
|
|
5474
5531
|
this.log(
|
|
5475
|
-
|
|
5532
|
+
`Getting the ${maxFiles} most recently changed files from commit history`,
|
|
5476
5533
|
"debug"
|
|
5477
5534
|
);
|
|
5478
5535
|
try {
|
|
@@ -5485,19 +5542,18 @@ var GitService = class {
|
|
|
5485
5542
|
const files = [];
|
|
5486
5543
|
let commitsProcessed = 0;
|
|
5487
5544
|
const logResult = await this.git.log({
|
|
5488
|
-
maxCount:
|
|
5489
|
-
//
|
|
5545
|
+
maxCount: maxFiles * 5,
|
|
5546
|
+
// 5 times the max files to scan to ensure we find enough files
|
|
5490
5547
|
format: {
|
|
5491
5548
|
hash: "%H",
|
|
5492
5549
|
date: "%ai",
|
|
5493
5550
|
message: "%s",
|
|
5494
5551
|
//the field name author_name can't follow the naming convention as we are using the git log command
|
|
5495
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
5496
5552
|
author_name: "%an"
|
|
5497
5553
|
}
|
|
5498
5554
|
});
|
|
5499
5555
|
for (const commit of logResult.all) {
|
|
5500
|
-
if (files.length >=
|
|
5556
|
+
if (files.length >= maxFiles) {
|
|
5501
5557
|
break;
|
|
5502
5558
|
}
|
|
5503
5559
|
commitsProcessed++;
|
|
@@ -5509,7 +5565,7 @@ var GitService = class {
|
|
|
5509
5565
|
]);
|
|
5510
5566
|
const commitFiles = filesOutput.split("\n").filter((file) => file.trim() !== "");
|
|
5511
5567
|
for (const file of commitFiles) {
|
|
5512
|
-
if (files.length >=
|
|
5568
|
+
if (files.length >= maxFiles) {
|
|
5513
5569
|
break;
|
|
5514
5570
|
}
|
|
5515
5571
|
const gitRelativePath = file.trim();
|
|
@@ -5527,7 +5583,7 @@ var GitService = class {
|
|
|
5527
5583
|
);
|
|
5528
5584
|
}
|
|
5529
5585
|
this.log(`Considering file: ${adjustedPath}`, "debug");
|
|
5530
|
-
if (!fileSet.has(adjustedPath) && FileUtils.shouldPackFile(path2.join(gitRoot, gitRelativePath))) {
|
|
5586
|
+
if (!fileSet.has(adjustedPath) && FileUtils.shouldPackFile(path2.join(gitRoot, gitRelativePath)) && !adjustedPath.startsWith("..")) {
|
|
5531
5587
|
fileSet.add(adjustedPath);
|
|
5532
5588
|
files.push(adjustedPath);
|
|
5533
5589
|
}
|
|
@@ -5542,8 +5598,8 @@ var GitService = class {
|
|
|
5542
5598
|
fileCount: files.length,
|
|
5543
5599
|
commitsProcessed,
|
|
5544
5600
|
totalCommitsAvailable: logResult.all.length,
|
|
5545
|
-
files: files.slice(0,
|
|
5546
|
-
// Log the files (should be all of them since we limit to
|
|
5601
|
+
files: files.slice(0, maxFiles),
|
|
5602
|
+
// Log the files (should be all of them since we limit to maxFiles)
|
|
5547
5603
|
gitRoot,
|
|
5548
5604
|
workingDir: this.repositoryPath,
|
|
5549
5605
|
relativePathFromGitRoot
|
|
@@ -10046,8 +10102,8 @@ async function forkSnyk(args, { display }) {
|
|
|
10046
10102
|
}
|
|
10047
10103
|
async function getSnykReport(reportPath, repoRoot, { skipPrompts = false }) {
|
|
10048
10104
|
debug15("get snyk report start %s %s", reportPath, repoRoot);
|
|
10049
|
-
const
|
|
10050
|
-
const { message: configMessage } =
|
|
10105
|
+
const config4 = await forkSnyk(["config"], { display: false });
|
|
10106
|
+
const { message: configMessage } = config4;
|
|
10051
10107
|
if (!configMessage.includes("api: ")) {
|
|
10052
10108
|
const snykLoginSpinner = createSpinner3().start();
|
|
10053
10109
|
if (!skipPrompts) {
|
|
@@ -10059,7 +10115,7 @@ async function getSnykReport(reportPath, repoRoot, { skipPrompts = false }) {
|
|
|
10059
10115
|
snykLoginSpinner.update({
|
|
10060
10116
|
text: "\u{1F513} Waiting for Snyk login to complete"
|
|
10061
10117
|
});
|
|
10062
|
-
debug15("no token in the config %s",
|
|
10118
|
+
debug15("no token in the config %s", config4);
|
|
10063
10119
|
await forkSnyk(["auth"], { display: true });
|
|
10064
10120
|
snykLoginSpinner.success({ text: "\u{1F513} Login to Snyk Successful" });
|
|
10065
10121
|
}
|
|
@@ -11222,7 +11278,8 @@ var Logger = class {
|
|
|
11222
11278
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11223
11279
|
level: logEntry.level,
|
|
11224
11280
|
message: logEntry.message,
|
|
11225
|
-
data: logEntry.data
|
|
11281
|
+
data: logEntry.data,
|
|
11282
|
+
version: packageJson.version
|
|
11226
11283
|
};
|
|
11227
11284
|
const controller = new AbortController();
|
|
11228
11285
|
const timeoutId = setTimeout(() => {
|
|
@@ -11275,11 +11332,7 @@ import { GraphQLClient as GraphQLClient2 } from "graphql-request";
|
|
|
11275
11332
|
import open4 from "open";
|
|
11276
11333
|
import { v4 as uuidv42 } from "uuid";
|
|
11277
11334
|
|
|
11278
|
-
// src/mcp/
|
|
11279
|
-
var DEFAULT_API_URL2 = "https://api.mobb.ai/v1/graphql";
|
|
11280
|
-
var API_KEY_HEADER_NAME2 = "x-mobb-key";
|
|
11281
|
-
|
|
11282
|
-
// src/mcp/tools/fixVulnerabilities/errors/VulnerabilityFixErrors.ts
|
|
11335
|
+
// src/mcp/core/Errors.ts
|
|
11283
11336
|
var ApiConnectionError = class extends Error {
|
|
11284
11337
|
constructor(message = "Failed to connect to the API") {
|
|
11285
11338
|
super(message);
|
|
@@ -11342,20 +11395,17 @@ var FailedToGetApiTokenError = class extends Error {
|
|
|
11342
11395
|
};
|
|
11343
11396
|
|
|
11344
11397
|
// src/mcp/services/McpGQLClient.ts
|
|
11345
|
-
var
|
|
11346
|
-
var LOGIN_CHECK_DELAY2 = 1 * 1e3;
|
|
11347
|
-
var config4 = new Configstore3(packageJson.name, { apiToken: "" });
|
|
11348
|
-
var BROWSER_COOLDOWN_MS = 5e3;
|
|
11349
|
-
var lastBrowserOpenTime = 0;
|
|
11398
|
+
var mobbConfigStore = new Configstore3(packageJson.name, { apiToken: "" });
|
|
11350
11399
|
var McpGQLClient = class {
|
|
11351
11400
|
constructor(args) {
|
|
11352
11401
|
__publicField(this, "client");
|
|
11353
11402
|
__publicField(this, "clientSdk");
|
|
11354
11403
|
__publicField(this, "_auth");
|
|
11355
11404
|
this._auth = args;
|
|
11356
|
-
const API_URL2 = process.env["API_URL"] ||
|
|
11405
|
+
const API_URL2 = process.env["API_URL"] || MCP_DEFAULT_API_URL;
|
|
11406
|
+
logDebug("creating graphql client", { API_URL: API_URL2, args });
|
|
11357
11407
|
this.client = new GraphQLClient2(API_URL2, {
|
|
11358
|
-
headers: args.type === "apiKey" ? { [
|
|
11408
|
+
headers: args.type === "apiKey" ? { [MCP_API_KEY_HEADER_NAME]: args.apiKey || "" } : {
|
|
11359
11409
|
Authorization: `Bearer ${args.token}`
|
|
11360
11410
|
},
|
|
11361
11411
|
requestMiddleware: (request) => {
|
|
@@ -11373,10 +11423,10 @@ var McpGQLClient = class {
|
|
|
11373
11423
|
}
|
|
11374
11424
|
getErrorContext() {
|
|
11375
11425
|
return {
|
|
11376
|
-
endpoint: process.env["API_URL"] ||
|
|
11426
|
+
endpoint: process.env["API_URL"] || MCP_DEFAULT_API_URL,
|
|
11377
11427
|
apiKey: this._auth.type === "apiKey" ? this._auth.apiKey : "",
|
|
11378
11428
|
headers: {
|
|
11379
|
-
[
|
|
11429
|
+
[MCP_API_KEY_HEADER_NAME]: this._auth.type === "apiKey" ? "[REDACTED]" : "undefined",
|
|
11380
11430
|
"x-hasura-request-id": "[DYNAMIC]"
|
|
11381
11431
|
}
|
|
11382
11432
|
};
|
|
@@ -11385,11 +11435,13 @@ var McpGQLClient = class {
|
|
|
11385
11435
|
try {
|
|
11386
11436
|
logDebug("GraphQL: Calling Me query for connection verification");
|
|
11387
11437
|
const result = await this.clientSdk.Me();
|
|
11388
|
-
|
|
11438
|
+
logDebug("GraphQL: Me query successful", { result });
|
|
11389
11439
|
return true;
|
|
11390
11440
|
} catch (e) {
|
|
11391
|
-
|
|
11392
|
-
|
|
11441
|
+
const error = e;
|
|
11442
|
+
logDebug(`verify connection failed ${error.toString()}`);
|
|
11443
|
+
if (error?.toString().includes("FetchError")) {
|
|
11444
|
+
logError("verify connection failed", { error });
|
|
11393
11445
|
return false;
|
|
11394
11446
|
}
|
|
11395
11447
|
}
|
|
@@ -11561,7 +11613,7 @@ var McpGQLClient = class {
|
|
|
11561
11613
|
try {
|
|
11562
11614
|
const res = await this.clientSdk.CreateCliLogin(variables, {
|
|
11563
11615
|
// We may have outdated API key in the config storage. Avoid using it for the login request.
|
|
11564
|
-
[
|
|
11616
|
+
[MCP_API_KEY_HEADER_NAME]: ""
|
|
11565
11617
|
});
|
|
11566
11618
|
const loginId = res.insert_cli_login_one?.id || "";
|
|
11567
11619
|
if (!loginId) {
|
|
@@ -11578,7 +11630,7 @@ var McpGQLClient = class {
|
|
|
11578
11630
|
try {
|
|
11579
11631
|
const res = await this.clientSdk.GetEncryptedApiToken(variables, {
|
|
11580
11632
|
// We may have outdated API key in the config storage. Avoid using it for the login request.
|
|
11581
|
-
[
|
|
11633
|
+
[MCP_API_KEY_HEADER_NAME]: ""
|
|
11582
11634
|
});
|
|
11583
11635
|
return res?.cli_login_by_pk?.encryptedApiToken || null;
|
|
11584
11636
|
} catch (e) {
|
|
@@ -11606,7 +11658,10 @@ var McpGQLClient = class {
|
|
|
11606
11658
|
result: res,
|
|
11607
11659
|
reportCount: res.fixReport?.length || 0
|
|
11608
11660
|
});
|
|
11609
|
-
return
|
|
11661
|
+
return {
|
|
11662
|
+
fixReport: res.fixReport?.[0] || null,
|
|
11663
|
+
expiredReport: res.expiredReport?.[0] || null
|
|
11664
|
+
};
|
|
11610
11665
|
} catch (e) {
|
|
11611
11666
|
logError("GraphQL: GetLatestReportByRepoUrl failed", {
|
|
11612
11667
|
error: e,
|
|
@@ -11655,7 +11710,8 @@ var McpGQLClient = class {
|
|
|
11655
11710
|
}
|
|
11656
11711
|
return {
|
|
11657
11712
|
fixes: res.fixReport?.[0]?.fixes || [],
|
|
11658
|
-
totalCount: res.fixReport?.[0]?.filteredFixesCount?.aggregate?.count || 0
|
|
11713
|
+
totalCount: res.fixReport?.[0]?.filteredFixesCount?.aggregate?.count || 0,
|
|
11714
|
+
expiredReport: res.expiredReport?.[0] || null
|
|
11659
11715
|
};
|
|
11660
11716
|
} catch (e) {
|
|
11661
11717
|
logError("GraphQL: GetReportFixes failed", {
|
|
@@ -11667,30 +11723,39 @@ var McpGQLClient = class {
|
|
|
11667
11723
|
}
|
|
11668
11724
|
}
|
|
11669
11725
|
};
|
|
11670
|
-
async function openBrowser(url) {
|
|
11671
|
-
|
|
11672
|
-
|
|
11673
|
-
|
|
11674
|
-
|
|
11726
|
+
async function openBrowser(url, isToolsCall) {
|
|
11727
|
+
if (isToolsCall) {
|
|
11728
|
+
const now = Date.now();
|
|
11729
|
+
const lastBrowserOpenTime = mobbConfigStore.get("lastBrowserOpenTime") || 0;
|
|
11730
|
+
if (now - lastBrowserOpenTime < MCP_TOOLS_BROWSER_COOLDOWN_MS) {
|
|
11731
|
+
logDebug(`browser cooldown active, skipping open for ${url}`);
|
|
11732
|
+
return;
|
|
11733
|
+
}
|
|
11675
11734
|
}
|
|
11676
11735
|
logDebug(`opening browser url ${url}`);
|
|
11677
11736
|
await open4(url);
|
|
11678
|
-
lastBrowserOpenTime
|
|
11737
|
+
mobbConfigStore.set("lastBrowserOpenTime", Date.now());
|
|
11679
11738
|
}
|
|
11680
|
-
async function getMcpGQLClient(
|
|
11681
|
-
|
|
11739
|
+
async function getMcpGQLClient({
|
|
11740
|
+
isToolsCall = false
|
|
11741
|
+
} = {}) {
|
|
11742
|
+
logDebug("getting config", { apiToken: mobbConfigStore.get("apiToken") });
|
|
11682
11743
|
const inGqlClient = new McpGQLClient({
|
|
11683
|
-
apiKey:
|
|
11744
|
+
apiKey: process.env["MOBB_API_KEY"] || process.env["API_KEY"] || // fallback for backward compatibility
|
|
11745
|
+
mobbConfigStore.get("apiToken") || "",
|
|
11684
11746
|
type: "apiKey"
|
|
11685
11747
|
});
|
|
11686
11748
|
const isConnected = await inGqlClient.verifyConnection();
|
|
11749
|
+
logDebug("isConnected", { isConnected });
|
|
11687
11750
|
if (!isConnected) {
|
|
11688
11751
|
throw new ApiConnectionError("Error: failed to connect to Mobb API");
|
|
11689
11752
|
}
|
|
11753
|
+
logDebug("verifying token");
|
|
11690
11754
|
const userVerify = await inGqlClient.verifyToken();
|
|
11691
11755
|
if (userVerify) {
|
|
11692
11756
|
return inGqlClient;
|
|
11693
11757
|
}
|
|
11758
|
+
logDebug("verifying token failed");
|
|
11694
11759
|
const { publicKey, privateKey } = crypto2.generateKeyPairSync("rsa", {
|
|
11695
11760
|
modulusLength: 2048
|
|
11696
11761
|
});
|
|
@@ -11705,10 +11770,10 @@ async function getMcpGQLClient() {
|
|
|
11705
11770
|
const webLoginUrl2 = `${WEB_APP_URL}/cli-login`;
|
|
11706
11771
|
const browserUrl = `${webLoginUrl2}/${loginId}?hostname=${os2.hostname()}`;
|
|
11707
11772
|
logDebug(`opening browser url ${browserUrl}`);
|
|
11708
|
-
await openBrowser(browserUrl);
|
|
11773
|
+
await openBrowser(browserUrl, isToolsCall);
|
|
11709
11774
|
logDebug(`waiting for login to complete`);
|
|
11710
11775
|
let newApiToken = null;
|
|
11711
|
-
for (let i = 0; i <
|
|
11776
|
+
for (let i = 0; i < MCP_LOGIN_MAX_WAIT / MCP_LOGIN_CHECK_DELAY; i++) {
|
|
11712
11777
|
const encryptedApiToken = await inGqlClient.getEncryptedApiToken({
|
|
11713
11778
|
loginId
|
|
11714
11779
|
});
|
|
@@ -11718,7 +11783,7 @@ async function getMcpGQLClient() {
|
|
|
11718
11783
|
logDebug("API token decrypted");
|
|
11719
11784
|
break;
|
|
11720
11785
|
}
|
|
11721
|
-
await sleep(
|
|
11786
|
+
await sleep(MCP_LOGIN_CHECK_DELAY);
|
|
11722
11787
|
}
|
|
11723
11788
|
if (!newApiToken) {
|
|
11724
11789
|
throw new FailedToGetApiTokenError(
|
|
@@ -11729,7 +11794,7 @@ async function getMcpGQLClient() {
|
|
|
11729
11794
|
const loginSuccess = await newGqlClient.verifyToken();
|
|
11730
11795
|
if (loginSuccess) {
|
|
11731
11796
|
logDebug(`set api token ${newApiToken}`);
|
|
11732
|
-
|
|
11797
|
+
mobbConfigStore.set("apiToken", newApiToken);
|
|
11733
11798
|
} else {
|
|
11734
11799
|
throw new AuthenticationError("Invalid API token");
|
|
11735
11800
|
}
|
|
@@ -11772,14 +11837,14 @@ var ToolRegistry = class {
|
|
|
11772
11837
|
|
|
11773
11838
|
// src/mcp/core/McpServer.ts
|
|
11774
11839
|
var McpServer = class {
|
|
11775
|
-
constructor(
|
|
11840
|
+
constructor(config4) {
|
|
11776
11841
|
__publicField(this, "server");
|
|
11777
11842
|
__publicField(this, "toolRegistry");
|
|
11778
11843
|
__publicField(this, "isEventHandlersSetup", false);
|
|
11779
11844
|
this.server = new Server(
|
|
11780
11845
|
{
|
|
11781
|
-
name:
|
|
11782
|
-
version:
|
|
11846
|
+
name: config4.name,
|
|
11847
|
+
version: config4.version
|
|
11783
11848
|
},
|
|
11784
11849
|
{
|
|
11785
11850
|
capabilities: {
|
|
@@ -11790,7 +11855,7 @@ var McpServer = class {
|
|
|
11790
11855
|
this.toolRegistry = new ToolRegistry();
|
|
11791
11856
|
this.setupHandlers();
|
|
11792
11857
|
this.setupProcessEventHandlers();
|
|
11793
|
-
logInfo("MCP server instance created",
|
|
11858
|
+
logInfo("MCP server instance created", config4);
|
|
11794
11859
|
}
|
|
11795
11860
|
setupProcessEventHandlers() {
|
|
11796
11861
|
if (this.isEventHandlersSetup) {
|
|
@@ -11838,16 +11903,10 @@ var McpServer = class {
|
|
|
11838
11903
|
}
|
|
11839
11904
|
async handleListToolsRequest(request) {
|
|
11840
11905
|
logInfo("Received list_tools request", { params: request.params });
|
|
11841
|
-
logInfo("Environment", {
|
|
11842
|
-
env: process.env
|
|
11843
|
-
});
|
|
11844
11906
|
logInfo("Request", {
|
|
11845
11907
|
request: JSON.parse(JSON.stringify(request))
|
|
11846
11908
|
});
|
|
11847
|
-
|
|
11848
|
-
server: this.server
|
|
11849
|
-
});
|
|
11850
|
-
void getMcpGQLClient();
|
|
11909
|
+
void getMcpGQLClient({ isToolsCall: true });
|
|
11851
11910
|
const toolsDefinitions = this.toolRegistry.getAllTools();
|
|
11852
11911
|
const response = {
|
|
11853
11912
|
tools: toolsDefinitions.map((tool) => ({
|
|
@@ -11867,15 +11926,9 @@ var McpServer = class {
|
|
|
11867
11926
|
async handleCallToolRequest(request) {
|
|
11868
11927
|
const { name, arguments: args } = request.params;
|
|
11869
11928
|
logInfo(`Received call tool request for ${name}`, { name, args });
|
|
11870
|
-
logInfo("Environment", {
|
|
11871
|
-
env: process.env
|
|
11872
|
-
});
|
|
11873
11929
|
logInfo("Request", {
|
|
11874
11930
|
request: JSON.parse(JSON.stringify(request))
|
|
11875
11931
|
});
|
|
11876
|
-
logInfo("Server", {
|
|
11877
|
-
server: this.server
|
|
11878
|
-
});
|
|
11879
11932
|
try {
|
|
11880
11933
|
const tool = this.toolRegistry.getTool(name);
|
|
11881
11934
|
if (!tool) {
|
|
@@ -11938,53 +11991,74 @@ var McpServer = class {
|
|
|
11938
11991
|
}
|
|
11939
11992
|
};
|
|
11940
11993
|
|
|
11941
|
-
// src/mcp/tools/
|
|
11994
|
+
// src/mcp/tools/checkForNewAvailableFixes/CheckForNewAvailableFixesTool.ts
|
|
11942
11995
|
import { z as z32 } from "zod";
|
|
11943
11996
|
|
|
11944
11997
|
// src/mcp/services/PathValidation.ts
|
|
11945
11998
|
import fs9 from "fs";
|
|
11946
11999
|
import path11 from "path";
|
|
11947
|
-
|
|
11948
|
-
|
|
11949
|
-
|
|
11950
|
-
|
|
11951
|
-
|
|
11952
|
-
|
|
11953
|
-
if (
|
|
11954
|
-
|
|
11955
|
-
|
|
11956
|
-
|
|
11957
|
-
|
|
11958
|
-
|
|
11959
|
-
|
|
11960
|
-
|
|
11961
|
-
|
|
11962
|
-
|
|
11963
|
-
|
|
11964
|
-
const decodedPath = decodeURIComponent(inputPath);
|
|
11965
|
-
if (decodedPath.includes("..") || decodedPath !== inputPath) {
|
|
11966
|
-
const error = `Path contains encoded traversal attempts: ${inputPath}`;
|
|
11967
|
-
logError(error);
|
|
11968
|
-
return { isValid: false, error };
|
|
11969
|
-
}
|
|
11970
|
-
if (inputPath.includes("\0") || inputPath.includes("\0")) {
|
|
11971
|
-
const error = `Path contains dangerous characters: ${inputPath}`;
|
|
12000
|
+
async function validatePath(inputPath) {
|
|
12001
|
+
logDebug("Validating MCP path", { inputPath });
|
|
12002
|
+
if (/^\/[a-zA-Z]:\//.test(inputPath)) {
|
|
12003
|
+
inputPath = inputPath.slice(1);
|
|
12004
|
+
}
|
|
12005
|
+
if (inputPath === "." || inputPath === "./") {
|
|
12006
|
+
if (process.env["WORKSPACE_FOLDER_PATHS"]) {
|
|
12007
|
+
logDebug("Fallback to workspace folder path", {
|
|
12008
|
+
inputPath,
|
|
12009
|
+
workspaceFolderPaths: process.env["WORKSPACE_FOLDER_PATHS"]
|
|
12010
|
+
});
|
|
12011
|
+
return {
|
|
12012
|
+
isValid: true,
|
|
12013
|
+
path: process.env["WORKSPACE_FOLDER_PATHS"]
|
|
12014
|
+
};
|
|
12015
|
+
} else {
|
|
12016
|
+
const error = `"." is not a valid path, please provide a full localpath to the repository`;
|
|
11972
12017
|
logError(error);
|
|
11973
|
-
return { isValid: false, error };
|
|
11974
|
-
}
|
|
11975
|
-
logDebug("Path validation successful", { inputPath });
|
|
11976
|
-
logDebug("Checking path existence", { inputPath });
|
|
11977
|
-
try {
|
|
11978
|
-
await fs9.promises.access(inputPath);
|
|
11979
|
-
logDebug("Path exists and is accessible", { inputPath });
|
|
11980
|
-
return { isValid: true };
|
|
11981
|
-
} catch (error) {
|
|
11982
|
-
const errorMessage = `Path does not exist or is not accessible: ${inputPath}`;
|
|
11983
|
-
logError(errorMessage, { error });
|
|
11984
|
-
return { isValid: false, error: errorMessage };
|
|
12018
|
+
return { isValid: false, error, path: inputPath };
|
|
11985
12019
|
}
|
|
11986
12020
|
}
|
|
11987
|
-
|
|
12021
|
+
if (inputPath.includes("..")) {
|
|
12022
|
+
const error = `Path contains path traversal patterns: ${inputPath}`;
|
|
12023
|
+
logError(error);
|
|
12024
|
+
return { isValid: false, error, path: inputPath };
|
|
12025
|
+
}
|
|
12026
|
+
const normalizedPath = path11.normalize(inputPath);
|
|
12027
|
+
if (normalizedPath.includes("..")) {
|
|
12028
|
+
const error = `Normalized path contains path traversal patterns: ${inputPath}`;
|
|
12029
|
+
logError(error);
|
|
12030
|
+
return { isValid: false, error, path: inputPath };
|
|
12031
|
+
}
|
|
12032
|
+
let decodedPath;
|
|
12033
|
+
try {
|
|
12034
|
+
decodedPath = decodeURIComponent(inputPath);
|
|
12035
|
+
} catch (err) {
|
|
12036
|
+
const error = `Failed to decode path: ${inputPath}`;
|
|
12037
|
+
logError(error, { err });
|
|
12038
|
+
return { isValid: false, error, path: inputPath };
|
|
12039
|
+
}
|
|
12040
|
+
if (decodedPath.includes("..") || decodedPath !== inputPath) {
|
|
12041
|
+
const error = `Path contains encoded traversal attempts: ${inputPath}`;
|
|
12042
|
+
logError(error);
|
|
12043
|
+
return { isValid: false, error, path: inputPath };
|
|
12044
|
+
}
|
|
12045
|
+
if (inputPath.includes("\0") || inputPath.includes("\0")) {
|
|
12046
|
+
const error = `Path contains dangerous characters: ${inputPath}`;
|
|
12047
|
+
logError(error);
|
|
12048
|
+
return { isValid: false, error, path: inputPath };
|
|
12049
|
+
}
|
|
12050
|
+
logDebug("Path validation successful", { inputPath });
|
|
12051
|
+
logDebug("Checking path existence", { inputPath });
|
|
12052
|
+
try {
|
|
12053
|
+
await fs9.promises.access(inputPath);
|
|
12054
|
+
logDebug("Path exists and is accessible", { inputPath });
|
|
12055
|
+
return { isValid: true, path: inputPath };
|
|
12056
|
+
} catch (error) {
|
|
12057
|
+
const errorMessage = `Path does not exist or is not accessible: ${inputPath}`;
|
|
12058
|
+
logError(errorMessage, { error });
|
|
12059
|
+
return { isValid: false, error: errorMessage, path: inputPath };
|
|
12060
|
+
}
|
|
12061
|
+
}
|
|
11988
12062
|
|
|
11989
12063
|
// src/mcp/tools/base/BaseTool.ts
|
|
11990
12064
|
import { z as z31 } from "zod";
|
|
@@ -11998,7 +12072,6 @@ var BaseTool = class {
|
|
|
11998
12072
|
};
|
|
11999
12073
|
}
|
|
12000
12074
|
async execute(args) {
|
|
12001
|
-
logInfo(`Executing tool: ${this.name}`, { args });
|
|
12002
12075
|
logInfo(`Authenticating tool: ${this.name}`, { args });
|
|
12003
12076
|
const mcpGqlClient = await getMcpGQLClient();
|
|
12004
12077
|
const userInfo = await mcpGqlClient.getUserInfo();
|
|
@@ -12053,7 +12126,8 @@ var applyFixesPrompt = ({
|
|
|
12053
12126
|
totalCount,
|
|
12054
12127
|
nextOffset,
|
|
12055
12128
|
shownCount,
|
|
12056
|
-
currentTool
|
|
12129
|
+
currentTool,
|
|
12130
|
+
offset = 0
|
|
12057
12131
|
}) => {
|
|
12058
12132
|
if (fixes.length === 0) {
|
|
12059
12133
|
return noFixesReturnedForParameters;
|
|
@@ -12111,7 +12185,7 @@ If you cannot apply a patch:
|
|
|
12111
12185
|
|
|
12112
12186
|
${fixList.map(
|
|
12113
12187
|
(fix, index) => `
|
|
12114
|
-
## Fix ${index + 1}: ${fix.vulnerabilityType}
|
|
12188
|
+
## Fix ${offset + index + 1}: ${fix.vulnerabilityType}
|
|
12115
12189
|
|
|
12116
12190
|
**\u{1F3AF} Target:** Apply this patch to fix a ${fix.vulnerabilityType.toLowerCase()} vulnerability
|
|
12117
12191
|
|
|
@@ -12164,7 +12238,7 @@ We were unable to find a previous vulnerability report for this repository. This
|
|
|
12164
12238
|
|
|
12165
12239
|
### \u{1F3AF} Recommended Actions
|
|
12166
12240
|
1. **Run a new security scan** to analyze your codebase
|
|
12167
|
-
- Use the \`
|
|
12241
|
+
- Use the \`scan_and_fix_vulnerabilities\` tool to start a fresh scan
|
|
12168
12242
|
- This will analyze your current code for security issues
|
|
12169
12243
|
|
|
12170
12244
|
2. **Verify repository access**
|
|
@@ -12178,6 +12252,27 @@ We were unable to find a previous vulnerability report for this repository. This
|
|
|
12178
12252
|
For assistance, please:
|
|
12179
12253
|
- Visit our documentation at https://docs.mobb.ai
|
|
12180
12254
|
- Contact support at support@mobb.ai`;
|
|
12255
|
+
var expiredReportPrompt = ({
|
|
12256
|
+
lastReportDate
|
|
12257
|
+
}) => `\u{1F50D} **MOBB SECURITY SCAN STATUS**
|
|
12258
|
+
|
|
12259
|
+
## Out-of-Date Vulnerability Report
|
|
12260
|
+
|
|
12261
|
+
Your most recent vulnerability report for this repository **expired on ${lastReportDate}** and is no longer available for fetching automated fixes.
|
|
12262
|
+
|
|
12263
|
+
### \u{1F4CB} Why Did This Happen?
|
|
12264
|
+
- Reports are automatically purged after a retention period for security and storage optimisation.
|
|
12265
|
+
- No new scans have been run since the last report expired.
|
|
12266
|
+
|
|
12267
|
+
### \u{1F3AF} Recommended Actions
|
|
12268
|
+
1. **Run a fresh security scan** to generate an up-to-date vulnerability report.
|
|
12269
|
+
- Use the \`scan_and_fix_vulnerabilities\` tool.
|
|
12270
|
+
2. **Verify repository access** if scans fail to run or the repository has moved.
|
|
12271
|
+
3. **Review your CI/CD pipeline** to ensure regular scans are triggered.
|
|
12272
|
+
|
|
12273
|
+
For more help:
|
|
12274
|
+
- Documentation: https://docs.mobb.ai
|
|
12275
|
+
- Support: support@mobb.ai`;
|
|
12181
12276
|
var noFixesAvailablePrompt = `There are no fixes available for this repository at this time.
|
|
12182
12277
|
`;
|
|
12183
12278
|
var fixesFoundPrompt = ({
|
|
@@ -12225,20 +12320,49 @@ ${applyFixesPrompt({
|
|
|
12225
12320
|
hasMore,
|
|
12226
12321
|
nextOffset: 0,
|
|
12227
12322
|
shownCount: fixReport.fixes.length,
|
|
12228
|
-
currentTool: "
|
|
12323
|
+
currentTool: "fetch_available_fixes",
|
|
12324
|
+
offset
|
|
12229
12325
|
})}`;
|
|
12230
12326
|
};
|
|
12231
|
-
var
|
|
12327
|
+
var nextStepsPrompt = ({ scannedFiles }) => `
|
|
12328
|
+
### \u{1F4C1} Scanned Files
|
|
12329
|
+
${scannedFiles.map((file) => `- ${file}`).join("\n")}
|
|
12330
|
+
|
|
12331
|
+
### Extend the scan scope
|
|
12332
|
+
|
|
12333
|
+
To scan a larger number of files, include the additional parameter:
|
|
12334
|
+
|
|
12335
|
+
- **maxFiles**: <number_of_files_to_scan>
|
|
12336
|
+
|
|
12337
|
+
This will scan up to the specified number of recently changed files.
|
|
12338
|
+
|
|
12339
|
+
### \u{1F504} Running a Fresh Scan
|
|
12340
|
+
|
|
12341
|
+
To perform a **rescan** of your repository (fetching a brand-new vulnerability report and updated fixes), include the additional parameter:
|
|
12342
|
+
|
|
12343
|
+
- **rescan**: true
|
|
12344
|
+
|
|
12345
|
+
This will start a new analysis, discard any cached results.
|
|
12346
|
+
|
|
12347
|
+
\u26A0\uFE0F *Note:* A full rescan may take longer to complete than simply fetching additional fixes because your repository is re-uploaded and re-analyzed from scratch.
|
|
12348
|
+
|
|
12349
|
+
`;
|
|
12350
|
+
var noFixesFoundPrompt = ({
|
|
12351
|
+
scannedFiles
|
|
12352
|
+
}) => `\u{1F50D} **MOBB SECURITY SCAN COMPLETED**
|
|
12232
12353
|
|
|
12233
12354
|
Mobb security scan completed successfully but found no automated fixes available at this time.
|
|
12355
|
+
|
|
12356
|
+
${nextStepsPrompt({ scannedFiles })}
|
|
12234
12357
|
`;
|
|
12235
12358
|
var fixesPrompt = ({
|
|
12236
12359
|
fixes,
|
|
12237
12360
|
totalCount,
|
|
12238
|
-
offset
|
|
12361
|
+
offset,
|
|
12362
|
+
scannedFiles
|
|
12239
12363
|
}) => {
|
|
12240
12364
|
if (totalCount === 0) {
|
|
12241
|
-
return noFixesFoundPrompt;
|
|
12365
|
+
return noFixesFoundPrompt({ scannedFiles });
|
|
12242
12366
|
}
|
|
12243
12367
|
const shownCount = fixes.length;
|
|
12244
12368
|
const nextOffset = offset + shownCount;
|
|
@@ -12251,28 +12375,491 @@ ${applyFixesPrompt({
|
|
|
12251
12375
|
hasMore,
|
|
12252
12376
|
nextOffset,
|
|
12253
12377
|
shownCount,
|
|
12254
|
-
currentTool: "
|
|
12378
|
+
currentTool: "scan_and_fix_vulnerabilities",
|
|
12379
|
+
offset
|
|
12255
12380
|
})}
|
|
12256
12381
|
|
|
12257
|
-
|
|
12382
|
+
${nextStepsPrompt({ scannedFiles })}
|
|
12383
|
+
`;
|
|
12384
|
+
};
|
|
12385
|
+
var noFreshFixesPrompt = `No fresh fixes available for this repository at this time.
|
|
12386
|
+
`;
|
|
12387
|
+
var initialScanInProgressPrompt = `Initial scan in progress. Call the tool again in 1 minute to check for available fixes.`;
|
|
12388
|
+
var freshFixesPrompt = ({ fixes }) => {
|
|
12389
|
+
return `Here are the fresh fixes to the vulnerabilities discovered by Mobb MCP
|
|
12258
12390
|
|
|
12259
|
-
|
|
12391
|
+
${applyFixesPrompt({
|
|
12392
|
+
fixes,
|
|
12393
|
+
totalCount: fixes.length,
|
|
12394
|
+
hasMore: false,
|
|
12395
|
+
nextOffset: 0,
|
|
12396
|
+
shownCount: fixes.length,
|
|
12397
|
+
currentTool: "fetch_available_fixes",
|
|
12398
|
+
offset: 0
|
|
12399
|
+
})}
|
|
12400
|
+
`;
|
|
12401
|
+
};
|
|
12260
12402
|
|
|
12261
|
-
|
|
12403
|
+
// src/mcp/services/GetLocalFiles.ts
|
|
12404
|
+
import fs10 from "fs/promises";
|
|
12405
|
+
import nodePath from "path";
|
|
12406
|
+
var getLocalFiles = async ({
|
|
12407
|
+
path: path13,
|
|
12408
|
+
maxFileSize = 1024 * 1024 * 5,
|
|
12409
|
+
maxFiles
|
|
12410
|
+
}) => {
|
|
12411
|
+
const resolvedRepoPath = await fs10.realpath(path13);
|
|
12412
|
+
const gitService = new GitService(resolvedRepoPath, log);
|
|
12413
|
+
const gitValidation = await gitService.validateRepository();
|
|
12414
|
+
let files = [];
|
|
12415
|
+
if (!gitValidation.isValid) {
|
|
12416
|
+
logDebug(
|
|
12417
|
+
"Git repository validation failed, using all files in the repository",
|
|
12418
|
+
{
|
|
12419
|
+
path: path13
|
|
12420
|
+
}
|
|
12421
|
+
);
|
|
12422
|
+
files = FileUtils.getLastChangedFiles({
|
|
12423
|
+
dir: path13,
|
|
12424
|
+
maxFileSize,
|
|
12425
|
+
maxFiles
|
|
12426
|
+
});
|
|
12427
|
+
logDebug("Found files in the repository", {
|
|
12428
|
+
files,
|
|
12429
|
+
fileCount: files.length
|
|
12430
|
+
});
|
|
12431
|
+
} else {
|
|
12432
|
+
logDebug("maxFiles", {
|
|
12433
|
+
maxFiles
|
|
12434
|
+
});
|
|
12435
|
+
const gitResult = await gitService.getChangedFiles();
|
|
12436
|
+
files = gitResult.files;
|
|
12437
|
+
if (files.length === 0 || maxFiles) {
|
|
12438
|
+
const recentResult = await gitService.getRecentlyChangedFiles({
|
|
12439
|
+
maxFiles
|
|
12440
|
+
});
|
|
12441
|
+
files = recentResult.files;
|
|
12442
|
+
logDebug(
|
|
12443
|
+
"No changes found, using recently changed files from git history",
|
|
12444
|
+
{
|
|
12445
|
+
files,
|
|
12446
|
+
fileCount: files.length,
|
|
12447
|
+
commitsChecked: recentResult.commitCount
|
|
12448
|
+
}
|
|
12449
|
+
);
|
|
12450
|
+
} else {
|
|
12451
|
+
logDebug("Found changed files in the git repository", {
|
|
12452
|
+
files,
|
|
12453
|
+
fileCount: files.length
|
|
12454
|
+
});
|
|
12455
|
+
}
|
|
12456
|
+
}
|
|
12457
|
+
files = files.filter(
|
|
12458
|
+
(file) => FileUtils.shouldPackFile(
|
|
12459
|
+
nodePath.resolve(resolvedRepoPath, file),
|
|
12460
|
+
maxFileSize
|
|
12461
|
+
)
|
|
12462
|
+
);
|
|
12463
|
+
const filesWithStats = await Promise.all(
|
|
12464
|
+
files.map(async (file) => {
|
|
12465
|
+
const absoluteFilePath = nodePath.resolve(resolvedRepoPath, file);
|
|
12466
|
+
const relativePath = nodePath.relative(resolvedRepoPath, absoluteFilePath);
|
|
12467
|
+
let fileStat;
|
|
12468
|
+
try {
|
|
12469
|
+
fileStat = await fs10.stat(absoluteFilePath);
|
|
12470
|
+
} catch (e) {
|
|
12471
|
+
logDebug("File not found", {
|
|
12472
|
+
file
|
|
12473
|
+
});
|
|
12474
|
+
}
|
|
12475
|
+
return {
|
|
12476
|
+
filename: nodePath.basename(absoluteFilePath),
|
|
12477
|
+
relativePath,
|
|
12478
|
+
fullPath: absoluteFilePath,
|
|
12479
|
+
lastEdited: fileStat?.mtime.getTime() ?? 0
|
|
12480
|
+
};
|
|
12481
|
+
})
|
|
12482
|
+
);
|
|
12483
|
+
return filesWithStats.filter((file) => file.lastEdited > 0);
|
|
12484
|
+
};
|
|
12262
12485
|
|
|
12263
|
-
|
|
12486
|
+
// src/mcp/services/ScanFiles.ts
|
|
12487
|
+
import fs11 from "fs";
|
|
12488
|
+
import path12 from "path";
|
|
12489
|
+
import AdmZip2 from "adm-zip";
|
|
12490
|
+
var scanFiles = async (fileList, repositoryPath, gqlClient) => {
|
|
12491
|
+
const repoUploadInfo = await initializeReport(gqlClient);
|
|
12492
|
+
const fixReportId = repoUploadInfo.fixReportId;
|
|
12493
|
+
const zipBuffer = await packFiles(fileList, repositoryPath);
|
|
12494
|
+
await uploadFiles(zipBuffer, repoUploadInfo);
|
|
12495
|
+
const projectId = await getProjectId(gqlClient);
|
|
12496
|
+
await runScan({ fixReportId, projectId, gqlClient });
|
|
12497
|
+
return {
|
|
12498
|
+
fixReportId,
|
|
12499
|
+
projectId
|
|
12500
|
+
};
|
|
12501
|
+
};
|
|
12502
|
+
var initializeReport = async (gqlClient) => {
|
|
12503
|
+
if (!gqlClient) {
|
|
12504
|
+
throw new GqlClientError();
|
|
12505
|
+
}
|
|
12506
|
+
try {
|
|
12507
|
+
const {
|
|
12508
|
+
uploadS3BucketInfo: { repoUploadInfo }
|
|
12509
|
+
} = await gqlClient.uploadS3BucketInfo();
|
|
12510
|
+
logInfo("Upload info retrieved", { uploadKey: repoUploadInfo?.uploadKey });
|
|
12511
|
+
return repoUploadInfo;
|
|
12512
|
+
} catch (error) {
|
|
12513
|
+
const message = error.message;
|
|
12514
|
+
throw new ReportInitializationError(`Error initializing report: ${message}`);
|
|
12515
|
+
}
|
|
12516
|
+
};
|
|
12517
|
+
var packFiles = async (fileList, repositoryPath) => {
|
|
12518
|
+
try {
|
|
12519
|
+
logInfo(`FilePacking: packing files from ${repositoryPath}`);
|
|
12520
|
+
const zip = new AdmZip2();
|
|
12521
|
+
let packedFilesCount = 0;
|
|
12522
|
+
const resolvedRepoPath = path12.resolve(repositoryPath);
|
|
12523
|
+
logInfo("FilePacking: compressing files");
|
|
12524
|
+
for (const filepath of fileList) {
|
|
12525
|
+
const absoluteFilepath = path12.join(repositoryPath, filepath);
|
|
12526
|
+
const resolvedFilePath = path12.resolve(absoluteFilepath);
|
|
12527
|
+
if (!resolvedFilePath.startsWith(resolvedRepoPath)) {
|
|
12528
|
+
logInfo(
|
|
12529
|
+
`FilePacking: skipping ${filepath} due to potential path traversal`
|
|
12530
|
+
);
|
|
12531
|
+
continue;
|
|
12532
|
+
}
|
|
12533
|
+
if (!FileUtils.shouldPackFile(absoluteFilepath, MCP_MAX_FILE_SIZE)) {
|
|
12534
|
+
logInfo(
|
|
12535
|
+
`FilePacking: ignoring ${filepath} because it is excluded or invalid`
|
|
12536
|
+
);
|
|
12537
|
+
continue;
|
|
12538
|
+
}
|
|
12539
|
+
let data;
|
|
12540
|
+
try {
|
|
12541
|
+
data = fs11.readFileSync(absoluteFilepath);
|
|
12542
|
+
} catch (fsError) {
|
|
12543
|
+
logInfo(
|
|
12544
|
+
`FilePacking: failed to read ${filepath} from filesystem: ${fsError}`
|
|
12545
|
+
);
|
|
12546
|
+
continue;
|
|
12547
|
+
}
|
|
12548
|
+
zip.addFile(filepath, data);
|
|
12549
|
+
packedFilesCount++;
|
|
12550
|
+
}
|
|
12551
|
+
const zipBuffer = zip.toBuffer();
|
|
12552
|
+
logInfo(
|
|
12553
|
+
`FilePacking: read ${packedFilesCount} source files. total size: ${zipBuffer.length} bytes`
|
|
12554
|
+
);
|
|
12555
|
+
logInfo("Files packed successfully", { fileCount: fileList.length });
|
|
12556
|
+
return zipBuffer;
|
|
12557
|
+
} catch (error) {
|
|
12558
|
+
const message = error.message;
|
|
12559
|
+
throw new FileProcessingError(`Error packing files: ${message}`);
|
|
12560
|
+
}
|
|
12561
|
+
};
|
|
12562
|
+
var uploadFiles = async (zipBuffer, repoUploadInfo) => {
|
|
12563
|
+
if (!repoUploadInfo) {
|
|
12564
|
+
throw new FileUploadError("Upload info is required");
|
|
12565
|
+
}
|
|
12566
|
+
try {
|
|
12567
|
+
await uploadFile({
|
|
12568
|
+
file: zipBuffer,
|
|
12569
|
+
url: repoUploadInfo.url,
|
|
12570
|
+
uploadFields: JSON.parse(repoUploadInfo.uploadFieldsJSON),
|
|
12571
|
+
uploadKey: repoUploadInfo.uploadKey
|
|
12572
|
+
});
|
|
12573
|
+
logInfo("File uploaded successfully");
|
|
12574
|
+
} catch (error) {
|
|
12575
|
+
logError("File upload failed", { error: error.message });
|
|
12576
|
+
throw new FileUploadError(
|
|
12577
|
+
`Failed to upload the file: ${error.message}`
|
|
12578
|
+
);
|
|
12579
|
+
}
|
|
12580
|
+
};
|
|
12581
|
+
var getProjectId = async (gqlClient) => {
|
|
12582
|
+
if (!gqlClient) {
|
|
12583
|
+
throw new GqlClientError();
|
|
12584
|
+
}
|
|
12585
|
+
const projectId = await gqlClient.getProjectId();
|
|
12586
|
+
logInfo("Project ID retrieved", { projectId });
|
|
12587
|
+
return projectId;
|
|
12588
|
+
};
|
|
12589
|
+
var runScan = async ({
|
|
12590
|
+
fixReportId,
|
|
12591
|
+
projectId,
|
|
12592
|
+
gqlClient
|
|
12593
|
+
}) => {
|
|
12594
|
+
if (!gqlClient) {
|
|
12595
|
+
throw new GqlClientError();
|
|
12596
|
+
}
|
|
12597
|
+
logInfo("Starting scan", { fixReportId, projectId });
|
|
12598
|
+
const submitVulnerabilityReportVariables = {
|
|
12599
|
+
fixReportId,
|
|
12600
|
+
projectId,
|
|
12601
|
+
repoUrl: "",
|
|
12602
|
+
reference: "no-branch",
|
|
12603
|
+
scanSource: "MCP" /* Mcp */
|
|
12604
|
+
};
|
|
12605
|
+
logInfo("Submitting vulnerability report");
|
|
12606
|
+
const submitRes = await gqlClient.submitVulnerabilityReport(
|
|
12607
|
+
submitVulnerabilityReportVariables
|
|
12608
|
+
);
|
|
12609
|
+
if (submitRes.submitVulnerabilityReport.__typename !== "VulnerabilityReport") {
|
|
12610
|
+
logError("Vulnerability report submission failed", {
|
|
12611
|
+
response: submitRes
|
|
12612
|
+
});
|
|
12613
|
+
throw new ScanError("\u{1F575}\uFE0F\u200D\u2642\uFE0F Mobb analysis failed");
|
|
12614
|
+
}
|
|
12615
|
+
logInfo("Vulnerability report submitted successfully", {
|
|
12616
|
+
analysisId: submitRes.submitVulnerabilityReport.fixReportId
|
|
12617
|
+
});
|
|
12618
|
+
logInfo("Starting analysis subscription");
|
|
12619
|
+
await gqlClient.subscribeToGetAnalysis({
|
|
12620
|
+
subscribeToAnalysisParams: {
|
|
12621
|
+
analysisId: submitRes.submitVulnerabilityReport.fixReportId
|
|
12622
|
+
},
|
|
12623
|
+
callback: () => {
|
|
12624
|
+
},
|
|
12625
|
+
callbackStates: ["Finished" /* Finished */],
|
|
12626
|
+
timeoutInMs: MCP_VUL_REPORT_DIGEST_TIMEOUT_MS
|
|
12627
|
+
});
|
|
12628
|
+
logInfo("Analysis subscription completed");
|
|
12629
|
+
};
|
|
12264
12630
|
|
|
12265
|
-
|
|
12631
|
+
// src/mcp/tools/checkForNewAvailableFixes/CheckForNewAvailableFixesService.ts
|
|
12632
|
+
function extractPathFromPatch(patch) {
|
|
12633
|
+
const match = patch?.match(/^diff --git a\/([^\s]+) b\//);
|
|
12634
|
+
return match?.[1] ?? null;
|
|
12635
|
+
}
|
|
12636
|
+
var _CheckForNewAvailableFixesService = class _CheckForNewAvailableFixesService {
|
|
12637
|
+
constructor() {
|
|
12638
|
+
/**
|
|
12639
|
+
* Cache of the last known total number of fixes per repository URL so that we
|
|
12640
|
+
* can determine whether *new* fixes have been generated since the user last
|
|
12641
|
+
* asked.
|
|
12642
|
+
*/
|
|
12643
|
+
__publicField(this, "path", "");
|
|
12644
|
+
__publicField(this, "filesLastScanned", {});
|
|
12645
|
+
__publicField(this, "freshFixes", []);
|
|
12646
|
+
__publicField(this, "reportedFixes", []);
|
|
12647
|
+
__publicField(this, "intervalId", null);
|
|
12648
|
+
__publicField(this, "isInitialScanComplete", false);
|
|
12649
|
+
}
|
|
12650
|
+
static getInstance() {
|
|
12651
|
+
if (!_CheckForNewAvailableFixesService.instance) {
|
|
12652
|
+
_CheckForNewAvailableFixesService.instance = new _CheckForNewAvailableFixesService();
|
|
12653
|
+
}
|
|
12654
|
+
return _CheckForNewAvailableFixesService.instance;
|
|
12655
|
+
}
|
|
12656
|
+
/**
|
|
12657
|
+
* Resets any cached state so the service can be reused between independent
|
|
12658
|
+
* MCP sessions.
|
|
12659
|
+
*/
|
|
12660
|
+
reset() {
|
|
12661
|
+
this.filesLastScanned = {};
|
|
12662
|
+
this.freshFixes = [];
|
|
12663
|
+
this.reportedFixes = [];
|
|
12664
|
+
if (this.intervalId) {
|
|
12665
|
+
clearInterval(this.intervalId);
|
|
12666
|
+
this.intervalId = null;
|
|
12667
|
+
}
|
|
12668
|
+
}
|
|
12669
|
+
/**
|
|
12670
|
+
* Stub implementation – in a future version this will query the backend for
|
|
12671
|
+
* the latest fixes count and compare it with the cached value. For now it
|
|
12672
|
+
* simply returns a placeholder string so that the tool can be wired into the
|
|
12673
|
+
* system and used in tests.
|
|
12674
|
+
*/
|
|
12675
|
+
async scan({ path: path13 }) {
|
|
12676
|
+
logInfo("Scanning for new fixes", { path: path13 });
|
|
12677
|
+
const gqlClient = await getMcpGQLClient();
|
|
12678
|
+
const isConnected = await gqlClient.verifyConnection();
|
|
12679
|
+
if (!isConnected) {
|
|
12680
|
+
logError("Failed to connect to the API, scan aborted");
|
|
12681
|
+
return;
|
|
12682
|
+
}
|
|
12683
|
+
logInfo("Connected to the API, assebling list of files to scan", { path: path13 });
|
|
12684
|
+
const files = await getLocalFiles({
|
|
12685
|
+
path: path13,
|
|
12686
|
+
maxFileSize: MCP_MAX_FILE_SIZE
|
|
12687
|
+
});
|
|
12688
|
+
logInfo("Active files", { files });
|
|
12689
|
+
const filesToScan = files.filter((file) => {
|
|
12690
|
+
const lastScannedEditTime = this.filesLastScanned[file.fullPath];
|
|
12691
|
+
if (!lastScannedEditTime) {
|
|
12692
|
+
return true;
|
|
12693
|
+
}
|
|
12694
|
+
return file.lastEdited > lastScannedEditTime;
|
|
12695
|
+
});
|
|
12696
|
+
if (filesToScan.length === 0) {
|
|
12697
|
+
logInfo("No files to scan", { path: path13 });
|
|
12698
|
+
return;
|
|
12699
|
+
}
|
|
12700
|
+
logInfo("Files to scan", { filesToScan });
|
|
12701
|
+
const { fixReportId, projectId } = await scanFiles(
|
|
12702
|
+
filesToScan.map((file) => file.relativePath),
|
|
12703
|
+
path13,
|
|
12704
|
+
gqlClient
|
|
12705
|
+
);
|
|
12706
|
+
logInfo("Scan completed", { fixReportId, projectId });
|
|
12707
|
+
const fixes = await gqlClient.getReportFixesPaginated({
|
|
12708
|
+
reportId: fixReportId,
|
|
12709
|
+
offset: 0,
|
|
12710
|
+
limit: 1e3
|
|
12711
|
+
});
|
|
12712
|
+
const newFixes = fixes?.fixes?.filter((fix) => !this.isAlreadyReported(fix));
|
|
12713
|
+
logInfo("Fixes retrieved", {
|
|
12714
|
+
count: fixes?.fixes?.length || 0,
|
|
12715
|
+
newFixes: newFixes?.length || 0
|
|
12716
|
+
});
|
|
12717
|
+
this.freshFixes = this.freshFixes.filter((fix) => !this.isFixFromOldScan(fix, filesToScan)).concat(newFixes || []);
|
|
12718
|
+
logInfo("Fresh fixes", { freshFixes: this.freshFixes });
|
|
12719
|
+
filesToScan.forEach((file) => {
|
|
12720
|
+
this.filesLastScanned[file.fullPath] = file.lastEdited;
|
|
12721
|
+
});
|
|
12722
|
+
this.isInitialScanComplete = true;
|
|
12723
|
+
}
|
|
12724
|
+
isAlreadyReported(fix) {
|
|
12725
|
+
return this.reportedFixes.some(
|
|
12726
|
+
(reportedFix) => reportedFix.sharedState?.id === fix.sharedState?.id
|
|
12727
|
+
);
|
|
12728
|
+
}
|
|
12729
|
+
isFixFromOldScan(fix, filesToScan) {
|
|
12730
|
+
const patch = fix.patchAndQuestions?.__typename === "FixData" ? fix.patchAndQuestions.patch : void 0;
|
|
12731
|
+
const fixFile = extractPathFromPatch(patch);
|
|
12732
|
+
if (!fixFile) {
|
|
12733
|
+
return false;
|
|
12734
|
+
}
|
|
12735
|
+
logInfo("isOldFix", {
|
|
12736
|
+
fixFile,
|
|
12737
|
+
filesToScan,
|
|
12738
|
+
isOldFix: filesToScan.some((file) => file.relativePath === fixFile)
|
|
12739
|
+
});
|
|
12740
|
+
return filesToScan.some((file) => file.relativePath === fixFile);
|
|
12741
|
+
}
|
|
12742
|
+
async getFreshFixes({ path: path13 }) {
|
|
12743
|
+
if (this.path !== path13) {
|
|
12744
|
+
this.path = path13;
|
|
12745
|
+
this.reset();
|
|
12746
|
+
}
|
|
12747
|
+
if (!this.intervalId) {
|
|
12748
|
+
logInfo("Starting periodic scan for new fixes", { path: path13 });
|
|
12749
|
+
this.intervalId = setInterval(() => {
|
|
12750
|
+
logDebug("Triggering periodic scan", { path: path13 });
|
|
12751
|
+
this.scan({ path: path13 }).catch((error) => {
|
|
12752
|
+
logError("Error during periodic scan", { error });
|
|
12753
|
+
});
|
|
12754
|
+
}, MCP_PERIODIC_CHECK_INTERVAL);
|
|
12755
|
+
logDebug("Triggering initial scan", { path: path13 });
|
|
12756
|
+
this.scan({ path: path13 }).catch((error) => {
|
|
12757
|
+
logError("Error during initial scan", { error });
|
|
12758
|
+
});
|
|
12759
|
+
}
|
|
12760
|
+
if (this.freshFixes.length > 0) {
|
|
12761
|
+
const freshFixes = this.freshFixes.splice(0, 3);
|
|
12762
|
+
if (freshFixes.length > 0) {
|
|
12763
|
+
this.reportedFixes.concat(freshFixes);
|
|
12764
|
+
return freshFixesPrompt({ fixes: freshFixes });
|
|
12765
|
+
}
|
|
12766
|
+
}
|
|
12767
|
+
if (!this.isInitialScanComplete) {
|
|
12768
|
+
return initialScanInProgressPrompt;
|
|
12769
|
+
}
|
|
12770
|
+
return noFreshFixesPrompt;
|
|
12771
|
+
}
|
|
12772
|
+
};
|
|
12773
|
+
__publicField(_CheckForNewAvailableFixesService, "instance");
|
|
12774
|
+
var CheckForNewAvailableFixesService = _CheckForNewAvailableFixesService;
|
|
12266
12775
|
|
|
12267
|
-
|
|
12776
|
+
// src/mcp/tools/checkForNewAvailableFixes/CheckForNewAvailableFixesTool.ts
|
|
12777
|
+
var CheckForNewAvailableFixesTool = class extends BaseTool {
|
|
12778
|
+
constructor() {
|
|
12779
|
+
super();
|
|
12780
|
+
__publicField(this, "name", "check_for_new_available_fixes");
|
|
12781
|
+
__publicField(this, "displayName", "Check for New Available Fixes");
|
|
12782
|
+
// A detailed description to guide the LLM on when and how to invoke this tool.
|
|
12783
|
+
__publicField(this, "description", `Continuesly monitors your code and scans for new security vulnerabilities.
|
|
12784
|
+
|
|
12785
|
+
When to invoke:
|
|
12786
|
+
\u2022 ALWAYS call this tool ONCE at the very end of every reasoning / code-generation session (after all code edits, linting, tests, etc.).
|
|
12787
|
+
\u2022 Invoke when the user explicitly asks to "check for new fixes", "look for fresh patches", or similar.
|
|
12788
|
+
\u2022 The repository must exist locally and be a valid Git repo with an 'origin' remote configured.
|
|
12789
|
+
|
|
12790
|
+
How to invoke:
|
|
12791
|
+
\u2022 Required argument:
|
|
12792
|
+
\u2013 path (string): absolute path to the repository root.
|
|
12793
|
+
|
|
12794
|
+
Behaviour:
|
|
12795
|
+
\u2022 If no new fixes are available, it returns a concise message indicating so.
|
|
12796
|
+
\u2022 If fixes are found, it returns a human-readable summary including total count and severity breakdown.
|
|
12797
|
+
|
|
12798
|
+
Example payload:
|
|
12799
|
+
{
|
|
12800
|
+
"path": "/home/user/my-project"
|
|
12801
|
+
}`);
|
|
12802
|
+
__publicField(this, "inputSchema", {
|
|
12803
|
+
type: "object",
|
|
12804
|
+
properties: {
|
|
12805
|
+
path: {
|
|
12806
|
+
type: "string",
|
|
12807
|
+
description: "Full local path to the cloned git repository to check for new available fixes"
|
|
12808
|
+
}
|
|
12809
|
+
},
|
|
12810
|
+
required: ["path"]
|
|
12811
|
+
});
|
|
12812
|
+
__publicField(this, "inputValidationSchema", z32.object({
|
|
12813
|
+
path: z32.string().describe(
|
|
12814
|
+
"Full local path to the cloned git repository to check for new available fixes"
|
|
12815
|
+
)
|
|
12816
|
+
}));
|
|
12817
|
+
__publicField(this, "newFixesService");
|
|
12818
|
+
this.newFixesService = new CheckForNewAvailableFixesService();
|
|
12819
|
+
}
|
|
12820
|
+
async executeInternal(args) {
|
|
12821
|
+
const pathValidationResult = await validatePath(args.path);
|
|
12822
|
+
if (!pathValidationResult.isValid) {
|
|
12823
|
+
throw new Error(
|
|
12824
|
+
`Invalid path: potential security risk detected in path: ${pathValidationResult.error}`
|
|
12825
|
+
);
|
|
12826
|
+
}
|
|
12827
|
+
const path13 = pathValidationResult.path;
|
|
12828
|
+
const resultText = await this.newFixesService.getFreshFixes({
|
|
12829
|
+
path: path13
|
|
12830
|
+
});
|
|
12831
|
+
logInfo("CheckForNewAvailableFixesTool execution completed", {
|
|
12832
|
+
resultText
|
|
12833
|
+
});
|
|
12834
|
+
return {
|
|
12835
|
+
content: [
|
|
12836
|
+
{
|
|
12837
|
+
type: "text",
|
|
12838
|
+
text: resultText
|
|
12839
|
+
}
|
|
12840
|
+
]
|
|
12841
|
+
};
|
|
12842
|
+
}
|
|
12268
12843
|
};
|
|
12269
12844
|
|
|
12270
|
-
// src/mcp/tools/
|
|
12271
|
-
|
|
12845
|
+
// src/mcp/tools/fetchAvailableFixes/FetchAvailableFixesTool.ts
|
|
12846
|
+
import { z as z33 } from "zod";
|
|
12847
|
+
|
|
12848
|
+
// src/mcp/tools/fetchAvailableFixes/FetchAvailableFixesService.ts
|
|
12849
|
+
var _FetchAvailableFixesService = class _FetchAvailableFixesService {
|
|
12272
12850
|
constructor() {
|
|
12273
12851
|
__publicField(this, "gqlClient", null);
|
|
12274
12852
|
__publicField(this, "currentOffset", 0);
|
|
12275
12853
|
}
|
|
12854
|
+
static getInstance() {
|
|
12855
|
+
if (!_FetchAvailableFixesService.instance) {
|
|
12856
|
+
_FetchAvailableFixesService.instance = new _FetchAvailableFixesService();
|
|
12857
|
+
}
|
|
12858
|
+
return _FetchAvailableFixesService.instance;
|
|
12859
|
+
}
|
|
12860
|
+
reset() {
|
|
12861
|
+
this.currentOffset = 0;
|
|
12862
|
+
}
|
|
12276
12863
|
async initializeGqlClient() {
|
|
12277
12864
|
if (!this.gqlClient) {
|
|
12278
12865
|
this.gqlClient = await getMcpGQLClient();
|
|
@@ -12282,40 +12869,44 @@ var AvailableFixesService = class {
|
|
|
12282
12869
|
async checkForAvailableFixes({
|
|
12283
12870
|
repoUrl,
|
|
12284
12871
|
limit = 3,
|
|
12285
|
-
offset
|
|
12872
|
+
offset
|
|
12286
12873
|
}) {
|
|
12287
12874
|
try {
|
|
12288
12875
|
logDebug("Checking for available fixes", { repoUrl, limit });
|
|
12289
12876
|
const gqlClient = await this.initializeGqlClient();
|
|
12290
12877
|
logDebug("GQL client initialized");
|
|
12291
12878
|
logDebug("querying for latest report", { repoUrl, limit });
|
|
12292
|
-
|
|
12293
|
-
|
|
12294
|
-
|
|
12295
|
-
} else if (this.currentOffset) {
|
|
12296
|
-
effectiveOffset = this.currentOffset ?? 0;
|
|
12297
|
-
} else {
|
|
12298
|
-
effectiveOffset = 0;
|
|
12299
|
-
}
|
|
12300
|
-
logDebug("effectiveOffset", { test: "j", effectiveOffset });
|
|
12301
|
-
const result = await gqlClient.getLatestReportByRepoUrl({
|
|
12879
|
+
const effectiveOffset = offset ?? (this.currentOffset || 0);
|
|
12880
|
+
logDebug("effectiveOffset", { effectiveOffset });
|
|
12881
|
+
const { fixReport, expiredReport } = await gqlClient.getLatestReportByRepoUrl({
|
|
12302
12882
|
repoUrl,
|
|
12303
12883
|
limit,
|
|
12304
12884
|
offset: effectiveOffset
|
|
12305
12885
|
});
|
|
12306
|
-
logDebug("received latest report result", {
|
|
12307
|
-
if (!
|
|
12308
|
-
|
|
12886
|
+
logDebug("received latest report result", { fixReport, expiredReport });
|
|
12887
|
+
if (!fixReport) {
|
|
12888
|
+
if (expiredReport) {
|
|
12889
|
+
const lastReportDate = expiredReport.expirationOn ? new Date(expiredReport.expirationOn).toLocaleString() : "Unknown date";
|
|
12890
|
+
logInfo("Expired report found", {
|
|
12891
|
+
repoUrl,
|
|
12892
|
+
expirationOn: expiredReport.expirationOn
|
|
12893
|
+
});
|
|
12894
|
+
return expiredReportPrompt({ lastReportDate });
|
|
12895
|
+
}
|
|
12896
|
+
logInfo("No report (active or expired) found for repository", {
|
|
12897
|
+
repoUrl
|
|
12898
|
+
});
|
|
12309
12899
|
return noReportFoundPrompt;
|
|
12310
12900
|
}
|
|
12311
12901
|
logInfo("Successfully retrieved available fixes", {
|
|
12312
12902
|
reportFound: true
|
|
12313
12903
|
});
|
|
12314
|
-
|
|
12315
|
-
|
|
12316
|
-
|
|
12317
|
-
offset: this.currentOffset
|
|
12904
|
+
const prompt = fixesFoundPrompt({
|
|
12905
|
+
fixReport,
|
|
12906
|
+
offset: effectiveOffset
|
|
12318
12907
|
});
|
|
12908
|
+
this.currentOffset = effectiveOffset + (fixReport.fixes?.length || 0);
|
|
12909
|
+
return prompt;
|
|
12319
12910
|
} catch (error) {
|
|
12320
12911
|
logError("Failed to check for available fixes", {
|
|
12321
12912
|
error,
|
|
@@ -12325,20 +12916,40 @@ var AvailableFixesService = class {
|
|
|
12325
12916
|
}
|
|
12326
12917
|
}
|
|
12327
12918
|
};
|
|
12919
|
+
__publicField(_FetchAvailableFixesService, "instance");
|
|
12920
|
+
var FetchAvailableFixesService = _FetchAvailableFixesService;
|
|
12328
12921
|
|
|
12329
|
-
// src/mcp/tools/
|
|
12330
|
-
var
|
|
12922
|
+
// src/mcp/tools/fetchAvailableFixes/FetchAvailableFixesTool.ts
|
|
12923
|
+
var FetchAvailableFixesTool = class extends BaseTool {
|
|
12331
12924
|
constructor() {
|
|
12332
|
-
super(
|
|
12333
|
-
__publicField(this, "name", "
|
|
12334
|
-
__publicField(this, "displayName", "
|
|
12335
|
-
__publicField(this, "description",
|
|
12925
|
+
super();
|
|
12926
|
+
__publicField(this, "name", "fetch_available_fixes");
|
|
12927
|
+
__publicField(this, "displayName", "Fetch Available Fixes");
|
|
12928
|
+
__publicField(this, "description", `Check the MOBB backend for pre-generated fixes (patch sets) that correspond to vulnerabilities detected in the supplied Git repository.
|
|
12929
|
+
|
|
12930
|
+
Use when:
|
|
12931
|
+
\u2022 You already have a local clone of a Git repository and want to know if MOBB has fixes available for it.
|
|
12932
|
+
\u2022 A vulnerability scan has been run previously and uploaded to the MOBB backend and you want to fetch the list or count of ready-to-apply fixes before triggering a full scan-and-fix flow.
|
|
12933
|
+
|
|
12934
|
+
Required argument:
|
|
12935
|
+
\u2022 path \u2013 absolute path to the local Git repository clone.
|
|
12936
|
+
|
|
12937
|
+
Optional arguments:
|
|
12938
|
+
\u2022 offset \u2013 pagination offset (integer).
|
|
12939
|
+
\u2022 limit \u2013 maximum number of fixes to return (integer).
|
|
12940
|
+
|
|
12941
|
+
The tool will:
|
|
12942
|
+
1. Validate that the provided path is secure and exists.
|
|
12943
|
+
2. Verify that the directory is a valid Git repository with an "origin" remote.
|
|
12944
|
+
3. Query the MOBB service by the origin remote URL and return a textual summary of available fixes (total and by severity) or a message if none are found.
|
|
12945
|
+
|
|
12946
|
+
Call this tool instead of scan_and_fix_vulnerabilities when you only need a fixes summary and do NOT want to perform scanning or code modifications.`);
|
|
12336
12947
|
__publicField(this, "inputSchema", {
|
|
12337
12948
|
type: "object",
|
|
12338
12949
|
properties: {
|
|
12339
12950
|
path: {
|
|
12340
12951
|
type: "string",
|
|
12341
|
-
description: "
|
|
12952
|
+
description: "Full local path to the cloned git repository to check for available fixes"
|
|
12342
12953
|
},
|
|
12343
12954
|
offset: {
|
|
12344
12955
|
type: "number",
|
|
@@ -12351,23 +12962,26 @@ var CheckForAvailableFixesTool = class extends BaseTool {
|
|
|
12351
12962
|
},
|
|
12352
12963
|
required: ["path"]
|
|
12353
12964
|
});
|
|
12354
|
-
__publicField(this, "inputValidationSchema",
|
|
12355
|
-
path:
|
|
12356
|
-
"
|
|
12965
|
+
__publicField(this, "inputValidationSchema", z33.object({
|
|
12966
|
+
path: z33.string().describe(
|
|
12967
|
+
"Full local path to the cloned git repository to check for available fixes"
|
|
12357
12968
|
),
|
|
12358
|
-
offset:
|
|
12359
|
-
limit:
|
|
12969
|
+
offset: z33.number().optional().describe("Optional offset for pagination"),
|
|
12970
|
+
limit: z33.number().optional().describe("Optional maximum number of fixes to return")
|
|
12360
12971
|
}));
|
|
12972
|
+
__publicField(this, "availableFixesService");
|
|
12973
|
+
this.availableFixesService = FetchAvailableFixesService.getInstance();
|
|
12974
|
+
this.availableFixesService.reset();
|
|
12361
12975
|
}
|
|
12362
12976
|
async executeInternal(args) {
|
|
12363
|
-
const
|
|
12364
|
-
const pathValidationResult = await pathValidation.validatePath(args.path);
|
|
12977
|
+
const pathValidationResult = await validatePath(args.path);
|
|
12365
12978
|
if (!pathValidationResult.isValid) {
|
|
12366
12979
|
throw new Error(
|
|
12367
12980
|
`Invalid path: potential security risk detected in path: ${pathValidationResult.error}`
|
|
12368
12981
|
);
|
|
12369
12982
|
}
|
|
12370
|
-
const
|
|
12983
|
+
const path13 = pathValidationResult.path;
|
|
12984
|
+
const gitService = new GitService(path13, log);
|
|
12371
12985
|
const gitValidation = await gitService.validateRepository();
|
|
12372
12986
|
if (!gitValidation.isValid) {
|
|
12373
12987
|
throw new Error(`Invalid git repository: ${gitValidation.error}`);
|
|
@@ -12380,13 +12994,12 @@ var CheckForAvailableFixesTool = class extends BaseTool {
|
|
|
12380
12994
|
if (!originUrl) {
|
|
12381
12995
|
throw new Error("No origin URL found for the repository");
|
|
12382
12996
|
}
|
|
12383
|
-
const
|
|
12384
|
-
const fixResult = await availableFixesService.checkForAvailableFixes({
|
|
12997
|
+
const fixResult = await this.availableFixesService.checkForAvailableFixes({
|
|
12385
12998
|
repoUrl: originUrl,
|
|
12386
12999
|
limit: args.limit,
|
|
12387
13000
|
offset: args.offset
|
|
12388
13001
|
});
|
|
12389
|
-
logInfo("
|
|
13002
|
+
logInfo("FetchAvailableFixesTool execution completed successfully", {
|
|
12390
13003
|
fixResult
|
|
12391
13004
|
});
|
|
12392
13005
|
return {
|
|
@@ -12400,55 +13013,13 @@ var CheckForAvailableFixesTool = class extends BaseTool {
|
|
|
12400
13013
|
}
|
|
12401
13014
|
};
|
|
12402
13015
|
|
|
12403
|
-
// src/mcp/tools/
|
|
12404
|
-
import
|
|
12405
|
-
|
|
12406
|
-
// src/mcp/services/FilePacking.ts
|
|
12407
|
-
import fs10 from "fs";
|
|
12408
|
-
import path12 from "path";
|
|
12409
|
-
import AdmZip2 from "adm-zip";
|
|
12410
|
-
var MAX_FILE_SIZE2 = 1024 * 1024 * 5;
|
|
12411
|
-
var FilePacking = class {
|
|
12412
|
-
async packFiles(sourceDirectoryPath, filesToPack) {
|
|
12413
|
-
logInfo(`FilePacking: packing files from ${sourceDirectoryPath}`);
|
|
12414
|
-
const zip = new AdmZip2();
|
|
12415
|
-
let packedFilesCount = 0;
|
|
12416
|
-
logInfo("FilePacking: compressing files");
|
|
12417
|
-
for (const filepath of filesToPack) {
|
|
12418
|
-
const absoluteFilepath = path12.join(sourceDirectoryPath, filepath);
|
|
12419
|
-
if (!FileUtils.shouldPackFile(absoluteFilepath, MAX_FILE_SIZE2)) {
|
|
12420
|
-
logInfo(
|
|
12421
|
-
`FilePacking: ignoring ${filepath} because it is excluded or invalid`
|
|
12422
|
-
);
|
|
12423
|
-
continue;
|
|
12424
|
-
}
|
|
12425
|
-
let data;
|
|
12426
|
-
try {
|
|
12427
|
-
data = fs10.readFileSync(absoluteFilepath);
|
|
12428
|
-
} catch (fsError) {
|
|
12429
|
-
logInfo(
|
|
12430
|
-
`FilePacking: failed to read ${filepath} from filesystem: ${fsError}`
|
|
12431
|
-
);
|
|
12432
|
-
continue;
|
|
12433
|
-
}
|
|
12434
|
-
zip.addFile(filepath, data);
|
|
12435
|
-
packedFilesCount++;
|
|
12436
|
-
}
|
|
12437
|
-
const zipBuffer = zip.toBuffer();
|
|
12438
|
-
logInfo(
|
|
12439
|
-
`FilePacking: read ${packedFilesCount} source files. total size: ${zipBuffer.length} bytes`
|
|
12440
|
-
);
|
|
12441
|
-
logInfo("FilePacking: Files packed successfully");
|
|
12442
|
-
return zipBuffer;
|
|
12443
|
-
}
|
|
12444
|
-
};
|
|
13016
|
+
// src/mcp/tools/scanAndFixVulnerabilities/ScanAndFixVulnerabilitiesTool.ts
|
|
13017
|
+
import z34 from "zod";
|
|
12445
13018
|
|
|
12446
|
-
// src/mcp/tools/
|
|
12447
|
-
var
|
|
12448
|
-
var VulnerabilityFixService = class {
|
|
13019
|
+
// src/mcp/tools/scanAndFixVulnerabilities/ScanAndFixVulnerabilitiesService.ts
|
|
13020
|
+
var _ScanAndFixVulnerabilitiesService = class _ScanAndFixVulnerabilitiesService {
|
|
12449
13021
|
constructor() {
|
|
12450
13022
|
__publicField(this, "gqlClient");
|
|
12451
|
-
__publicField(this, "filePacking");
|
|
12452
13023
|
/**
|
|
12453
13024
|
* Stores the fix report id that is created on the first run so that subsequent
|
|
12454
13025
|
* calls can skip the expensive packing/uploading/scan flow and directly fetch
|
|
@@ -12456,7 +13027,32 @@ var VulnerabilityFixService = class {
|
|
|
12456
13027
|
*/
|
|
12457
13028
|
__publicField(this, "storedFixReportId");
|
|
12458
13029
|
__publicField(this, "currentOffset", 0);
|
|
12459
|
-
|
|
13030
|
+
/**
|
|
13031
|
+
* Timestamp when the fixReportId was created
|
|
13032
|
+
* Used to expire the fixReportId after REPORT_ID_EXPIRATION_MS hours
|
|
13033
|
+
*/
|
|
13034
|
+
__publicField(this, "fixReportIdTimestamp");
|
|
13035
|
+
}
|
|
13036
|
+
static getInstance() {
|
|
13037
|
+
if (!_ScanAndFixVulnerabilitiesService.instance) {
|
|
13038
|
+
_ScanAndFixVulnerabilitiesService.instance = new _ScanAndFixVulnerabilitiesService();
|
|
13039
|
+
}
|
|
13040
|
+
return _ScanAndFixVulnerabilitiesService.instance;
|
|
13041
|
+
}
|
|
13042
|
+
reset() {
|
|
13043
|
+
this.storedFixReportId = void 0;
|
|
13044
|
+
this.currentOffset = void 0;
|
|
13045
|
+
this.fixReportIdTimestamp = void 0;
|
|
13046
|
+
}
|
|
13047
|
+
/**
|
|
13048
|
+
* Checks if the stored fixReportId has expired (older than 2 hours)
|
|
13049
|
+
*/
|
|
13050
|
+
isFixReportIdExpired() {
|
|
13051
|
+
if (!this.fixReportIdTimestamp) {
|
|
13052
|
+
return true;
|
|
13053
|
+
}
|
|
13054
|
+
const currentTime = Date.now();
|
|
13055
|
+
return currentTime - this.fixReportIdTimestamp > MCP_REPORT_ID_EXPIRATION_MS;
|
|
12460
13056
|
}
|
|
12461
13057
|
async processVulnerabilities({
|
|
12462
13058
|
fileList,
|
|
@@ -12467,37 +13063,44 @@ var VulnerabilityFixService = class {
|
|
|
12467
13063
|
}) {
|
|
12468
13064
|
try {
|
|
12469
13065
|
this.gqlClient = await this.initializeGqlClient();
|
|
13066
|
+
logInfo("storedFixReportId", {
|
|
13067
|
+
storedFixReportId: this.storedFixReportId,
|
|
13068
|
+
currentOffset: this.currentOffset,
|
|
13069
|
+
fixReportIdTimestamp: this.fixReportIdTimestamp,
|
|
13070
|
+
isExpired: this.storedFixReportId ? this.isFixReportIdExpired() : null
|
|
13071
|
+
});
|
|
12470
13072
|
let fixReportId = this.storedFixReportId;
|
|
12471
|
-
if (!fixReportId || isRescan) {
|
|
13073
|
+
if (!fixReportId || isRescan || this.isFixReportIdExpired()) {
|
|
13074
|
+
this.reset();
|
|
12472
13075
|
this.validateFiles(fileList);
|
|
12473
|
-
const
|
|
12474
|
-
|
|
12475
|
-
|
|
12476
|
-
|
|
12477
|
-
|
|
12478
|
-
|
|
12479
|
-
await this.runScan({ fixReportId, projectId });
|
|
12480
|
-
}
|
|
12481
|
-
let effectiveOffset;
|
|
12482
|
-
if (offset !== void 0) {
|
|
12483
|
-
effectiveOffset = offset;
|
|
12484
|
-
} else if (fixReportId) {
|
|
12485
|
-
effectiveOffset = this.currentOffset ?? 0;
|
|
12486
|
-
} else {
|
|
12487
|
-
effectiveOffset = 0;
|
|
13076
|
+
const scanResult = await scanFiles(
|
|
13077
|
+
fileList,
|
|
13078
|
+
repositoryPath,
|
|
13079
|
+
this.gqlClient
|
|
13080
|
+
);
|
|
13081
|
+
fixReportId = scanResult.fixReportId;
|
|
12488
13082
|
}
|
|
13083
|
+
const effectiveOffset = offset ?? (this.currentOffset || 0);
|
|
12489
13084
|
logDebug("effectiveOffset", { effectiveOffset });
|
|
12490
13085
|
const fixes = await this.getReportFixes(
|
|
12491
13086
|
fixReportId,
|
|
12492
13087
|
effectiveOffset,
|
|
12493
13088
|
limit
|
|
12494
13089
|
);
|
|
12495
|
-
|
|
12496
|
-
|
|
13090
|
+
if (fixes.totalCount > 0) {
|
|
13091
|
+
this.storedFixReportId = fixReportId;
|
|
13092
|
+
this.fixReportIdTimestamp = Date.now();
|
|
13093
|
+
} else {
|
|
13094
|
+
this.reset();
|
|
13095
|
+
}
|
|
13096
|
+
const prompt = fixesPrompt({
|
|
12497
13097
|
fixes: fixes.fixes,
|
|
12498
13098
|
totalCount: fixes.totalCount,
|
|
12499
|
-
offset: effectiveOffset
|
|
13099
|
+
offset: effectiveOffset,
|
|
13100
|
+
scannedFiles: [...fileList]
|
|
12500
13101
|
});
|
|
13102
|
+
this.currentOffset = effectiveOffset + (fixes.fixes?.length || 0);
|
|
13103
|
+
return prompt;
|
|
12501
13104
|
} catch (error) {
|
|
12502
13105
|
const message = error.message;
|
|
12503
13106
|
logError("Vulnerability processing failed", { error: message });
|
|
@@ -12514,106 +13117,11 @@ var VulnerabilityFixService = class {
|
|
|
12514
13117
|
const isConnected = await gqlClient.verifyConnection();
|
|
12515
13118
|
if (!isConnected) {
|
|
12516
13119
|
throw new ApiConnectionError(
|
|
12517
|
-
"Failed to connect to the API. Please check your
|
|
13120
|
+
"Failed to connect to the API. Please check your MOBB_API_KEY"
|
|
12518
13121
|
);
|
|
12519
13122
|
}
|
|
12520
13123
|
return gqlClient;
|
|
12521
13124
|
}
|
|
12522
|
-
async initializeReport() {
|
|
12523
|
-
if (!this.gqlClient) {
|
|
12524
|
-
throw new GqlClientError();
|
|
12525
|
-
}
|
|
12526
|
-
try {
|
|
12527
|
-
const {
|
|
12528
|
-
uploadS3BucketInfo: { repoUploadInfo }
|
|
12529
|
-
} = await this.gqlClient.uploadS3BucketInfo();
|
|
12530
|
-
logInfo("Upload info retrieved", { uploadKey: repoUploadInfo?.uploadKey });
|
|
12531
|
-
return repoUploadInfo;
|
|
12532
|
-
} catch (error) {
|
|
12533
|
-
const message = error.message;
|
|
12534
|
-
throw new ReportInitializationError(
|
|
12535
|
-
`Error initializing report: ${message}`
|
|
12536
|
-
);
|
|
12537
|
-
}
|
|
12538
|
-
}
|
|
12539
|
-
async packFiles(fileList, repositoryPath) {
|
|
12540
|
-
try {
|
|
12541
|
-
const zipBuffer = await this.filePacking.packFiles(
|
|
12542
|
-
repositoryPath,
|
|
12543
|
-
fileList
|
|
12544
|
-
);
|
|
12545
|
-
logInfo("Files packed successfully", { fileCount: fileList.length });
|
|
12546
|
-
return zipBuffer;
|
|
12547
|
-
} catch (error) {
|
|
12548
|
-
const message = error.message;
|
|
12549
|
-
throw new FileProcessingError(`Error packing files: ${message}`);
|
|
12550
|
-
}
|
|
12551
|
-
}
|
|
12552
|
-
async uploadFiles(zipBuffer, repoUploadInfo) {
|
|
12553
|
-
if (!repoUploadInfo) {
|
|
12554
|
-
throw new FileUploadError("Upload info is required");
|
|
12555
|
-
}
|
|
12556
|
-
try {
|
|
12557
|
-
await uploadFile({
|
|
12558
|
-
file: zipBuffer,
|
|
12559
|
-
url: repoUploadInfo.url,
|
|
12560
|
-
uploadFields: JSON.parse(repoUploadInfo.uploadFieldsJSON),
|
|
12561
|
-
uploadKey: repoUploadInfo.uploadKey
|
|
12562
|
-
});
|
|
12563
|
-
logInfo("File uploaded successfully");
|
|
12564
|
-
} catch (error) {
|
|
12565
|
-
logError("File upload failed", { error: error.message });
|
|
12566
|
-
throw new FileUploadError(
|
|
12567
|
-
`Failed to upload the file: ${error.message}`
|
|
12568
|
-
);
|
|
12569
|
-
}
|
|
12570
|
-
}
|
|
12571
|
-
async getProjectId() {
|
|
12572
|
-
if (!this.gqlClient) {
|
|
12573
|
-
throw new GqlClientError();
|
|
12574
|
-
}
|
|
12575
|
-
const projectId = await this.gqlClient.getProjectId();
|
|
12576
|
-
logInfo("Project ID retrieved", { projectId });
|
|
12577
|
-
return projectId;
|
|
12578
|
-
}
|
|
12579
|
-
async runScan(params) {
|
|
12580
|
-
if (!this.gqlClient) {
|
|
12581
|
-
throw new GqlClientError();
|
|
12582
|
-
}
|
|
12583
|
-
const { fixReportId, projectId } = params;
|
|
12584
|
-
logInfo("Starting scan", { fixReportId, projectId });
|
|
12585
|
-
const submitVulnerabilityReportVariables = {
|
|
12586
|
-
fixReportId,
|
|
12587
|
-
projectId,
|
|
12588
|
-
repoUrl: "",
|
|
12589
|
-
reference: "no-branch",
|
|
12590
|
-
scanSource: "MCP" /* Mcp */
|
|
12591
|
-
};
|
|
12592
|
-
logInfo("Submitting vulnerability report");
|
|
12593
|
-
const submitRes = await this.gqlClient.submitVulnerabilityReport(
|
|
12594
|
-
submitVulnerabilityReportVariables
|
|
12595
|
-
);
|
|
12596
|
-
if (submitRes.submitVulnerabilityReport.__typename !== "VulnerabilityReport") {
|
|
12597
|
-
logError("Vulnerability report submission failed", {
|
|
12598
|
-
response: submitRes
|
|
12599
|
-
});
|
|
12600
|
-
throw new ScanError("\u{1F575}\uFE0F\u200D\u2642\uFE0F Mobb analysis failed");
|
|
12601
|
-
}
|
|
12602
|
-
logInfo("Vulnerability report submitted successfully", {
|
|
12603
|
-
analysisId: submitRes.submitVulnerabilityReport.fixReportId
|
|
12604
|
-
});
|
|
12605
|
-
logInfo("Starting analysis subscription");
|
|
12606
|
-
await this.gqlClient.subscribeToGetAnalysis({
|
|
12607
|
-
subscribeToAnalysisParams: {
|
|
12608
|
-
analysisId: submitRes.submitVulnerabilityReport.fixReportId
|
|
12609
|
-
},
|
|
12610
|
-
callback: () => {
|
|
12611
|
-
},
|
|
12612
|
-
callbackStates: ["Finished" /* Finished */],
|
|
12613
|
-
timeoutInMs: VUL_REPORT_DIGEST_TIMEOUT_MS2
|
|
12614
|
-
});
|
|
12615
|
-
logInfo("Analysis subscription completed");
|
|
12616
|
-
}
|
|
12617
13125
|
async getReportFixes(fixReportId, offset, limit) {
|
|
12618
13126
|
logDebug("getReportFixes", { fixReportId, offset, limit });
|
|
12619
13127
|
if (!this.gqlClient) {
|
|
@@ -12631,28 +13139,68 @@ var VulnerabilityFixService = class {
|
|
|
12631
13139
|
};
|
|
12632
13140
|
}
|
|
12633
13141
|
};
|
|
13142
|
+
__publicField(_ScanAndFixVulnerabilitiesService, "instance");
|
|
13143
|
+
var ScanAndFixVulnerabilitiesService = _ScanAndFixVulnerabilitiesService;
|
|
12634
13144
|
|
|
12635
|
-
// src/mcp/tools/
|
|
12636
|
-
var
|
|
13145
|
+
// src/mcp/tools/scanAndFixVulnerabilities/ScanAndFixVulnerabilitiesTool.ts
|
|
13146
|
+
var ScanAndFixVulnerabilitiesTool = class extends BaseTool {
|
|
12637
13147
|
constructor() {
|
|
12638
|
-
super(
|
|
12639
|
-
__publicField(this, "name", "
|
|
12640
|
-
__publicField(this, "displayName", "Fix Vulnerabilities");
|
|
12641
|
-
|
|
12642
|
-
__publicField(this, "
|
|
12643
|
-
|
|
12644
|
-
|
|
13148
|
+
super();
|
|
13149
|
+
__publicField(this, "name", "scan_and_fix_vulnerabilities");
|
|
13150
|
+
__publicField(this, "displayName", "Scan and Fix Vulnerabilities");
|
|
13151
|
+
// A detailed description to guide the LLM on when and how to invoke this tool.
|
|
13152
|
+
__publicField(this, "description", `Scans a given local repository for security vulnerabilities and returns auto-generated code fixes.
|
|
13153
|
+
|
|
13154
|
+
When to invoke:
|
|
13155
|
+
\u2022 Use when the user explicitly asks to "scan for vulnerabilities", "run a security check", or "test for security issues" in a local repository.
|
|
13156
|
+
\u2022 The repository must exist on disk; supply its absolute path with the required "path" argument.
|
|
13157
|
+
\u2022 Ideal after the user makes code changes (added/modified/staged files) but before committing, or whenever they request a full rescan.
|
|
13158
|
+
|
|
13159
|
+
How to invoke:
|
|
13160
|
+
\u2022 Required argument:
|
|
13161
|
+
\u2013 path (string): absolute path to the repository root.
|
|
13162
|
+
\u2022 Optional arguments:
|
|
13163
|
+
\u2013 offset (number): pagination offset used when the result set is large.
|
|
13164
|
+
\u2013 limit (number): maximum number of fixes to include in the response.
|
|
13165
|
+
\u2013 maxFiles (number): maximum number of files to scan (default: ${MCP_DEFAULT_MAX_FILES_TO_SCAN}). Provide this value to increase the scope of the scan.
|
|
13166
|
+
\u2013 rescan (boolean): true to force a complete rescan even if cached results exist.
|
|
13167
|
+
|
|
13168
|
+
Behaviour:
|
|
13169
|
+
\u2022 If the directory is a valid Git repository, the tool scans the changed files in the repository. If there are no changes, it scans the files included in the las commit.
|
|
13170
|
+
\u2022 If the directory is not a valid Git repository, the tool falls back to scanning recently changed files in the folder.
|
|
13171
|
+
\u2022 If maxFiles is provided, the tool scans the maxFiles most recently changed files in the repository.
|
|
13172
|
+
\u2022 By default, only new, modified, or staged files are scanned; if none are found, it checks recently changed files.
|
|
13173
|
+
\u2022 The tool NEVER commits or pushes changes; it only returns proposed diffs/fixes as text.
|
|
13174
|
+
|
|
13175
|
+
Return value:
|
|
13176
|
+
The response is an object with a single "content" array containing one text element. The text is either:
|
|
13177
|
+
\u2022 A human-readable summary of the fixes / patches, or
|
|
13178
|
+
\u2022 A diagnostic or error message if the scan fails or finds nothing to fix.
|
|
13179
|
+
|
|
13180
|
+
Example payload:
|
|
13181
|
+
{
|
|
13182
|
+
"path": "/home/user/my-project",
|
|
13183
|
+
"limit": 20,
|
|
13184
|
+
"maxFiles": 50,
|
|
13185
|
+
"rescan": false
|
|
13186
|
+
}`);
|
|
13187
|
+
__publicField(this, "inputValidationSchema", z34.object({
|
|
13188
|
+
path: z34.string().describe(
|
|
13189
|
+
"Full local path to repository to scan and fix vulnerabilities"
|
|
12645
13190
|
),
|
|
12646
|
-
offset:
|
|
12647
|
-
limit:
|
|
12648
|
-
|
|
13191
|
+
offset: z34.number().optional().describe("Optional offset for pagination"),
|
|
13192
|
+
limit: z34.number().optional().describe("Optional maximum number of results to return"),
|
|
13193
|
+
maxFiles: z34.number().optional().describe(
|
|
13194
|
+
`Optional maximum number of files to scan (default: ${MCP_DEFAULT_MAX_FILES_TO_SCAN}). Increase for comprehensive scans of larger codebases or decrease for faster focused scans.`
|
|
13195
|
+
),
|
|
13196
|
+
rescan: z34.boolean().optional().describe("Optional whether to rescan the repository")
|
|
12649
13197
|
}));
|
|
12650
13198
|
__publicField(this, "inputSchema", {
|
|
12651
13199
|
type: "object",
|
|
12652
13200
|
properties: {
|
|
12653
13201
|
path: {
|
|
12654
13202
|
type: "string",
|
|
12655
|
-
description: "
|
|
13203
|
+
description: "Full local path to repository to scan and fix vulnerabilities"
|
|
12656
13204
|
},
|
|
12657
13205
|
offset: {
|
|
12658
13206
|
type: "number",
|
|
@@ -12662,6 +13210,10 @@ var FixVulnerabilitiesTool = class extends BaseTool {
|
|
|
12662
13210
|
type: "number",
|
|
12663
13211
|
description: "[Optional] maximum number of results to return"
|
|
12664
13212
|
},
|
|
13213
|
+
maxFiles: {
|
|
13214
|
+
type: "number",
|
|
13215
|
+
description: `[Optional] maximum number of files to scan (default: ${MCP_DEFAULT_MAX_FILES_TO_SCAN}). Use higher values for more comprehensive scans or lower values for faster performance.`
|
|
13216
|
+
},
|
|
12665
13217
|
rescan: {
|
|
12666
13218
|
type: "boolean",
|
|
12667
13219
|
description: "[Optional] whether to rescan the repository"
|
|
@@ -12669,55 +13221,29 @@ var FixVulnerabilitiesTool = class extends BaseTool {
|
|
|
12669
13221
|
},
|
|
12670
13222
|
required: ["path"]
|
|
12671
13223
|
});
|
|
13224
|
+
__publicField(this, "vulnerabilityFixService");
|
|
13225
|
+
this.vulnerabilityFixService = ScanAndFixVulnerabilitiesService.getInstance();
|
|
13226
|
+
this.vulnerabilityFixService.reset();
|
|
12672
13227
|
}
|
|
12673
13228
|
async executeInternal(args) {
|
|
12674
|
-
logInfo("Executing tool:
|
|
13229
|
+
logInfo("Executing tool: scan_and_fix_vulnerabilities", { path: args.path });
|
|
12675
13230
|
if (!args.path) {
|
|
12676
13231
|
throw new Error("Invalid arguments: Missing required parameter 'path'");
|
|
12677
13232
|
}
|
|
12678
|
-
const
|
|
12679
|
-
const pathValidationResult = await pathValidation.validatePath(args.path);
|
|
13233
|
+
const pathValidationResult = await validatePath(args.path);
|
|
12680
13234
|
if (!pathValidationResult.isValid) {
|
|
12681
13235
|
throw new Error(
|
|
12682
13236
|
`Invalid path: potential security risk detected in path: ${pathValidationResult.error}`
|
|
12683
13237
|
);
|
|
12684
13238
|
}
|
|
12685
|
-
const
|
|
12686
|
-
const
|
|
12687
|
-
|
|
12688
|
-
|
|
12689
|
-
|
|
12690
|
-
|
|
12691
|
-
|
|
12692
|
-
|
|
12693
|
-
}
|
|
12694
|
-
);
|
|
12695
|
-
files = FileUtils.getLastChangedFiles(args.path);
|
|
12696
|
-
logDebug("Found files in the repository", {
|
|
12697
|
-
files,
|
|
12698
|
-
fileCount: files.length
|
|
12699
|
-
});
|
|
12700
|
-
} else {
|
|
12701
|
-
const gitResult = await gitService.getChangedFiles();
|
|
12702
|
-
files = gitResult.files;
|
|
12703
|
-
if (files.length === 0) {
|
|
12704
|
-
const recentResult = await gitService.getRecentlyChangedFiles();
|
|
12705
|
-
files = recentResult.files;
|
|
12706
|
-
logDebug(
|
|
12707
|
-
"No changes found, using recently changed files from git history",
|
|
12708
|
-
{
|
|
12709
|
-
files,
|
|
12710
|
-
fileCount: files.length,
|
|
12711
|
-
commitsChecked: recentResult.commitCount
|
|
12712
|
-
}
|
|
12713
|
-
);
|
|
12714
|
-
} else {
|
|
12715
|
-
logDebug("Found changed files in the git repository", {
|
|
12716
|
-
files,
|
|
12717
|
-
fileCount: files.length
|
|
12718
|
-
});
|
|
12719
|
-
}
|
|
12720
|
-
}
|
|
13239
|
+
const path13 = pathValidationResult.path;
|
|
13240
|
+
const files = await getLocalFiles({
|
|
13241
|
+
path: path13,
|
|
13242
|
+
maxFileSize: 1024 * 1024 * 5,
|
|
13243
|
+
// 5MB
|
|
13244
|
+
maxFiles: args.maxFiles
|
|
13245
|
+
});
|
|
13246
|
+
logInfo("Files", { files });
|
|
12721
13247
|
if (files.length === 0) {
|
|
12722
13248
|
return {
|
|
12723
13249
|
content: [
|
|
@@ -12729,13 +13255,12 @@ var FixVulnerabilitiesTool = class extends BaseTool {
|
|
|
12729
13255
|
};
|
|
12730
13256
|
}
|
|
12731
13257
|
try {
|
|
12732
|
-
const
|
|
12733
|
-
|
|
12734
|
-
fileList: files,
|
|
13258
|
+
const fixResult = await this.vulnerabilityFixService.processVulnerabilities({
|
|
13259
|
+
fileList: files.map((file) => file.relativePath),
|
|
12735
13260
|
repositoryPath: args.path,
|
|
12736
13261
|
offset: args.offset,
|
|
12737
13262
|
limit: args.limit,
|
|
12738
|
-
isRescan: args.rescan
|
|
13263
|
+
isRescan: args.rescan || !!args.maxFiles
|
|
12739
13264
|
});
|
|
12740
13265
|
const result = {
|
|
12741
13266
|
content: [
|
|
@@ -12774,7 +13299,7 @@ function createMcpServer() {
|
|
|
12774
13299
|
logDebug("Creating MCP server");
|
|
12775
13300
|
const server = new McpServer({
|
|
12776
13301
|
name: "mobb-mcp",
|
|
12777
|
-
version:
|
|
13302
|
+
version: packageJson.version
|
|
12778
13303
|
});
|
|
12779
13304
|
const enabledToolsEnv = process.env["TOOLS_ENABLED"];
|
|
12780
13305
|
const enabledToolsSet = enabledToolsEnv ? new Set(
|
|
@@ -12788,10 +13313,12 @@ function createMcpServer() {
|
|
|
12788
13313
|
logDebug(`Skipping tool (disabled): ${tool.name}`);
|
|
12789
13314
|
}
|
|
12790
13315
|
};
|
|
12791
|
-
const
|
|
12792
|
-
const
|
|
12793
|
-
|
|
12794
|
-
registerIfEnabled(
|
|
13316
|
+
const scanAndFixVulnerabilitiesTool = new ScanAndFixVulnerabilitiesTool();
|
|
13317
|
+
const fetchAvailableFixesTool = new FetchAvailableFixesTool();
|
|
13318
|
+
const checkForNewAvailableFixesTool = new CheckForNewAvailableFixesTool();
|
|
13319
|
+
registerIfEnabled(scanAndFixVulnerabilitiesTool);
|
|
13320
|
+
registerIfEnabled(fetchAvailableFixesTool);
|
|
13321
|
+
registerIfEnabled(checkForNewAvailableFixesTool);
|
|
12795
13322
|
logInfo("MCP server created and configured");
|
|
12796
13323
|
return server;
|
|
12797
13324
|
}
|
|
@@ -12825,7 +13352,7 @@ var mcpHandler = async (_args) => {
|
|
|
12825
13352
|
};
|
|
12826
13353
|
|
|
12827
13354
|
// src/args/commands/review.ts
|
|
12828
|
-
import
|
|
13355
|
+
import fs12 from "fs";
|
|
12829
13356
|
import chalk9 from "chalk";
|
|
12830
13357
|
function reviewBuilder(yargs2) {
|
|
12831
13358
|
return yargs2.option("f", {
|
|
@@ -12862,7 +13389,7 @@ function reviewBuilder(yargs2) {
|
|
|
12862
13389
|
).help();
|
|
12863
13390
|
}
|
|
12864
13391
|
function validateReviewOptions(argv) {
|
|
12865
|
-
if (!
|
|
13392
|
+
if (!fs12.existsSync(argv.f)) {
|
|
12866
13393
|
throw new CliError(`
|
|
12867
13394
|
Can't access ${chalk9.bold(argv.f)}`);
|
|
12868
13395
|
}
|