mobbdev 1.0.171 → 1.0.176
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/README.md +7 -1
- package/dist/index.mjs +2729 -179
- package/package.json +8 -2
package/dist/index.mjs
CHANGED
|
@@ -32,7 +32,7 @@ var init_env = __esm({
|
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
// src/mcp/core/configs.ts
|
|
35
|
-
var MCP_DEFAULT_API_URL, MCP_API_KEY_HEADER_NAME, MCP_LOGIN_MAX_WAIT, MCP_LOGIN_CHECK_DELAY, MCP_VUL_REPORT_DIGEST_TIMEOUT_MS, MCP_MAX_FILE_SIZE, MCP_PERIODIC_CHECK_INTERVAL, MCP_DEFAULT_MAX_FILES_TO_SCAN, MCP_REPORT_ID_EXPIRATION_MS, MCP_TOOLS_BROWSER_COOLDOWN_MS, MCP_DEFAULT_LIMIT, isAutoScan;
|
|
35
|
+
var MCP_DEFAULT_API_URL, MCP_API_KEY_HEADER_NAME, MCP_LOGIN_MAX_WAIT, MCP_LOGIN_CHECK_DELAY, MCP_VUL_REPORT_DIGEST_TIMEOUT_MS, MCP_MAX_FILE_SIZE, MCP_PERIODIC_CHECK_INTERVAL, MCP_DEFAULT_MAX_FILES_TO_SCAN, MCP_REPORT_ID_EXPIRATION_MS, MCP_TOOLS_BROWSER_COOLDOWN_MS, MCP_DEFAULT_LIMIT, isAutoScan, MVS_AUTO_FIX_OVERRIDE, MCP_AUTO_FIX_DEBUG_MODE;
|
|
36
36
|
var init_configs = __esm({
|
|
37
37
|
"src/mcp/core/configs.ts"() {
|
|
38
38
|
"use strict";
|
|
@@ -49,6 +49,8 @@ var init_configs = __esm({
|
|
|
49
49
|
MCP_TOOLS_BROWSER_COOLDOWN_MS = 24 * 60 * 60 * 1e3;
|
|
50
50
|
MCP_DEFAULT_LIMIT = 3;
|
|
51
51
|
isAutoScan = process.env["AUTO_SCAN"] !== "false";
|
|
52
|
+
MVS_AUTO_FIX_OVERRIDE = process.env["MVS_AUTO_FIX"];
|
|
53
|
+
MCP_AUTO_FIX_DEBUG_MODE = true;
|
|
52
54
|
}
|
|
53
55
|
});
|
|
54
56
|
|
|
@@ -578,16 +580,16 @@ var init_FileUtils = __esm({
|
|
|
578
580
|
FileUtils = class {
|
|
579
581
|
// Important project configuration files that should always be included
|
|
580
582
|
static isExcludedFileType(filepath) {
|
|
581
|
-
const
|
|
582
|
-
if (IMPORTANT_PROJECT_FILES.includes(
|
|
583
|
+
const basename2 = path.basename(filepath).toLowerCase();
|
|
584
|
+
if (IMPORTANT_PROJECT_FILES.includes(basename2)) {
|
|
583
585
|
return false;
|
|
584
586
|
}
|
|
585
587
|
const ext = path.extname(filepath).toLowerCase();
|
|
586
|
-
const isSupported = SUPPORTED_EXTENSIONS.includes(ext) || SUPPORTED_EXTENSIONS.includes(
|
|
588
|
+
const isSupported = SUPPORTED_EXTENSIONS.includes(ext) || SUPPORTED_EXTENSIONS.includes(basename2);
|
|
587
589
|
if (!isSupported) {
|
|
588
590
|
return true;
|
|
589
591
|
}
|
|
590
|
-
if (EXCLUDED_FILE_PATTERNS.some((pattern) =>
|
|
592
|
+
if (EXCLUDED_FILE_PATTERNS.some((pattern) => basename2.endsWith(pattern))) {
|
|
591
593
|
return true;
|
|
592
594
|
}
|
|
593
595
|
return false;
|
|
@@ -739,6 +741,26 @@ var init_GitService = __esm({
|
|
|
739
741
|
repositoryPath
|
|
740
742
|
});
|
|
741
743
|
}
|
|
744
|
+
/**
|
|
745
|
+
* Checks if the current path is within a git repository
|
|
746
|
+
* @returns Promise<boolean> True if it's a git repository, false otherwise
|
|
747
|
+
*/
|
|
748
|
+
async isGitRepository() {
|
|
749
|
+
try {
|
|
750
|
+
const isRepo = await this.git.checkIsRepo();
|
|
751
|
+
if (!isRepo) {
|
|
752
|
+
this.log("[GitService] Not a git repository", "debug");
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
await this.git.revparse(["--show-toplevel"]);
|
|
756
|
+
return true;
|
|
757
|
+
} catch (error) {
|
|
758
|
+
this.log("[GitService] Not a git repository", "debug", {
|
|
759
|
+
error: String(error)
|
|
760
|
+
});
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
742
764
|
/**
|
|
743
765
|
* Validates that the path is a valid git repository
|
|
744
766
|
*/
|
|
@@ -782,9 +804,12 @@ var init_GitService = __esm({
|
|
|
782
804
|
if (gitRelativePath.startsWith(relativePathFromGitRoot + "/")) {
|
|
783
805
|
return gitRelativePath.substring(relativePathFromGitRoot.length + 1);
|
|
784
806
|
}
|
|
807
|
+
const safeInput = path2.basename(
|
|
808
|
+
String(gitRelativePath || "").replace("\0", "").replace(/^(\.\.(\/|\\$))+/, "")
|
|
809
|
+
);
|
|
785
810
|
return path2.relative(
|
|
786
811
|
this.repositoryPath,
|
|
787
|
-
path2.join(gitRoot,
|
|
812
|
+
path2.join(gitRoot, safeInput)
|
|
788
813
|
);
|
|
789
814
|
});
|
|
790
815
|
this.log("[GitService] Git status retrieved", "info", {
|
|
@@ -1209,6 +1234,83 @@ ${rootContent}`;
|
|
|
1209
1234
|
if (!content) return null;
|
|
1210
1235
|
return ignore().add(content);
|
|
1211
1236
|
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Gets the git repository root directory path
|
|
1239
|
+
* @returns Absolute path to the git repository root
|
|
1240
|
+
*/
|
|
1241
|
+
async getGitRoot() {
|
|
1242
|
+
this.log("[GitService] Getting git repository root", "debug");
|
|
1243
|
+
try {
|
|
1244
|
+
const gitRoot = await this.git.revparse(["--show-toplevel"]);
|
|
1245
|
+
this.log("[GitService] Git root retrieved", "debug", { gitRoot });
|
|
1246
|
+
return gitRoot;
|
|
1247
|
+
} catch (error) {
|
|
1248
|
+
const errorMessage = `Failed to get git repository root: ${error.message}`;
|
|
1249
|
+
this.log(`[GitService] ${errorMessage}`, "error", { error });
|
|
1250
|
+
throw new Error(errorMessage);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Ensures that a specific entry exists in the .gitignore file
|
|
1255
|
+
* Creates .gitignore if it doesn't exist, adds the entry if not present
|
|
1256
|
+
* @param entry The entry to add to .gitignore (e.g., '.mobb', 'node_modules')
|
|
1257
|
+
* @returns True if entry was added, false if it already existed
|
|
1258
|
+
*/
|
|
1259
|
+
async ensureGitignoreEntry(entry) {
|
|
1260
|
+
this.log("[GitService] Ensuring .gitignore entry", "debug", { entry });
|
|
1261
|
+
try {
|
|
1262
|
+
const gitRoot = await this.getGitRoot();
|
|
1263
|
+
const gitignorePath = path2.join(gitRoot, ".gitignore");
|
|
1264
|
+
let gitignoreContent = "";
|
|
1265
|
+
if (fs3.existsSync(gitignorePath)) {
|
|
1266
|
+
gitignoreContent = fs3.readFileSync(gitignorePath, "utf8");
|
|
1267
|
+
this.log("[GitService] .gitignore file exists", "debug");
|
|
1268
|
+
} else {
|
|
1269
|
+
this.log("[GitService] Creating .gitignore file", "info", {
|
|
1270
|
+
gitignorePath
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
if (gitignoreContent.includes(entry)) {
|
|
1274
|
+
this.log("[GitService] Entry already exists in .gitignore", "debug", {
|
|
1275
|
+
entry
|
|
1276
|
+
});
|
|
1277
|
+
return false;
|
|
1278
|
+
}
|
|
1279
|
+
this.log("[GitService] Adding entry to .gitignore", "info", { entry });
|
|
1280
|
+
const newLine = gitignoreContent.endsWith("\n") || gitignoreContent === "" ? "" : "\n";
|
|
1281
|
+
const updatedContent = `${gitignoreContent}${newLine}${entry}
|
|
1282
|
+
`;
|
|
1283
|
+
fs3.writeFileSync(gitignorePath, updatedContent, "utf8");
|
|
1284
|
+
this.log("[GitService] .gitignore updated successfully", "debug", {
|
|
1285
|
+
entry
|
|
1286
|
+
});
|
|
1287
|
+
return true;
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
const errorMessage = `Failed to ensure .gitignore entry: ${error.message}`;
|
|
1290
|
+
this.log(`[GitService] ${errorMessage}`, "error", { error, entry });
|
|
1291
|
+
throw new Error(errorMessage);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Checks if the .gitignore file exists in the repository root
|
|
1296
|
+
* @returns True if .gitignore exists, false otherwise
|
|
1297
|
+
*/
|
|
1298
|
+
async gitignoreExists() {
|
|
1299
|
+
this.log("[GitService] Checking if .gitignore exists", "debug");
|
|
1300
|
+
try {
|
|
1301
|
+
const gitRoot = await this.getGitRoot();
|
|
1302
|
+
const gitignorePath = path2.join(gitRoot, ".gitignore");
|
|
1303
|
+
const exists = fs3.existsSync(gitignorePath);
|
|
1304
|
+
this.log("[GitService] .gitignore existence check complete", "debug", {
|
|
1305
|
+
exists
|
|
1306
|
+
});
|
|
1307
|
+
return exists;
|
|
1308
|
+
} catch (error) {
|
|
1309
|
+
const errorMessage = `Failed to check .gitignore existence: ${error.message}`;
|
|
1310
|
+
this.log(`[GitService] ${errorMessage}`, "error", { error });
|
|
1311
|
+
throw new Error(errorMessage);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1212
1314
|
};
|
|
1213
1315
|
}
|
|
1214
1316
|
});
|
|
@@ -1570,6 +1672,7 @@ var IssueLanguage_Enum = /* @__PURE__ */ ((IssueLanguage_Enum2) => {
|
|
|
1570
1672
|
return IssueLanguage_Enum2;
|
|
1571
1673
|
})(IssueLanguage_Enum || {});
|
|
1572
1674
|
var IssueType_Enum = /* @__PURE__ */ ((IssueType_Enum2) => {
|
|
1675
|
+
IssueType_Enum2["ActionNotPinnedToCommitSha"] = "ACTION_NOT_PINNED_TO_COMMIT_SHA";
|
|
1573
1676
|
IssueType_Enum2["AutoEscapeFalse"] = "AUTO_ESCAPE_FALSE";
|
|
1574
1677
|
IssueType_Enum2["AvoidBuiltinShadowing"] = "AVOID_BUILTIN_SHADOWING";
|
|
1575
1678
|
IssueType_Enum2["AvoidIdentityComparisonCachedTypes"] = "AVOID_IDENTITY_COMPARISON_CACHED_TYPES";
|
|
@@ -1778,6 +1881,7 @@ var FixDetailsFragmentDoc = `
|
|
|
1778
1881
|
gitBlameLogin
|
|
1779
1882
|
severityValue
|
|
1780
1883
|
vulnerabilityReportIssues {
|
|
1884
|
+
category
|
|
1781
1885
|
parsedIssueType
|
|
1782
1886
|
parsedSeverity
|
|
1783
1887
|
vulnerabilityReportIssueTags {
|
|
@@ -1786,6 +1890,9 @@ var FixDetailsFragmentDoc = `
|
|
|
1786
1890
|
}
|
|
1787
1891
|
sharedState {
|
|
1788
1892
|
id
|
|
1893
|
+
downloadedBy {
|
|
1894
|
+
downloadSource
|
|
1895
|
+
}
|
|
1789
1896
|
}
|
|
1790
1897
|
patchAndQuestions {
|
|
1791
1898
|
__typename
|
|
@@ -1870,6 +1977,10 @@ var FixReportSummaryFieldsFragmentDoc = `
|
|
|
1870
1977
|
vulnerabilityReport {
|
|
1871
1978
|
scanDate
|
|
1872
1979
|
vendor
|
|
1980
|
+
projectId
|
|
1981
|
+
project {
|
|
1982
|
+
organizationId
|
|
1983
|
+
}
|
|
1873
1984
|
totalVulnerabilityReportIssuesCount: vulnerabilityReportIssues_aggregate {
|
|
1874
1985
|
aggregate {
|
|
1875
1986
|
count
|
|
@@ -2374,6 +2485,13 @@ var UpdateDownloadedFixDataDocument = `
|
|
|
2374
2485
|
}
|
|
2375
2486
|
}
|
|
2376
2487
|
`;
|
|
2488
|
+
var GetUserMvsAutoFixDocument = `
|
|
2489
|
+
query GetUserMvsAutoFix($userEmail: String!) {
|
|
2490
|
+
user_email_notification_settings(where: {user: {email: {_eq: $userEmail}}}) {
|
|
2491
|
+
mvs_auto_fix
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
`;
|
|
2377
2495
|
var defaultWrapper = (action, _operationName, _operationType, _variables) => action();
|
|
2378
2496
|
function getSdk(client, withWrapper = defaultWrapper) {
|
|
2379
2497
|
return {
|
|
@@ -2451,6 +2569,9 @@ function getSdk(client, withWrapper = defaultWrapper) {
|
|
|
2451
2569
|
},
|
|
2452
2570
|
updateDownloadedFixData(variables, requestHeaders, signal) {
|
|
2453
2571
|
return withWrapper((wrappedRequestHeaders) => client.request({ document: UpdateDownloadedFixDataDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), "updateDownloadedFixData", "mutation", variables);
|
|
2572
|
+
},
|
|
2573
|
+
GetUserMvsAutoFix(variables, requestHeaders, signal) {
|
|
2574
|
+
return withWrapper((wrappedRequestHeaders) => client.request({ document: GetUserMvsAutoFixDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), "GetUserMvsAutoFix", "query", variables);
|
|
2454
2575
|
}
|
|
2455
2576
|
};
|
|
2456
2577
|
}
|
|
@@ -2753,7 +2874,8 @@ var fixDetailsData = {
|
|
|
2753
2874
|
["NO_ASSERT" /* NoAssert */]: void 0,
|
|
2754
2875
|
["FUNCTION_CALL_WITHOUT_PARENTHESES" /* FunctionCallWithoutParentheses */]: void 0,
|
|
2755
2876
|
["SPRING_DEFAULT_PERMIT" /* SpringDefaultPermit */]: void 0,
|
|
2756
|
-
["RETURN_IN_INIT" /* ReturnInInit */]: void 0
|
|
2877
|
+
["RETURN_IN_INIT" /* ReturnInInit */]: void 0,
|
|
2878
|
+
["ACTION_NOT_PINNED_TO_COMMIT_SHA" /* ActionNotPinnedToCommitSha */]: void 0
|
|
2757
2879
|
};
|
|
2758
2880
|
|
|
2759
2881
|
// src/features/analysis/scm/shared/src/getIssueType.ts
|
|
@@ -2890,7 +3012,8 @@ var issueTypeMap = {
|
|
|
2890
3012
|
["NO_ASSERT" /* NoAssert */]: "No Assert",
|
|
2891
3013
|
["FUNCTION_CALL_WITHOUT_PARENTHESES" /* FunctionCallWithoutParentheses */]: "Function Call Without Parentheses",
|
|
2892
3014
|
["SPRING_DEFAULT_PERMIT" /* SpringDefaultPermit */]: "Spring Default Permit",
|
|
2893
|
-
["RETURN_IN_INIT" /* ReturnInInit */]: "Return in Init"
|
|
3015
|
+
["RETURN_IN_INIT" /* ReturnInInit */]: "Return in Init",
|
|
3016
|
+
["ACTION_NOT_PINNED_TO_COMMIT_SHA" /* ActionNotPinnedToCommitSha */]: "Action Not Pinned to Commit Sha"
|
|
2894
3017
|
};
|
|
2895
3018
|
var issueTypeZ = z.nativeEnum(IssueType_Enum);
|
|
2896
3019
|
var getIssueTypeFriendlyString = (issueType) => {
|
|
@@ -6250,7 +6373,7 @@ async function getAdoSdk(params) {
|
|
|
6250
6373
|
const url = new URL(repoUrl);
|
|
6251
6374
|
const origin2 = url.origin.toLowerCase().endsWith(".visualstudio.com") ? DEFUALT_ADO_ORIGIN : url.origin.toLowerCase();
|
|
6252
6375
|
const params2 = `path=/&versionDescriptor[versionOptions]=0&versionDescriptor[versionType]=commit&versionDescriptor[version]=${branch}&resolveLfs=true&$format=zip&api-version=5.0&download=true`;
|
|
6253
|
-
const
|
|
6376
|
+
const path15 = [
|
|
6254
6377
|
prefixPath,
|
|
6255
6378
|
owner,
|
|
6256
6379
|
projectName,
|
|
@@ -6261,7 +6384,7 @@ async function getAdoSdk(params) {
|
|
|
6261
6384
|
"items",
|
|
6262
6385
|
"items"
|
|
6263
6386
|
].filter(Boolean).join("/");
|
|
6264
|
-
return new URL(`${
|
|
6387
|
+
return new URL(`${path15}?${params2}`, origin2).toString();
|
|
6265
6388
|
},
|
|
6266
6389
|
async getAdoBranchList({ repoUrl }) {
|
|
6267
6390
|
try {
|
|
@@ -7716,14 +7839,14 @@ function getGithubSdk(params = {}) {
|
|
|
7716
7839
|
};
|
|
7717
7840
|
},
|
|
7718
7841
|
async getGithubBlameRanges(params2) {
|
|
7719
|
-
const { ref, gitHubUrl, path:
|
|
7842
|
+
const { ref, gitHubUrl, path: path15 } = params2;
|
|
7720
7843
|
const { owner, repo } = parseGithubOwnerAndRepo(gitHubUrl);
|
|
7721
7844
|
const res = await octokit.graphql(
|
|
7722
7845
|
GET_BLAME_DOCUMENT,
|
|
7723
7846
|
{
|
|
7724
7847
|
owner,
|
|
7725
7848
|
repo,
|
|
7726
|
-
path:
|
|
7849
|
+
path: path15,
|
|
7727
7850
|
ref
|
|
7728
7851
|
}
|
|
7729
7852
|
);
|
|
@@ -8030,11 +8153,11 @@ var GithubSCMLib = class extends SCMLib {
|
|
|
8030
8153
|
markdownComment: comment
|
|
8031
8154
|
});
|
|
8032
8155
|
}
|
|
8033
|
-
async getRepoBlameRanges(ref,
|
|
8156
|
+
async getRepoBlameRanges(ref, path15) {
|
|
8034
8157
|
this._validateUrl();
|
|
8035
8158
|
return await this.githubSdk.getGithubBlameRanges({
|
|
8036
8159
|
ref,
|
|
8037
|
-
path:
|
|
8160
|
+
path: path15,
|
|
8038
8161
|
gitHubUrl: this.url
|
|
8039
8162
|
});
|
|
8040
8163
|
}
|
|
@@ -8448,13 +8571,13 @@ function parseGitlabOwnerAndRepo(gitlabUrl) {
|
|
|
8448
8571
|
const { organization, repoName, projectPath } = parsingResult;
|
|
8449
8572
|
return { owner: organization, repo: repoName, projectPath };
|
|
8450
8573
|
}
|
|
8451
|
-
async function getGitlabBlameRanges({ ref, gitlabUrl, path:
|
|
8574
|
+
async function getGitlabBlameRanges({ ref, gitlabUrl, path: path15 }, options) {
|
|
8452
8575
|
const { projectPath } = parseGitlabOwnerAndRepo(gitlabUrl);
|
|
8453
8576
|
const api2 = getGitBeaker({
|
|
8454
8577
|
url: gitlabUrl,
|
|
8455
8578
|
gitlabAuthToken: options?.gitlabAuthToken
|
|
8456
8579
|
});
|
|
8457
|
-
const resp = await api2.RepositoryFiles.allFileBlames(projectPath,
|
|
8580
|
+
const resp = await api2.RepositoryFiles.allFileBlames(projectPath, path15, ref);
|
|
8458
8581
|
let lineNumber = 1;
|
|
8459
8582
|
return resp.filter((range) => range.lines).map((range) => {
|
|
8460
8583
|
const oldLineNumber = lineNumber;
|
|
@@ -8630,10 +8753,10 @@ var GitlabSCMLib = class extends SCMLib {
|
|
|
8630
8753
|
markdownComment: comment
|
|
8631
8754
|
});
|
|
8632
8755
|
}
|
|
8633
|
-
async getRepoBlameRanges(ref,
|
|
8756
|
+
async getRepoBlameRanges(ref, path15) {
|
|
8634
8757
|
this._validateUrl();
|
|
8635
8758
|
return await getGitlabBlameRanges(
|
|
8636
|
-
{ ref, path:
|
|
8759
|
+
{ ref, path: path15, gitlabUrl: this.url },
|
|
8637
8760
|
{
|
|
8638
8761
|
url: this.url,
|
|
8639
8762
|
gitlabAuthToken: this.accessToken
|
|
@@ -9711,7 +9834,7 @@ async function postIssueComment(params) {
|
|
|
9711
9834
|
fpDescription
|
|
9712
9835
|
} = params;
|
|
9713
9836
|
const {
|
|
9714
|
-
path:
|
|
9837
|
+
path: path15,
|
|
9715
9838
|
startLine,
|
|
9716
9839
|
vulnerabilityReportIssue: {
|
|
9717
9840
|
vulnerabilityReportIssueTags,
|
|
@@ -9726,7 +9849,7 @@ async function postIssueComment(params) {
|
|
|
9726
9849
|
Refresh the page in order to see the changes.`,
|
|
9727
9850
|
pull_number: pullRequest,
|
|
9728
9851
|
commit_id: commitSha,
|
|
9729
|
-
path:
|
|
9852
|
+
path: path15,
|
|
9730
9853
|
line: startLine
|
|
9731
9854
|
});
|
|
9732
9855
|
const commentId = commentRes.data.id;
|
|
@@ -9760,7 +9883,7 @@ async function postFixComment(params) {
|
|
|
9760
9883
|
scanner
|
|
9761
9884
|
} = params;
|
|
9762
9885
|
const {
|
|
9763
|
-
path:
|
|
9886
|
+
path: path15,
|
|
9764
9887
|
startLine,
|
|
9765
9888
|
vulnerabilityReportIssue: { fixId, vulnerabilityReportIssueTags, category },
|
|
9766
9889
|
vulnerabilityReportIssueId
|
|
@@ -9778,7 +9901,7 @@ async function postFixComment(params) {
|
|
|
9778
9901
|
Refresh the page in order to see the changes.`,
|
|
9779
9902
|
pull_number: pullRequest,
|
|
9780
9903
|
commit_id: commitSha,
|
|
9781
|
-
path:
|
|
9904
|
+
path: path15,
|
|
9782
9905
|
line: startLine
|
|
9783
9906
|
});
|
|
9784
9907
|
const commentId = commentRes.data.id;
|
|
@@ -12166,6 +12289,132 @@ import { GraphQLClient as GraphQLClient2 } from "graphql-request";
|
|
|
12166
12289
|
import { v4 as uuidv42 } from "uuid";
|
|
12167
12290
|
init_configs();
|
|
12168
12291
|
|
|
12292
|
+
// src/mcp/types.ts
|
|
12293
|
+
import { z as z31 } from "zod";
|
|
12294
|
+
var ScanAndFixVulnerabilitiesToolSchema = z31.object({
|
|
12295
|
+
path: z31.string()
|
|
12296
|
+
});
|
|
12297
|
+
var VulnerabilityReportIssueTagSchema = z31.object({
|
|
12298
|
+
vulnerability_report_issue_tag_value: z31.nativeEnum(
|
|
12299
|
+
Vulnerability_Report_Issue_Tag_Enum
|
|
12300
|
+
)
|
|
12301
|
+
});
|
|
12302
|
+
var VulnerabilityReportIssueSchema = z31.object({
|
|
12303
|
+
category: z31.any().optional().nullable(),
|
|
12304
|
+
parsedIssueType: z31.nativeEnum(IssueType_Enum).nullable().optional(),
|
|
12305
|
+
parsedSeverity: z31.nativeEnum(Vulnerability_Severity_Enum).nullable().optional(),
|
|
12306
|
+
vulnerabilityReportIssueTags: z31.array(VulnerabilityReportIssueTagSchema)
|
|
12307
|
+
});
|
|
12308
|
+
var SharedStateSchema = z31.object({
|
|
12309
|
+
__typename: z31.literal("fix_shared_state").optional(),
|
|
12310
|
+
id: z31.any(),
|
|
12311
|
+
// GraphQL uses `any` type for UUID
|
|
12312
|
+
downloadedBy: z31.array(z31.any().nullable()).optional().nullable()
|
|
12313
|
+
});
|
|
12314
|
+
var UnstructuredFixExtraContextSchema = z31.object({
|
|
12315
|
+
__typename: z31.literal("UnstructuredFixExtraContext").optional(),
|
|
12316
|
+
key: z31.string(),
|
|
12317
|
+
value: z31.any()
|
|
12318
|
+
// GraphQL JSON type
|
|
12319
|
+
});
|
|
12320
|
+
var FixExtraContextResponseSchema = z31.object({
|
|
12321
|
+
__typename: z31.literal("FixExtraContextResponse").optional(),
|
|
12322
|
+
extraContext: z31.array(UnstructuredFixExtraContextSchema),
|
|
12323
|
+
fixDescription: z31.string()
|
|
12324
|
+
});
|
|
12325
|
+
var FixDataSchema = z31.object({
|
|
12326
|
+
__typename: z31.literal("FixData"),
|
|
12327
|
+
patch: z31.string(),
|
|
12328
|
+
patchOriginalEncodingBase64: z31.string(),
|
|
12329
|
+
extraContext: FixExtraContextResponseSchema
|
|
12330
|
+
});
|
|
12331
|
+
var GetFixNoFixErrorSchema = z31.object({
|
|
12332
|
+
__typename: z31.literal("GetFixNoFixError")
|
|
12333
|
+
});
|
|
12334
|
+
var PatchAndQuestionsSchema = z31.union([FixDataSchema, GetFixNoFixErrorSchema]);
|
|
12335
|
+
var McpFixSchema = z31.object({
|
|
12336
|
+
__typename: z31.literal("fix").optional(),
|
|
12337
|
+
id: z31.any(),
|
|
12338
|
+
// GraphQL uses `any` type for UUID
|
|
12339
|
+
confidence: z31.number(),
|
|
12340
|
+
safeIssueType: z31.string().nullable(),
|
|
12341
|
+
severityText: z31.string().nullable(),
|
|
12342
|
+
gitBlameLogin: z31.string().nullable().optional(),
|
|
12343
|
+
// Optional in GraphQL
|
|
12344
|
+
severityValue: z31.number().nullable(),
|
|
12345
|
+
vulnerabilityReportIssues: z31.array(VulnerabilityReportIssueSchema),
|
|
12346
|
+
sharedState: SharedStateSchema.nullable().optional(),
|
|
12347
|
+
// Optional in GraphQL
|
|
12348
|
+
patchAndQuestions: PatchAndQuestionsSchema,
|
|
12349
|
+
// Additional field added by the client
|
|
12350
|
+
fixUrl: z31.string().optional()
|
|
12351
|
+
});
|
|
12352
|
+
var FixAggregateSchema = z31.object({
|
|
12353
|
+
__typename: z31.literal("fix_aggregate").optional(),
|
|
12354
|
+
aggregate: z31.object({
|
|
12355
|
+
__typename: z31.literal("fix_aggregate_fields").optional(),
|
|
12356
|
+
count: z31.number()
|
|
12357
|
+
}).nullable()
|
|
12358
|
+
});
|
|
12359
|
+
var VulnerabilityReportIssueAggregateSchema = z31.object({
|
|
12360
|
+
__typename: z31.literal("vulnerability_report_issue_aggregate").optional(),
|
|
12361
|
+
aggregate: z31.object({
|
|
12362
|
+
__typename: z31.literal("vulnerability_report_issue_aggregate_fields").optional(),
|
|
12363
|
+
count: z31.number()
|
|
12364
|
+
}).nullable()
|
|
12365
|
+
});
|
|
12366
|
+
var RepoSchema = z31.object({
|
|
12367
|
+
__typename: z31.literal("repo").optional(),
|
|
12368
|
+
originalUrl: z31.string()
|
|
12369
|
+
});
|
|
12370
|
+
var ProjectSchema = z31.object({
|
|
12371
|
+
id: z31.any(),
|
|
12372
|
+
// GraphQL uses `any` type for UUID
|
|
12373
|
+
organizationId: z31.any()
|
|
12374
|
+
// GraphQL uses `any` type for UUID
|
|
12375
|
+
});
|
|
12376
|
+
var VulnerabilityReportSchema = z31.object({
|
|
12377
|
+
scanDate: z31.any().nullable(),
|
|
12378
|
+
// GraphQL uses `any` type for timestamp
|
|
12379
|
+
vendor: z31.string(),
|
|
12380
|
+
// GraphQL generates as string, not enum
|
|
12381
|
+
project: ProjectSchema,
|
|
12382
|
+
totalVulnerabilityReportIssuesCount: VulnerabilityReportIssueAggregateSchema,
|
|
12383
|
+
notFixableVulnerabilityReportIssuesCount: VulnerabilityReportIssueAggregateSchema
|
|
12384
|
+
});
|
|
12385
|
+
var FixReportSummarySchema = z31.object({
|
|
12386
|
+
__typename: z31.literal("fixReport").optional(),
|
|
12387
|
+
id: z31.any(),
|
|
12388
|
+
// GraphQL uses `any` type for UUID
|
|
12389
|
+
createdOn: z31.any(),
|
|
12390
|
+
// GraphQL uses `any` type for timestamp
|
|
12391
|
+
repo: RepoSchema.nullable(),
|
|
12392
|
+
issueTypes: z31.any().nullable(),
|
|
12393
|
+
// GraphQL uses `any` type for JSON
|
|
12394
|
+
CRITICAL: FixAggregateSchema,
|
|
12395
|
+
HIGH: FixAggregateSchema,
|
|
12396
|
+
MEDIUM: FixAggregateSchema,
|
|
12397
|
+
LOW: FixAggregateSchema,
|
|
12398
|
+
fixes: z31.array(McpFixSchema),
|
|
12399
|
+
userFixes: z31.array(McpFixSchema).optional(),
|
|
12400
|
+
// Present in some responses but can be omitted
|
|
12401
|
+
filteredFixesCount: FixAggregateSchema,
|
|
12402
|
+
totalFixesCount: FixAggregateSchema,
|
|
12403
|
+
vulnerabilityReport: VulnerabilityReportSchema
|
|
12404
|
+
});
|
|
12405
|
+
var ExpiredReportSchema = z31.object({
|
|
12406
|
+
__typename: z31.literal("fixReport").optional(),
|
|
12407
|
+
id: z31.any(),
|
|
12408
|
+
// GraphQL uses `any` type for UUID
|
|
12409
|
+
expirationOn: z31.any().nullable()
|
|
12410
|
+
// GraphQL uses `any` type for timestamp
|
|
12411
|
+
});
|
|
12412
|
+
var GetLatestReportByRepoUrlResponseSchema = z31.object({
|
|
12413
|
+
__typename: z31.literal("query_root").optional(),
|
|
12414
|
+
fixReport: z31.array(FixReportSummarySchema),
|
|
12415
|
+
expiredReport: z31.array(ExpiredReportSchema)
|
|
12416
|
+
});
|
|
12417
|
+
|
|
12169
12418
|
// src/mcp/services/ConfigStoreService.ts
|
|
12170
12419
|
init_configs();
|
|
12171
12420
|
import Configstore4 from "configstore";
|
|
@@ -12496,12 +12745,55 @@ var McpGQLClient = class {
|
|
|
12496
12745
|
organizationId: organization.id,
|
|
12497
12746
|
projectName
|
|
12498
12747
|
});
|
|
12499
|
-
|
|
12500
|
-
|
|
12501
|
-
|
|
12502
|
-
|
|
12503
|
-
|
|
12504
|
-
|
|
12748
|
+
try {
|
|
12749
|
+
const createdProject = await this.clientSdk.CreateProject({
|
|
12750
|
+
organizationId: organization.id,
|
|
12751
|
+
projectName
|
|
12752
|
+
});
|
|
12753
|
+
logDebug("[GraphQL] CreateProject successful", {
|
|
12754
|
+
result: createdProject
|
|
12755
|
+
});
|
|
12756
|
+
return createdProject.createProject.projectId;
|
|
12757
|
+
} catch (createError) {
|
|
12758
|
+
const errorMessage = createError instanceof Error ? createError.message : String(createError);
|
|
12759
|
+
const isConstraintViolation = errorMessage.includes(
|
|
12760
|
+
"duplicate key value violates unique constraint"
|
|
12761
|
+
) && errorMessage.includes("project_name_organization_id_key");
|
|
12762
|
+
if (isConstraintViolation) {
|
|
12763
|
+
logDebug(
|
|
12764
|
+
"[GraphQL] Project creation failed due to constraint violation, retrying fetch",
|
|
12765
|
+
{
|
|
12766
|
+
organizationId: organization.id,
|
|
12767
|
+
projectName,
|
|
12768
|
+
error: errorMessage
|
|
12769
|
+
}
|
|
12770
|
+
);
|
|
12771
|
+
const retryOrgAndProjectRes = await this.clientSdk.getLastOrgAndNamedProject({
|
|
12772
|
+
email: userEmail,
|
|
12773
|
+
projectName
|
|
12774
|
+
});
|
|
12775
|
+
const retryProjectId = retryOrgAndProjectRes.user?.[0]?.userOrganizationsAndUserOrganizationRoles?.[0]?.organization?.projects?.[0]?.id;
|
|
12776
|
+
if (retryProjectId) {
|
|
12777
|
+
logDebug(
|
|
12778
|
+
"[GraphQL] Successfully found existing project after constraint violation",
|
|
12779
|
+
{
|
|
12780
|
+
projectId: retryProjectId,
|
|
12781
|
+
projectName
|
|
12782
|
+
}
|
|
12783
|
+
);
|
|
12784
|
+
return retryProjectId;
|
|
12785
|
+
}
|
|
12786
|
+
logError(
|
|
12787
|
+
"[GraphQL] Failed to find project even after constraint violation retry",
|
|
12788
|
+
{
|
|
12789
|
+
organizationId: organization.id,
|
|
12790
|
+
projectName,
|
|
12791
|
+
retryResult: retryOrgAndProjectRes
|
|
12792
|
+
}
|
|
12793
|
+
);
|
|
12794
|
+
}
|
|
12795
|
+
throw createError;
|
|
12796
|
+
}
|
|
12505
12797
|
} catch (e) {
|
|
12506
12798
|
logError("[GraphQL] getProjectId failed", {
|
|
12507
12799
|
error: e,
|
|
@@ -12559,18 +12851,56 @@ var McpGQLClient = class {
|
|
|
12559
12851
|
return null;
|
|
12560
12852
|
}
|
|
12561
12853
|
}
|
|
12562
|
-
|
|
12854
|
+
generateFixUrl({
|
|
12855
|
+
fixId,
|
|
12856
|
+
organizationId,
|
|
12857
|
+
projectId,
|
|
12858
|
+
reportId
|
|
12859
|
+
}) {
|
|
12860
|
+
if (!organizationId || !projectId || !reportId) {
|
|
12861
|
+
return void 0;
|
|
12862
|
+
}
|
|
12863
|
+
const appBaseUrl = this.apiUrl.replace("/v1/graphql", "").replace("api.", "");
|
|
12864
|
+
return `${appBaseUrl}/organization/${organizationId}/project/${projectId}/report/${reportId}/fix/${fixId}`;
|
|
12865
|
+
}
|
|
12866
|
+
mergeUserAndSystemFixes({
|
|
12867
|
+
reportData,
|
|
12868
|
+
limit
|
|
12869
|
+
}) {
|
|
12563
12870
|
if (!reportData) return [];
|
|
12871
|
+
const reportMetadata = {
|
|
12872
|
+
id: reportData.id,
|
|
12873
|
+
organizationId: reportData.vulnerabilityReport?.project?.organizationId,
|
|
12874
|
+
projectId: reportData.vulnerabilityReport?.project?.id
|
|
12875
|
+
};
|
|
12564
12876
|
const { userFixes = [], fixes = [] } = reportData;
|
|
12565
12877
|
const fixMap = /* @__PURE__ */ new Map();
|
|
12566
12878
|
for (const fix of userFixes) {
|
|
12567
12879
|
if (fix.id) {
|
|
12568
|
-
|
|
12880
|
+
const fixWithUrl = {
|
|
12881
|
+
...fix,
|
|
12882
|
+
fixUrl: this.generateFixUrl({
|
|
12883
|
+
fixId: fix.id,
|
|
12884
|
+
organizationId: reportMetadata?.organizationId,
|
|
12885
|
+
projectId: reportMetadata?.projectId,
|
|
12886
|
+
reportId: reportMetadata?.id
|
|
12887
|
+
})
|
|
12888
|
+
};
|
|
12889
|
+
fixMap.set(fix.id, fixWithUrl);
|
|
12569
12890
|
}
|
|
12570
12891
|
}
|
|
12571
12892
|
for (const fix of fixes) {
|
|
12572
12893
|
if (fix.id && !fixMap.has(fix.id)) {
|
|
12573
|
-
|
|
12894
|
+
const fixWithUrl = {
|
|
12895
|
+
...fix,
|
|
12896
|
+
fixUrl: this.generateFixUrl({
|
|
12897
|
+
fixId: fix.id,
|
|
12898
|
+
organizationId: reportMetadata?.organizationId,
|
|
12899
|
+
projectId: reportMetadata?.projectId,
|
|
12900
|
+
reportId: reportMetadata?.id
|
|
12901
|
+
})
|
|
12902
|
+
};
|
|
12903
|
+
fixMap.set(fix.id, fixWithUrl);
|
|
12574
12904
|
}
|
|
12575
12905
|
}
|
|
12576
12906
|
return Array.from(fixMap.values()).slice(0, limit);
|
|
@@ -12589,6 +12919,52 @@ var McpGQLClient = class {
|
|
|
12589
12919
|
logDebug("[GraphQL] No fixes found to update download status");
|
|
12590
12920
|
}
|
|
12591
12921
|
}
|
|
12922
|
+
async updateAutoAppliedFixesStatus(fixIds) {
|
|
12923
|
+
if (fixIds.length > 0) {
|
|
12924
|
+
const resUpdate = await this.clientSdk.updateDownloadedFixData({
|
|
12925
|
+
fixIds,
|
|
12926
|
+
source: "AUTO_MVS" /* AutoMvs */
|
|
12927
|
+
});
|
|
12928
|
+
logDebug("[GraphQL] updateAutoAppliedFixesStatus successful", {
|
|
12929
|
+
result: resUpdate,
|
|
12930
|
+
fixIds,
|
|
12931
|
+
note: "Auto-applied via MVS automation"
|
|
12932
|
+
});
|
|
12933
|
+
} else {
|
|
12934
|
+
logDebug("[GraphQL] No auto-applied fixes to update status");
|
|
12935
|
+
}
|
|
12936
|
+
}
|
|
12937
|
+
async getMvsAutoFixSettings() {
|
|
12938
|
+
try {
|
|
12939
|
+
const envOverride = process.env["MVS_AUTO_FIX"];
|
|
12940
|
+
if (envOverride !== void 0) {
|
|
12941
|
+
const overrideValue = envOverride.toLowerCase() === "true";
|
|
12942
|
+
logDebug("[Environment] Using MVS_AUTO_FIX override", {
|
|
12943
|
+
envValue: envOverride,
|
|
12944
|
+
resolvedValue: overrideValue
|
|
12945
|
+
});
|
|
12946
|
+
return overrideValue;
|
|
12947
|
+
}
|
|
12948
|
+
const userInfo = await this.getUserInfo();
|
|
12949
|
+
if (!userInfo?.email) {
|
|
12950
|
+
throw new Error("User email not found");
|
|
12951
|
+
}
|
|
12952
|
+
logDebug("[GraphQL] Calling GetUserMvsAutoFix query", {
|
|
12953
|
+
userEmail: userInfo.email
|
|
12954
|
+
});
|
|
12955
|
+
const result = await this.clientSdk.GetUserMvsAutoFix({
|
|
12956
|
+
userEmail: userInfo.email
|
|
12957
|
+
});
|
|
12958
|
+
logDebug("[GraphQL] GetUserMvsAutoFix successful", { result });
|
|
12959
|
+
return result.user_email_notification_settings?.[0]?.mvs_auto_fix ?? true;
|
|
12960
|
+
} catch (e) {
|
|
12961
|
+
logError("[GraphQL] GetUserMvsAutoFix failed", {
|
|
12962
|
+
error: e,
|
|
12963
|
+
...this.getErrorContext()
|
|
12964
|
+
});
|
|
12965
|
+
throw e;
|
|
12966
|
+
}
|
|
12967
|
+
}
|
|
12592
12968
|
async getLatestReportByRepoUrl({
|
|
12593
12969
|
repoUrl,
|
|
12594
12970
|
limit = MCP_DEFAULT_LIMIT,
|
|
@@ -12611,23 +12987,27 @@ var McpGQLClient = class {
|
|
|
12611
12987
|
error: err
|
|
12612
12988
|
});
|
|
12613
12989
|
}
|
|
12614
|
-
const
|
|
12990
|
+
const resp = await this.clientSdk.GetLatestReportByRepoUrl({
|
|
12615
12991
|
repoUrl,
|
|
12616
12992
|
limit,
|
|
12617
12993
|
offset,
|
|
12618
12994
|
currentUserEmail
|
|
12619
12995
|
});
|
|
12620
12996
|
logDebug("[GraphQL] GetLatestReportByRepoUrl successful", {
|
|
12621
|
-
result:
|
|
12622
|
-
reportCount:
|
|
12997
|
+
result: resp,
|
|
12998
|
+
reportCount: resp.fixReport?.length || 0
|
|
12999
|
+
});
|
|
13000
|
+
const latestReport = resp.fixReport?.[0] && FixReportSummarySchema.parse(resp.fixReport?.[0]);
|
|
13001
|
+
const fixes = this.mergeUserAndSystemFixes({
|
|
13002
|
+
reportData: latestReport,
|
|
13003
|
+
limit
|
|
12623
13004
|
});
|
|
12624
|
-
const fixes = this.mergeUserAndSystemFixes(res.fixReport?.[0], limit);
|
|
12625
13005
|
return {
|
|
12626
|
-
fixReport:
|
|
12627
|
-
...
|
|
13006
|
+
fixReport: latestReport ? {
|
|
13007
|
+
...latestReport,
|
|
12628
13008
|
fixes
|
|
12629
13009
|
} : null,
|
|
12630
|
-
expiredReport:
|
|
13010
|
+
expiredReport: resp.expiredReport?.[0] || null
|
|
12631
13011
|
};
|
|
12632
13012
|
} catch (e) {
|
|
12633
13013
|
logError("[GraphQL] GetLatestReportByRepoUrl failed", {
|
|
@@ -12687,11 +13067,20 @@ var McpGQLClient = class {
|
|
|
12687
13067
|
if (res.fixReport.length === 0) {
|
|
12688
13068
|
return null;
|
|
12689
13069
|
}
|
|
12690
|
-
const
|
|
13070
|
+
const latestReport = FixReportSummarySchema.parse(res.fixReport?.[0]);
|
|
13071
|
+
const fixes = this.mergeUserAndSystemFixes({
|
|
13072
|
+
reportData: latestReport,
|
|
13073
|
+
limit
|
|
13074
|
+
});
|
|
12691
13075
|
return {
|
|
12692
13076
|
fixes,
|
|
12693
13077
|
totalCount: res.fixReport?.[0]?.filteredFixesCount?.aggregate?.count || 0,
|
|
12694
|
-
expiredReport: res.expiredReport?.[0] || null
|
|
13078
|
+
expiredReport: res.expiredReport?.[0] || null,
|
|
13079
|
+
fixReport: res.fixReport?.[0] ? {
|
|
13080
|
+
id: res.fixReport[0].id,
|
|
13081
|
+
organizationId: res.fixReport[0].vulnerabilityReport?.project?.organizationId,
|
|
13082
|
+
projectId: res.fixReport[0].vulnerabilityReport?.projectId
|
|
13083
|
+
} : void 0
|
|
12695
13084
|
};
|
|
12696
13085
|
} catch (e) {
|
|
12697
13086
|
logError("[GraphQL] GetReportFixes failed", {
|
|
@@ -12778,6 +13167,7 @@ var McpServer = class {
|
|
|
12778
13167
|
__publicField(this, "server");
|
|
12779
13168
|
__publicField(this, "toolRegistry");
|
|
12780
13169
|
__publicField(this, "isEventHandlersSetup", false);
|
|
13170
|
+
__publicField(this, "eventHandlers", /* @__PURE__ */ new Map());
|
|
12781
13171
|
this.server = new Server(
|
|
12782
13172
|
{
|
|
12783
13173
|
name: config4.name,
|
|
@@ -12795,7 +13185,10 @@ var McpServer = class {
|
|
|
12795
13185
|
logInfo("MCP server instance created");
|
|
12796
13186
|
logDebug("MCP server instance config", { config: config4 });
|
|
12797
13187
|
}
|
|
12798
|
-
handleProcessSignal(
|
|
13188
|
+
handleProcessSignal({
|
|
13189
|
+
signal,
|
|
13190
|
+
error
|
|
13191
|
+
}) {
|
|
12799
13192
|
const messages = {
|
|
12800
13193
|
SIGINT: "MCP server interrupted",
|
|
12801
13194
|
SIGTERM: "MCP server terminated",
|
|
@@ -12850,9 +13243,11 @@ var McpServer = class {
|
|
|
12850
13243
|
"warning"
|
|
12851
13244
|
];
|
|
12852
13245
|
signals.forEach((signal) => {
|
|
12853
|
-
|
|
12854
|
-
this.handleProcessSignal(signal, error);
|
|
12855
|
-
}
|
|
13246
|
+
const handler = (error) => {
|
|
13247
|
+
this.handleProcessSignal({ signal, error });
|
|
13248
|
+
};
|
|
13249
|
+
this.eventHandlers.set(signal, handler);
|
|
13250
|
+
process.on(signal, handler);
|
|
12856
13251
|
});
|
|
12857
13252
|
this.isEventHandlersSetup = true;
|
|
12858
13253
|
logInfo("Process event handlers registered");
|
|
@@ -13005,11 +13400,17 @@ var McpServer = class {
|
|
|
13005
13400
|
}
|
|
13006
13401
|
async stop() {
|
|
13007
13402
|
logDebug("MCP server shutting down");
|
|
13403
|
+
this.eventHandlers.forEach((handler, signal) => {
|
|
13404
|
+
process.removeListener(signal, handler);
|
|
13405
|
+
});
|
|
13406
|
+
this.eventHandlers.clear();
|
|
13407
|
+
this.isEventHandlersSetup = false;
|
|
13408
|
+
logDebug("Process event handlers cleaned up");
|
|
13008
13409
|
}
|
|
13009
13410
|
};
|
|
13010
13411
|
|
|
13011
13412
|
// src/mcp/tools/checkForNewAvailableFixes/CheckForNewAvailableFixesTool.ts
|
|
13012
|
-
import { z as
|
|
13413
|
+
import { z as z34 } from "zod";
|
|
13013
13414
|
|
|
13014
13415
|
// src/mcp/services/PathValidation.ts
|
|
13015
13416
|
import fs10 from "fs";
|
|
@@ -13078,7 +13479,7 @@ async function validatePath(inputPath) {
|
|
|
13078
13479
|
}
|
|
13079
13480
|
|
|
13080
13481
|
// src/mcp/tools/base/BaseTool.ts
|
|
13081
|
-
import { z as
|
|
13482
|
+
import { z as z32 } from "zod";
|
|
13082
13483
|
var BaseTool = class {
|
|
13083
13484
|
getDefinition() {
|
|
13084
13485
|
return {
|
|
@@ -13106,7 +13507,7 @@ var BaseTool = class {
|
|
|
13106
13507
|
try {
|
|
13107
13508
|
return this.inputValidationSchema.parse(args);
|
|
13108
13509
|
} catch (error) {
|
|
13109
|
-
if (error instanceof
|
|
13510
|
+
if (error instanceof z32.ZodError) {
|
|
13110
13511
|
const errorDetails = error.errors.map((e) => {
|
|
13111
13512
|
const fieldPath = e.path.length > 0 ? e.path.join(".") : "root";
|
|
13112
13513
|
const message = e.message === "Required" ? `Missing required parameter '${fieldPath}'` : `Invalid value for '${fieldPath}': ${e.message}`;
|
|
@@ -13478,7 +13879,71 @@ ${nextStepsPrompt({ scannedFiles })}
|
|
|
13478
13879
|
`;
|
|
13479
13880
|
};
|
|
13480
13881
|
var noFreshFixesPrompt = `No fresh fixes available for this repository at this time. No user message needed. Do not show any comments to the user.`;
|
|
13882
|
+
var continuousMonitoringSection = `## \u{1F504} Continuous Monitoring Active
|
|
13883
|
+
|
|
13884
|
+
The Mobb MCP is continuously monitoring your code and will **automatically apply fixes** when new vulnerabilities are detected in files you modify or create.`;
|
|
13885
|
+
var autoFixSettingsSection = `## \u2699\uFE0F Auto-Fix Settings
|
|
13886
|
+
|
|
13887
|
+
If you prefer to review fixes manually before they're applied:
|
|
13888
|
+
- **Turn off automatic fixes** in your Mobb app settings at https://app.mobb.ai/ setting section
|
|
13889
|
+
- When disabled, you'll receive available fixes that you can apply manually through the \`${MCP_TOOL_CHECK_FOR_NEW_AVAILABLE_FIXES}\` MCP tool`;
|
|
13890
|
+
var whatHappensNextSection = `## \u{1F50D} What Happens Next
|
|
13891
|
+
|
|
13892
|
+
- Continue coding normally - Mobb is watching in the background
|
|
13893
|
+
- Any new security issues will be automatically patched
|
|
13894
|
+
- You'll be notified when fixes are applied to your codebase
|
|
13895
|
+
- Review and commit changes when ready
|
|
13896
|
+
|
|
13897
|
+
Your code is being actively protected! \u{1F680}`;
|
|
13898
|
+
var noVulnerabilitiesAutoFixPrompt = `\u{1F6E1}\uFE0F **MOBB AUTO-FIX MONITORING ACTIVE**
|
|
13899
|
+
|
|
13900
|
+
## \u2705 No Fixable Vulnerabilities Detected
|
|
13901
|
+
|
|
13902
|
+
No fixable vulnerabilities were detected so far.
|
|
13903
|
+
|
|
13904
|
+
${continuousMonitoringSection}
|
|
13905
|
+
|
|
13906
|
+
${autoFixSettingsSection}
|
|
13907
|
+
|
|
13908
|
+
${whatHappensNextSection}`;
|
|
13481
13909
|
var initialScanInProgressPrompt = `Initial scan is still in progress. No user message needed. Do not show any comments to the user.`;
|
|
13910
|
+
var authenticationRequiredPrompt = `# AUTHENTICATION REQUIRED: MOBB ACCESS NOT AUTHORIZED
|
|
13911
|
+
|
|
13912
|
+
## MCP SERVICE UNAVAILABLE
|
|
13913
|
+
|
|
13914
|
+
The Mobb MCP (Model Context Protocol) service is currently unavailable because authentication with Mobb has not been completed.
|
|
13915
|
+
|
|
13916
|
+
## REQUIRED ACTION
|
|
13917
|
+
|
|
13918
|
+
To use Mobb's automated security fix capabilities, you need to:
|
|
13919
|
+
|
|
13920
|
+
1. **Authorize Mobb Access**
|
|
13921
|
+
- Restart Mobb MCP
|
|
13922
|
+
- A browser window will open for you to log in and authorize access
|
|
13923
|
+
|
|
13924
|
+
2. **Complete Authentication**
|
|
13925
|
+
- Sign in to your Mobb account (or create one if needed)
|
|
13926
|
+
- Authorize the CLI tool to access Mobb services
|
|
13927
|
+
- This creates a secure token for future automated scans
|
|
13928
|
+
|
|
13929
|
+
## WHAT HAPPENS NEXT
|
|
13930
|
+
|
|
13931
|
+
Once authentication is complete:
|
|
13932
|
+
- The MCP service will automatically start monitoring your repository
|
|
13933
|
+
- New security vulnerabilities will be detected and fixed automatically
|
|
13934
|
+
- You'll receive notifications about applied fixes through this interface
|
|
13935
|
+
|
|
13936
|
+
## TROUBLESHOOTING
|
|
13937
|
+
|
|
13938
|
+
If you're experiencing authentication issues:
|
|
13939
|
+
- Ensure you have an active internet connection
|
|
13940
|
+
- Check that you can access https://app.mobb.ai in your browser
|
|
13941
|
+
- Make sure your browser isn't blocking authentication pop-ups
|
|
13942
|
+
- Try running a manual scan with the \`--debug\` flag for detailed output
|
|
13943
|
+
|
|
13944
|
+
For assistance:
|
|
13945
|
+
- Documentation: https://docs.mobb.ai
|
|
13946
|
+
- Support: support@mobb.ai`;
|
|
13482
13947
|
var freshFixesPrompt = ({
|
|
13483
13948
|
fixes,
|
|
13484
13949
|
limit,
|
|
@@ -13499,6 +13964,67 @@ ${applyFixesPrompt({
|
|
|
13499
13964
|
})}
|
|
13500
13965
|
`;
|
|
13501
13966
|
};
|
|
13967
|
+
function extractTargetFileFromPatch(patch) {
|
|
13968
|
+
const match = patch?.match(/^diff --git a\/([^\s]+) b\//);
|
|
13969
|
+
return match?.[1] || "Unknown file";
|
|
13970
|
+
}
|
|
13971
|
+
function formatSeverity(severityText, severityValue) {
|
|
13972
|
+
if (severityText) return severityText;
|
|
13973
|
+
if (severityValue !== null && severityValue !== void 0) {
|
|
13974
|
+
if (severityValue >= 9) return "Critical";
|
|
13975
|
+
if (severityValue >= 7) return "High";
|
|
13976
|
+
if (severityValue >= 4) return "Medium";
|
|
13977
|
+
return "Low";
|
|
13978
|
+
}
|
|
13979
|
+
return "Unknown";
|
|
13980
|
+
}
|
|
13981
|
+
var appliedFixesSummaryPrompt = ({
|
|
13982
|
+
fixes,
|
|
13983
|
+
gqlClient
|
|
13984
|
+
}) => {
|
|
13985
|
+
const fixIds = fixes.map((fix) => fix.id);
|
|
13986
|
+
void gqlClient.updateFixesDownloadStatus(fixIds);
|
|
13987
|
+
const patchTime = (/* @__PURE__ */ new Date()).toLocaleString();
|
|
13988
|
+
return `\u{1F6E1}\uFE0F **MOBB AUTO-FIX: VULNERABILITIES PATCHED**
|
|
13989
|
+
|
|
13990
|
+
## \u2705 ${fixes.length} Security ${fixes.length === 1 ? "Fix" : "Fixes"} Automatically Applied
|
|
13991
|
+
|
|
13992
|
+
Mobb has automatically detected and fixed ${fixes.length} security ${fixes.length === 1 ? "vulnerability" : "vulnerabilities"} in your codebase.
|
|
13993
|
+
|
|
13994
|
+
## \u{1F4CB} Applied Fixes Summary
|
|
13995
|
+
|
|
13996
|
+
${fixes.map((fix, index) => {
|
|
13997
|
+
const vulnerabilityType = friendlyType(fix.safeIssueType || "Unknown");
|
|
13998
|
+
const severity = formatSeverity(fix.severityText, fix.severityValue);
|
|
13999
|
+
const description = fix.patchAndQuestions?.__typename === "FixData" ? fix.patchAndQuestions.extraContext?.fixDescription || "Security vulnerability fix" : "Security vulnerability fix";
|
|
14000
|
+
const patch = fix.patchAndQuestions?.__typename === "FixData" ? fix.patchAndQuestions.patch : void 0;
|
|
14001
|
+
const targetFile = extractTargetFileFromPatch(patch);
|
|
14002
|
+
const gitBlameLogin = fix.gitBlameLogin || "Unknown";
|
|
14003
|
+
return `### ${index + 1}. ${vulnerabilityType}
|
|
14004
|
+
|
|
14005
|
+
**\u{1F3AF} Vulnerability Type:** ${vulnerabilityType}
|
|
14006
|
+
**\u{1F4CA} Severity:** ${severity}
|
|
14007
|
+
**\u{1F4DD} Description:** ${description}
|
|
14008
|
+
**\u{1F464} Git Blame:** ${gitBlameLogin}
|
|
14009
|
+
**\u{1F4C1} Target File:** \`${targetFile}\`
|
|
14010
|
+
**\u23F0 Patch Applied:** ${patchTime}
|
|
14011
|
+
|
|
14012
|
+
---`;
|
|
14013
|
+
}).join("\n")}
|
|
14014
|
+
|
|
14015
|
+
${continuousMonitoringSection}
|
|
14016
|
+
|
|
14017
|
+
${autoFixSettingsSection}
|
|
14018
|
+
|
|
14019
|
+
## \u{1F4CB} Next Steps
|
|
14020
|
+
|
|
14021
|
+
1. **Review the changes** - Check the modified files to understand what was fixed
|
|
14022
|
+
2. **Test your application** - Ensure the fixes don't break existing functionality
|
|
14023
|
+
3. **Commit the changes** - Add and commit the security fixes to your repository
|
|
14024
|
+
4. **Continue coding** - Mobb will keep protecting your code automatically
|
|
14025
|
+
|
|
14026
|
+
${whatHappensNextSection}`;
|
|
14027
|
+
};
|
|
13502
14028
|
|
|
13503
14029
|
// src/mcp/services/GetLocalFiles.ts
|
|
13504
14030
|
init_FileUtils();
|
|
@@ -13507,23 +14033,23 @@ init_configs();
|
|
|
13507
14033
|
import fs11 from "fs/promises";
|
|
13508
14034
|
import nodePath from "path";
|
|
13509
14035
|
var getLocalFiles = async ({
|
|
13510
|
-
path:
|
|
14036
|
+
path: path15,
|
|
13511
14037
|
maxFileSize = MCP_MAX_FILE_SIZE,
|
|
13512
14038
|
maxFiles,
|
|
13513
14039
|
isAllFilesScan,
|
|
13514
14040
|
scanContext
|
|
13515
14041
|
}) => {
|
|
13516
14042
|
logDebug(`[${scanContext}] Starting getLocalFiles`, {
|
|
13517
|
-
path:
|
|
14043
|
+
path: path15,
|
|
13518
14044
|
maxFileSize,
|
|
13519
14045
|
maxFiles,
|
|
13520
14046
|
isAllFilesScan
|
|
13521
14047
|
});
|
|
13522
14048
|
try {
|
|
13523
|
-
const resolvedRepoPath = await fs11.realpath(
|
|
14049
|
+
const resolvedRepoPath = await fs11.realpath(path15);
|
|
13524
14050
|
logDebug(`[${scanContext}] Resolved repository path`, {
|
|
13525
14051
|
resolvedRepoPath,
|
|
13526
|
-
originalPath:
|
|
14052
|
+
originalPath: path15
|
|
13527
14053
|
});
|
|
13528
14054
|
const gitService = new GitService(resolvedRepoPath, log);
|
|
13529
14055
|
const gitValidation = await gitService.validateRepository();
|
|
@@ -13536,7 +14062,7 @@ var getLocalFiles = async ({
|
|
|
13536
14062
|
if (!gitValidation.isValid || isAllFilesScan) {
|
|
13537
14063
|
try {
|
|
13538
14064
|
files = await FileUtils.getLastChangedFiles({
|
|
13539
|
-
dir:
|
|
14065
|
+
dir: path15,
|
|
13540
14066
|
maxFileSize,
|
|
13541
14067
|
maxFiles,
|
|
13542
14068
|
isAllFilesScan
|
|
@@ -13628,53 +14154,1773 @@ var getLocalFiles = async ({
|
|
|
13628
14154
|
logError(`${scanContext}Unexpected error in getLocalFiles`, {
|
|
13629
14155
|
error: error instanceof Error ? error.message : String(error),
|
|
13630
14156
|
stack: error instanceof Error ? error.stack : void 0,
|
|
13631
|
-
path:
|
|
14157
|
+
path: path15
|
|
13632
14158
|
});
|
|
13633
14159
|
throw error;
|
|
13634
14160
|
}
|
|
13635
14161
|
};
|
|
13636
14162
|
|
|
13637
|
-
// src/mcp/services/
|
|
13638
|
-
init_GitService();
|
|
13639
|
-
init_configs();
|
|
13640
|
-
|
|
13641
|
-
// src/mcp/services/FileOperations.ts
|
|
13642
|
-
init_FileUtils();
|
|
14163
|
+
// src/mcp/services/LocalMobbFolderService.ts
|
|
13643
14164
|
import fs12 from "fs";
|
|
13644
14165
|
import path12 from "path";
|
|
13645
|
-
import
|
|
13646
|
-
|
|
14166
|
+
import { z as z33 } from "zod";
|
|
14167
|
+
init_GitService();
|
|
14168
|
+
function extractPathFromPatch(patch) {
|
|
14169
|
+
const match = patch?.match(/diff --git a\/([^\s]+) b\//);
|
|
14170
|
+
return match?.[1] ?? null;
|
|
14171
|
+
}
|
|
14172
|
+
function parsedIssueTypeRes(issueType) {
|
|
14173
|
+
return z33.nativeEnum(IssueType_Enum).safeParse(issueType);
|
|
14174
|
+
}
|
|
14175
|
+
var LocalMobbFolderService = class {
|
|
13647
14176
|
/**
|
|
13648
|
-
* Creates a
|
|
13649
|
-
* @param
|
|
13650
|
-
* @param repositoryPath Base path for resolving relative file paths
|
|
13651
|
-
* @param maxFileSize Maximum size allowed for individual files
|
|
13652
|
-
* @returns ZIP archive as a Buffer with metadata
|
|
14177
|
+
* Creates a new LocalMobbFolderService instance
|
|
14178
|
+
* @param repoPath Path to the repository or directory (supports both git and non-git)
|
|
13653
14179
|
*/
|
|
13654
|
-
|
|
13655
|
-
|
|
13656
|
-
|
|
13657
|
-
|
|
13658
|
-
|
|
13659
|
-
|
|
13660
|
-
|
|
13661
|
-
|
|
13662
|
-
|
|
13663
|
-
|
|
13664
|
-
|
|
14180
|
+
constructor(repoPath) {
|
|
14181
|
+
__publicField(this, "defaultMobbFolderName", ".mobb");
|
|
14182
|
+
__publicField(this, "mobbIgnorePattern", "**/.mobb");
|
|
14183
|
+
__publicField(this, "repoPath");
|
|
14184
|
+
__publicField(this, "gitService");
|
|
14185
|
+
this.repoPath = repoPath;
|
|
14186
|
+
try {
|
|
14187
|
+
this.gitService = new GitService(
|
|
14188
|
+
repoPath,
|
|
14189
|
+
(message, level, data) => {
|
|
14190
|
+
if (level === "debug") {
|
|
14191
|
+
logDebug(message, data);
|
|
14192
|
+
} else if (level === "info") {
|
|
14193
|
+
logInfo(message, data);
|
|
14194
|
+
} else if (level === "error") {
|
|
14195
|
+
logError(message, data);
|
|
14196
|
+
}
|
|
14197
|
+
}
|
|
14198
|
+
);
|
|
14199
|
+
} catch (error) {
|
|
14200
|
+
logDebug("[LocalMobbFolderService] GitService initialization failed", {
|
|
14201
|
+
repoPath,
|
|
14202
|
+
error: String(error)
|
|
14203
|
+
});
|
|
14204
|
+
this.gitService = null;
|
|
14205
|
+
}
|
|
14206
|
+
logDebug("[LocalMobbFolderService] Service initialized", { repoPath });
|
|
14207
|
+
}
|
|
14208
|
+
/**
|
|
14209
|
+
* Gets or creates the .mobb folder in the repository path
|
|
14210
|
+
* For git repositories: ensures .gitignore exists and contains wildcard pattern to ignore .mobb folders
|
|
14211
|
+
* For non-git directories: skips gitignore functionality but creates .mobb folder normally
|
|
14212
|
+
* @returns Path to the .mobb folder
|
|
14213
|
+
* @throws Error if path validation fails or folder creation fails
|
|
14214
|
+
*/
|
|
14215
|
+
async getFolder() {
|
|
14216
|
+
logDebug("[LocalMobbFolderService] Getting .mobb folder");
|
|
14217
|
+
try {
|
|
14218
|
+
const isGitRepo = this.gitService ? await this.gitService.isGitRepository() : false;
|
|
14219
|
+
logDebug("[LocalMobbFolderService] Repository type detected", {
|
|
14220
|
+
isGitRepo
|
|
14221
|
+
});
|
|
14222
|
+
if (isGitRepo && this.gitService) {
|
|
14223
|
+
try {
|
|
14224
|
+
const gitRoot = await this.gitService.getGitRoot();
|
|
14225
|
+
logDebug("[LocalMobbFolderService] Git root found", { gitRoot });
|
|
14226
|
+
const wasAdded = await this.gitService.ensureGitignoreEntry(
|
|
14227
|
+
this.mobbIgnorePattern
|
|
14228
|
+
);
|
|
14229
|
+
if (wasAdded) {
|
|
14230
|
+
logInfo(
|
|
14231
|
+
"[LocalMobbFolderService] Added .mobb wildcard pattern to .gitignore",
|
|
14232
|
+
{
|
|
14233
|
+
pattern: this.mobbIgnorePattern
|
|
14234
|
+
}
|
|
14235
|
+
);
|
|
14236
|
+
} else {
|
|
14237
|
+
logDebug(
|
|
14238
|
+
"[LocalMobbFolderService] .mobb pattern already in .gitignore"
|
|
14239
|
+
);
|
|
14240
|
+
}
|
|
14241
|
+
} catch (gitError) {
|
|
14242
|
+
logDebug(
|
|
14243
|
+
"[LocalMobbFolderService] Git operations failed, treating as non-git repo",
|
|
14244
|
+
{ gitError }
|
|
14245
|
+
);
|
|
14246
|
+
}
|
|
14247
|
+
} else {
|
|
14248
|
+
logInfo(
|
|
14249
|
+
"[LocalMobbFolderService] Non-git repository detected, skipping .gitignore operations"
|
|
13665
14250
|
);
|
|
13666
|
-
continue;
|
|
13667
14251
|
}
|
|
13668
|
-
|
|
13669
|
-
|
|
13670
|
-
|
|
13671
|
-
|
|
13672
|
-
|
|
14252
|
+
const mobbFolderPath = path12.join(
|
|
14253
|
+
this.repoPath,
|
|
14254
|
+
this.defaultMobbFolderName
|
|
14255
|
+
);
|
|
14256
|
+
if (!fs12.existsSync(mobbFolderPath)) {
|
|
14257
|
+
logInfo("[LocalMobbFolderService] Creating .mobb folder", {
|
|
14258
|
+
mobbFolderPath
|
|
14259
|
+
});
|
|
14260
|
+
fs12.mkdirSync(mobbFolderPath, { recursive: true });
|
|
14261
|
+
} else {
|
|
14262
|
+
logDebug("[LocalMobbFolderService] .mobb folder already exists");
|
|
13673
14263
|
}
|
|
13674
|
-
const
|
|
13675
|
-
if (
|
|
13676
|
-
|
|
13677
|
-
|
|
14264
|
+
const stats = fs12.statSync(mobbFolderPath);
|
|
14265
|
+
if (!stats.isDirectory()) {
|
|
14266
|
+
throw new Error(`Path exists but is not a directory: ${mobbFolderPath}`);
|
|
14267
|
+
}
|
|
14268
|
+
logDebug("[LocalMobbFolderService] .mobb folder ready", {
|
|
14269
|
+
mobbFolderPath,
|
|
14270
|
+
repoPath: this.repoPath,
|
|
14271
|
+
isGitRepo
|
|
14272
|
+
});
|
|
14273
|
+
return mobbFolderPath;
|
|
14274
|
+
} catch (error) {
|
|
14275
|
+
const errorMessage = `Failed to get/create .mobb folder: ${error}`;
|
|
14276
|
+
logError(errorMessage, { repoPath: this.repoPath, error });
|
|
14277
|
+
throw new Error(errorMessage);
|
|
14278
|
+
}
|
|
14279
|
+
}
|
|
14280
|
+
/**
|
|
14281
|
+
* Public method to get or create the .mobb folder
|
|
14282
|
+
* This is the main entry point for accessing the Mobb folder
|
|
14283
|
+
* @returns Promise<string> Path to the .mobb folder
|
|
14284
|
+
*/
|
|
14285
|
+
async getMobbFolder() {
|
|
14286
|
+
return await this.getFolder();
|
|
14287
|
+
}
|
|
14288
|
+
/**
|
|
14289
|
+
* Validates the repository (git or non-git) and ensures it's ready for Mobb operations
|
|
14290
|
+
* For git repositories: validates git structure and accessibility
|
|
14291
|
+
* For non-git directories: validates directory exists and is accessible
|
|
14292
|
+
* @returns Promise<boolean> True if repository/directory is valid
|
|
14293
|
+
* @throws Error if repository validation fails
|
|
14294
|
+
*/
|
|
14295
|
+
async validateRepository() {
|
|
14296
|
+
const isGitRepo = this.gitService ? await this.gitService.isGitRepository() : false;
|
|
14297
|
+
if (isGitRepo && this.gitService) {
|
|
14298
|
+
const result = await this.gitService.validateRepository();
|
|
14299
|
+
if (!result.isValid) {
|
|
14300
|
+
throw new Error(result.error || "Git repository validation failed");
|
|
14301
|
+
}
|
|
14302
|
+
logDebug("[LocalMobbFolderService] Git repository validated successfully");
|
|
14303
|
+
} else {
|
|
14304
|
+
try {
|
|
14305
|
+
const stats = fs12.statSync(this.repoPath);
|
|
14306
|
+
if (!stats.isDirectory()) {
|
|
14307
|
+
throw new Error(
|
|
14308
|
+
`Path exists but is not a directory: ${this.repoPath}`
|
|
14309
|
+
);
|
|
14310
|
+
}
|
|
14311
|
+
fs12.accessSync(this.repoPath, fs12.constants.R_OK | fs12.constants.W_OK);
|
|
14312
|
+
logDebug(
|
|
14313
|
+
"[LocalMobbFolderService] Non-git directory validated successfully"
|
|
14314
|
+
);
|
|
14315
|
+
} catch (error) {
|
|
14316
|
+
throw new Error(`Directory validation failed: ${error}`);
|
|
14317
|
+
}
|
|
14318
|
+
}
|
|
14319
|
+
return true;
|
|
14320
|
+
}
|
|
14321
|
+
/**
|
|
14322
|
+
* Extracts patch content from McpFix object
|
|
14323
|
+
* @param fix McpFix object
|
|
14324
|
+
* @returns Patch content or null if not available
|
|
14325
|
+
*/
|
|
14326
|
+
extractPatchFromFix(fix) {
|
|
14327
|
+
if (fix.patchAndQuestions.__typename === "FixData") {
|
|
14328
|
+
return fix.patchAndQuestions.patch || null;
|
|
14329
|
+
}
|
|
14330
|
+
return null;
|
|
14331
|
+
}
|
|
14332
|
+
/**
|
|
14333
|
+
* Extracts severity level from McpFix object
|
|
14334
|
+
* @param fix McpFix object
|
|
14335
|
+
* @returns Severity string
|
|
14336
|
+
*/
|
|
14337
|
+
extractSeverity(fix) {
|
|
14338
|
+
return fix.severityText?.toLowerCase() || "unknown";
|
|
14339
|
+
}
|
|
14340
|
+
/**
|
|
14341
|
+
* Extracts vulnerability type from McpFix object
|
|
14342
|
+
* @param fix McpFix object
|
|
14343
|
+
* @returns Vulnerability type string
|
|
14344
|
+
*/
|
|
14345
|
+
extractVulnerabilityType(fix) {
|
|
14346
|
+
return fix.safeIssueType || "security-vulnerability";
|
|
14347
|
+
}
|
|
14348
|
+
/**
|
|
14349
|
+
* Extracts filename from McpFix object by parsing the patch
|
|
14350
|
+
* @param fix McpFix object
|
|
14351
|
+
* @returns Filename or null if not determinable
|
|
14352
|
+
*/
|
|
14353
|
+
extractFileNameFromFix(fix) {
|
|
14354
|
+
const patch = this.extractPatchFromFix(fix);
|
|
14355
|
+
if (!patch) {
|
|
14356
|
+
return null;
|
|
14357
|
+
}
|
|
14358
|
+
const extractedPath = extractPathFromPatch(patch);
|
|
14359
|
+
if (extractedPath) {
|
|
14360
|
+
const pathParts = extractedPath.split("/");
|
|
14361
|
+
const fileName = pathParts[pathParts.length - 1];
|
|
14362
|
+
return fileName || null;
|
|
14363
|
+
}
|
|
14364
|
+
return `fix-${fix.id}`;
|
|
14365
|
+
}
|
|
14366
|
+
/**
|
|
14367
|
+
* Saves a git patch as a file in the .mobb folder
|
|
14368
|
+
* @param fix McpFix object containing all fix information
|
|
14369
|
+
* @param customFileName Optional custom filename (overrides generated format)
|
|
14370
|
+
* @returns Result of the save operation including file path
|
|
14371
|
+
*/
|
|
14372
|
+
async savePatch(fix, customFileName) {
|
|
14373
|
+
const patch = this.extractPatchFromFix(fix);
|
|
14374
|
+
const severity = this.extractSeverity(fix);
|
|
14375
|
+
const vulnerabilityType = this.extractVulnerabilityType(fix);
|
|
14376
|
+
const fileName = this.extractFileNameFromFix(fix);
|
|
14377
|
+
logDebug("[LocalMobbFolderService] Saving patch", {
|
|
14378
|
+
fixId: fix.id,
|
|
14379
|
+
fileName,
|
|
14380
|
+
severity,
|
|
14381
|
+
vulnerabilityType,
|
|
14382
|
+
customFileName
|
|
14383
|
+
});
|
|
14384
|
+
try {
|
|
14385
|
+
if (!patch || patch.trim().length === 0) {
|
|
14386
|
+
const error = `No valid patch content found for fix ${fix.id}`;
|
|
14387
|
+
logError("[LocalMobbFolderService] Invalid patch content", {
|
|
14388
|
+
error,
|
|
14389
|
+
fixId: fix.id
|
|
14390
|
+
});
|
|
14391
|
+
return {
|
|
14392
|
+
success: false,
|
|
14393
|
+
error,
|
|
14394
|
+
message: "Failed to save patch: no valid patch content"
|
|
14395
|
+
};
|
|
14396
|
+
}
|
|
14397
|
+
if (!fileName) {
|
|
14398
|
+
const error = `Could not determine filename for fix ${fix.id}`;
|
|
14399
|
+
logError("[LocalMobbFolderService] Missing fileName", {
|
|
14400
|
+
error,
|
|
14401
|
+
fixId: fix.id
|
|
14402
|
+
});
|
|
14403
|
+
return {
|
|
14404
|
+
success: false,
|
|
14405
|
+
error,
|
|
14406
|
+
message: "Failed to save patch: could not determine filename"
|
|
14407
|
+
};
|
|
14408
|
+
}
|
|
14409
|
+
const mobbFolderPath = await this.getFolder();
|
|
14410
|
+
let baseFileName;
|
|
14411
|
+
if (customFileName) {
|
|
14412
|
+
baseFileName = customFileName.endsWith(".patch") ? customFileName : `${customFileName}.patch`;
|
|
14413
|
+
} else {
|
|
14414
|
+
baseFileName = this.generatePatchFileName(
|
|
14415
|
+
severity,
|
|
14416
|
+
vulnerabilityType,
|
|
14417
|
+
fileName
|
|
14418
|
+
);
|
|
14419
|
+
}
|
|
14420
|
+
const uniqueFileName = this.getUniqueFileName(
|
|
14421
|
+
mobbFolderPath,
|
|
14422
|
+
baseFileName
|
|
14423
|
+
);
|
|
14424
|
+
const filePath = path12.join(mobbFolderPath, uniqueFileName);
|
|
14425
|
+
await fs12.promises.writeFile(filePath, patch, "utf8");
|
|
14426
|
+
logInfo("[LocalMobbFolderService] Patch saved successfully", {
|
|
14427
|
+
filePath,
|
|
14428
|
+
fileName: uniqueFileName,
|
|
14429
|
+
patchSize: patch.length
|
|
14430
|
+
});
|
|
14431
|
+
return {
|
|
14432
|
+
success: true,
|
|
14433
|
+
filePath,
|
|
14434
|
+
fileName: uniqueFileName,
|
|
14435
|
+
message: `Patch saved successfully as ${uniqueFileName}`
|
|
14436
|
+
};
|
|
14437
|
+
} catch (error) {
|
|
14438
|
+
const errorMessage = `Failed to save patch for fix ${fix.id}: ${error}`;
|
|
14439
|
+
logError(errorMessage, {
|
|
14440
|
+
fixId: fix.id,
|
|
14441
|
+
fileName,
|
|
14442
|
+
severity,
|
|
14443
|
+
vulnerabilityType,
|
|
14444
|
+
customFileName,
|
|
14445
|
+
error
|
|
14446
|
+
});
|
|
14447
|
+
return {
|
|
14448
|
+
success: false,
|
|
14449
|
+
error: errorMessage,
|
|
14450
|
+
message: errorMessage
|
|
14451
|
+
};
|
|
14452
|
+
}
|
|
14453
|
+
}
|
|
14454
|
+
/**
|
|
14455
|
+
* Generates a filename for a patch using the format: severity-vulnerabilityType-in-fileName.patch
|
|
14456
|
+
* @param severity Severity level of the vulnerability
|
|
14457
|
+
* @param vulnerabilityType Type of vulnerability
|
|
14458
|
+
* @param fileName Name of the file being patched
|
|
14459
|
+
* @returns Generated filename with .patch extension
|
|
14460
|
+
*/
|
|
14461
|
+
generatePatchFileName(severity, vulnerabilityType, fileName) {
|
|
14462
|
+
const safeSeverity = this.sanitizeForFilename(severity);
|
|
14463
|
+
const safeVulnType = this.sanitizeForFilename(vulnerabilityType);
|
|
14464
|
+
const safeFileName = this.sanitizeForFilename(fileName);
|
|
14465
|
+
return `${safeSeverity}-${safeVulnType}-in-${safeFileName}.patch`;
|
|
14466
|
+
}
|
|
14467
|
+
/**
|
|
14468
|
+
* Sanitizes a string to be safe for use in filenames
|
|
14469
|
+
* @param input String to sanitize
|
|
14470
|
+
* @returns Sanitized string safe for filenames
|
|
14471
|
+
*/
|
|
14472
|
+
sanitizeForFilename(input) {
|
|
14473
|
+
return input.toLowerCase().replace(/[^a-z0-9.-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
14474
|
+
}
|
|
14475
|
+
/**
|
|
14476
|
+
* Gets a unique filename by adding index suffix if file already exists
|
|
14477
|
+
* @param folderPath Path to the folder where file will be saved
|
|
14478
|
+
* @param baseFileName Base filename to make unique
|
|
14479
|
+
* @returns Unique filename that doesn't conflict with existing files
|
|
14480
|
+
*/
|
|
14481
|
+
getUniqueFileName(folderPath, baseFileName) {
|
|
14482
|
+
const baseName = path12.parse(baseFileName).name;
|
|
14483
|
+
const extension = path12.parse(baseFileName).ext;
|
|
14484
|
+
let uniqueFileName = baseFileName;
|
|
14485
|
+
let index = 1;
|
|
14486
|
+
while (fs12.existsSync(path12.join(folderPath, uniqueFileName))) {
|
|
14487
|
+
uniqueFileName = `${baseName}-${index}${extension}`;
|
|
14488
|
+
index++;
|
|
14489
|
+
if (index > 1e3) {
|
|
14490
|
+
const timestamp = Date.now();
|
|
14491
|
+
uniqueFileName = `${baseName}-${timestamp}${extension}`;
|
|
14492
|
+
break;
|
|
14493
|
+
}
|
|
14494
|
+
}
|
|
14495
|
+
if (uniqueFileName !== baseFileName) {
|
|
14496
|
+
logDebug(
|
|
14497
|
+
"[LocalMobbFolderService] Generated unique filename to avoid conflict",
|
|
14498
|
+
{
|
|
14499
|
+
original: baseFileName,
|
|
14500
|
+
unique: uniqueFileName
|
|
14501
|
+
}
|
|
14502
|
+
);
|
|
14503
|
+
}
|
|
14504
|
+
return uniqueFileName;
|
|
14505
|
+
}
|
|
14506
|
+
/**
|
|
14507
|
+
* Logs fix details in markdown format to patchInfo.md file
|
|
14508
|
+
* Prepends the new fix information to maintain chronological order (newest first)
|
|
14509
|
+
* @param fix McpFix object containing fix details
|
|
14510
|
+
* @param savedPatchFileName The filename of the saved patch file
|
|
14511
|
+
* @returns Result of the log operation
|
|
14512
|
+
*/
|
|
14513
|
+
async logPatch(fix, savedPatchFileName) {
|
|
14514
|
+
logDebug("[LocalMobbFolderService] Logging patch info", { fixId: fix.id });
|
|
14515
|
+
try {
|
|
14516
|
+
const mobbFolderPath = await this.getFolder();
|
|
14517
|
+
const patchInfoPath = path12.join(mobbFolderPath, "patchInfo.md");
|
|
14518
|
+
const markdownContent = this.generateFixMarkdown(fix, savedPatchFileName);
|
|
14519
|
+
let existingContent = "";
|
|
14520
|
+
if (fs12.existsSync(patchInfoPath)) {
|
|
14521
|
+
existingContent = await fs12.promises.readFile(patchInfoPath, "utf8");
|
|
14522
|
+
logDebug("[LocalMobbFolderService] Existing patchInfo.md found");
|
|
14523
|
+
} else {
|
|
14524
|
+
logDebug("[LocalMobbFolderService] Creating new patchInfo.md file");
|
|
14525
|
+
}
|
|
14526
|
+
const separator = existingContent ? "\n\n================================================================================\n\n" : "";
|
|
14527
|
+
const updatedContent = `${markdownContent}${separator}${existingContent}`;
|
|
14528
|
+
await fs12.promises.writeFile(patchInfoPath, updatedContent, "utf8");
|
|
14529
|
+
logInfo("[LocalMobbFolderService] Patch info logged successfully", {
|
|
14530
|
+
patchInfoPath,
|
|
14531
|
+
fixId: fix.id,
|
|
14532
|
+
contentLength: updatedContent.length
|
|
14533
|
+
});
|
|
14534
|
+
return {
|
|
14535
|
+
success: true,
|
|
14536
|
+
filePath: patchInfoPath,
|
|
14537
|
+
message: `Fix details logged to patchInfo.md`
|
|
14538
|
+
};
|
|
14539
|
+
} catch (error) {
|
|
14540
|
+
const errorMessage = `Failed to log patch info: ${error}`;
|
|
14541
|
+
logError(errorMessage, { fixId: fix.id, error });
|
|
14542
|
+
return {
|
|
14543
|
+
success: false,
|
|
14544
|
+
error: errorMessage,
|
|
14545
|
+
message: errorMessage
|
|
14546
|
+
};
|
|
14547
|
+
}
|
|
14548
|
+
}
|
|
14549
|
+
/**
|
|
14550
|
+
* Generates markdown formatted content for a fix
|
|
14551
|
+
* @param fix McpFix object to format
|
|
14552
|
+
* @param savedPatchFileName The filename of the saved patch file
|
|
14553
|
+
* @returns Markdown formatted string
|
|
14554
|
+
*/
|
|
14555
|
+
generateFixMarkdown(fix, savedPatchFileName) {
|
|
14556
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
14557
|
+
const patch = this.extractPatchFromFix(fix);
|
|
14558
|
+
const relativePatchedFilePath = patch ? extractPathFromPatch(patch) : null;
|
|
14559
|
+
const patchedFilePath = relativePatchedFilePath ? path12.resolve(this.repoPath, relativePatchedFilePath) : null;
|
|
14560
|
+
const fixIdentifier = savedPatchFileName ? savedPatchFileName.replace(".patch", "") : fix.id;
|
|
14561
|
+
let markdown = `# Fix ${fixIdentifier}
|
|
14562
|
+
|
|
14563
|
+
`;
|
|
14564
|
+
markdown += `**Logged:** ${timestamp}
|
|
14565
|
+
|
|
14566
|
+
`;
|
|
14567
|
+
if (patchedFilePath) {
|
|
14568
|
+
markdown += `**Patched File:** ${patchedFilePath}
|
|
14569
|
+
|
|
14570
|
+
`;
|
|
14571
|
+
}
|
|
14572
|
+
if (fix.fixUrl) {
|
|
14573
|
+
markdown += `**Fix URL:** [View Fix](${fix.fixUrl})
|
|
14574
|
+
|
|
14575
|
+
`;
|
|
14576
|
+
}
|
|
14577
|
+
if (fix.severityText) {
|
|
14578
|
+
markdown += `**Severity:** ${fix.severityText}`;
|
|
14579
|
+
if (fix.severityValue !== null && fix.severityValue !== void 0) {
|
|
14580
|
+
markdown += ` (${fix.severityValue})`;
|
|
14581
|
+
}
|
|
14582
|
+
markdown += `
|
|
14583
|
+
|
|
14584
|
+
`;
|
|
14585
|
+
}
|
|
14586
|
+
markdown += `**Confidence:** ${fix.confidence}
|
|
14587
|
+
|
|
14588
|
+
`;
|
|
14589
|
+
if (fix.safeIssueType) {
|
|
14590
|
+
markdown += `**Issue Type:** ${fix.safeIssueType}
|
|
14591
|
+
|
|
14592
|
+
`;
|
|
14593
|
+
}
|
|
14594
|
+
const parseIssueTypeRes = parsedIssueTypeRes(fix.safeIssueType || void 0);
|
|
14595
|
+
const staticData = parseIssueTypeRes.success ? fixDetailsData[parseIssueTypeRes.data] : null;
|
|
14596
|
+
const { issueDescription: issueDescription2, fixInstructions } = staticData || {};
|
|
14597
|
+
markdown += `
|
|
14598
|
+
## Vulnerability Details
|
|
14599
|
+
|
|
14600
|
+
`;
|
|
14601
|
+
if (issueDescription2) {
|
|
14602
|
+
markdown += `### Issue
|
|
14603
|
+
|
|
14604
|
+
`;
|
|
14605
|
+
markdown += `${issueDescription2}
|
|
14606
|
+
|
|
14607
|
+
`;
|
|
14608
|
+
}
|
|
14609
|
+
if (fixInstructions) {
|
|
14610
|
+
markdown += `### How to Fix
|
|
14611
|
+
|
|
14612
|
+
`;
|
|
14613
|
+
markdown += `${fixInstructions}
|
|
14614
|
+
|
|
14615
|
+
`;
|
|
14616
|
+
}
|
|
14617
|
+
if (fix.vulnerabilityReportIssues.length > 0) {
|
|
14618
|
+
fix.vulnerabilityReportIssues.forEach((issue) => {
|
|
14619
|
+
if (issue.vulnerabilityReportIssueTags && issue.vulnerabilityReportIssueTags.length > 0) {
|
|
14620
|
+
markdown += `**Tags:** ${issue.vulnerabilityReportIssueTags.map((tag) => tag.vulnerability_report_issue_tag_value).join(", ")}
|
|
14621
|
+
|
|
14622
|
+
`;
|
|
14623
|
+
}
|
|
14624
|
+
});
|
|
14625
|
+
}
|
|
14626
|
+
if (fix.patchAndQuestions.__typename === "FixData") {
|
|
14627
|
+
const fixData = fix.patchAndQuestions;
|
|
14628
|
+
if (fixData.extraContext.fixDescription) {
|
|
14629
|
+
markdown += `## Fix Description
|
|
14630
|
+
|
|
14631
|
+
`;
|
|
14632
|
+
markdown += `${fixData.extraContext.fixDescription}
|
|
14633
|
+
|
|
14634
|
+
`;
|
|
14635
|
+
}
|
|
14636
|
+
if (fixData.extraContext.extraContext.length > 0) {
|
|
14637
|
+
markdown += `## Additional Context
|
|
14638
|
+
|
|
14639
|
+
`;
|
|
14640
|
+
fixData.extraContext.extraContext.forEach((context) => {
|
|
14641
|
+
markdown += `**${context.key}:** ${JSON.stringify(context.value)}
|
|
14642
|
+
|
|
14643
|
+
`;
|
|
14644
|
+
});
|
|
14645
|
+
}
|
|
14646
|
+
const patchLines = fixData.patch.split("\n");
|
|
14647
|
+
const previewLines = patchLines.slice(0, 10);
|
|
14648
|
+
markdown += `## Patch Preview
|
|
14649
|
+
|
|
14650
|
+
`;
|
|
14651
|
+
markdown += `\`\`\`diff
|
|
14652
|
+
`;
|
|
14653
|
+
markdown += `${previewLines.join("\n")}`;
|
|
14654
|
+
if (patchLines.length > 10) {
|
|
14655
|
+
markdown += `
|
|
14656
|
+
... (${patchLines.length - 10} more lines)`;
|
|
14657
|
+
}
|
|
14658
|
+
markdown += `
|
|
14659
|
+
\`\`\`
|
|
14660
|
+
|
|
14661
|
+
`;
|
|
14662
|
+
} else if (fix.patchAndQuestions.__typename === "GetFixNoFixError") {
|
|
14663
|
+
markdown += `## Fix Status
|
|
14664
|
+
|
|
14665
|
+
`;
|
|
14666
|
+
markdown += `\u26A0\uFE0F **No fix available** - This vulnerability could not be automatically fixed.
|
|
14667
|
+
|
|
14668
|
+
`;
|
|
14669
|
+
}
|
|
14670
|
+
return markdown;
|
|
14671
|
+
}
|
|
14672
|
+
// /**
|
|
14673
|
+
// * Gets information about a local Mobb folder
|
|
14674
|
+
// * @param folderPath Path to the folder to examine
|
|
14675
|
+
// * @returns Folder information including existence and basic stats
|
|
14676
|
+
// */
|
|
14677
|
+
// public async getFolderInfo(folderPath: string): Promise<FolderInfo> {
|
|
14678
|
+
// logDebug('[LocalMobbFolderService] Getting folder info', { folderPath })
|
|
14679
|
+
// const folderInfo: FolderInfo = {
|
|
14680
|
+
// path: folderPath,
|
|
14681
|
+
// exists: false,
|
|
14682
|
+
// isDirectory: false,
|
|
14683
|
+
// }
|
|
14684
|
+
// try {
|
|
14685
|
+
// const stats = await fs.promises.stat(folderPath)
|
|
14686
|
+
// folderInfo.exists = true
|
|
14687
|
+
// folderInfo.isDirectory = stats.isDirectory()
|
|
14688
|
+
// if (folderInfo.isDirectory) {
|
|
14689
|
+
// // Count files in directory
|
|
14690
|
+
// const files = await fs.promises.readdir(folderPath)
|
|
14691
|
+
// folderInfo.filesCount = files.length
|
|
14692
|
+
// // Calculate directory size (optional, can be expensive for large directories)
|
|
14693
|
+
// let totalSize = 0
|
|
14694
|
+
// for (const file of files) {
|
|
14695
|
+
// try {
|
|
14696
|
+
// const filePath = path.join(folderPath, file)
|
|
14697
|
+
// const fileStats = await fs.promises.stat(filePath)
|
|
14698
|
+
// if (fileStats.isFile()) {
|
|
14699
|
+
// totalSize += fileStats.size
|
|
14700
|
+
// }
|
|
14701
|
+
// } catch (error) {
|
|
14702
|
+
// logDebug('[LocalMobbFolderService] Error getting file size', { file, error })
|
|
14703
|
+
// }
|
|
14704
|
+
// }
|
|
14705
|
+
// folderInfo.size = totalSize
|
|
14706
|
+
// }
|
|
14707
|
+
// } catch (error) {
|
|
14708
|
+
// logDebug('[LocalMobbFolderService] Folder does not exist or is not accessible', {
|
|
14709
|
+
// folderPath,
|
|
14710
|
+
// error,
|
|
14711
|
+
// })
|
|
14712
|
+
// }
|
|
14713
|
+
// return folderInfo
|
|
14714
|
+
// }
|
|
14715
|
+
// /**
|
|
14716
|
+
// * Creates a local Mobb folder at the specified path
|
|
14717
|
+
// * @param basePath Base directory where the Mobb folder should be created
|
|
14718
|
+
// * @param folderName Optional custom folder name (defaults to '.mobb')
|
|
14719
|
+
// * @returns Result of the folder creation operation
|
|
14720
|
+
// */
|
|
14721
|
+
// public async createMobbFolder(
|
|
14722
|
+
// basePath: string,
|
|
14723
|
+
// folderName: string = this.defaultMobbFolderName
|
|
14724
|
+
// ): Promise<FolderCreationResult> {
|
|
14725
|
+
// const targetPath = path.join(basePath, folderName)
|
|
14726
|
+
// logInfo('[LocalMobbFolderService] Creating Mobb folder', { targetPath })
|
|
14727
|
+
// try {
|
|
14728
|
+
// // Check if base path exists
|
|
14729
|
+
// const basePathExists = await this.pathExists(basePath)
|
|
14730
|
+
// if (!basePathExists) {
|
|
14731
|
+
// const error = `Base path does not exist: ${basePath}`
|
|
14732
|
+
// logError(error)
|
|
14733
|
+
// return {
|
|
14734
|
+
// success: false,
|
|
14735
|
+
// path: targetPath,
|
|
14736
|
+
// error,
|
|
14737
|
+
// }
|
|
14738
|
+
// }
|
|
14739
|
+
// // Check if target folder already exists
|
|
14740
|
+
// const folderExists = await this.pathExists(targetPath)
|
|
14741
|
+
// if (folderExists) {
|
|
14742
|
+
// logInfo('[LocalMobbFolderService] Mobb folder already exists', { targetPath })
|
|
14743
|
+
// return {
|
|
14744
|
+
// success: true,
|
|
14745
|
+
// path: targetPath,
|
|
14746
|
+
// }
|
|
14747
|
+
// }
|
|
14748
|
+
// // Create the folder
|
|
14749
|
+
// await fs.promises.mkdir(targetPath, { recursive: true })
|
|
14750
|
+
// logInfo('[LocalMobbFolderService] Mobb folder created successfully', { targetPath })
|
|
14751
|
+
// return {
|
|
14752
|
+
// success: true,
|
|
14753
|
+
// path: targetPath,
|
|
14754
|
+
// }
|
|
14755
|
+
// } catch (error) {
|
|
14756
|
+
// const errorMessage = `Failed to create Mobb folder: ${error}`
|
|
14757
|
+
// logError(errorMessage, { targetPath, error })
|
|
14758
|
+
// return {
|
|
14759
|
+
// success: false,
|
|
14760
|
+
// path: targetPath,
|
|
14761
|
+
// error: errorMessage,
|
|
14762
|
+
// }
|
|
14763
|
+
// }
|
|
14764
|
+
// }
|
|
14765
|
+
// /**
|
|
14766
|
+
// * Finds the nearest Mobb folder by traversing up the directory tree
|
|
14767
|
+
// * @param startPath Starting directory to search from
|
|
14768
|
+
// * @param folderName Optional custom folder name to search for (defaults to '.mobb')
|
|
14769
|
+
// * @returns Path to the found Mobb folder or null if not found
|
|
14770
|
+
// */
|
|
14771
|
+
// public async findNearestMobbFolder(
|
|
14772
|
+
// startPath: string,
|
|
14773
|
+
// folderName: string = this.defaultMobbFolderName
|
|
14774
|
+
// ): Promise<string | null> {
|
|
14775
|
+
// logDebug('[LocalMobbFolderService] Searching for nearest Mobb folder', {
|
|
14776
|
+
// startPath,
|
|
14777
|
+
// folderName,
|
|
14778
|
+
// })
|
|
14779
|
+
// let currentPath = path.resolve(startPath)
|
|
14780
|
+
// const rootPath = path.parse(currentPath).root
|
|
14781
|
+
// while (currentPath !== rootPath) {
|
|
14782
|
+
// const mobbFolderPath = path.join(currentPath, folderName)
|
|
14783
|
+
// const folderInfo = await this.getFolderInfo(mobbFolderPath)
|
|
14784
|
+
// if (folderInfo.exists && folderInfo.isDirectory) {
|
|
14785
|
+
// logDebug('[LocalMobbFolderService] Found Mobb folder', { mobbFolderPath })
|
|
14786
|
+
// return mobbFolderPath
|
|
14787
|
+
// }
|
|
14788
|
+
// // Move up one directory
|
|
14789
|
+
// const parentPath = path.dirname(currentPath)
|
|
14790
|
+
// if (parentPath === currentPath) {
|
|
14791
|
+
// break // Reached the root
|
|
14792
|
+
// }
|
|
14793
|
+
// currentPath = parentPath
|
|
14794
|
+
// }
|
|
14795
|
+
// logDebug('[LocalMobbFolderService] No Mobb folder found')
|
|
14796
|
+
// return null
|
|
14797
|
+
// }
|
|
14798
|
+
// /**
|
|
14799
|
+
// * Cleans up a Mobb folder by removing its contents
|
|
14800
|
+
// * @param folderPath Path to the Mobb folder to clean
|
|
14801
|
+
// * @returns Result of the cleanup operation
|
|
14802
|
+
// */
|
|
14803
|
+
// public async cleanMobbFolder(folderPath: string): Promise<FolderOperationResult> {
|
|
14804
|
+
// logInfo('[LocalMobbFolderService] Cleaning Mobb folder', { folderPath })
|
|
14805
|
+
// try {
|
|
14806
|
+
// const folderInfo = await this.getFolderInfo(folderPath)
|
|
14807
|
+
// if (!folderInfo.exists) {
|
|
14808
|
+
// return {
|
|
14809
|
+
// success: false,
|
|
14810
|
+
// message: `Folder does not exist: ${folderPath}`,
|
|
14811
|
+
// error: 'Folder not found',
|
|
14812
|
+
// }
|
|
14813
|
+
// }
|
|
14814
|
+
// if (!folderInfo.isDirectory) {
|
|
14815
|
+
// return {
|
|
14816
|
+
// success: false,
|
|
14817
|
+
// message: `Path is not a directory: ${folderPath}`,
|
|
14818
|
+
// error: 'Not a directory',
|
|
14819
|
+
// }
|
|
14820
|
+
// }
|
|
14821
|
+
// // Remove all contents
|
|
14822
|
+
// const files = await fs.promises.readdir(folderPath)
|
|
14823
|
+
// for (const file of files) {
|
|
14824
|
+
// const filePath = path.join(folderPath, file)
|
|
14825
|
+
// try {
|
|
14826
|
+
// const stats = await fs.promises.stat(filePath)
|
|
14827
|
+
// if (stats.isDirectory()) {
|
|
14828
|
+
// await fs.promises.rmdir(filePath, { recursive: true })
|
|
14829
|
+
// } else {
|
|
14830
|
+
// await fs.promises.unlink(filePath)
|
|
14831
|
+
// }
|
|
14832
|
+
// } catch (error) {
|
|
14833
|
+
// logError('[LocalMobbFolderService] Error removing file/folder', {
|
|
14834
|
+
// filePath,
|
|
14835
|
+
// error,
|
|
14836
|
+
// })
|
|
14837
|
+
// }
|
|
14838
|
+
// }
|
|
14839
|
+
// logInfo('[LocalMobbFolderService] Mobb folder cleaned successfully', { folderPath })
|
|
14840
|
+
// return {
|
|
14841
|
+
// success: true,
|
|
14842
|
+
// message: `Successfully cleaned folder: ${folderPath}`,
|
|
14843
|
+
// }
|
|
14844
|
+
// } catch (error) {
|
|
14845
|
+
// const errorMessage = `Failed to clean Mobb folder: ${error}`
|
|
14846
|
+
// logError(errorMessage, { folderPath, error })
|
|
14847
|
+
// return {
|
|
14848
|
+
// success: false,
|
|
14849
|
+
// message: errorMessage,
|
|
14850
|
+
// error: String(error),
|
|
14851
|
+
// }
|
|
14852
|
+
// }
|
|
14853
|
+
// }
|
|
14854
|
+
// /**
|
|
14855
|
+
// * Validates that a path is safe for Mobb folder operations
|
|
14856
|
+
// * @param folderPath Path to validate
|
|
14857
|
+
// * @returns True if the path is safe to use
|
|
14858
|
+
// */
|
|
14859
|
+
// public validatePath(folderPath: string): boolean {
|
|
14860
|
+
// logDebug('[LocalMobbFolderService] Validating path', { folderPath })
|
|
14861
|
+
// // Check for path traversal attempts
|
|
14862
|
+
// if (folderPath.includes('..') || folderPath.includes('\0')) {
|
|
14863
|
+
// logError('[LocalMobbFolderService] Dangerous path detected', { folderPath })
|
|
14864
|
+
// return false
|
|
14865
|
+
// }
|
|
14866
|
+
// // Check for absolute path requirements (optional, depending on your needs)
|
|
14867
|
+
// const normalizedPath = path.normalize(folderPath)
|
|
14868
|
+
// if (normalizedPath.includes('..')) {
|
|
14869
|
+
// logError('[LocalMobbFolderService] Path traversal detected after normalization', {
|
|
14870
|
+
// folderPath,
|
|
14871
|
+
// normalizedPath,
|
|
14872
|
+
// })
|
|
14873
|
+
// return false
|
|
14874
|
+
// }
|
|
14875
|
+
// return true
|
|
14876
|
+
// }
|
|
14877
|
+
// /**
|
|
14878
|
+
// * Helper method to check if a path exists
|
|
14879
|
+
// * @param targetPath Path to check
|
|
14880
|
+
// * @returns True if the path exists
|
|
14881
|
+
// */
|
|
14882
|
+
// private async pathExists(targetPath: string): Promise<boolean> {
|
|
14883
|
+
// try {
|
|
14884
|
+
// await fs.promises.access(targetPath)
|
|
14885
|
+
// return true
|
|
14886
|
+
// } catch {
|
|
14887
|
+
// return false
|
|
14888
|
+
// }
|
|
14889
|
+
// }
|
|
14890
|
+
};
|
|
14891
|
+
|
|
14892
|
+
// src/mcp/services/PatchApplicationService.ts
|
|
14893
|
+
init_configs();
|
|
14894
|
+
import {
|
|
14895
|
+
existsSync as existsSync2,
|
|
14896
|
+
mkdirSync,
|
|
14897
|
+
readFileSync,
|
|
14898
|
+
unlinkSync,
|
|
14899
|
+
writeFileSync
|
|
14900
|
+
} from "fs";
|
|
14901
|
+
import fs13 from "fs/promises";
|
|
14902
|
+
import parseDiff2 from "parse-diff";
|
|
14903
|
+
import path13 from "path";
|
|
14904
|
+
var PatchApplicationService = class {
|
|
14905
|
+
/**
|
|
14906
|
+
* Gets the appropriate comment syntax for a file based on its extension
|
|
14907
|
+
*/
|
|
14908
|
+
static getCommentSyntax(filePath) {
|
|
14909
|
+
const ext = path13.extname(filePath).toLowerCase();
|
|
14910
|
+
const basename2 = path13.basename(filePath);
|
|
14911
|
+
const commentMap = {
|
|
14912
|
+
// C-style languages (single line comments)
|
|
14913
|
+
".js": "//",
|
|
14914
|
+
".jsx": "//",
|
|
14915
|
+
".mjs": "//",
|
|
14916
|
+
".cjs": "//",
|
|
14917
|
+
".ts": "//",
|
|
14918
|
+
".tsx": "//",
|
|
14919
|
+
".java": "//",
|
|
14920
|
+
".c": "//",
|
|
14921
|
+
".cpp": "//",
|
|
14922
|
+
".cc": "//",
|
|
14923
|
+
".cxx": "//",
|
|
14924
|
+
".c++": "//",
|
|
14925
|
+
".pcc": "//",
|
|
14926
|
+
".tpp": "//",
|
|
14927
|
+
".C": "//",
|
|
14928
|
+
".h": "//",
|
|
14929
|
+
".hpp": "//",
|
|
14930
|
+
".hh": "//",
|
|
14931
|
+
".hxx": "//",
|
|
14932
|
+
".inl": "//",
|
|
14933
|
+
".ipp": "//",
|
|
14934
|
+
".cs": "//",
|
|
14935
|
+
".php": "//",
|
|
14936
|
+
".tpl": "//",
|
|
14937
|
+
".phtml": "//",
|
|
14938
|
+
".go": "//",
|
|
14939
|
+
".rs": "//",
|
|
14940
|
+
".swift": "//",
|
|
14941
|
+
".kt": "//",
|
|
14942
|
+
".kts": "//",
|
|
14943
|
+
".ktm": "//",
|
|
14944
|
+
".scala": "//",
|
|
14945
|
+
".dart": "//",
|
|
14946
|
+
".cls": "//",
|
|
14947
|
+
// Apex
|
|
14948
|
+
".sol": "//",
|
|
14949
|
+
// Solidity
|
|
14950
|
+
".move": "//",
|
|
14951
|
+
// Move
|
|
14952
|
+
".hack": "//",
|
|
14953
|
+
// Hack
|
|
14954
|
+
".hck": "//",
|
|
14955
|
+
// Hack
|
|
14956
|
+
".jl": "//",
|
|
14957
|
+
// Julia
|
|
14958
|
+
".proto": "//",
|
|
14959
|
+
// Protocol Buffers
|
|
14960
|
+
// Python-style languages
|
|
14961
|
+
".py": "#",
|
|
14962
|
+
".pyx": "#",
|
|
14963
|
+
".pyi": "#",
|
|
14964
|
+
".sh": "#",
|
|
14965
|
+
".bash": "#",
|
|
14966
|
+
".zsh": "#",
|
|
14967
|
+
".fish": "#",
|
|
14968
|
+
".pl": "#",
|
|
14969
|
+
".pm": "#",
|
|
14970
|
+
".rb": "#",
|
|
14971
|
+
".r": "#",
|
|
14972
|
+
".R": "#",
|
|
14973
|
+
".yaml": "#",
|
|
14974
|
+
".yml": "#",
|
|
14975
|
+
".toml": "#",
|
|
14976
|
+
".conf": "#",
|
|
14977
|
+
".cfg": "#",
|
|
14978
|
+
".ini": "#",
|
|
14979
|
+
".dockerfile": "#",
|
|
14980
|
+
".Dockerfile": "#",
|
|
14981
|
+
".tf": "#",
|
|
14982
|
+
// Terraform
|
|
14983
|
+
".tfvars": "#",
|
|
14984
|
+
// Terraform
|
|
14985
|
+
".hcl": "#",
|
|
14986
|
+
// Terraform/HCL
|
|
14987
|
+
".promql": "#",
|
|
14988
|
+
// PromQL
|
|
14989
|
+
".cairo": "#",
|
|
14990
|
+
// Cairo
|
|
14991
|
+
".circom": "#",
|
|
14992
|
+
// Circom
|
|
14993
|
+
// SQL
|
|
14994
|
+
".sql": "--",
|
|
14995
|
+
// HTML/XML style
|
|
14996
|
+
".html": "<!--",
|
|
14997
|
+
".htm": "<!--",
|
|
14998
|
+
".xml": "<!--",
|
|
14999
|
+
".plist": "<!--",
|
|
15000
|
+
".xhtml": "<!--",
|
|
15001
|
+
".svg": "<!--",
|
|
15002
|
+
".vue": "<!--",
|
|
15003
|
+
// Vue templates
|
|
15004
|
+
// CSS
|
|
15005
|
+
".css": "/*",
|
|
15006
|
+
".less": "//",
|
|
15007
|
+
".scss": "//",
|
|
15008
|
+
".sass": "//",
|
|
15009
|
+
// Other scripting
|
|
15010
|
+
".lua": "--",
|
|
15011
|
+
".vim": '"',
|
|
15012
|
+
".vimrc": '"',
|
|
15013
|
+
".ql": "//",
|
|
15014
|
+
// QL
|
|
15015
|
+
".qll": "//",
|
|
15016
|
+
// QL
|
|
15017
|
+
// Assembly
|
|
15018
|
+
".asm": ";",
|
|
15019
|
+
".s": ";",
|
|
15020
|
+
// Batch/PowerShell
|
|
15021
|
+
".bat": "REM",
|
|
15022
|
+
".cmd": "REM",
|
|
15023
|
+
".ps1": "#",
|
|
15024
|
+
// Functional languages
|
|
15025
|
+
".hs": "--",
|
|
15026
|
+
".lhs": "--",
|
|
15027
|
+
".elm": "--",
|
|
15028
|
+
".ml": "(*",
|
|
15029
|
+
".mli": "(*",
|
|
15030
|
+
".fs": "//",
|
|
15031
|
+
".fsx": "//",
|
|
15032
|
+
".fsi": "//",
|
|
15033
|
+
// Lisp family
|
|
15034
|
+
".lisp": ";",
|
|
15035
|
+
".lsp": ";",
|
|
15036
|
+
".cl": ";",
|
|
15037
|
+
".el": ";",
|
|
15038
|
+
// Emacs Lisp
|
|
15039
|
+
".scm": ";",
|
|
15040
|
+
".ss": ";",
|
|
15041
|
+
".clj": ";",
|
|
15042
|
+
".cljs": ";",
|
|
15043
|
+
".cljc": ";",
|
|
15044
|
+
".edn": ";",
|
|
15045
|
+
// Clojure EDN
|
|
15046
|
+
// Configuration files
|
|
15047
|
+
".properties": "#",
|
|
15048
|
+
".gitignore": "#",
|
|
15049
|
+
".env": "#",
|
|
15050
|
+
".editorconfig": "#",
|
|
15051
|
+
".ex": "#",
|
|
15052
|
+
// Elixir
|
|
15053
|
+
".exs": "#",
|
|
15054
|
+
// Elixir
|
|
15055
|
+
".json": "//",
|
|
15056
|
+
// JSON (though rarely commented)
|
|
15057
|
+
".ipynb": "#",
|
|
15058
|
+
// Jupyter notebooks (JSON-based)
|
|
15059
|
+
".jsonnet": "//",
|
|
15060
|
+
// Jsonnet
|
|
15061
|
+
".libsonnet": "//",
|
|
15062
|
+
// Jsonnet
|
|
15063
|
+
Dockerfile: "#",
|
|
15064
|
+
// Docker files without extension
|
|
15065
|
+
// Default fallback
|
|
15066
|
+
"": "//"
|
|
15067
|
+
};
|
|
15068
|
+
if (commentMap[basename2]) {
|
|
15069
|
+
return commentMap[basename2];
|
|
15070
|
+
}
|
|
15071
|
+
return commentMap[ext] || commentMap[""] || "//";
|
|
15072
|
+
}
|
|
15073
|
+
/**
|
|
15074
|
+
* Writes content to a file and adds Mobb fix comment in debug mode
|
|
15075
|
+
*/
|
|
15076
|
+
static writeFileWithFixComment({
|
|
15077
|
+
filePath,
|
|
15078
|
+
content,
|
|
15079
|
+
fix,
|
|
15080
|
+
scanContext
|
|
15081
|
+
}) {
|
|
15082
|
+
let finalContent = content;
|
|
15083
|
+
if (MCP_AUTO_FIX_DEBUG_MODE) {
|
|
15084
|
+
const fixType = fix.safeIssueType || "Security Issue";
|
|
15085
|
+
let fixLink;
|
|
15086
|
+
if (fix.fixUrl) {
|
|
15087
|
+
fixLink = fix.fixUrl;
|
|
15088
|
+
} else {
|
|
15089
|
+
const apiUrl = process.env["API_URL"] || MCP_DEFAULT_API_URL;
|
|
15090
|
+
const appBaseUrl = apiUrl.replace("/v1/graphql", "").replace("api.", "");
|
|
15091
|
+
fixLink = `${appBaseUrl}/fixes/${fix.id}`;
|
|
15092
|
+
}
|
|
15093
|
+
const commentPrefix = this.getCommentSyntax(filePath);
|
|
15094
|
+
const lines = content.split("\n");
|
|
15095
|
+
const lastLine = lines[lines.length - 1]?.trim() || "";
|
|
15096
|
+
const isMobbComment = lastLine.includes("Mobb security fix applied:");
|
|
15097
|
+
const spacing = isMobbComment ? "\n" : "\n\n";
|
|
15098
|
+
let comment;
|
|
15099
|
+
if (commentPrefix === "<!--") {
|
|
15100
|
+
comment = `${spacing}<!-- Mobb security fix applied: ${fixType} ${fixLink} -->`;
|
|
15101
|
+
} else if (commentPrefix === "/*") {
|
|
15102
|
+
comment = `${spacing}/* Mobb security fix applied: ${fixType} ${fixLink} */`;
|
|
15103
|
+
} else if (commentPrefix === "(*") {
|
|
15104
|
+
comment = `${spacing}(* Mobb security fix applied: ${fixType} ${fixLink} *)`;
|
|
15105
|
+
} else {
|
|
15106
|
+
comment = `${spacing}${commentPrefix} Mobb security fix applied: ${fixType} ${fixLink}`;
|
|
15107
|
+
}
|
|
15108
|
+
finalContent = content + comment;
|
|
15109
|
+
logInfo(
|
|
15110
|
+
`[${scanContext}] Debug mode: Adding fix comment to ${filePath}`,
|
|
15111
|
+
{
|
|
15112
|
+
fixId: fix.id,
|
|
15113
|
+
fixType,
|
|
15114
|
+
fixLink,
|
|
15115
|
+
commentSyntax: commentPrefix,
|
|
15116
|
+
spacing: isMobbComment ? "single line" : "empty line above"
|
|
15117
|
+
}
|
|
15118
|
+
);
|
|
15119
|
+
}
|
|
15120
|
+
const dirPath = path13.dirname(filePath);
|
|
15121
|
+
mkdirSync(dirPath, { recursive: true });
|
|
15122
|
+
writeFileSync(filePath, finalContent, "utf8");
|
|
15123
|
+
return filePath;
|
|
15124
|
+
}
|
|
15125
|
+
/**
|
|
15126
|
+
* Extracts target file path from a fix
|
|
15127
|
+
*/
|
|
15128
|
+
static getFixTargetFile(fix) {
|
|
15129
|
+
if (fix.patchAndQuestions?.__typename !== "FixData") {
|
|
15130
|
+
return null;
|
|
15131
|
+
}
|
|
15132
|
+
const patch = fix.patchAndQuestions.patch;
|
|
15133
|
+
if (!patch) return null;
|
|
15134
|
+
const match = patch.match(/^\+\+\+ b\/(.+)$/m);
|
|
15135
|
+
return match?.[1] || null;
|
|
15136
|
+
}
|
|
15137
|
+
/**
|
|
15138
|
+
* Groups fixes by target file and returns ranked fixes with fallback options
|
|
15139
|
+
*/
|
|
15140
|
+
static selectBestFixesPerFile(fixes) {
|
|
15141
|
+
const fileToFixMap = /* @__PURE__ */ new Map();
|
|
15142
|
+
const standaloneFixesFixes = [];
|
|
15143
|
+
for (const fix of fixes) {
|
|
15144
|
+
const targetFile = this.getFixTargetFile(fix);
|
|
15145
|
+
if (!targetFile) {
|
|
15146
|
+
standaloneFixesFixes.push(fix);
|
|
15147
|
+
continue;
|
|
15148
|
+
}
|
|
15149
|
+
if (!fileToFixMap.has(targetFile)) {
|
|
15150
|
+
fileToFixMap.set(targetFile, []);
|
|
15151
|
+
}
|
|
15152
|
+
const fixes2 = fileToFixMap.get(targetFile);
|
|
15153
|
+
if (fixes2) {
|
|
15154
|
+
fixes2.push(fix);
|
|
15155
|
+
}
|
|
15156
|
+
}
|
|
15157
|
+
for (const [_targetFile, fileFixes] of fileToFixMap) {
|
|
15158
|
+
fileFixes.sort((a, b) => (b.severityValue ?? 0) - (a.severityValue ?? 0));
|
|
15159
|
+
}
|
|
15160
|
+
return { fileFixGroups: fileToFixMap, standaloneFixesFixes };
|
|
15161
|
+
}
|
|
15162
|
+
/**
|
|
15163
|
+
* Processes invalid fixes from file modification checks
|
|
15164
|
+
*/
|
|
15165
|
+
static processInvalidFixes(invalidFixes, failedFixes, scanContext) {
|
|
15166
|
+
if (invalidFixes.length === 0) return;
|
|
15167
|
+
for (const invalidFix of invalidFixes) {
|
|
15168
|
+
logWarn(
|
|
15169
|
+
`[${scanContext}] Patch ${invalidFix.fix.id} not applied - file modified after scan`,
|
|
15170
|
+
{
|
|
15171
|
+
fixId: invalidFix.fix.id,
|
|
15172
|
+
issueType: invalidFix.fix.safeIssueType,
|
|
15173
|
+
severity: invalidFix.fix.severityText,
|
|
15174
|
+
severityValue: invalidFix.fix.severityValue,
|
|
15175
|
+
confidence: invalidFix.fix.confidence,
|
|
15176
|
+
reason: invalidFix.reason
|
|
15177
|
+
}
|
|
15178
|
+
);
|
|
15179
|
+
}
|
|
15180
|
+
logInfo(
|
|
15181
|
+
`[${scanContext}] Discarded ${invalidFixes.length} fixes due to file modifications after scan`,
|
|
15182
|
+
{
|
|
15183
|
+
discardedFixes: invalidFixes.map((f) => ({
|
|
15184
|
+
fixId: f.fix.id,
|
|
15185
|
+
reason: f.reason
|
|
15186
|
+
}))
|
|
15187
|
+
}
|
|
15188
|
+
);
|
|
15189
|
+
failedFixes.push(
|
|
15190
|
+
...invalidFixes.map((f) => ({ fix: f.fix, error: f.reason }))
|
|
15191
|
+
);
|
|
15192
|
+
}
|
|
15193
|
+
/**
|
|
15194
|
+
* Applies standalone fixes (fixes that don't target specific files)
|
|
15195
|
+
*/
|
|
15196
|
+
static async applyStandaloneFixes({
|
|
15197
|
+
fixes,
|
|
15198
|
+
repositoryPath,
|
|
15199
|
+
appliedFixes,
|
|
15200
|
+
failedFixes,
|
|
15201
|
+
scanContext
|
|
15202
|
+
}) {
|
|
15203
|
+
for (const fix of fixes) {
|
|
15204
|
+
try {
|
|
15205
|
+
const result = await this.applySingleFix({
|
|
15206
|
+
fix,
|
|
15207
|
+
repositoryPath,
|
|
15208
|
+
scanContext
|
|
15209
|
+
});
|
|
15210
|
+
if (result.success) {
|
|
15211
|
+
appliedFixes.push(fix);
|
|
15212
|
+
logInfo(
|
|
15213
|
+
`[${scanContext}] Successfully applied standalone fix ${fix.id}`,
|
|
15214
|
+
{
|
|
15215
|
+
issueType: fix.safeIssueType,
|
|
15216
|
+
severity: fix.severityText
|
|
15217
|
+
}
|
|
15218
|
+
);
|
|
15219
|
+
} else {
|
|
15220
|
+
failedFixes.push({ fix, error: result.error || "Unknown error" });
|
|
15221
|
+
}
|
|
15222
|
+
} catch (error) {
|
|
15223
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
15224
|
+
logError(`[${scanContext}] Failed to apply standalone fix ${fix.id}`, {
|
|
15225
|
+
error: errorMessage,
|
|
15226
|
+
issueType: fix.safeIssueType
|
|
15227
|
+
});
|
|
15228
|
+
failedFixes.push({ fix, error: errorMessage });
|
|
15229
|
+
}
|
|
15230
|
+
}
|
|
15231
|
+
}
|
|
15232
|
+
/**
|
|
15233
|
+
* Attempts to apply a single fix and handles the result
|
|
15234
|
+
*/
|
|
15235
|
+
static async attemptFixApplication({
|
|
15236
|
+
fix,
|
|
15237
|
+
targetFile,
|
|
15238
|
+
attemptNumber,
|
|
15239
|
+
totalAttempts,
|
|
15240
|
+
repositoryPath,
|
|
15241
|
+
scanContext
|
|
15242
|
+
}) {
|
|
15243
|
+
logInfo(
|
|
15244
|
+
`[${scanContext}] Attempting fix ${fix.id} for ${targetFile} (priority ${attemptNumber}/${totalAttempts})`,
|
|
15245
|
+
{
|
|
15246
|
+
issueType: fix.safeIssueType,
|
|
15247
|
+
severity: fix.severityText,
|
|
15248
|
+
severityValue: fix.severityValue
|
|
15249
|
+
}
|
|
15250
|
+
);
|
|
15251
|
+
try {
|
|
15252
|
+
const result = await this.applySingleFix({
|
|
15253
|
+
fix,
|
|
15254
|
+
repositoryPath,
|
|
15255
|
+
scanContext
|
|
15256
|
+
});
|
|
15257
|
+
if (result.success) {
|
|
15258
|
+
logInfo(
|
|
15259
|
+
`[${scanContext}] Successfully applied fix ${fix.id} to ${targetFile}`,
|
|
15260
|
+
{
|
|
15261
|
+
issueType: fix.safeIssueType,
|
|
15262
|
+
severity: fix.severityText,
|
|
15263
|
+
attemptNumber
|
|
15264
|
+
}
|
|
15265
|
+
);
|
|
15266
|
+
return { success: true };
|
|
15267
|
+
} else {
|
|
15268
|
+
logInfo(
|
|
15269
|
+
`[${scanContext}] Fix ${fix.id} failed for ${targetFile}, trying next option if available`,
|
|
15270
|
+
{
|
|
15271
|
+
error: result.error,
|
|
15272
|
+
attemptNumber,
|
|
15273
|
+
remainingOptions: totalAttempts - attemptNumber
|
|
15274
|
+
}
|
|
15275
|
+
);
|
|
15276
|
+
return { success: false, error: result.error };
|
|
15277
|
+
}
|
|
15278
|
+
} catch (error) {
|
|
15279
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
15280
|
+
logInfo(
|
|
15281
|
+
`[${scanContext}] Fix ${fix.id} threw exception for ${targetFile}, trying next option if available`,
|
|
15282
|
+
{
|
|
15283
|
+
error: errorMessage,
|
|
15284
|
+
issueType: fix.safeIssueType,
|
|
15285
|
+
attemptNumber,
|
|
15286
|
+
remainingOptions: totalAttempts - attemptNumber
|
|
15287
|
+
}
|
|
15288
|
+
);
|
|
15289
|
+
return { success: false, error: errorMessage };
|
|
15290
|
+
}
|
|
15291
|
+
}
|
|
15292
|
+
/**
|
|
15293
|
+
* Applies fixes for a specific file with fallback logic
|
|
15294
|
+
*/
|
|
15295
|
+
static async applyFixesForFile({
|
|
15296
|
+
targetFile,
|
|
15297
|
+
rankedFixes,
|
|
15298
|
+
repositoryPath,
|
|
15299
|
+
appliedFixes,
|
|
15300
|
+
failedFixes,
|
|
15301
|
+
skippedFixes,
|
|
15302
|
+
scanContext
|
|
15303
|
+
}) {
|
|
15304
|
+
let appliedForFile = false;
|
|
15305
|
+
for (let i = 0; i < rankedFixes.length; i++) {
|
|
15306
|
+
const fix = rankedFixes[i];
|
|
15307
|
+
if (!fix) continue;
|
|
15308
|
+
if (appliedForFile) {
|
|
15309
|
+
const reason = `Another fix was already successfully applied to ${targetFile}`;
|
|
15310
|
+
skippedFixes.push({
|
|
15311
|
+
fix,
|
|
15312
|
+
reason
|
|
15313
|
+
});
|
|
15314
|
+
logWarn(
|
|
15315
|
+
`[${scanContext}] Patch ${fix.id} not applied - file already patched`,
|
|
15316
|
+
{
|
|
15317
|
+
fixId: fix.id,
|
|
15318
|
+
targetFile,
|
|
15319
|
+
issueType: fix.safeIssueType,
|
|
15320
|
+
severity: fix.severityText,
|
|
15321
|
+
severityValue: fix.severityValue,
|
|
15322
|
+
reason,
|
|
15323
|
+
confidence: fix.confidence
|
|
15324
|
+
}
|
|
15325
|
+
);
|
|
15326
|
+
continue;
|
|
15327
|
+
}
|
|
15328
|
+
const result = await this.attemptFixApplication({
|
|
15329
|
+
fix,
|
|
15330
|
+
targetFile,
|
|
15331
|
+
attemptNumber: i + 1,
|
|
15332
|
+
totalAttempts: rankedFixes.length,
|
|
15333
|
+
repositoryPath,
|
|
15334
|
+
scanContext
|
|
15335
|
+
});
|
|
15336
|
+
if (result.success) {
|
|
15337
|
+
appliedFixes.push(fix);
|
|
15338
|
+
appliedForFile = true;
|
|
15339
|
+
} else {
|
|
15340
|
+
if (i === rankedFixes.length - 1) {
|
|
15341
|
+
failedFixes.push({ fix, error: result.error || "Unknown error" });
|
|
15342
|
+
}
|
|
15343
|
+
}
|
|
15344
|
+
}
|
|
15345
|
+
}
|
|
15346
|
+
/**
|
|
15347
|
+
* Applies fixes grouped by file with fallback logic
|
|
15348
|
+
*/
|
|
15349
|
+
static async applyFileGroupFixes({
|
|
15350
|
+
fileFixGroups,
|
|
15351
|
+
repositoryPath,
|
|
15352
|
+
appliedFixes,
|
|
15353
|
+
failedFixes,
|
|
15354
|
+
skippedFixes,
|
|
15355
|
+
scanContext
|
|
15356
|
+
}) {
|
|
15357
|
+
for (const [targetFile, rankedFixes] of fileFixGroups) {
|
|
15358
|
+
await this.applyFixesForFile({
|
|
15359
|
+
targetFile,
|
|
15360
|
+
rankedFixes,
|
|
15361
|
+
repositoryPath,
|
|
15362
|
+
appliedFixes,
|
|
15363
|
+
failedFixes,
|
|
15364
|
+
skippedFixes,
|
|
15365
|
+
scanContext
|
|
15366
|
+
});
|
|
15367
|
+
}
|
|
15368
|
+
}
|
|
15369
|
+
/**
|
|
15370
|
+
* Logs skipped fixes as informational warnings
|
|
15371
|
+
*/
|
|
15372
|
+
static logSkippedFixes(skippedFixes, scanContext) {
|
|
15373
|
+
if (skippedFixes.length === 0) return;
|
|
15374
|
+
logWarn(
|
|
15375
|
+
`[${scanContext}] ${skippedFixes.length} fixes skipped because other fixes were successfully applied to their target files`,
|
|
15376
|
+
{
|
|
15377
|
+
skippedFixes: skippedFixes.map((f) => ({
|
|
15378
|
+
fixId: f.fix.id,
|
|
15379
|
+
reason: f.reason,
|
|
15380
|
+
severity: f.fix.severityText,
|
|
15381
|
+
issueType: f.fix.safeIssueType
|
|
15382
|
+
}))
|
|
15383
|
+
}
|
|
15384
|
+
);
|
|
15385
|
+
}
|
|
15386
|
+
/**
|
|
15387
|
+
* Checks if target files have been modified since scan time
|
|
15388
|
+
*/
|
|
15389
|
+
static async checkFileModificationTimes({
|
|
15390
|
+
fixes,
|
|
15391
|
+
repositoryPath,
|
|
15392
|
+
scanStartTime,
|
|
15393
|
+
scanContext
|
|
15394
|
+
}) {
|
|
15395
|
+
const validFixes = [];
|
|
15396
|
+
const invalidFixes = [];
|
|
15397
|
+
for (const fix of fixes) {
|
|
15398
|
+
const targetFile = this.getFixTargetFile(fix);
|
|
15399
|
+
if (!targetFile) {
|
|
15400
|
+
validFixes.push(fix);
|
|
15401
|
+
continue;
|
|
15402
|
+
}
|
|
15403
|
+
try {
|
|
15404
|
+
const absolutePath = path13.resolve(repositoryPath, targetFile);
|
|
15405
|
+
if (existsSync2(absolutePath)) {
|
|
15406
|
+
const stats = await fs13.stat(absolutePath);
|
|
15407
|
+
const fileModTime = stats.mtime.getTime();
|
|
15408
|
+
if (fileModTime > scanStartTime) {
|
|
15409
|
+
logError(
|
|
15410
|
+
`[${scanContext}] Target file ${targetFile} was modified after scan started (file: ${new Date(fileModTime).toISOString()}, scan: ${new Date(scanStartTime).toISOString()})`
|
|
15411
|
+
);
|
|
15412
|
+
invalidFixes.push({
|
|
15413
|
+
fix,
|
|
15414
|
+
reason: `Target file ${targetFile} was modified after scan started (file: ${new Date(fileModTime).toISOString()}, scan: ${new Date(scanStartTime).toISOString()})`
|
|
15415
|
+
});
|
|
15416
|
+
} else {
|
|
15417
|
+
validFixes.push(fix);
|
|
15418
|
+
}
|
|
15419
|
+
} else {
|
|
15420
|
+
validFixes.push(fix);
|
|
15421
|
+
}
|
|
15422
|
+
} catch (error) {
|
|
15423
|
+
logError(
|
|
15424
|
+
`[${scanContext}] Error checking modification time for ${targetFile}`,
|
|
15425
|
+
{
|
|
15426
|
+
error: error instanceof Error ? error.message : String(error)
|
|
15427
|
+
}
|
|
15428
|
+
);
|
|
15429
|
+
validFixes.push(fix);
|
|
15430
|
+
}
|
|
15431
|
+
}
|
|
15432
|
+
return { validFixes, invalidFixes };
|
|
15433
|
+
}
|
|
15434
|
+
/**
|
|
15435
|
+
* Applies multiple patches to the repository
|
|
15436
|
+
*/
|
|
15437
|
+
static async applyFixes({
|
|
15438
|
+
fixes,
|
|
15439
|
+
repositoryPath,
|
|
15440
|
+
scanStartTime,
|
|
15441
|
+
gqlClient,
|
|
15442
|
+
scanContext
|
|
15443
|
+
}) {
|
|
15444
|
+
const appliedFixes = [];
|
|
15445
|
+
const failedFixes = [];
|
|
15446
|
+
const skippedFixes = [];
|
|
15447
|
+
const resolvedRepoPath = await fs13.realpath(repositoryPath);
|
|
15448
|
+
logInfo(
|
|
15449
|
+
`[${scanContext}] Starting patch application for ${fixes.length} fixes`,
|
|
15450
|
+
{
|
|
15451
|
+
repositoryPath,
|
|
15452
|
+
resolvedRepoPath,
|
|
15453
|
+
fixIds: fixes.map((f) => f.id),
|
|
15454
|
+
scanStartTime: scanStartTime ? new Date(scanStartTime).toISOString() : "not provided"
|
|
15455
|
+
}
|
|
15456
|
+
);
|
|
15457
|
+
let processedFixes = fixes;
|
|
15458
|
+
if (scanStartTime) {
|
|
15459
|
+
const { validFixes, invalidFixes } = await this.checkFileModificationTimes({
|
|
15460
|
+
fixes,
|
|
15461
|
+
repositoryPath: resolvedRepoPath,
|
|
15462
|
+
scanStartTime,
|
|
15463
|
+
scanContext
|
|
15464
|
+
});
|
|
15465
|
+
this.processInvalidFixes(invalidFixes, failedFixes, scanContext);
|
|
15466
|
+
processedFixes = validFixes;
|
|
15467
|
+
}
|
|
15468
|
+
const { fileFixGroups, standaloneFixesFixes } = this.selectBestFixesPerFile(processedFixes);
|
|
15469
|
+
logInfo(
|
|
15470
|
+
`[${scanContext}] Grouped fixes: ${standaloneFixesFixes.length} standalone fixes, ${fileFixGroups.size} file groups`,
|
|
15471
|
+
{
|
|
15472
|
+
originalCount: fixes.length,
|
|
15473
|
+
totalFixesToApply: standaloneFixesFixes.length + fileFixGroups.size
|
|
15474
|
+
}
|
|
15475
|
+
);
|
|
15476
|
+
await this.applyStandaloneFixes({
|
|
15477
|
+
fixes: standaloneFixesFixes,
|
|
15478
|
+
repositoryPath: resolvedRepoPath,
|
|
15479
|
+
appliedFixes,
|
|
15480
|
+
failedFixes,
|
|
15481
|
+
scanContext
|
|
15482
|
+
});
|
|
15483
|
+
await this.applyFileGroupFixes({
|
|
15484
|
+
fileFixGroups,
|
|
15485
|
+
repositoryPath: resolvedRepoPath,
|
|
15486
|
+
appliedFixes,
|
|
15487
|
+
failedFixes,
|
|
15488
|
+
skippedFixes,
|
|
15489
|
+
scanContext
|
|
15490
|
+
});
|
|
15491
|
+
this.logSkippedFixes(skippedFixes, scanContext);
|
|
15492
|
+
logInfo(`[${scanContext}] Patch application completed`, {
|
|
15493
|
+
total: fixes.length,
|
|
15494
|
+
applied: appliedFixes.length,
|
|
15495
|
+
failed: failedFixes.length,
|
|
15496
|
+
skipped: skippedFixes.length
|
|
15497
|
+
});
|
|
15498
|
+
if (appliedFixes.length > 0 && gqlClient) {
|
|
15499
|
+
try {
|
|
15500
|
+
const appliedFixIds = appliedFixes.map((fix) => fix.id).filter(Boolean);
|
|
15501
|
+
await gqlClient.updateAutoAppliedFixesStatus(appliedFixIds);
|
|
15502
|
+
logDebug(
|
|
15503
|
+
`[${scanContext}] Successfully updated download status for auto-applied fixes`,
|
|
15504
|
+
{
|
|
15505
|
+
appliedFixIds,
|
|
15506
|
+
count: appliedFixIds.length
|
|
15507
|
+
}
|
|
15508
|
+
);
|
|
15509
|
+
} catch (error) {
|
|
15510
|
+
logError(
|
|
15511
|
+
`[${scanContext}] Failed to update download status for auto-applied fixes`,
|
|
15512
|
+
{
|
|
15513
|
+
error: error instanceof Error ? error.message : String(error),
|
|
15514
|
+
appliedFixCount: appliedFixes.length
|
|
15515
|
+
}
|
|
15516
|
+
);
|
|
15517
|
+
}
|
|
15518
|
+
}
|
|
15519
|
+
return {
|
|
15520
|
+
success: appliedFixes.length > 0,
|
|
15521
|
+
appliedFixes,
|
|
15522
|
+
failedFixes
|
|
15523
|
+
};
|
|
15524
|
+
}
|
|
15525
|
+
/**
|
|
15526
|
+
* Validates fix data and returns patch content
|
|
15527
|
+
*/
|
|
15528
|
+
static validateFixData(fix, scanContext) {
|
|
15529
|
+
if (fix.patchAndQuestions?.__typename !== "FixData") {
|
|
15530
|
+
const error = "Fix does not contain patch data";
|
|
15531
|
+
logError(`[${scanContext}] Fix ${fix.id} does not contain patch data`);
|
|
15532
|
+
return { success: false, error };
|
|
15533
|
+
}
|
|
15534
|
+
const patch = fix.patchAndQuestions.patch;
|
|
15535
|
+
if (!patch) {
|
|
15536
|
+
const error = "Fix has empty patch content";
|
|
15537
|
+
logError(`[${scanContext}] Fix ${fix.id} has empty patch content`);
|
|
15538
|
+
return { success: false, error };
|
|
15539
|
+
}
|
|
15540
|
+
return { success: true, patch };
|
|
15541
|
+
}
|
|
15542
|
+
/**
|
|
15543
|
+
* Logs patch application information with error handling
|
|
15544
|
+
*/
|
|
15545
|
+
static logPatchInfo(fix, patch, targetFiles, scanContext) {
|
|
15546
|
+
try {
|
|
15547
|
+
logInfo(`[${scanContext}] Applying patch for fix ${fix.id}`, {
|
|
15548
|
+
fixType: fix.safeIssueType,
|
|
15549
|
+
severity: fix.severityText,
|
|
15550
|
+
confidence: fix.confidence,
|
|
15551
|
+
targetFiles,
|
|
15552
|
+
patchLength: patch.length,
|
|
15553
|
+
patchPreview: patch.substring(0, 200) + (patch.length > 200 ? "..." : ""),
|
|
15554
|
+
description: fix.patchAndQuestions.__typename === "FixData" ? fix.patchAndQuestions.extraContext?.fixDescription : void 0
|
|
15555
|
+
});
|
|
15556
|
+
} catch (logError2) {
|
|
15557
|
+
logInfo(
|
|
15558
|
+
`[${scanContext}] Applying patch for fix ${fix.id} - fixType: ${fix.safeIssueType}`
|
|
15559
|
+
);
|
|
15560
|
+
}
|
|
15561
|
+
}
|
|
15562
|
+
/**
|
|
15563
|
+
* Parses a patch and validates it contains changes
|
|
15564
|
+
*/
|
|
15565
|
+
static parsePatchSafely({
|
|
15566
|
+
patch,
|
|
15567
|
+
fixId,
|
|
15568
|
+
scanContext
|
|
15569
|
+
}) {
|
|
15570
|
+
const parsedPatch = parseDiff2(patch);
|
|
15571
|
+
if (!parsedPatch || parsedPatch.length === 0) {
|
|
15572
|
+
throw new Error("Failed to parse patch - no changes found");
|
|
15573
|
+
}
|
|
15574
|
+
logDebug(`[${scanContext}] Parsed patch for fix ${fixId}`, {
|
|
15575
|
+
filesCount: parsedPatch.length,
|
|
15576
|
+
files: parsedPatch.map((f) => ({
|
|
15577
|
+
from: f.from,
|
|
15578
|
+
to: f.to,
|
|
15579
|
+
deletions: f.deletions,
|
|
15580
|
+
additions: f.additions
|
|
15581
|
+
}))
|
|
15582
|
+
});
|
|
15583
|
+
return parsedPatch;
|
|
15584
|
+
}
|
|
15585
|
+
/**
|
|
15586
|
+
* Resolves file paths for patch application
|
|
15587
|
+
*/
|
|
15588
|
+
static resolveFilePaths({
|
|
15589
|
+
targetFile,
|
|
15590
|
+
repositoryPath,
|
|
15591
|
+
scanContext
|
|
15592
|
+
}) {
|
|
15593
|
+
const sanitizedRepoPath = String(repositoryPath || "").replace("\0", "").replace(/^(\.\.(\/|\\))+/, "");
|
|
15594
|
+
const sanitizedTargetFile = String(targetFile || "").replace("\0", "").replace(/^(\.\.(\/|\\))+/, "");
|
|
15595
|
+
const absoluteFilePath = path13.resolve(
|
|
15596
|
+
sanitizedRepoPath,
|
|
15597
|
+
sanitizedTargetFile
|
|
15598
|
+
);
|
|
15599
|
+
const relativePath = path13.relative(sanitizedRepoPath, absoluteFilePath);
|
|
15600
|
+
if (relativePath.startsWith("..")) {
|
|
15601
|
+
throw new Error(
|
|
15602
|
+
`Security violation: target file ${targetFile} resolves outside repository`
|
|
15603
|
+
);
|
|
15604
|
+
}
|
|
15605
|
+
logDebug(`[${scanContext}] Resolving file path for ${targetFile}`, {
|
|
15606
|
+
repositoryPath: sanitizedRepoPath,
|
|
15607
|
+
targetFile: sanitizedTargetFile,
|
|
15608
|
+
absoluteFilePath,
|
|
15609
|
+
relativePath,
|
|
15610
|
+
exists: existsSync2(absoluteFilePath)
|
|
15611
|
+
});
|
|
15612
|
+
return { absoluteFilePath, relativePath };
|
|
15613
|
+
}
|
|
15614
|
+
/**
|
|
15615
|
+
* Handles file creation from patch
|
|
15616
|
+
*/
|
|
15617
|
+
static handleFileCreation({
|
|
15618
|
+
fileDiff,
|
|
15619
|
+
absoluteFilePath,
|
|
15620
|
+
relativePath,
|
|
15621
|
+
repositoryPath,
|
|
15622
|
+
fix,
|
|
15623
|
+
appliedFiles,
|
|
15624
|
+
scanContext
|
|
15625
|
+
}) {
|
|
15626
|
+
const newContent = this.applyHunksToEmptyFile(fileDiff.chunks);
|
|
15627
|
+
const actualPath = this.writeFileWithFixComment({
|
|
15628
|
+
filePath: absoluteFilePath,
|
|
15629
|
+
content: newContent,
|
|
15630
|
+
fix,
|
|
15631
|
+
scanContext
|
|
15632
|
+
});
|
|
15633
|
+
appliedFiles.push(path13.relative(repositoryPath, actualPath));
|
|
15634
|
+
logDebug(`[${scanContext}] Created new file: ${relativePath}`);
|
|
15635
|
+
}
|
|
15636
|
+
/**
|
|
15637
|
+
* Handles file deletion from patch
|
|
15638
|
+
*/
|
|
15639
|
+
static handleFileDeletion({
|
|
15640
|
+
absoluteFilePath,
|
|
15641
|
+
relativePath,
|
|
15642
|
+
appliedFiles,
|
|
15643
|
+
scanContext
|
|
15644
|
+
}) {
|
|
15645
|
+
if (existsSync2(absoluteFilePath)) {
|
|
15646
|
+
unlinkSync(absoluteFilePath);
|
|
15647
|
+
appliedFiles.push(relativePath);
|
|
15648
|
+
logDebug(`[${scanContext}] Deleted file: ${relativePath}`);
|
|
15649
|
+
}
|
|
15650
|
+
}
|
|
15651
|
+
/**
|
|
15652
|
+
* Handles file modification from patch
|
|
15653
|
+
*/
|
|
15654
|
+
static handleFileModification({
|
|
15655
|
+
fileDiff,
|
|
15656
|
+
absoluteFilePath,
|
|
15657
|
+
relativePath,
|
|
15658
|
+
targetFile,
|
|
15659
|
+
repositoryPath,
|
|
15660
|
+
fix,
|
|
15661
|
+
appliedFiles,
|
|
15662
|
+
scanContext
|
|
15663
|
+
}) {
|
|
15664
|
+
if (!existsSync2(absoluteFilePath)) {
|
|
15665
|
+
throw new Error(
|
|
15666
|
+
`Target file does not exist: ${targetFile} (resolved to: ${absoluteFilePath})`
|
|
15667
|
+
);
|
|
15668
|
+
}
|
|
15669
|
+
const originalContent = readFileSync(absoluteFilePath, "utf8");
|
|
15670
|
+
const modifiedContent = this.applyHunksToFile(
|
|
15671
|
+
originalContent,
|
|
15672
|
+
fileDiff.chunks
|
|
15673
|
+
);
|
|
15674
|
+
if (modifiedContent !== originalContent) {
|
|
15675
|
+
const actualPath = this.writeFileWithFixComment({
|
|
15676
|
+
filePath: absoluteFilePath,
|
|
15677
|
+
content: modifiedContent,
|
|
15678
|
+
fix,
|
|
15679
|
+
scanContext
|
|
15680
|
+
});
|
|
15681
|
+
appliedFiles.push(path13.relative(repositoryPath, actualPath));
|
|
15682
|
+
logDebug(`[${scanContext}] Modified file: ${relativePath}`);
|
|
15683
|
+
}
|
|
15684
|
+
}
|
|
15685
|
+
/**
|
|
15686
|
+
* Processes a single file diff from a parsed patch
|
|
15687
|
+
*/
|
|
15688
|
+
static processFileDiff({
|
|
15689
|
+
fileDiff,
|
|
15690
|
+
repositoryPath,
|
|
15691
|
+
fix,
|
|
15692
|
+
appliedFiles,
|
|
15693
|
+
scanContext
|
|
15694
|
+
}) {
|
|
15695
|
+
const targetFile = fileDiff.to || fileDiff.from;
|
|
15696
|
+
if (!targetFile || targetFile === "/dev/null") {
|
|
15697
|
+
return;
|
|
15698
|
+
}
|
|
15699
|
+
const { absoluteFilePath, relativePath } = this.resolveFilePaths({
|
|
15700
|
+
targetFile,
|
|
15701
|
+
repositoryPath,
|
|
15702
|
+
scanContext
|
|
15703
|
+
});
|
|
15704
|
+
if (fileDiff.from === "/dev/null") {
|
|
15705
|
+
this.handleFileCreation({
|
|
15706
|
+
fileDiff,
|
|
15707
|
+
absoluteFilePath,
|
|
15708
|
+
relativePath,
|
|
15709
|
+
repositoryPath,
|
|
15710
|
+
fix,
|
|
15711
|
+
appliedFiles,
|
|
15712
|
+
scanContext
|
|
15713
|
+
});
|
|
15714
|
+
} else if (fileDiff.to === "/dev/null") {
|
|
15715
|
+
this.handleFileDeletion({
|
|
15716
|
+
absoluteFilePath,
|
|
15717
|
+
relativePath,
|
|
15718
|
+
appliedFiles,
|
|
15719
|
+
scanContext
|
|
15720
|
+
});
|
|
15721
|
+
} else {
|
|
15722
|
+
this.handleFileModification({
|
|
15723
|
+
fileDiff,
|
|
15724
|
+
absoluteFilePath,
|
|
15725
|
+
relativePath,
|
|
15726
|
+
targetFile,
|
|
15727
|
+
repositoryPath,
|
|
15728
|
+
fix,
|
|
15729
|
+
appliedFiles,
|
|
15730
|
+
scanContext
|
|
15731
|
+
});
|
|
15732
|
+
}
|
|
15733
|
+
}
|
|
15734
|
+
/**
|
|
15735
|
+
* Applies a single patch using git apply
|
|
15736
|
+
*/
|
|
15737
|
+
static async applySingleFix({
|
|
15738
|
+
fix,
|
|
15739
|
+
repositoryPath,
|
|
15740
|
+
scanContext
|
|
15741
|
+
}) {
|
|
15742
|
+
const validation = this.validateFixData(fix, scanContext);
|
|
15743
|
+
if (!validation.success) {
|
|
15744
|
+
return { success: false, error: validation.error };
|
|
15745
|
+
}
|
|
15746
|
+
const patch = validation.patch;
|
|
15747
|
+
let targetFiles = [];
|
|
15748
|
+
try {
|
|
15749
|
+
targetFiles = this.extractTargetFilesFromPatch(patch);
|
|
15750
|
+
} catch (extractError) {
|
|
15751
|
+
logError(
|
|
15752
|
+
`[${scanContext}] Failed to extract target files from patch for fix ${fix.id}`,
|
|
15753
|
+
{
|
|
15754
|
+
error: extractError instanceof Error ? extractError.message : String(extractError)
|
|
15755
|
+
}
|
|
15756
|
+
);
|
|
15757
|
+
}
|
|
15758
|
+
this.logPatchInfo(fix, patch, targetFiles, scanContext);
|
|
15759
|
+
try {
|
|
15760
|
+
logDebug(`[${scanContext}] Applying patch in memory for fix ${fix.id}`, {
|
|
15761
|
+
repositoryPath,
|
|
15762
|
+
targetFiles
|
|
15763
|
+
});
|
|
15764
|
+
const parsedPatch = this.parsePatchSafely({
|
|
15765
|
+
patch,
|
|
15766
|
+
fixId: fix.id,
|
|
15767
|
+
scanContext
|
|
15768
|
+
});
|
|
15769
|
+
const appliedFiles = [];
|
|
15770
|
+
for (const fileDiff of parsedPatch) {
|
|
15771
|
+
this.processFileDiff({
|
|
15772
|
+
fileDiff,
|
|
15773
|
+
repositoryPath,
|
|
15774
|
+
fix,
|
|
15775
|
+
appliedFiles,
|
|
15776
|
+
scanContext
|
|
15777
|
+
});
|
|
15778
|
+
}
|
|
15779
|
+
logInfo(
|
|
15780
|
+
`[${scanContext}] In-memory patch application successful for fix ${fix.id}`,
|
|
15781
|
+
{
|
|
15782
|
+
appliedFiles,
|
|
15783
|
+
filesCount: appliedFiles.length
|
|
15784
|
+
}
|
|
15785
|
+
);
|
|
15786
|
+
return { success: true };
|
|
15787
|
+
} catch (patchError) {
|
|
15788
|
+
const errorMessage = patchError instanceof Error ? patchError.message : String(patchError);
|
|
15789
|
+
logError(
|
|
15790
|
+
`[${scanContext}] In-memory patch application failed for fix ${fix.id}`,
|
|
15791
|
+
{
|
|
15792
|
+
errorMessage,
|
|
15793
|
+
patchErrorType: patchError instanceof Error ? patchError.name : typeof patchError,
|
|
15794
|
+
targetFiles
|
|
15795
|
+
}
|
|
15796
|
+
);
|
|
15797
|
+
return {
|
|
15798
|
+
success: false,
|
|
15799
|
+
error: `Patch application failed: ${errorMessage}`
|
|
15800
|
+
};
|
|
15801
|
+
}
|
|
15802
|
+
}
|
|
15803
|
+
/**
|
|
15804
|
+
* Extracts the file path from a git patch
|
|
15805
|
+
*/
|
|
15806
|
+
static extractPathFromPatch(patch) {
|
|
15807
|
+
if (!patch) return null;
|
|
15808
|
+
const match = patch.match(/^diff --git a\/([^\s]+) b\//);
|
|
15809
|
+
return match?.[1] ?? null;
|
|
15810
|
+
}
|
|
15811
|
+
/**
|
|
15812
|
+
* Extracts target file paths from a git patch
|
|
15813
|
+
*/
|
|
15814
|
+
static extractTargetFilesFromPatch(patch) {
|
|
15815
|
+
const targetFiles = [];
|
|
15816
|
+
const lines = patch.split("\n");
|
|
15817
|
+
for (const line of lines) {
|
|
15818
|
+
if (line.startsWith("+++ b/")) {
|
|
15819
|
+
const filePath = line.substring(6);
|
|
15820
|
+
if (filePath && !targetFiles.includes(filePath)) {
|
|
15821
|
+
targetFiles.push(filePath);
|
|
15822
|
+
}
|
|
15823
|
+
} else if (line.startsWith("diff --git a/") && line.includes(" b/")) {
|
|
15824
|
+
const match = line.match(/diff --git a\/.+ b\/(.+)/);
|
|
15825
|
+
if (match?.[1] && !targetFiles.includes(match[1])) {
|
|
15826
|
+
targetFiles.push(match[1]);
|
|
15827
|
+
}
|
|
15828
|
+
}
|
|
15829
|
+
}
|
|
15830
|
+
return targetFiles;
|
|
15831
|
+
}
|
|
15832
|
+
/**
|
|
15833
|
+
* Apply hunks to an empty file (for file creation)
|
|
15834
|
+
*/
|
|
15835
|
+
static applyHunksToEmptyFile(chunks) {
|
|
15836
|
+
const lines = [];
|
|
15837
|
+
for (const chunk of chunks) {
|
|
15838
|
+
for (const change of chunk.changes) {
|
|
15839
|
+
if (change.type === "add") {
|
|
15840
|
+
lines.push(change.content.substring(1));
|
|
15841
|
+
}
|
|
15842
|
+
}
|
|
15843
|
+
}
|
|
15844
|
+
return lines.join("\n");
|
|
15845
|
+
}
|
|
15846
|
+
/**
|
|
15847
|
+
* Apply hunks to an existing file content
|
|
15848
|
+
*/
|
|
15849
|
+
static applyHunksToFile(originalContent, chunks) {
|
|
15850
|
+
const originalLines = originalContent.split("\n");
|
|
15851
|
+
const result = [...originalLines];
|
|
15852
|
+
const sortedChunks = chunks.sort((a, b) => b.oldStart - a.oldStart);
|
|
15853
|
+
for (const chunk of sortedChunks) {
|
|
15854
|
+
const { oldStart, oldLines } = chunk;
|
|
15855
|
+
const startIndex = oldStart - 1;
|
|
15856
|
+
const newLines = [];
|
|
15857
|
+
for (const change of chunk.changes) {
|
|
15858
|
+
if (change.type === "add") {
|
|
15859
|
+
newLines.push(change.content.substring(1));
|
|
15860
|
+
} else if (change.type === "normal") {
|
|
15861
|
+
newLines.push(change.content.substring(1));
|
|
15862
|
+
}
|
|
15863
|
+
}
|
|
15864
|
+
result.splice(startIndex, oldLines, ...newLines);
|
|
15865
|
+
}
|
|
15866
|
+
return result.join("\n");
|
|
15867
|
+
}
|
|
15868
|
+
};
|
|
15869
|
+
|
|
15870
|
+
// src/mcp/services/ScanFiles.ts
|
|
15871
|
+
init_GitService();
|
|
15872
|
+
init_configs();
|
|
15873
|
+
|
|
15874
|
+
// src/mcp/services/FileOperations.ts
|
|
15875
|
+
init_FileUtils();
|
|
15876
|
+
import fs14 from "fs";
|
|
15877
|
+
import path14 from "path";
|
|
15878
|
+
import AdmZip2 from "adm-zip";
|
|
15879
|
+
var FileOperations = class {
|
|
15880
|
+
/**
|
|
15881
|
+
* Creates a ZIP archive containing the specified source files
|
|
15882
|
+
* @param fileList Array of relative file paths to include
|
|
15883
|
+
* @param repositoryPath Base path for resolving relative file paths
|
|
15884
|
+
* @param maxFileSize Maximum size allowed for individual files
|
|
15885
|
+
* @returns ZIP archive as a Buffer with metadata
|
|
15886
|
+
*/
|
|
15887
|
+
async createSourceCodeArchive({
|
|
15888
|
+
fileList,
|
|
15889
|
+
repositoryPath,
|
|
15890
|
+
maxFileSize
|
|
15891
|
+
}) {
|
|
15892
|
+
logDebug("[FileOperations] Packing files");
|
|
15893
|
+
const zip = new AdmZip2();
|
|
15894
|
+
let packedFilesCount = 0;
|
|
15895
|
+
const packedFiles = [];
|
|
15896
|
+
const excludedFiles = [];
|
|
15897
|
+
const resolvedRepoPath = path14.resolve(repositoryPath);
|
|
15898
|
+
for (const filepath of fileList) {
|
|
15899
|
+
const absoluteFilepath = path14.join(repositoryPath, filepath);
|
|
15900
|
+
const resolvedFilePath = path14.resolve(absoluteFilepath);
|
|
15901
|
+
if (!resolvedFilePath.startsWith(resolvedRepoPath)) {
|
|
15902
|
+
const reason = "potential path traversal security risk";
|
|
15903
|
+
logDebug(`[FileOperations] Skipping ${filepath} due to ${reason}`);
|
|
15904
|
+
excludedFiles.push({ file: filepath, reason });
|
|
15905
|
+
continue;
|
|
15906
|
+
}
|
|
15907
|
+
if (!FileUtils.shouldPackFile(absoluteFilepath, maxFileSize)) {
|
|
15908
|
+
const reason = "file is too large, binary, or matches exclusion rules";
|
|
15909
|
+
logDebug(`[FileOperations] Excluding ${filepath} - ${reason}`);
|
|
15910
|
+
excludedFiles.push({ file: filepath, reason });
|
|
15911
|
+
continue;
|
|
15912
|
+
}
|
|
15913
|
+
const fileContent = await this.readSourceFile({
|
|
15914
|
+
absoluteFilepath,
|
|
15915
|
+
relativeFilepath: filepath
|
|
15916
|
+
});
|
|
15917
|
+
if (fileContent) {
|
|
15918
|
+
zip.addFile(filepath, fileContent);
|
|
15919
|
+
packedFilesCount++;
|
|
15920
|
+
packedFiles.push(filepath);
|
|
15921
|
+
} else {
|
|
15922
|
+
const reason = "failed to read file content";
|
|
15923
|
+
excludedFiles.push({ file: filepath, reason });
|
|
13678
15924
|
}
|
|
13679
15925
|
}
|
|
13680
15926
|
const archiveBuffer = zip.toBuffer();
|
|
@@ -13694,12 +15940,15 @@ var FileOperations = class {
|
|
|
13694
15940
|
* @param repositoryPath Base path for validation
|
|
13695
15941
|
* @returns Array of validated file paths
|
|
13696
15942
|
*/
|
|
13697
|
-
async validateFilePaths(
|
|
13698
|
-
|
|
15943
|
+
async validateFilePaths({
|
|
15944
|
+
fileList,
|
|
15945
|
+
repositoryPath
|
|
15946
|
+
}) {
|
|
15947
|
+
const resolvedRepoPath = path14.resolve(repositoryPath);
|
|
13699
15948
|
const validatedPaths = [];
|
|
13700
15949
|
for (const filepath of fileList) {
|
|
13701
|
-
const absoluteFilepath =
|
|
13702
|
-
const resolvedFilePath =
|
|
15950
|
+
const absoluteFilepath = path14.join(repositoryPath, filepath);
|
|
15951
|
+
const resolvedFilePath = path14.resolve(absoluteFilepath);
|
|
13703
15952
|
if (!resolvedFilePath.startsWith(resolvedRepoPath)) {
|
|
13704
15953
|
logDebug(
|
|
13705
15954
|
`[FileOperations] Rejecting ${filepath} - path traversal attempt detected`
|
|
@@ -13707,7 +15956,7 @@ var FileOperations = class {
|
|
|
13707
15956
|
continue;
|
|
13708
15957
|
}
|
|
13709
15958
|
try {
|
|
13710
|
-
await
|
|
15959
|
+
await fs14.promises.access(absoluteFilepath, fs14.constants.R_OK);
|
|
13711
15960
|
validatedPaths.push(filepath);
|
|
13712
15961
|
} catch (error) {
|
|
13713
15962
|
logDebug(
|
|
@@ -13726,8 +15975,8 @@ var FileOperations = class {
|
|
|
13726
15975
|
const fileDataArray = [];
|
|
13727
15976
|
for (const absolutePath of filePaths) {
|
|
13728
15977
|
try {
|
|
13729
|
-
const content = await
|
|
13730
|
-
const relativePath =
|
|
15978
|
+
const content = await fs14.promises.readFile(absolutePath);
|
|
15979
|
+
const relativePath = path14.basename(absolutePath);
|
|
13731
15980
|
fileDataArray.push({
|
|
13732
15981
|
relativePath,
|
|
13733
15982
|
absolutePath,
|
|
@@ -13747,9 +15996,12 @@ var FileOperations = class {
|
|
|
13747
15996
|
* @param relativeFilepath Relative path for logging purposes
|
|
13748
15997
|
* @returns File content as Buffer or null if failed
|
|
13749
15998
|
*/
|
|
13750
|
-
async readSourceFile(
|
|
15999
|
+
async readSourceFile({
|
|
16000
|
+
absoluteFilepath,
|
|
16001
|
+
relativeFilepath
|
|
16002
|
+
}) {
|
|
13751
16003
|
try {
|
|
13752
|
-
return await
|
|
16004
|
+
return await fs14.promises.readFile(absoluteFilepath);
|
|
13753
16005
|
} catch (fsError) {
|
|
13754
16006
|
logError(
|
|
13755
16007
|
`[FileOperations] Failed to read ${relativeFilepath} from filesystem: ${fsError}`
|
|
@@ -13770,19 +16022,19 @@ var scanFiles = async ({
|
|
|
13770
16022
|
const repoUploadInfo = await initializeSecurityReport(gqlClient, scanContext);
|
|
13771
16023
|
const fixReportId = repoUploadInfo.fixReportId;
|
|
13772
16024
|
const fileOperations = new FileOperations();
|
|
13773
|
-
const packingResult = await fileOperations.createSourceCodeArchive(
|
|
16025
|
+
const packingResult = await fileOperations.createSourceCodeArchive({
|
|
13774
16026
|
fileList,
|
|
13775
16027
|
repositoryPath,
|
|
13776
|
-
MCP_MAX_FILE_SIZE
|
|
13777
|
-
);
|
|
16028
|
+
maxFileSize: MCP_MAX_FILE_SIZE
|
|
16029
|
+
});
|
|
13778
16030
|
logDebug(
|
|
13779
16031
|
`[${scanContext}] Files ${packingResult.packedFilesCount} packed successfully, ${packingResult.totalSize} bytes`
|
|
13780
16032
|
);
|
|
13781
|
-
await uploadSourceCodeArchive(
|
|
13782
|
-
packingResult.archive,
|
|
16033
|
+
await uploadSourceCodeArchive({
|
|
16034
|
+
archiveBuffer: packingResult.archive,
|
|
13783
16035
|
repoUploadInfo,
|
|
13784
16036
|
scanContext
|
|
13785
|
-
);
|
|
16037
|
+
});
|
|
13786
16038
|
const projectId = await getProjectId(gqlClient, scanContext);
|
|
13787
16039
|
const gitService = new GitService(repositoryPath);
|
|
13788
16040
|
const { branch } = await gitService.getCurrentCommitAndBranch();
|
|
@@ -13832,7 +16084,11 @@ var initializeSecurityReport = async (gqlClient, scanContext) => {
|
|
|
13832
16084
|
);
|
|
13833
16085
|
}
|
|
13834
16086
|
};
|
|
13835
|
-
var uploadSourceCodeArchive = async (
|
|
16087
|
+
var uploadSourceCodeArchive = async ({
|
|
16088
|
+
archiveBuffer,
|
|
16089
|
+
repoUploadInfo,
|
|
16090
|
+
scanContext
|
|
16091
|
+
}) => {
|
|
13836
16092
|
if (!repoUploadInfo) {
|
|
13837
16093
|
throw new FileUploadError("Upload info is required for source code archive");
|
|
13838
16094
|
}
|
|
@@ -13875,6 +16131,7 @@ var executeSecurityScan = async ({
|
|
|
13875
16131
|
if (!gqlClient) {
|
|
13876
16132
|
throw new GqlClientError();
|
|
13877
16133
|
}
|
|
16134
|
+
const scanStartTime = Date.now();
|
|
13878
16135
|
logInfo(`[${scanContext}] Starting scan`);
|
|
13879
16136
|
const submitVulnerabilityReportVariables = {
|
|
13880
16137
|
fixReportId,
|
|
@@ -13973,14 +16230,17 @@ var executeSecurityScan = async ({
|
|
|
13973
16230
|
`Security analysis failed: ${error.message || "Unknown error"}`
|
|
13974
16231
|
);
|
|
13975
16232
|
}
|
|
16233
|
+
const scanDuration = Date.now() - scanStartTime;
|
|
13976
16234
|
logDebug(`[${scanContext}] Security scan completed successfully`, {
|
|
13977
16235
|
fixReportId,
|
|
13978
|
-
projectId
|
|
16236
|
+
projectId,
|
|
16237
|
+
filesScanned: fileCount,
|
|
16238
|
+
scanDurationMs: scanDuration
|
|
13979
16239
|
});
|
|
13980
16240
|
};
|
|
13981
16241
|
|
|
13982
16242
|
// src/mcp/tools/checkForNewAvailableFixes/CheckForNewAvailableFixesService.ts
|
|
13983
|
-
function
|
|
16243
|
+
function extractPathFromPatch2(patch) {
|
|
13984
16244
|
const match = patch?.match(/^diff --git a\/([^\s]+) b\//);
|
|
13985
16245
|
return match?.[1] ?? null;
|
|
13986
16246
|
}
|
|
@@ -13997,6 +16257,7 @@ var _CheckForNewAvailableFixesService = class _CheckForNewAvailableFixesService
|
|
|
13997
16257
|
__publicField(this, "reportedFixes", []);
|
|
13998
16258
|
__publicField(this, "intervalId", null);
|
|
13999
16259
|
__publicField(this, "isInitialScanComplete", false);
|
|
16260
|
+
__publicField(this, "hasAuthenticationFailed", false);
|
|
14000
16261
|
__publicField(this, "gqlClient", null);
|
|
14001
16262
|
__publicField(this, "fullScanPathsScanned", []);
|
|
14002
16263
|
this.fullScanPathsScanned = configStore.get("fullScanPathsScanned") || [];
|
|
@@ -14015,6 +16276,7 @@ var _CheckForNewAvailableFixesService = class _CheckForNewAvailableFixesService
|
|
|
14015
16276
|
this.filesLastScanned = {};
|
|
14016
16277
|
this.freshFixes = [];
|
|
14017
16278
|
this.reportedFixes = [];
|
|
16279
|
+
this.hasAuthenticationFailed = false;
|
|
14018
16280
|
this.fullScanPathsScanned = configStore.get("fullScanPathsScanned") || [];
|
|
14019
16281
|
if (this.intervalId) {
|
|
14020
16282
|
clearInterval(this.intervalId);
|
|
@@ -14026,33 +16288,40 @@ var _CheckForNewAvailableFixesService = class _CheckForNewAvailableFixesService
|
|
|
14026
16288
|
* since the last scan.
|
|
14027
16289
|
*/
|
|
14028
16290
|
async scanForSecurityVulnerabilities({
|
|
14029
|
-
path:
|
|
16291
|
+
path: path15,
|
|
14030
16292
|
isAllDetectionRulesScan,
|
|
14031
16293
|
isAllFilesScan,
|
|
14032
16294
|
scanContext
|
|
14033
16295
|
}) {
|
|
16296
|
+
this.hasAuthenticationFailed = false;
|
|
14034
16297
|
logDebug(`[${scanContext}] Scanning for new security vulnerabilities`, {
|
|
14035
|
-
path:
|
|
16298
|
+
path: path15
|
|
14036
16299
|
});
|
|
14037
16300
|
if (!this.gqlClient) {
|
|
14038
16301
|
logInfo(`[${scanContext}] No GQL client found, skipping scan`);
|
|
16302
|
+
this.hasAuthenticationFailed = true;
|
|
14039
16303
|
throw new Error("No GQL client found");
|
|
14040
16304
|
}
|
|
14041
16305
|
try {
|
|
14042
16306
|
const isConnected = await this.gqlClient.verifyApiConnection();
|
|
14043
16307
|
if (!isConnected) {
|
|
14044
16308
|
logError(`[${scanContext}] Failed to connect to the API, scan aborted`);
|
|
16309
|
+
this.hasAuthenticationFailed = true;
|
|
14045
16310
|
throw new ApiConnectionError();
|
|
14046
16311
|
}
|
|
14047
16312
|
logDebug(
|
|
14048
16313
|
`[${scanContext}] Connected to the API, assembling list of files to scan`,
|
|
14049
|
-
{ path:
|
|
16314
|
+
{ path: path15 }
|
|
14050
16315
|
);
|
|
14051
16316
|
const files = await getLocalFiles({
|
|
14052
|
-
path:
|
|
16317
|
+
path: path15,
|
|
14053
16318
|
isAllFilesScan,
|
|
14054
16319
|
scanContext
|
|
14055
16320
|
});
|
|
16321
|
+
const scanStartTime = Date.now();
|
|
16322
|
+
logDebug(
|
|
16323
|
+
`[${scanContext}] setting scan start time to ${new Date(scanStartTime).toISOString()}`
|
|
16324
|
+
);
|
|
14056
16325
|
logDebug(`[${scanContext}] Active files`, { files });
|
|
14057
16326
|
const filesToScan = files.filter((file) => {
|
|
14058
16327
|
const lastScannedEditTime = this.filesLastScanned[file.fullPath];
|
|
@@ -14070,13 +16339,13 @@ var _CheckForNewAvailableFixesService = class _CheckForNewAvailableFixesService
|
|
|
14070
16339
|
});
|
|
14071
16340
|
const { fixReportId, projectId } = await scanFiles({
|
|
14072
16341
|
fileList: filesToScan.map((file) => file.relativePath),
|
|
14073
|
-
repositoryPath:
|
|
16342
|
+
repositoryPath: path15,
|
|
14074
16343
|
gqlClient: this.gqlClient,
|
|
14075
16344
|
isAllDetectionRulesScan,
|
|
14076
16345
|
scanContext
|
|
14077
16346
|
});
|
|
14078
16347
|
logInfo(
|
|
14079
|
-
`[${scanContext}] Security scan completed for ${
|
|
16348
|
+
`[${scanContext}] Security scan completed for ${path15} reportId: ${fixReportId} projectId: ${projectId}`
|
|
14080
16349
|
);
|
|
14081
16350
|
if (isAllFilesScan) {
|
|
14082
16351
|
return;
|
|
@@ -14092,7 +16361,43 @@ var _CheckForNewAvailableFixesService = class _CheckForNewAvailableFixesService
|
|
|
14092
16361
|
logInfo(
|
|
14093
16362
|
`[${scanContext}] Security fixes retrieved, total: ${fixes?.fixes?.length || 0}, new: ${newFixes?.length || 0}`
|
|
14094
16363
|
);
|
|
14095
|
-
|
|
16364
|
+
if (newFixes && newFixes.length > 0) {
|
|
16365
|
+
try {
|
|
16366
|
+
const isMvsAutoFixEnabled = await this.gqlClient.getMvsAutoFixSettings();
|
|
16367
|
+
logDebug(
|
|
16368
|
+
`[${scanContext}] mvs_auto_fix setting: ${isMvsAutoFixEnabled}`
|
|
16369
|
+
);
|
|
16370
|
+
if (isMvsAutoFixEnabled) {
|
|
16371
|
+
const mvsAutoFixFilteredFixes = this.filterFixesForMvsAutoFix(
|
|
16372
|
+
newFixes,
|
|
16373
|
+
scanContext
|
|
16374
|
+
);
|
|
16375
|
+
await this.applyFixes({
|
|
16376
|
+
newFixes: mvsAutoFixFilteredFixes,
|
|
16377
|
+
scanContext,
|
|
16378
|
+
scanStartTime
|
|
16379
|
+
});
|
|
16380
|
+
} else {
|
|
16381
|
+
this.updateAvailableFixes({
|
|
16382
|
+
newFixes,
|
|
16383
|
+
scannedFiles: filesToScan,
|
|
16384
|
+
scanContext
|
|
16385
|
+
});
|
|
16386
|
+
}
|
|
16387
|
+
} catch (error) {
|
|
16388
|
+
logError(
|
|
16389
|
+
`[${scanContext}] Failed to check mvs_auto_fix setting, falling back to cache`,
|
|
16390
|
+
{
|
|
16391
|
+
error: error instanceof Error ? error.message : String(error)
|
|
16392
|
+
}
|
|
16393
|
+
);
|
|
16394
|
+
this.updateAvailableFixes({
|
|
16395
|
+
newFixes,
|
|
16396
|
+
scannedFiles: filesToScan,
|
|
16397
|
+
scanContext
|
|
16398
|
+
});
|
|
16399
|
+
}
|
|
16400
|
+
}
|
|
14096
16401
|
this.updateFilesScanTimestamps(filesToScan);
|
|
14097
16402
|
this.isInitialScanComplete = true;
|
|
14098
16403
|
} catch (error) {
|
|
@@ -14104,6 +16409,7 @@ var _CheckForNewAvailableFixesService = class _CheckForNewAvailableFixesService
|
|
|
14104
16409
|
error: errorMessage
|
|
14105
16410
|
}
|
|
14106
16411
|
);
|
|
16412
|
+
this.hasAuthenticationFailed = true;
|
|
14107
16413
|
return;
|
|
14108
16414
|
}
|
|
14109
16415
|
if (errorMessage.includes("ReportInitializationError")) {
|
|
@@ -14122,8 +16428,134 @@ var _CheckForNewAvailableFixesService = class _CheckForNewAvailableFixesService
|
|
|
14122
16428
|
throw error;
|
|
14123
16429
|
}
|
|
14124
16430
|
}
|
|
14125
|
-
|
|
14126
|
-
|
|
16431
|
+
/**
|
|
16432
|
+
* Applies fixes directly to the repository when mvs_auto_fix is enabled
|
|
16433
|
+
*/
|
|
16434
|
+
async applyFixes({
|
|
16435
|
+
newFixes,
|
|
16436
|
+
scanContext,
|
|
16437
|
+
scanStartTime
|
|
16438
|
+
}) {
|
|
16439
|
+
if (newFixes.length === 0) {
|
|
16440
|
+
logInfo(`[${scanContext}] No new fixes to apply (all already reported)`);
|
|
16441
|
+
return;
|
|
16442
|
+
}
|
|
16443
|
+
logInfo(`[${scanContext}] Auto-applying ${newFixes.length} new fixes`);
|
|
16444
|
+
const repositoryPath = this.path;
|
|
16445
|
+
logDebug(`[${scanContext}] Repository path for patch application`, {
|
|
16446
|
+
repositoryPath
|
|
16447
|
+
});
|
|
16448
|
+
const applicationResult = await PatchApplicationService.applyFixes({
|
|
16449
|
+
fixes: newFixes,
|
|
16450
|
+
repositoryPath,
|
|
16451
|
+
scanStartTime,
|
|
16452
|
+
gqlClient: this.gqlClient || void 0,
|
|
16453
|
+
scanContext
|
|
16454
|
+
});
|
|
16455
|
+
if (applicationResult.appliedFixes.length > 0) {
|
|
16456
|
+
logInfo(
|
|
16457
|
+
`[${scanContext}] Successfully auto-applied ${applicationResult.appliedFixes.length} fixes`
|
|
16458
|
+
);
|
|
16459
|
+
this.reportedFixes.push(...applicationResult.appliedFixes);
|
|
16460
|
+
await this.saveAndLogAppliedFixes({
|
|
16461
|
+
appliedFixes: applicationResult.appliedFixes,
|
|
16462
|
+
repositoryPath,
|
|
16463
|
+
scanContext
|
|
16464
|
+
});
|
|
16465
|
+
}
|
|
16466
|
+
if (applicationResult.failedFixes.length > 0) {
|
|
16467
|
+
logError(
|
|
16468
|
+
`[${scanContext}] Failed to auto-apply ${applicationResult.failedFixes.length} fixes`,
|
|
16469
|
+
{
|
|
16470
|
+
failedFixes: applicationResult.failedFixes.map((f) => ({
|
|
16471
|
+
fixId: f.fix.id,
|
|
16472
|
+
error: f.error
|
|
16473
|
+
}))
|
|
16474
|
+
}
|
|
16475
|
+
);
|
|
16476
|
+
}
|
|
16477
|
+
}
|
|
16478
|
+
/**
|
|
16479
|
+
* Saves patch files and logs fix details for successfully applied fixes
|
|
16480
|
+
*/
|
|
16481
|
+
async saveAndLogAppliedFixes({
|
|
16482
|
+
appliedFixes,
|
|
16483
|
+
repositoryPath,
|
|
16484
|
+
scanContext
|
|
16485
|
+
}) {
|
|
16486
|
+
try {
|
|
16487
|
+
const localMobbService = new LocalMobbFolderService(repositoryPath);
|
|
16488
|
+
logDebug(
|
|
16489
|
+
`[${scanContext}] Saving and logging ${appliedFixes.length} applied fixes`,
|
|
16490
|
+
{
|
|
16491
|
+
repositoryPath,
|
|
16492
|
+
fixIds: appliedFixes.map((f) => f.id)
|
|
16493
|
+
}
|
|
16494
|
+
);
|
|
16495
|
+
for (const fix of appliedFixes) {
|
|
16496
|
+
try {
|
|
16497
|
+
const saveResult = await localMobbService.savePatch(fix);
|
|
16498
|
+
if (saveResult.success) {
|
|
16499
|
+
logDebug(
|
|
16500
|
+
`[${scanContext}] Patch saved successfully for fix ${fix.id}`,
|
|
16501
|
+
{
|
|
16502
|
+
filePath: saveResult.filePath,
|
|
16503
|
+
fileName: saveResult.fileName
|
|
16504
|
+
}
|
|
16505
|
+
);
|
|
16506
|
+
} else {
|
|
16507
|
+
logDebug(
|
|
16508
|
+
`[${scanContext}] Failed to save patch for fix ${fix.id}`,
|
|
16509
|
+
{
|
|
16510
|
+
error: saveResult.error
|
|
16511
|
+
}
|
|
16512
|
+
);
|
|
16513
|
+
}
|
|
16514
|
+
const logResult = await localMobbService.logPatch(
|
|
16515
|
+
fix,
|
|
16516
|
+
saveResult.fileName
|
|
16517
|
+
);
|
|
16518
|
+
if (logResult.success) {
|
|
16519
|
+
logDebug(
|
|
16520
|
+
`[${scanContext}] Fix details logged successfully for fix ${fix.id}`,
|
|
16521
|
+
{
|
|
16522
|
+
filePath: logResult.filePath
|
|
16523
|
+
}
|
|
16524
|
+
);
|
|
16525
|
+
} else {
|
|
16526
|
+
logError(
|
|
16527
|
+
`[${scanContext}] Failed to log fix details for fix ${fix.id}`,
|
|
16528
|
+
{
|
|
16529
|
+
error: logResult.error
|
|
16530
|
+
}
|
|
16531
|
+
);
|
|
16532
|
+
}
|
|
16533
|
+
} catch (error) {
|
|
16534
|
+
logError(`[${scanContext}] Error processing applied fix ${fix.id}`, {
|
|
16535
|
+
error: String(error),
|
|
16536
|
+
fix
|
|
16537
|
+
});
|
|
16538
|
+
}
|
|
16539
|
+
}
|
|
16540
|
+
logInfo(`[${scanContext}] Finished saving and logging applied fixes`, {
|
|
16541
|
+
processedCount: appliedFixes.length
|
|
16542
|
+
});
|
|
16543
|
+
} catch (error) {
|
|
16544
|
+
logError(`[${scanContext}] Error in saveAndLogAppliedFixes`, {
|
|
16545
|
+
error: String(error),
|
|
16546
|
+
repositoryPath,
|
|
16547
|
+
appliedFixCount: appliedFixes.length
|
|
16548
|
+
});
|
|
16549
|
+
}
|
|
16550
|
+
}
|
|
16551
|
+
updateAvailableFixes({
|
|
16552
|
+
newFixes,
|
|
16553
|
+
scannedFiles,
|
|
16554
|
+
scanContext
|
|
16555
|
+
}) {
|
|
16556
|
+
this.freshFixes = this.freshFixes.filter(
|
|
16557
|
+
(fix) => !this.isFixFromOldScan({ fix, scannedFiles, scanContext })
|
|
16558
|
+
).concat(newFixes).sort((a, b) => {
|
|
14127
16559
|
return (b.severityValue ?? 0) - (a.severityValue ?? 0);
|
|
14128
16560
|
});
|
|
14129
16561
|
logInfo(
|
|
@@ -14137,63 +16569,161 @@ var _CheckForNewAvailableFixesService = class _CheckForNewAvailableFixesService
|
|
|
14137
16569
|
}
|
|
14138
16570
|
isFixAlreadyReported(fix) {
|
|
14139
16571
|
return this.reportedFixes.some(
|
|
14140
|
-
(reportedFix) => reportedFix.sharedState?.id === fix.sharedState?.id
|
|
16572
|
+
(reportedFix) => reportedFix.sharedState?.id === fix.sharedState?.id || reportedFix.id === fix.id
|
|
14141
16573
|
);
|
|
14142
16574
|
}
|
|
14143
|
-
|
|
16575
|
+
/**
|
|
16576
|
+
* Filters fixes for MVS_AUTO_FIX flow to only include fixes with:
|
|
16577
|
+
* - category 'Fixable' (or no category field - backward compatibility)
|
|
16578
|
+
* - do NOT have downloadSource 'AUTO_MVS'
|
|
16579
|
+
*/
|
|
16580
|
+
filterFixesForMvsAutoFix(fixes, scanContext) {
|
|
16581
|
+
const filteredFixes = fixes.filter((fix) => {
|
|
16582
|
+
const hasFixableCategory = fix.vulnerabilityReportIssues?.some(
|
|
16583
|
+
(issue) => !issue.category || // Handle missing field gracefully
|
|
16584
|
+
issue.category === "Fixable" /* Fixable */
|
|
16585
|
+
) ?? true;
|
|
16586
|
+
const hasAutoMvsDownloadSource = fix.sharedState?.downloadedBy?.some(
|
|
16587
|
+
(d) => d.downloadSource === "AUTO_MVS" /* AutoMvs */
|
|
16588
|
+
) ?? false;
|
|
16589
|
+
const doesNotHaveAutoMvsDownloadSource = !hasAutoMvsDownloadSource;
|
|
16590
|
+
const shouldInclude = hasFixableCategory && doesNotHaveAutoMvsDownloadSource;
|
|
16591
|
+
if (!shouldInclude) {
|
|
16592
|
+
const filteringReasons = [];
|
|
16593
|
+
if (!hasFixableCategory) {
|
|
16594
|
+
filteringReasons.push("not fixable category");
|
|
16595
|
+
}
|
|
16596
|
+
if (!doesNotHaveAutoMvsDownloadSource) {
|
|
16597
|
+
filteringReasons.push("has AUTO_MVS download source");
|
|
16598
|
+
}
|
|
16599
|
+
const reasonText = filteringReasons.join(" and ");
|
|
16600
|
+
logDebug(
|
|
16601
|
+
`[${scanContext}] Fix ${fix.id} filtered out - ${reasonText}`,
|
|
16602
|
+
{
|
|
16603
|
+
fixId: fix.id,
|
|
16604
|
+
hasFixableCategory,
|
|
16605
|
+
doesNotHaveAutoMvsDownloadSource,
|
|
16606
|
+
filteringReasons,
|
|
16607
|
+
categories: fix.vulnerabilityReportIssues?.map(
|
|
16608
|
+
(issue) => issue.category
|
|
16609
|
+
),
|
|
16610
|
+
downloadSources: fix.sharedState?.downloadedBy?.map(
|
|
16611
|
+
(d) => d.downloadSource
|
|
16612
|
+
)
|
|
16613
|
+
}
|
|
16614
|
+
);
|
|
16615
|
+
}
|
|
16616
|
+
return shouldInclude;
|
|
16617
|
+
});
|
|
16618
|
+
logInfo(`[${scanContext}] MVS_AUTO_FIX filtering completed`, {
|
|
16619
|
+
originalCount: fixes.length,
|
|
16620
|
+
filteredCount: filteredFixes.length,
|
|
16621
|
+
removedCount: fixes.length - filteredFixes.length
|
|
16622
|
+
});
|
|
16623
|
+
return filteredFixes;
|
|
16624
|
+
}
|
|
16625
|
+
isFixFromOldScan({
|
|
16626
|
+
fix,
|
|
16627
|
+
scannedFiles,
|
|
16628
|
+
scanContext
|
|
16629
|
+
}) {
|
|
14144
16630
|
const patch = fix.patchAndQuestions?.__typename === "FixData" ? fix.patchAndQuestions.patch : void 0;
|
|
14145
|
-
const fixFile =
|
|
16631
|
+
const fixFile = extractPathFromPatch2(patch);
|
|
14146
16632
|
if (!fixFile) {
|
|
14147
16633
|
return false;
|
|
14148
16634
|
}
|
|
14149
16635
|
logDebug(`[${scanContext}] Checking if fix is from old scan`, {
|
|
14150
16636
|
fixFile,
|
|
14151
|
-
|
|
14152
|
-
isFromOldScan:
|
|
16637
|
+
scannedFiles,
|
|
16638
|
+
isFromOldScan: scannedFiles.some((file) => file.relativePath === fixFile)
|
|
14153
16639
|
});
|
|
14154
|
-
return
|
|
16640
|
+
return scannedFiles.some((file) => file.relativePath === fixFile);
|
|
14155
16641
|
}
|
|
14156
|
-
async getFreshFixes({ path:
|
|
16642
|
+
async getFreshFixes({ path: path15 }) {
|
|
14157
16643
|
const scanContext = ScanContext.USER_REQUEST;
|
|
14158
|
-
logDebug(`[${scanContext}] Getting fresh fixes`, { path:
|
|
14159
|
-
if (this.path !==
|
|
14160
|
-
this.path =
|
|
16644
|
+
logDebug(`[${scanContext}] Getting fresh fixes`, { path: path15 });
|
|
16645
|
+
if (this.path !== path15) {
|
|
16646
|
+
this.path = path15;
|
|
14161
16647
|
this.reset();
|
|
14162
|
-
logInfo(`[${scanContext}] Reset service state for new path`, { path:
|
|
16648
|
+
logInfo(`[${scanContext}] Reset service state for new path`, { path: path15 });
|
|
16649
|
+
}
|
|
16650
|
+
try {
|
|
16651
|
+
this.gqlClient = await createAuthenticatedMcpGQLClient();
|
|
16652
|
+
} catch (error) {
|
|
16653
|
+
const errorMessage = error.message;
|
|
16654
|
+
if (errorMessage.includes("Authentication failed") || errorMessage.includes("access-denied") || errorMessage.includes("Authentication hook unauthorized")) {
|
|
16655
|
+
logError(
|
|
16656
|
+
`[${scanContext}] Failed to create authenticated client due to authentication failure`,
|
|
16657
|
+
{
|
|
16658
|
+
error: errorMessage
|
|
16659
|
+
}
|
|
16660
|
+
);
|
|
16661
|
+
this.hasAuthenticationFailed = true;
|
|
16662
|
+
return authenticationRequiredPrompt;
|
|
16663
|
+
}
|
|
16664
|
+
throw error;
|
|
16665
|
+
}
|
|
16666
|
+
this.triggerScan({ path: path15, gqlClient: this.gqlClient });
|
|
16667
|
+
let isMvsAutoFixEnabled = null;
|
|
16668
|
+
try {
|
|
16669
|
+
isMvsAutoFixEnabled = await this.gqlClient.getMvsAutoFixSettings();
|
|
16670
|
+
logDebug(`[${scanContext}] mvs_auto_fix setting: ${isMvsAutoFixEnabled}`);
|
|
16671
|
+
if (isMvsAutoFixEnabled) {
|
|
16672
|
+
if (this.reportedFixes.length > 0) {
|
|
16673
|
+
return this.generateAppliedFixesResponse(scanContext);
|
|
16674
|
+
}
|
|
16675
|
+
} else {
|
|
16676
|
+
if (this.freshFixes.length > 0) {
|
|
16677
|
+
return this.generateFreshFixesResponse(scanContext);
|
|
16678
|
+
}
|
|
16679
|
+
}
|
|
16680
|
+
} catch (error) {
|
|
16681
|
+
logError(`[${scanContext}] Failed to check mvs_auto_fix setting`, {
|
|
16682
|
+
error: error instanceof Error ? error.message : String(error)
|
|
16683
|
+
});
|
|
16684
|
+
if (this.freshFixes.length > 0) {
|
|
16685
|
+
return this.generateFreshFixesResponse(scanContext);
|
|
16686
|
+
}
|
|
14163
16687
|
}
|
|
14164
|
-
this.
|
|
14165
|
-
|
|
14166
|
-
if (this.freshFixes.length > 0) {
|
|
14167
|
-
return this.generateFreshFixesResponse(scanContext);
|
|
16688
|
+
if (this.hasAuthenticationFailed) {
|
|
16689
|
+
return authenticationRequiredPrompt;
|
|
14168
16690
|
}
|
|
14169
16691
|
if (!this.isInitialScanComplete) {
|
|
14170
16692
|
return initialScanInProgressPrompt;
|
|
14171
16693
|
}
|
|
16694
|
+
if (isMvsAutoFixEnabled === true) {
|
|
16695
|
+
return noVulnerabilitiesAutoFixPrompt;
|
|
16696
|
+
}
|
|
14172
16697
|
return noFreshFixesPrompt;
|
|
14173
16698
|
}
|
|
14174
16699
|
triggerScan({
|
|
14175
|
-
path:
|
|
16700
|
+
path: path15,
|
|
14176
16701
|
gqlClient
|
|
14177
16702
|
}) {
|
|
16703
|
+
if (this.path !== path15) {
|
|
16704
|
+
this.path = path15;
|
|
16705
|
+
this.reset();
|
|
16706
|
+
logInfo(`Reset service state for new path in triggerScan`, { path: path15 });
|
|
16707
|
+
}
|
|
14178
16708
|
this.gqlClient = gqlClient;
|
|
14179
16709
|
if (!this.intervalId) {
|
|
14180
|
-
this.startPeriodicScanning(
|
|
14181
|
-
this.executeInitialScan(
|
|
14182
|
-
void this.executeInitialFullScan(
|
|
16710
|
+
this.startPeriodicScanning(path15);
|
|
16711
|
+
this.executeInitialScan(path15);
|
|
16712
|
+
void this.executeInitialFullScan(path15);
|
|
14183
16713
|
}
|
|
14184
16714
|
}
|
|
14185
|
-
startPeriodicScanning(
|
|
16715
|
+
startPeriodicScanning(path15) {
|
|
14186
16716
|
const scanContext = ScanContext.BACKGROUND_PERIODIC;
|
|
14187
16717
|
logDebug(
|
|
14188
16718
|
`[${scanContext}] Starting periodic scan for new security vulnerabilities`,
|
|
14189
16719
|
{
|
|
14190
|
-
path:
|
|
16720
|
+
path: path15
|
|
14191
16721
|
}
|
|
14192
16722
|
);
|
|
14193
16723
|
this.intervalId = setInterval(() => {
|
|
14194
|
-
logDebug(`[${scanContext}] Triggering periodic security scan`, { path:
|
|
16724
|
+
logDebug(`[${scanContext}] Triggering periodic security scan`, { path: path15 });
|
|
14195
16725
|
this.scanForSecurityVulnerabilities({
|
|
14196
|
-
path:
|
|
16726
|
+
path: path15,
|
|
14197
16727
|
scanContext
|
|
14198
16728
|
}).catch((error) => {
|
|
14199
16729
|
logError(`[${scanContext}] Error during periodic security scan`, {
|
|
@@ -14202,45 +16732,45 @@ var _CheckForNewAvailableFixesService = class _CheckForNewAvailableFixesService
|
|
|
14202
16732
|
});
|
|
14203
16733
|
}, MCP_PERIODIC_CHECK_INTERVAL);
|
|
14204
16734
|
}
|
|
14205
|
-
async executeInitialFullScan(
|
|
16735
|
+
async executeInitialFullScan(path15) {
|
|
14206
16736
|
const scanContext = ScanContext.FULL_SCAN;
|
|
14207
|
-
logDebug(`[${scanContext}] Triggering initial full security scan`, { path:
|
|
16737
|
+
logDebug(`[${scanContext}] Triggering initial full security scan`, { path: path15 });
|
|
14208
16738
|
logDebug(`[${scanContext}] Full scan paths scanned`, {
|
|
14209
16739
|
fullScanPathsScanned: this.fullScanPathsScanned
|
|
14210
16740
|
});
|
|
14211
|
-
if (this.fullScanPathsScanned.includes(
|
|
16741
|
+
if (this.fullScanPathsScanned.includes(path15)) {
|
|
14212
16742
|
logDebug(`[${scanContext}] Full scan already executed for this path`, {
|
|
14213
|
-
path:
|
|
16743
|
+
path: path15
|
|
14214
16744
|
});
|
|
14215
16745
|
return;
|
|
14216
16746
|
}
|
|
14217
16747
|
configStore.set("fullScanPathsScanned", [
|
|
14218
16748
|
...this.fullScanPathsScanned,
|
|
14219
|
-
|
|
16749
|
+
path15
|
|
14220
16750
|
]);
|
|
14221
16751
|
try {
|
|
14222
16752
|
await this.scanForSecurityVulnerabilities({
|
|
14223
|
-
path:
|
|
16753
|
+
path: path15,
|
|
14224
16754
|
isAllFilesScan: true,
|
|
14225
16755
|
isAllDetectionRulesScan: true,
|
|
14226
16756
|
scanContext: ScanContext.FULL_SCAN
|
|
14227
16757
|
});
|
|
14228
|
-
if (!this.fullScanPathsScanned.includes(
|
|
14229
|
-
this.fullScanPathsScanned.push(
|
|
16758
|
+
if (!this.fullScanPathsScanned.includes(path15)) {
|
|
16759
|
+
this.fullScanPathsScanned.push(path15);
|
|
14230
16760
|
configStore.set("fullScanPathsScanned", this.fullScanPathsScanned);
|
|
14231
16761
|
}
|
|
14232
|
-
logInfo(`[${scanContext}] Full scan completed`, { path:
|
|
16762
|
+
logInfo(`[${scanContext}] Full scan completed`, { path: path15 });
|
|
14233
16763
|
} catch (error) {
|
|
14234
16764
|
logError(`[${scanContext}] Error during initial full security scan`, {
|
|
14235
16765
|
error
|
|
14236
16766
|
});
|
|
14237
16767
|
}
|
|
14238
16768
|
}
|
|
14239
|
-
executeInitialScan(
|
|
16769
|
+
executeInitialScan(path15) {
|
|
14240
16770
|
const scanContext = ScanContext.BACKGROUND_INITIAL;
|
|
14241
|
-
logDebug(`[${scanContext}] Triggering initial security scan`, { path:
|
|
16771
|
+
logDebug(`[${scanContext}] Triggering initial security scan`, { path: path15 });
|
|
14242
16772
|
this.scanForSecurityVulnerabilities({
|
|
14243
|
-
path:
|
|
16773
|
+
path: path15,
|
|
14244
16774
|
scanContext: ScanContext.BACKGROUND_INITIAL
|
|
14245
16775
|
}).catch((error) => {
|
|
14246
16776
|
logError(`[${scanContext}] Error during initial security scan`, { error });
|
|
@@ -14262,6 +16792,20 @@ var _CheckForNewAvailableFixesService = class _CheckForNewAvailableFixesService
|
|
|
14262
16792
|
logInfo(`[${scanContext}] No fresh fixes to report`);
|
|
14263
16793
|
return noFreshFixesPrompt;
|
|
14264
16794
|
}
|
|
16795
|
+
generateAppliedFixesResponse(scanContext = ScanContext.USER_REQUEST) {
|
|
16796
|
+
const appliedFixesToShow = this.reportedFixes;
|
|
16797
|
+
if (appliedFixesToShow.length > 0) {
|
|
16798
|
+
logInfo(
|
|
16799
|
+
`[${scanContext}] Reporting ${appliedFixesToShow.length} applied fixes to user`
|
|
16800
|
+
);
|
|
16801
|
+
return appliedFixesSummaryPrompt({
|
|
16802
|
+
fixes: appliedFixesToShow,
|
|
16803
|
+
gqlClient: this.gqlClient
|
|
16804
|
+
});
|
|
16805
|
+
}
|
|
16806
|
+
logInfo(`[${scanContext}] No applied fixes to report`);
|
|
16807
|
+
return noFreshFixesPrompt;
|
|
16808
|
+
}
|
|
14265
16809
|
};
|
|
14266
16810
|
__publicField(_CheckForNewAvailableFixesService, "instance");
|
|
14267
16811
|
var CheckForNewAvailableFixesService = _CheckForNewAvailableFixesService;
|
|
@@ -14285,8 +16829,10 @@ How to invoke:
|
|
|
14285
16829
|
\u2013 path (string): absolute path to the repository root.
|
|
14286
16830
|
|
|
14287
16831
|
Behaviour:
|
|
14288
|
-
\u2022 If
|
|
14289
|
-
\u2022 If
|
|
16832
|
+
\u2022 If auto-fix is enabled: Returns a list of fixes that were automatically applied to your codebase.
|
|
16833
|
+
\u2022 If auto-fix is disabled: Returns a list of fresh fixes available for manual review and application.
|
|
16834
|
+
\u2022 If no fixes are available, it returns a concise message indicating so.
|
|
16835
|
+
\u2022 Results include a human-readable summary with total count and severity breakdown.
|
|
14290
16836
|
|
|
14291
16837
|
Example payload:
|
|
14292
16838
|
{
|
|
@@ -14302,8 +16848,8 @@ Example payload:
|
|
|
14302
16848
|
},
|
|
14303
16849
|
required: ["path"]
|
|
14304
16850
|
});
|
|
14305
|
-
__publicField(this, "inputValidationSchema",
|
|
14306
|
-
path:
|
|
16851
|
+
__publicField(this, "inputValidationSchema", z34.object({
|
|
16852
|
+
path: z34.string().describe(
|
|
14307
16853
|
"Full local path to the cloned git repository to check for new available fixes"
|
|
14308
16854
|
)
|
|
14309
16855
|
}));
|
|
@@ -14320,9 +16866,9 @@ Example payload:
|
|
|
14320
16866
|
`Invalid path: potential security risk detected in path: ${pathValidationResult.error}`
|
|
14321
16867
|
);
|
|
14322
16868
|
}
|
|
14323
|
-
const
|
|
16869
|
+
const path15 = pathValidationResult.path;
|
|
14324
16870
|
const resultText = await this.newFixesService.getFreshFixes({
|
|
14325
|
-
path:
|
|
16871
|
+
path: path15
|
|
14326
16872
|
});
|
|
14327
16873
|
logInfo("CheckForNewAvailableFixesTool execution completed", {
|
|
14328
16874
|
resultText
|
|
@@ -14333,7 +16879,7 @@ Example payload:
|
|
|
14333
16879
|
|
|
14334
16880
|
// src/mcp/tools/fetchAvailableFixes/FetchAvailableFixesTool.ts
|
|
14335
16881
|
init_GitService();
|
|
14336
|
-
import { z as
|
|
16882
|
+
import { z as z35 } from "zod";
|
|
14337
16883
|
|
|
14338
16884
|
// src/mcp/tools/fetchAvailableFixes/FetchAvailableFixesService.ts
|
|
14339
16885
|
init_configs();
|
|
@@ -14452,12 +16998,12 @@ Call this tool instead of ${MCP_TOOL_SCAN_AND_FIX_VULNERABILITIES} when you only
|
|
|
14452
16998
|
},
|
|
14453
16999
|
required: ["path"]
|
|
14454
17000
|
});
|
|
14455
|
-
__publicField(this, "inputValidationSchema",
|
|
14456
|
-
path:
|
|
17001
|
+
__publicField(this, "inputValidationSchema", z35.object({
|
|
17002
|
+
path: z35.string().describe(
|
|
14457
17003
|
"Full local path to the cloned git repository to check for available fixes"
|
|
14458
17004
|
),
|
|
14459
|
-
offset:
|
|
14460
|
-
limit:
|
|
17005
|
+
offset: z35.number().optional().describe("Optional offset for pagination"),
|
|
17006
|
+
limit: z35.number().optional().describe("Optional maximum number of fixes to return")
|
|
14461
17007
|
}));
|
|
14462
17008
|
__publicField(this, "availableFixesService");
|
|
14463
17009
|
this.availableFixesService = FetchAvailableFixesService.getInstance();
|
|
@@ -14470,8 +17016,8 @@ Call this tool instead of ${MCP_TOOL_SCAN_AND_FIX_VULNERABILITIES} when you only
|
|
|
14470
17016
|
`Invalid path: potential security risk detected in path: ${pathValidationResult.error}`
|
|
14471
17017
|
);
|
|
14472
17018
|
}
|
|
14473
|
-
const
|
|
14474
|
-
const gitService = new GitService(
|
|
17019
|
+
const path15 = pathValidationResult.path;
|
|
17020
|
+
const gitService = new GitService(path15, log);
|
|
14475
17021
|
const gitValidation = await gitService.validateRepository();
|
|
14476
17022
|
if (!gitValidation.isValid) {
|
|
14477
17023
|
throw new Error(`Invalid git repository: ${gitValidation.error}`);
|
|
@@ -14498,7 +17044,7 @@ Call this tool instead of ${MCP_TOOL_SCAN_AND_FIX_VULNERABILITIES} when you only
|
|
|
14498
17044
|
|
|
14499
17045
|
// src/mcp/tools/scanAndFixVulnerabilities/ScanAndFixVulnerabilitiesTool.ts
|
|
14500
17046
|
init_configs();
|
|
14501
|
-
import
|
|
17047
|
+
import z36 from "zod";
|
|
14502
17048
|
|
|
14503
17049
|
// src/mcp/tools/scanAndFixVulnerabilities/ScanAndFixVulnerabilitiesService.ts
|
|
14504
17050
|
init_configs();
|
|
@@ -14573,11 +17119,11 @@ var _ScanAndFixVulnerabilitiesService = class _ScanAndFixVulnerabilitiesService
|
|
|
14573
17119
|
const effectiveOffset = offset ?? (this.currentOffset || 0);
|
|
14574
17120
|
const effectiveLimit = limit ?? MCP_DEFAULT_LIMIT;
|
|
14575
17121
|
logDebug("effectiveOffset", { effectiveOffset });
|
|
14576
|
-
const fixes = await this.getReportFixes(
|
|
17122
|
+
const fixes = await this.getReportFixes({
|
|
14577
17123
|
fixReportId,
|
|
14578
|
-
effectiveOffset,
|
|
14579
|
-
effectiveLimit
|
|
14580
|
-
);
|
|
17124
|
+
offset: effectiveOffset,
|
|
17125
|
+
limit: effectiveLimit
|
|
17126
|
+
});
|
|
14581
17127
|
logInfo(`Found ${fixes.totalCount} fixes`);
|
|
14582
17128
|
if (fixes.totalCount > 0) {
|
|
14583
17129
|
this.storedFixReportId = fixReportId;
|
|
@@ -14617,7 +17163,11 @@ var _ScanAndFixVulnerabilitiesService = class _ScanAndFixVulnerabilitiesService
|
|
|
14617
17163
|
this.gqlClient = gqlClient;
|
|
14618
17164
|
return gqlClient;
|
|
14619
17165
|
}
|
|
14620
|
-
async getReportFixes(
|
|
17166
|
+
async getReportFixes({
|
|
17167
|
+
fixReportId,
|
|
17168
|
+
offset,
|
|
17169
|
+
limit
|
|
17170
|
+
}) {
|
|
14621
17171
|
logDebug("getReportFixes", { fixReportId, offset, limit });
|
|
14622
17172
|
if (!this.gqlClient) {
|
|
14623
17173
|
throw new GqlClientError();
|
|
@@ -14679,16 +17229,16 @@ Example payload:
|
|
|
14679
17229
|
"maxFiles": 50,
|
|
14680
17230
|
"rescan": false
|
|
14681
17231
|
}`);
|
|
14682
|
-
__publicField(this, "inputValidationSchema",
|
|
14683
|
-
path:
|
|
17232
|
+
__publicField(this, "inputValidationSchema", z36.object({
|
|
17233
|
+
path: z36.string().describe(
|
|
14684
17234
|
"Full local path to repository to scan and fix vulnerabilities"
|
|
14685
17235
|
),
|
|
14686
|
-
offset:
|
|
14687
|
-
limit:
|
|
14688
|
-
maxFiles:
|
|
17236
|
+
offset: z36.number().optional().describe("Optional offset for pagination"),
|
|
17237
|
+
limit: z36.number().optional().describe("Optional maximum number of results to return"),
|
|
17238
|
+
maxFiles: z36.number().optional().describe(
|
|
14689
17239
|
`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.`
|
|
14690
17240
|
),
|
|
14691
|
-
rescan:
|
|
17241
|
+
rescan: z36.boolean().optional().describe("Optional whether to rescan the repository")
|
|
14692
17242
|
}));
|
|
14693
17243
|
__publicField(this, "inputSchema", {
|
|
14694
17244
|
type: "object",
|
|
@@ -14733,9 +17283,9 @@ Example payload:
|
|
|
14733
17283
|
`Invalid path: potential security risk detected in path: ${pathValidationResult.error}`
|
|
14734
17284
|
);
|
|
14735
17285
|
}
|
|
14736
|
-
const
|
|
17286
|
+
const path15 = pathValidationResult.path;
|
|
14737
17287
|
const files = await getLocalFiles({
|
|
14738
|
-
path:
|
|
17288
|
+
path: path15,
|
|
14739
17289
|
maxFileSize: MCP_MAX_FILE_SIZE,
|
|
14740
17290
|
maxFiles: args.maxFiles,
|
|
14741
17291
|
scanContext: ScanContext.USER_REQUEST
|
|
@@ -14754,7 +17304,7 @@ Example payload:
|
|
|
14754
17304
|
try {
|
|
14755
17305
|
const fixResult = await this.vulnerabilityFixService.processVulnerabilities({
|
|
14756
17306
|
fileList: files.map((file) => file.relativePath),
|
|
14757
|
-
repositoryPath:
|
|
17307
|
+
repositoryPath: path15,
|
|
14758
17308
|
offset: args.offset,
|
|
14759
17309
|
limit: args.limit,
|
|
14760
17310
|
isRescan: args.rescan || !!args.maxFiles
|
|
@@ -14835,7 +17385,7 @@ var mcpHandler = async (_args) => {
|
|
|
14835
17385
|
};
|
|
14836
17386
|
|
|
14837
17387
|
// src/args/commands/review.ts
|
|
14838
|
-
import
|
|
17388
|
+
import fs15 from "fs";
|
|
14839
17389
|
import chalk9 from "chalk";
|
|
14840
17390
|
function reviewBuilder(yargs2) {
|
|
14841
17391
|
return yargs2.option("f", {
|
|
@@ -14872,7 +17422,7 @@ function reviewBuilder(yargs2) {
|
|
|
14872
17422
|
).help();
|
|
14873
17423
|
}
|
|
14874
17424
|
function validateReviewOptions(argv) {
|
|
14875
|
-
if (!
|
|
17425
|
+
if (!fs15.existsSync(argv.f)) {
|
|
14876
17426
|
throw new CliError(`
|
|
14877
17427
|
Can't access ${chalk9.bold(argv.f)}`);
|
|
14878
17428
|
}
|