@aws/lsp-codewhisperer 0.0.64 → 0.0.66
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/CHANGELOG.md +57 -0
- package/out/client/token/bearer-token-service.json +1 -1
- package/out/language-server/agenticChat/agenticChatController.d.ts +13 -0
- package/out/language-server/agenticChat/agenticChatController.js +547 -125
- package/out/language-server/agenticChat/agenticChatController.js.map +1 -1
- package/out/language-server/agenticChat/constants/constants.d.ts +12 -0
- package/out/language-server/agenticChat/constants/constants.js +73 -1
- package/out/language-server/agenticChat/constants/constants.js.map +1 -1
- package/out/language-server/agenticChat/constants/toolConstants.d.ts +24 -0
- package/out/language-server/agenticChat/constants/toolConstants.js +35 -0
- package/out/language-server/agenticChat/constants/toolConstants.js.map +1 -0
- package/out/language-server/agenticChat/context/additionalContextProvider.d.ts +1 -1
- package/out/language-server/agenticChat/context/additionalContextProvider.js +44 -4
- package/out/language-server/agenticChat/context/additionalContextProvider.js.map +1 -1
- package/out/language-server/agenticChat/context/agenticChatTriggerContext.d.ts +11 -2
- package/out/language-server/agenticChat/context/agenticChatTriggerContext.js +32 -2
- package/out/language-server/agenticChat/context/agenticChatTriggerContext.js.map +1 -1
- package/out/language-server/agenticChat/context/contextCommandsProvider.js +1 -1
- package/out/language-server/agenticChat/context/contextCommandsProvider.js.map +1 -1
- package/out/language-server/agenticChat/context/contextUtils.d.ts +16 -0
- package/out/language-server/agenticChat/context/contextUtils.js +29 -0
- package/out/language-server/agenticChat/context/contextUtils.js.map +1 -1
- package/out/language-server/agenticChat/qAgenticChatServer.js +6 -2
- package/out/language-server/agenticChat/qAgenticChatServer.js.map +1 -1
- package/out/language-server/agenticChat/tools/chatDb/chatDb.d.ts +11 -3
- package/out/language-server/agenticChat/tools/chatDb/chatDb.js +103 -31
- package/out/language-server/agenticChat/tools/chatDb/chatDb.js.map +1 -1
- package/out/language-server/agenticChat/tools/chatDb/util.d.ts +4 -0
- package/out/language-server/agenticChat/tools/chatDb/util.js.map +1 -1
- package/out/language-server/agenticChat/tools/executeBash.d.ts +15 -5
- package/out/language-server/agenticChat/tools/executeBash.js +97 -22
- package/out/language-server/agenticChat/tools/executeBash.js.map +1 -1
- package/out/language-server/agenticChat/tools/mcp/mcpManager.js +1 -1
- package/out/language-server/agenticChat/tools/mcp/mcpManager.js.map +1 -1
- package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReview.d.ts +211 -0
- package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReview.js +630 -0
- package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReview.js.map +1 -0
- package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewConstants.d.ts +34 -0
- package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewConstants.js +200 -0
- package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewConstants.js.map +1 -0
- package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewErrors.d.ts +12 -0
- package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewErrors.js +32 -0
- package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewErrors.js.map +1 -0
- package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewSchemas.d.ts +289 -0
- package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewSchemas.js +140 -0
- package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewSchemas.js.map +1 -0
- package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewTypes.d.ts +58 -0
- package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewTypes.js +3 -0
- package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewTypes.js.map +1 -0
- package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewUtils.d.ts +156 -0
- package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewUtils.js +363 -0
- package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewUtils.js.map +1 -0
- package/out/language-server/agenticChat/tools/toolServer.d.ts +1 -0
- package/out/language-server/agenticChat/tools/toolServer.js +90 -39
- package/out/language-server/agenticChat/tools/toolServer.js.map +1 -1
- package/out/language-server/chat/chatSessionService.d.ts +1 -0
- package/out/language-server/chat/chatSessionService.js +5 -2
- package/out/language-server/chat/chatSessionService.js.map +1 -1
- package/out/language-server/chat/quickActions.d.ts +7 -1
- package/out/language-server/chat/quickActions.js +7 -1
- package/out/language-server/chat/quickActions.js.map +1 -1
- package/out/language-server/chat/telemetry/chatTelemetryController.d.ts +4 -3
- package/out/language-server/chat/telemetry/chatTelemetryController.js +22 -3
- package/out/language-server/chat/telemetry/chatTelemetryController.js.map +1 -1
- package/out/language-server/configuration/qConfigurationServer.d.ts +1 -0
- package/out/language-server/configuration/qConfigurationServer.js.map +1 -1
- package/out/language-server/inline-completion/auto-trigger/autoTrigger.d.ts +2 -0
- package/out/language-server/inline-completion/auto-trigger/autoTrigger.js +69 -1
- package/out/language-server/inline-completion/auto-trigger/autoTrigger.js.map +1 -1
- package/out/language-server/inline-completion/auto-trigger/editPredictionAutoTrigger.js +6 -1
- package/out/language-server/inline-completion/auto-trigger/editPredictionAutoTrigger.js.map +1 -1
- package/out/language-server/inline-completion/auto-trigger/editPredictionConfig.d.ts +1 -0
- package/out/language-server/inline-completion/auto-trigger/editPredictionConfig.js +1 -0
- package/out/language-server/inline-completion/auto-trigger/editPredictionConfig.js.map +1 -1
- package/out/language-server/inline-completion/codeWhispererServer.js +72 -43
- package/out/language-server/inline-completion/codeWhispererServer.js.map +1 -1
- package/out/language-server/workspaceContext/artifactManager.d.ts +4 -1
- package/out/language-server/workspaceContext/artifactManager.js +16 -1
- package/out/language-server/workspaceContext/artifactManager.js.map +1 -1
- package/out/language-server/workspaceContext/dependency/dependencyDiscoverer.d.ts +2 -1
- package/out/language-server/workspaceContext/dependency/dependencyDiscoverer.js +9 -6
- package/out/language-server/workspaceContext/dependency/dependencyDiscoverer.js.map +1 -1
- package/out/language-server/workspaceContext/dependency/dependencyHandler/LanguageDependencyHandler.d.ts +7 -2
- package/out/language-server/workspaceContext/dependency/dependencyHandler/LanguageDependencyHandler.js +20 -7
- package/out/language-server/workspaceContext/dependency/dependencyHandler/LanguageDependencyHandler.js.map +1 -1
- package/out/language-server/workspaceContext/dependency/dependencyHandler/LanguageDependencyHandlerFactory.d.ts +2 -2
- package/out/language-server/workspaceContext/dependency/dependencyHandler/LanguageDependencyHandlerFactory.js +4 -4
- package/out/language-server/workspaceContext/dependency/dependencyHandler/LanguageDependencyHandlerFactory.js.map +1 -1
- package/out/language-server/workspaceContext/fileUploadJobManager.js +3 -1
- package/out/language-server/workspaceContext/fileUploadJobManager.js.map +1 -1
- package/out/language-server/workspaceContext/workspaceContextServer.js +32 -19
- package/out/language-server/workspaceContext/workspaceContextServer.js.map +1 -1
- package/out/language-server/workspaceContext/workspaceFolderManager.d.ts +5 -3
- package/out/language-server/workspaceContext/workspaceFolderManager.js +80 -59
- package/out/language-server/workspaceContext/workspaceFolderManager.js.map +1 -1
- package/out/shared/activeUserTracker.d.ts +52 -0
- package/out/shared/activeUserTracker.js +164 -0
- package/out/shared/activeUserTracker.js.map +1 -0
- package/out/shared/amazonQServiceManager/configurationUtils.js +6 -0
- package/out/shared/amazonQServiceManager/configurationUtils.js.map +1 -1
- package/out/shared/codeWhispererService.js +3 -1
- package/out/shared/codeWhispererService.js.map +1 -1
- package/out/shared/telemetry/telemetryService.d.ts +2 -0
- package/out/shared/telemetry/telemetryService.js +2 -0
- package/out/shared/telemetry/telemetryService.js.map +1 -1
- package/out/shared/telemetry/types.d.ts +9 -1
- package/out/shared/telemetry/types.js +1 -0
- package/out/shared/telemetry/types.js.map +1 -1
- package/package.json +8 -6
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* eslint-disable import/no-nodejs-modules */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.QCodeReview = void 0;
|
|
5
|
+
const qCodeReviewConstants_1 = require("./qCodeReviewConstants");
|
|
6
|
+
const qCodeReviewUtils_1 = require("./qCodeReviewUtils");
|
|
7
|
+
const qCodeReviewSchemas_1 = require("./qCodeReviewSchemas");
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
9
|
+
const crypto = require("crypto");
|
|
10
|
+
const path = require("path");
|
|
11
|
+
const JSZip = require("jszip");
|
|
12
|
+
const fs_1 = require("fs");
|
|
13
|
+
const qCodeReviewErrors_1 = require("./qCodeReviewErrors");
|
|
14
|
+
const lsp_core_1 = require("@aws/lsp-core");
|
|
15
|
+
class QCodeReview {
|
|
16
|
+
static CUSTOMER_CODE_BASE_PATH = 'customerCodeBaseFolder';
|
|
17
|
+
static CODE_ARTIFACT_PATH = 'code_artifact';
|
|
18
|
+
static CUSTOMER_CODE_ZIP_NAME = 'customerCode.zip';
|
|
19
|
+
static CODE_DIFF_PATH = 'code_artifact/codeDiff/customerCodeDiff.diff';
|
|
20
|
+
static RULE_ARTIFACT_PATH = '.amazonq/rules';
|
|
21
|
+
static MAX_POLLING_ATTEMPTS = 30;
|
|
22
|
+
static MID_POLLING_ATTEMPTS = 15;
|
|
23
|
+
static POLLING_INTERVAL_MS = 10000;
|
|
24
|
+
static UPLOAD_INTENT = 'AGENTIC_CODE_REVIEW';
|
|
25
|
+
static SCAN_SCOPE = 'AGENTIC';
|
|
26
|
+
static MAX_FINDINGS_COUNT = 50;
|
|
27
|
+
static ERROR_MESSAGES = {
|
|
28
|
+
MISSING_CLIENT: 'CodeWhisperer client not available',
|
|
29
|
+
MISSING_ARTIFACTS: `Missing fileLevelArtifacts and folderLevelArtifacts for ${qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME} tool. Ask user to provide a specific file / folder / workspace which has code that can be scanned.`,
|
|
30
|
+
MISSING_FILES_TO_SCAN: `There are no valid files to scan. Ask user to provide a specific file / folder / workspace which has code that can be scanned.`,
|
|
31
|
+
UPLOAD_FAILED: `Failed to upload artifact for code review in ${qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME} tool.`,
|
|
32
|
+
START_CODE_ANALYSIS_FAILED: (jobId) => `Failed to start code analysis in ${qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME} tool for jobId - ${jobId}`,
|
|
33
|
+
CODE_ANALYSIS_FAILED: (jobId, message) => `Code analysis failed for jobId - ${jobId} due to ${message}`,
|
|
34
|
+
SCAN_FAILED: 'Code scan failed',
|
|
35
|
+
TIMEOUT: (attempts) => `Code scan timed out after ${attempts} attempts. Ask user to provide a smaller size of code to scan.`,
|
|
36
|
+
};
|
|
37
|
+
credentialsProvider;
|
|
38
|
+
logging;
|
|
39
|
+
telemetry;
|
|
40
|
+
workspace;
|
|
41
|
+
codeWhispererClient;
|
|
42
|
+
cancellationToken;
|
|
43
|
+
writableStream;
|
|
44
|
+
constructor(features) {
|
|
45
|
+
this.credentialsProvider = features.credentialsProvider;
|
|
46
|
+
this.logging = features.logging;
|
|
47
|
+
this.telemetry = features.telemetry;
|
|
48
|
+
this.workspace = features.workspace;
|
|
49
|
+
}
|
|
50
|
+
static toolName = qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME;
|
|
51
|
+
static toolDescription = qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_DESCRIPTION;
|
|
52
|
+
static inputSchema = qCodeReviewSchemas_1.Q_CODE_REVIEW_INPUT_SCHEMA;
|
|
53
|
+
/**
|
|
54
|
+
* Main execution method for the QCodeReview tool
|
|
55
|
+
* @param input User input parameters for code review
|
|
56
|
+
* @param context Execution context containing clients and tokens
|
|
57
|
+
* @returns Output containing code review results or error message
|
|
58
|
+
*/
|
|
59
|
+
async execute(input, context) {
|
|
60
|
+
let chatStreamWriter;
|
|
61
|
+
try {
|
|
62
|
+
this.logging.info(`Executing ${qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME}: ${JSON.stringify(input)}`);
|
|
63
|
+
// 1. Validate input
|
|
64
|
+
const setup = await this.validateInputAndSetup(input, context);
|
|
65
|
+
this.checkCancellation();
|
|
66
|
+
chatStreamWriter = this.writableStream?.getWriter();
|
|
67
|
+
await chatStreamWriter?.write('Initiating code review...');
|
|
68
|
+
// 2. Prepare code artifact and upload to service
|
|
69
|
+
const uploadResult = await this.prepareAndUploadArtifacts(setup);
|
|
70
|
+
this.checkCancellation();
|
|
71
|
+
// 3. Start code analysis
|
|
72
|
+
const analysisResult = await this.startCodeAnalysis(setup, uploadResult);
|
|
73
|
+
this.checkCancellation();
|
|
74
|
+
await chatStreamWriter?.write('Reviewing your code...');
|
|
75
|
+
// 4. Wait for scan to complete
|
|
76
|
+
await this.pollForCompletion(analysisResult.jobId, setup.scanName, setup.artifactType, chatStreamWriter);
|
|
77
|
+
this.checkCancellation();
|
|
78
|
+
// 5. Process scan result
|
|
79
|
+
const results = await this.processResults(setup, uploadResult.isCodeDiffPresent, analysisResult.jobId);
|
|
80
|
+
return {
|
|
81
|
+
output: {
|
|
82
|
+
kind: 'json',
|
|
83
|
+
success: true,
|
|
84
|
+
content: results,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (error instanceof lsp_core_1.CancellationError) {
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
output: {
|
|
94
|
+
kind: 'json',
|
|
95
|
+
success: false,
|
|
96
|
+
content: {
|
|
97
|
+
errorMessage: error.message,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
await chatStreamWriter?.close();
|
|
104
|
+
chatStreamWriter?.releaseLock();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Validates user input and sets up the execution environment
|
|
109
|
+
* @param input User input parameters for code review
|
|
110
|
+
* @param context Execution context containing clients and tokens
|
|
111
|
+
* @returns Setup object with validated parameters or error message
|
|
112
|
+
*/
|
|
113
|
+
async validateInputAndSetup(input, context) {
|
|
114
|
+
this.cancellationToken = context.cancellationToken;
|
|
115
|
+
this.writableStream = context.writableStream;
|
|
116
|
+
this.codeWhispererClient = context.codeWhispererClient;
|
|
117
|
+
if (!this.codeWhispererClient) {
|
|
118
|
+
throw new Error(QCodeReview.ERROR_MESSAGES.MISSING_CLIENT);
|
|
119
|
+
}
|
|
120
|
+
// parse input
|
|
121
|
+
const validatedInput = qCodeReviewSchemas_1.Z_Q_CODE_REVIEW_INPUT_SCHEMA.parse(input);
|
|
122
|
+
const fileArtifacts = validatedInput.fileLevelArtifacts || [];
|
|
123
|
+
const folderArtifacts = validatedInput.folderLevelArtifacts || [];
|
|
124
|
+
const ruleArtifacts = validatedInput.ruleArtifacts || [];
|
|
125
|
+
if (fileArtifacts.length === 0 && folderArtifacts.length === 0) {
|
|
126
|
+
qCodeReviewUtils_1.QCodeReviewUtils.emitMetric('MissingFilesOrFolders', {}, qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME, this.logging, this.telemetry, this.credentialsProvider.getConnectionMetadata()?.sso?.startUrl);
|
|
127
|
+
throw new qCodeReviewErrors_1.QCodeReviewValidationError(QCodeReview.ERROR_MESSAGES.MISSING_ARTIFACTS);
|
|
128
|
+
}
|
|
129
|
+
const isFullReviewRequest = validatedInput.scopeOfReview?.toUpperCase() === qCodeReviewConstants_1.FULL_REVIEW;
|
|
130
|
+
const artifactType = fileArtifacts.length > 0 ? 'FILE' : 'FOLDER';
|
|
131
|
+
// Setting java as default language
|
|
132
|
+
// TODO: Remove requirement of programming language
|
|
133
|
+
const programmingLanguage = 'java';
|
|
134
|
+
const scanName = 'Standard-' + (0, crypto_1.randomUUID)();
|
|
135
|
+
this.logging.info(`Agentic scan name: ${scanName}`);
|
|
136
|
+
return {
|
|
137
|
+
fileArtifacts,
|
|
138
|
+
folderArtifacts,
|
|
139
|
+
isFullReviewRequest,
|
|
140
|
+
artifactType,
|
|
141
|
+
programmingLanguage,
|
|
142
|
+
scanName,
|
|
143
|
+
ruleArtifacts,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Prepares and uploads code artifacts for analysis
|
|
148
|
+
* @param setup Setup object with validated parameters
|
|
149
|
+
* @returns Upload result with uploadId or error message
|
|
150
|
+
*/
|
|
151
|
+
async prepareAndUploadArtifacts(setup) {
|
|
152
|
+
const { zipBuffer, md5Hash, isCodeDiffPresent } = await this.prepareFilesAndFoldersForUpload(setup.fileArtifacts, setup.folderArtifacts, setup.ruleArtifacts, setup.isFullReviewRequest);
|
|
153
|
+
const uploadUrlResponse = await this.codeWhispererClient.createUploadUrl({
|
|
154
|
+
contentLength: zipBuffer.length,
|
|
155
|
+
contentMd5: md5Hash,
|
|
156
|
+
uploadIntent: QCodeReview.UPLOAD_INTENT,
|
|
157
|
+
uploadContext: {
|
|
158
|
+
codeAnalysisUploadContext: {
|
|
159
|
+
codeScanName: setup.scanName,
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
if (!uploadUrlResponse.uploadUrl || !uploadUrlResponse.uploadId) {
|
|
164
|
+
qCodeReviewUtils_1.QCodeReviewUtils.emitMetric('createUploadUrlFailed', {
|
|
165
|
+
codeScanName: setup.scanName,
|
|
166
|
+
contentLength: zipBuffer.length,
|
|
167
|
+
uploadIntent: QCodeReview.UPLOAD_INTENT,
|
|
168
|
+
response: uploadUrlResponse,
|
|
169
|
+
}, qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME, this.logging, this.telemetry, this.credentialsProvider.getConnectionMetadata()?.sso?.startUrl);
|
|
170
|
+
throw new qCodeReviewErrors_1.QCodeReviewValidationError(QCodeReview.ERROR_MESSAGES.UPLOAD_FAILED);
|
|
171
|
+
}
|
|
172
|
+
await qCodeReviewUtils_1.QCodeReviewUtils.uploadFileToPresignedUrl(uploadUrlResponse.uploadUrl, zipBuffer, uploadUrlResponse.requestHeaders || {}, this.logging);
|
|
173
|
+
qCodeReviewUtils_1.QCodeReviewUtils.emitMetric('uploadArtifactSuccess', {
|
|
174
|
+
codeScanName: setup.scanName,
|
|
175
|
+
codeArtifactId: uploadUrlResponse.uploadId,
|
|
176
|
+
artifactSize: zipBuffer.length,
|
|
177
|
+
artifactType: setup.artifactType,
|
|
178
|
+
}, qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME, this.logging, this.telemetry, this.credentialsProvider.getConnectionMetadata()?.sso?.startUrl);
|
|
179
|
+
return { uploadId: uploadUrlResponse.uploadId, isCodeDiffPresent };
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Initiates code analysis with the uploaded artifacts
|
|
183
|
+
* @param setup Setup object with validated parameters
|
|
184
|
+
* @param uploadResult Result from artifact upload containing uploadId
|
|
185
|
+
* @returns Code scan jobId and status
|
|
186
|
+
*/
|
|
187
|
+
async startCodeAnalysis(setup, uploadResult) {
|
|
188
|
+
const createResponse = await this.codeWhispererClient.startCodeAnalysis({
|
|
189
|
+
artifacts: { SourceCode: uploadResult.uploadId },
|
|
190
|
+
programmingLanguage: { languageName: setup.programmingLanguage },
|
|
191
|
+
clientToken: qCodeReviewUtils_1.QCodeReviewUtils.generateClientToken(),
|
|
192
|
+
codeScanName: setup.scanName,
|
|
193
|
+
scope: QCodeReview.SCAN_SCOPE,
|
|
194
|
+
codeDiffMetadata: uploadResult.isCodeDiffPresent ? { codeDiffPath: '/code_artifact/codeDiff/' } : undefined,
|
|
195
|
+
});
|
|
196
|
+
if (!createResponse.jobId) {
|
|
197
|
+
qCodeReviewUtils_1.QCodeReviewUtils.emitMetric('startCodeAnalysisFailed', {
|
|
198
|
+
artifacts: { SourceCode: uploadResult.uploadId },
|
|
199
|
+
programmingLanguage: { languageName: setup.programmingLanguage },
|
|
200
|
+
codeScanName: setup.scanName,
|
|
201
|
+
scope: QCodeReview.SCAN_SCOPE,
|
|
202
|
+
artifactType: setup.artifactType,
|
|
203
|
+
response: createResponse,
|
|
204
|
+
}, qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME, this.logging, this.telemetry, this.credentialsProvider.getConnectionMetadata()?.sso?.startUrl);
|
|
205
|
+
throw new qCodeReviewErrors_1.QCodeReviewInternalError(QCodeReview.ERROR_MESSAGES.START_CODE_ANALYSIS_FAILED(createResponse.jobId));
|
|
206
|
+
}
|
|
207
|
+
this.logging.info(`Code scan created with job ID: ${createResponse.jobId}`);
|
|
208
|
+
return {
|
|
209
|
+
jobId: createResponse.jobId,
|
|
210
|
+
status: createResponse.status,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Polls for completion of the code analysis job
|
|
215
|
+
* @param jobId ID of the code analysis job
|
|
216
|
+
* @param scanName Name of the code scan
|
|
217
|
+
* @param artifactType Type of artifact being scanned (FILE or FOLDER)
|
|
218
|
+
* @param chatStreamWriter Stream writer for sending progress updates
|
|
219
|
+
*/
|
|
220
|
+
async pollForCompletion(jobId, scanName, artifactType, chatStreamWriter) {
|
|
221
|
+
let status = 'Pending';
|
|
222
|
+
let attemptCount = 0;
|
|
223
|
+
while (status === 'Pending' && attemptCount < QCodeReview.MAX_POLLING_ATTEMPTS) {
|
|
224
|
+
this.logging.info(`Code scan status: ${status}, waiting...`);
|
|
225
|
+
await new Promise(resolve => setTimeout(resolve, QCodeReview.POLLING_INTERVAL_MS));
|
|
226
|
+
const statusResponse = await this.getCodeAnalysisStatus(jobId);
|
|
227
|
+
status = statusResponse.status;
|
|
228
|
+
attemptCount++;
|
|
229
|
+
if (statusResponse.errorMessage) {
|
|
230
|
+
qCodeReviewUtils_1.QCodeReviewUtils.emitMetric('codeAnalysisFailed', {
|
|
231
|
+
codeScanName: scanName,
|
|
232
|
+
codeReviewId: jobId,
|
|
233
|
+
status,
|
|
234
|
+
artifactType,
|
|
235
|
+
message: statusResponse.errorMessage,
|
|
236
|
+
}, qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME, this.logging, this.telemetry, this.credentialsProvider.getConnectionMetadata()?.sso?.startUrl);
|
|
237
|
+
throw new qCodeReviewErrors_1.QCodeReviewInternalError(QCodeReview.ERROR_MESSAGES.CODE_ANALYSIS_FAILED(jobId, statusResponse.errorMessage));
|
|
238
|
+
}
|
|
239
|
+
if (attemptCount == QCodeReview.MID_POLLING_ATTEMPTS) {
|
|
240
|
+
await chatStreamWriter?.write('Still reviewing your code, it is taking just a bit longer than usual...');
|
|
241
|
+
}
|
|
242
|
+
this.checkCancellation('Command execution cancelled while waiting for scan to complete');
|
|
243
|
+
}
|
|
244
|
+
if (status === 'Pending') {
|
|
245
|
+
qCodeReviewUtils_1.QCodeReviewUtils.emitMetric('codeAnalysisTimeout', {
|
|
246
|
+
codeScanName: scanName,
|
|
247
|
+
codeReviewId: jobId,
|
|
248
|
+
status: 'Timeout',
|
|
249
|
+
maxAttempts: QCodeReview.MAX_POLLING_ATTEMPTS,
|
|
250
|
+
}, qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME, this.logging, this.telemetry, this.credentialsProvider.getConnectionMetadata()?.sso?.startUrl);
|
|
251
|
+
throw new qCodeReviewErrors_1.QCodeReviewTimeoutError(QCodeReview.ERROR_MESSAGES.TIMEOUT(QCodeReview.MAX_POLLING_ATTEMPTS));
|
|
252
|
+
}
|
|
253
|
+
this.logging.info(`Code scan completed with status: ${status}`);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Processes the results of the completed code analysis
|
|
257
|
+
* @param setup Setup object with validated parameters
|
|
258
|
+
* @param isCodeDiffPresent If code diff is present in upload artifact
|
|
259
|
+
* @param jobId ID of the code analysis job
|
|
260
|
+
* @returns Processed results with findings grouped by file
|
|
261
|
+
*/
|
|
262
|
+
async processResults(setup, isCodeDiffPresent, jobId) {
|
|
263
|
+
const { totalFindings, findingsExceededLimit } = await this.collectFindings(jobId, setup.isFullReviewRequest, isCodeDiffPresent, setup.programmingLanguage);
|
|
264
|
+
qCodeReviewUtils_1.QCodeReviewUtils.emitMetric('codeAnalysisSucces', {
|
|
265
|
+
codeScanName: setup.scanName,
|
|
266
|
+
codeReviewId: jobId,
|
|
267
|
+
findingsCount: totalFindings.length,
|
|
268
|
+
}, qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME, this.logging, this.telemetry, this.credentialsProvider.getConnectionMetadata()?.sso?.startUrl);
|
|
269
|
+
const aggregatedCodeScanIssueList = this.aggregateFindingsByFile(findingsExceededLimit ? totalFindings.slice(0, QCodeReview.MAX_FINDINGS_COUNT) : totalFindings, setup.fileArtifacts, setup.folderArtifacts);
|
|
270
|
+
this.logging.info('Findings count grouped by file');
|
|
271
|
+
aggregatedCodeScanIssueList.forEach(item => this.logging.info(`File path - ${item.filePath} Findings count - ${item.issues.length}`));
|
|
272
|
+
return {
|
|
273
|
+
codeReviewId: jobId,
|
|
274
|
+
message: `${qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME} tool completed successfully.${findingsExceededLimit ? ` Inform the user that we are limiting findings to top ${QCodeReview.MAX_FINDINGS_COUNT} based on severity.` : ''}`,
|
|
275
|
+
findingsByFile: JSON.stringify(aggregatedCodeScanIssueList),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Collects findings from the code analysis job
|
|
280
|
+
* @param jobId ID of the code analysis job
|
|
281
|
+
* @param isFullReviewRequest Whether this is a full review or diff review
|
|
282
|
+
* @param isCodeDiffPresent Whether code diff is present in the artifacts
|
|
283
|
+
* @param programmingLanguage Programming language
|
|
284
|
+
* @returns Object containing collected findings and whether limit was exceeded
|
|
285
|
+
*/
|
|
286
|
+
async collectFindings(jobId, isFullReviewRequest, isCodeDiffPresent, programmingLanguage) {
|
|
287
|
+
let totalFindings = [];
|
|
288
|
+
let nextFindingToken = undefined;
|
|
289
|
+
let findingsExceededLimit = false;
|
|
290
|
+
const lookForCodeDiffFindings = !isFullReviewRequest && isCodeDiffPresent;
|
|
291
|
+
this.logging.info(`Collect findings for jobId: ${jobId}, isFullReviewRequest: ${isFullReviewRequest}, isCodeDiffPresent: ${isCodeDiffPresent}`);
|
|
292
|
+
this.logging.info(`Look for code diff findings only - ${lookForCodeDiffFindings}`);
|
|
293
|
+
do {
|
|
294
|
+
this.logging.info(`GetFindings for job ID: ${jobId}`);
|
|
295
|
+
const findingsResponse = await this.getCodeAnalysisFindings(jobId, nextFindingToken);
|
|
296
|
+
nextFindingToken = findingsResponse.nextToken;
|
|
297
|
+
const parsedFindings = this.parseFindings(findingsResponse.codeAnalysisFindings, jobId, programmingLanguage) || [];
|
|
298
|
+
const filteredFindings = lookForCodeDiffFindings
|
|
299
|
+
? parsedFindings.filter(finding => 'CodeDiff' === finding.findingContext)
|
|
300
|
+
: parsedFindings;
|
|
301
|
+
totalFindings = totalFindings.concat(filteredFindings);
|
|
302
|
+
if (totalFindings.length > QCodeReview.MAX_FINDINGS_COUNT) {
|
|
303
|
+
findingsExceededLimit = true;
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
} while (nextFindingToken);
|
|
307
|
+
this.logging.info(`Total findings: ${totalFindings.length}`);
|
|
308
|
+
return { totalFindings, findingsExceededLimit };
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Gets the current status of a code analysis job
|
|
312
|
+
* @param jobId ID of the code analysis job
|
|
313
|
+
* @returns Status response from the CodeWhisperer service
|
|
314
|
+
*/
|
|
315
|
+
async getCodeAnalysisStatus(jobId) {
|
|
316
|
+
return await this.codeWhispererClient.getCodeAnalysis({ jobId });
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Retrieves findings from a code analysis job
|
|
320
|
+
* @param jobId ID of the code analysis job
|
|
321
|
+
* @param nextToken Pagination token for retrieving next batch of findings
|
|
322
|
+
* @returns Findings response from the CodeWhisperer service
|
|
323
|
+
*/
|
|
324
|
+
async getCodeAnalysisFindings(jobId, nextToken) {
|
|
325
|
+
return await this.codeWhispererClient.listCodeAnalysisFindings({
|
|
326
|
+
jobId,
|
|
327
|
+
nextToken,
|
|
328
|
+
codeAnalysisFindingsSchema: 'codeanalysis/findings/1.0',
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Create a zip archive of the files and folders to be scanned and calculate MD5 hash
|
|
333
|
+
* @param fileArtifacts Array of file artifacts containing path and programming language
|
|
334
|
+
* @param folderArtifacts Array of folder artifacts containing path
|
|
335
|
+
* @param ruleArtifacts Array of file paths to user selected rules
|
|
336
|
+
* @param isFullReviewRequest If user asked for Full review or Partial review
|
|
337
|
+
* @returns An object containing the zip file buffer and its MD5 hash
|
|
338
|
+
*/
|
|
339
|
+
async prepareFilesAndFoldersForUpload(fileArtifacts, folderArtifacts, ruleArtifacts, isFullReviewRequest) {
|
|
340
|
+
try {
|
|
341
|
+
this.logging.info(`Preparing ${fileArtifacts.length} files and ${folderArtifacts.length} folders for upload`);
|
|
342
|
+
const codeArtifactZip = new JSZip();
|
|
343
|
+
const customerCodeZip = new JSZip();
|
|
344
|
+
// Process files and folders
|
|
345
|
+
const codeDiff = await this.processArtifacts(fileArtifacts, folderArtifacts, ruleArtifacts, customerCodeZip, !isFullReviewRequest);
|
|
346
|
+
let numberOfFilesInCustomerCodeZip = qCodeReviewUtils_1.QCodeReviewUtils.countZipFiles(customerCodeZip);
|
|
347
|
+
if (numberOfFilesInCustomerCodeZip > ruleArtifacts.length) {
|
|
348
|
+
// Validates that there are actual files to scan, other than rule artifacts
|
|
349
|
+
this.logging.info(`Total files in customerCodeZip - ${numberOfFilesInCustomerCodeZip}`);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
throw new qCodeReviewErrors_1.QCodeReviewValidationError(QCodeReview.ERROR_MESSAGES.MISSING_FILES_TO_SCAN);
|
|
353
|
+
}
|
|
354
|
+
// Generate user code zip buffer
|
|
355
|
+
const customerCodeBuffer = await qCodeReviewUtils_1.QCodeReviewUtils.generateZipBuffer(customerCodeZip);
|
|
356
|
+
qCodeReviewUtils_1.QCodeReviewUtils.logZipStructure(customerCodeZip, 'User code', this.logging);
|
|
357
|
+
// Add user code zip to the main artifact zip
|
|
358
|
+
codeArtifactZip.file(`${QCodeReview.CODE_ARTIFACT_PATH}/${QCodeReview.CUSTOMER_CODE_ZIP_NAME}`, customerCodeBuffer);
|
|
359
|
+
let isCodeDiffPresent = false;
|
|
360
|
+
// Add code diff file if we have any diffs
|
|
361
|
+
if (codeDiff.trim()) {
|
|
362
|
+
this.logging.info(`Adding code diff to zip of size: ${codeDiff.length}`);
|
|
363
|
+
isCodeDiffPresent = true;
|
|
364
|
+
codeArtifactZip.file(QCodeReview.CODE_DIFF_PATH, codeDiff);
|
|
365
|
+
}
|
|
366
|
+
// Generate the final code artifact zip
|
|
367
|
+
const zipBuffer = await qCodeReviewUtils_1.QCodeReviewUtils.generateZipBuffer(codeArtifactZip);
|
|
368
|
+
qCodeReviewUtils_1.QCodeReviewUtils.logZipStructure(codeArtifactZip, 'Code artifact', this.logging);
|
|
369
|
+
// Calculate MD5 hash of the zip buffer
|
|
370
|
+
const md5Hash = crypto.createHash('md5').update(zipBuffer).digest('hex');
|
|
371
|
+
this.logging.info(`Created zip archive, size: ${zipBuffer.byteLength} bytes, MD5: ${md5Hash}`);
|
|
372
|
+
return { zipBuffer, md5Hash, isCodeDiffPresent };
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
this.logging.error(`Error preparing files for upload: ${error}`);
|
|
376
|
+
throw error;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Processes file, folder, and rule artifacts for inclusion in the zip archive
|
|
381
|
+
* @param fileArtifacts Array of file artifacts to process
|
|
382
|
+
* @param folderArtifacts Array of folder artifacts to process
|
|
383
|
+
* @param ruleArtifacts Array of rule artifacts to process
|
|
384
|
+
* @param customerCodeZip JSZip instance for the customer code
|
|
385
|
+
* @param isCodeDiffScan Whether this is a code diff scan
|
|
386
|
+
* @returns Combined code diff string from all artifacts
|
|
387
|
+
*/
|
|
388
|
+
async processArtifacts(fileArtifacts, folderArtifacts, ruleArtifacts, customerCodeZip, isCodeDiffScan) {
|
|
389
|
+
let codeDiff = '';
|
|
390
|
+
// Process files
|
|
391
|
+
codeDiff += await this.processFileArtifacts(fileArtifacts, customerCodeZip, isCodeDiffScan);
|
|
392
|
+
// Process folders
|
|
393
|
+
codeDiff += await this.processFolderArtifacts(folderArtifacts, customerCodeZip, isCodeDiffScan);
|
|
394
|
+
// Process rule artifacts
|
|
395
|
+
await this.processRuleArtifacts(ruleArtifacts, customerCodeZip);
|
|
396
|
+
return codeDiff;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Processes file artifacts for inclusion in the zip archive
|
|
400
|
+
* @param fileArtifacts Array of file artifacts to process
|
|
401
|
+
* @param customerCodeZip JSZip instance for the customer code
|
|
402
|
+
* @param isCodeDiffScan Whether this is a code diff scan
|
|
403
|
+
* @returns Combined code diff string from file artifacts
|
|
404
|
+
*/
|
|
405
|
+
async processFileArtifacts(fileArtifacts, customerCodeZip, isCodeDiffScan) {
|
|
406
|
+
let codeDiff = '';
|
|
407
|
+
for (const artifact of fileArtifacts) {
|
|
408
|
+
await qCodeReviewUtils_1.QCodeReviewUtils.withErrorHandling(async () => {
|
|
409
|
+
let fileName = path.basename(artifact.path);
|
|
410
|
+
if (!fileName.startsWith('.') &&
|
|
411
|
+
!qCodeReviewUtils_1.QCodeReviewUtils.shouldSkipFile(fileName) &&
|
|
412
|
+
(0, fs_1.existsSync)(artifact.path)) {
|
|
413
|
+
const fileContent = await this.workspace.fs.readFile(artifact.path);
|
|
414
|
+
let normalizedArtifactPath = qCodeReviewUtils_1.QCodeReviewUtils.convertToUnixPath(artifact.path);
|
|
415
|
+
customerCodeZip.file(`${QCodeReview.CUSTOMER_CODE_BASE_PATH}${normalizedArtifactPath}`, fileContent);
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
this.logging.info(`Skipping file - ${artifact.path}`);
|
|
419
|
+
}
|
|
420
|
+
}, 'Failed to read file', this.logging, artifact.path);
|
|
421
|
+
codeDiff += await qCodeReviewUtils_1.QCodeReviewUtils.processArtifactWithDiff(artifact, isCodeDiffScan, this.logging);
|
|
422
|
+
}
|
|
423
|
+
return codeDiff;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Processes folder artifacts for inclusion in the zip archive
|
|
427
|
+
* @param folderArtifacts Array of folder artifacts to process
|
|
428
|
+
* @param customerCodeZip JSZip instance for the customer code
|
|
429
|
+
* @param isCodeDiffScan Whether this is a code diff scan
|
|
430
|
+
* @returns Combined code diff string from folder artifacts
|
|
431
|
+
*/
|
|
432
|
+
async processFolderArtifacts(folderArtifacts, customerCodeZip, isCodeDiffScan) {
|
|
433
|
+
let codeDiff = '';
|
|
434
|
+
for (const folderArtifact of folderArtifacts) {
|
|
435
|
+
await qCodeReviewUtils_1.QCodeReviewUtils.withErrorHandling(async () => {
|
|
436
|
+
await this.addFolderToZip(customerCodeZip, folderArtifact.path, QCodeReview.CUSTOMER_CODE_BASE_PATH);
|
|
437
|
+
}, 'Failed to add folder', this.logging, folderArtifact.path);
|
|
438
|
+
codeDiff += await qCodeReviewUtils_1.QCodeReviewUtils.processArtifactWithDiff(folderArtifact, isCodeDiffScan, this.logging);
|
|
439
|
+
}
|
|
440
|
+
return codeDiff;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Processes rule artifacts for inclusion in the zip archive
|
|
444
|
+
* @param ruleArtifacts Array of rule artifacts to process
|
|
445
|
+
* @param customerCodeZip JSZip instance for the customer code
|
|
446
|
+
*/
|
|
447
|
+
async processRuleArtifacts(ruleArtifacts, customerCodeZip) {
|
|
448
|
+
for (const artifact of ruleArtifacts) {
|
|
449
|
+
await qCodeReviewUtils_1.QCodeReviewUtils.withErrorHandling(async () => {
|
|
450
|
+
let fileName = path.basename(artifact.path);
|
|
451
|
+
if (!fileName.startsWith('.') &&
|
|
452
|
+
!qCodeReviewUtils_1.QCodeReviewUtils.shouldSkipFile(fileName) &&
|
|
453
|
+
(0, fs_1.existsSync)(artifact.path)) {
|
|
454
|
+
const fileContent = await this.workspace.fs.readFile(artifact.path);
|
|
455
|
+
customerCodeZip.file(`${QCodeReview.CUSTOMER_CODE_BASE_PATH}/${QCodeReview.RULE_ARTIFACT_PATH}/${fileName}`, fileContent);
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
this.logging.info(`Skipping file - ${artifact.path}`);
|
|
459
|
+
}
|
|
460
|
+
}, 'Failed to read file', this.logging, artifact.path);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Recursively add a folder and its contents to a zip archive
|
|
465
|
+
* @param zip JSZip instance to add files to
|
|
466
|
+
* @param folderPath Path to the folder to add
|
|
467
|
+
* @param zipPath Relative path within the zip archive
|
|
468
|
+
*/
|
|
469
|
+
async addFolderToZip(zip, folderPath, zipPath) {
|
|
470
|
+
try {
|
|
471
|
+
const entries = await this.workspace.fs.readdir(folderPath);
|
|
472
|
+
for (const entry of entries) {
|
|
473
|
+
const name = entry.name;
|
|
474
|
+
const fullPath = path.join(entry.parentPath, name);
|
|
475
|
+
if (entry.isFile()) {
|
|
476
|
+
if (name.startsWith('.') || qCodeReviewUtils_1.QCodeReviewUtils.shouldSkipFile(name) || !(0, fs_1.existsSync)(fullPath)) {
|
|
477
|
+
this.logging.info(`Skipping file - ${fullPath}`);
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
const content = await this.workspace.fs.readFile(fullPath);
|
|
481
|
+
let normalizedArtifactPath = qCodeReviewUtils_1.QCodeReviewUtils.convertToUnixPath(fullPath);
|
|
482
|
+
zip.file(`${zipPath}${normalizedArtifactPath}`, content);
|
|
483
|
+
}
|
|
484
|
+
else if (entry.isDirectory()) {
|
|
485
|
+
if (qCodeReviewUtils_1.QCodeReviewUtils.shouldSkipDirectory(name)) {
|
|
486
|
+
this.logging.info(`Skipping directory - ${fullPath}`);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
await this.addFolderToZip(zip, fullPath, zipPath);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
catch (error) {
|
|
494
|
+
this.logging.error(`Error adding folder to zip: ${error}`);
|
|
495
|
+
throw error;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Parse and validate findings JSON response
|
|
500
|
+
* @param findingsJson Raw JSON string from the code analysis response
|
|
501
|
+
* @param jobId Code scan job Id
|
|
502
|
+
* @param programmingLanguage programming language
|
|
503
|
+
* @returns Parsed and validated findings array
|
|
504
|
+
*/
|
|
505
|
+
parseFindings(findingsJson, jobId, programmingLanguage) {
|
|
506
|
+
try {
|
|
507
|
+
const findingsResponseJSON = JSON.parse(findingsJson);
|
|
508
|
+
// Normalize ruleId fields
|
|
509
|
+
for (const finding of findingsResponseJSON) {
|
|
510
|
+
if (finding['ruleId'] == null) {
|
|
511
|
+
finding['ruleId'] = undefined;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return qCodeReviewSchemas_1.Q_FINDINGS_SCHEMA.parse(findingsResponseJSON).map(issue => ({
|
|
515
|
+
startLine: issue.startLine - 1 >= 0 ? issue.startLine - 1 : 0,
|
|
516
|
+
endLine: issue.endLine,
|
|
517
|
+
comment: `${issue.title.trim()}: ${issue.description.text.trim()}`,
|
|
518
|
+
title: issue.title,
|
|
519
|
+
description: issue.description,
|
|
520
|
+
detectorId: issue.detectorId,
|
|
521
|
+
detectorName: issue.detectorName,
|
|
522
|
+
findingId: issue.findingId,
|
|
523
|
+
ruleId: issue.ruleId != null ? issue.ruleId : undefined,
|
|
524
|
+
relatedVulnerabilities: issue.relatedVulnerabilities,
|
|
525
|
+
severity: issue.severity,
|
|
526
|
+
recommendation: issue.remediation.recommendation,
|
|
527
|
+
suggestedFixes: issue.suggestedFixes != undefined ? issue.suggestedFixes : [],
|
|
528
|
+
scanJobId: jobId,
|
|
529
|
+
language: programmingLanguage,
|
|
530
|
+
autoDetected: false,
|
|
531
|
+
filePath: issue.filePath,
|
|
532
|
+
findingContext: issue.findingContext,
|
|
533
|
+
}));
|
|
534
|
+
}
|
|
535
|
+
catch (e) {
|
|
536
|
+
this.logging.error(`Error parsing findings in response: ${e}`);
|
|
537
|
+
throw new qCodeReviewErrors_1.QCodeReviewInternalError('Error parsing findings in response');
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Aggregate findings by file path
|
|
542
|
+
* @param findings Array of findings
|
|
543
|
+
* @param fileArtifacts Array of file artifacts being scanned
|
|
544
|
+
* @param folderArtifacts Array of folder artifacts being scanned
|
|
545
|
+
* @returns Array of findings grouped by resolved file path
|
|
546
|
+
*/
|
|
547
|
+
aggregateFindingsByFile(findings, fileArtifacts, folderArtifacts) {
|
|
548
|
+
const aggregatedCodeScanIssueMap = new Map();
|
|
549
|
+
for (const finding of findings) {
|
|
550
|
+
const resolvedPath = this.resolveFilePath(finding.filePath, fileArtifacts, folderArtifacts);
|
|
551
|
+
if (resolvedPath) {
|
|
552
|
+
if (aggregatedCodeScanIssueMap.has(resolvedPath)) {
|
|
553
|
+
aggregatedCodeScanIssueMap.get(resolvedPath)?.push(finding);
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
aggregatedCodeScanIssueMap.set(resolvedPath, [finding]);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
this.logging.warn(`Could not resolve finding file path: ${finding.filePath}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return Array.from(aggregatedCodeScanIssueMap.entries()).map(([filePath, issues]) => ({
|
|
564
|
+
filePath,
|
|
565
|
+
issues,
|
|
566
|
+
}));
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Resolve finding file path to actual file path
|
|
570
|
+
* @param findingPath Relative file path from the finding
|
|
571
|
+
* @param fileArtifacts Array of file artifacts being scanned
|
|
572
|
+
* @param folderArtifacts Array of folder artifacts being scanned
|
|
573
|
+
* @returns Resolved absolute file path or null if not found
|
|
574
|
+
*/
|
|
575
|
+
resolveFilePath(findingPath, fileArtifacts, folderArtifacts) {
|
|
576
|
+
// 1. Check if finding path matches one of the file artifacts
|
|
577
|
+
for (const fileArtifact of fileArtifacts) {
|
|
578
|
+
const normalizedFilePath = path.normalize(fileArtifact.path);
|
|
579
|
+
const normalizedFindingPath = path.normalize(findingPath);
|
|
580
|
+
if (normalizedFilePath.endsWith(normalizedFindingPath)) {
|
|
581
|
+
return normalizedFilePath;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
// 2. Check if finding path falls under one of the folder artifacts
|
|
585
|
+
for (const folderArtifact of folderArtifacts) {
|
|
586
|
+
const normalizedFolderPath = path.normalize(folderArtifact.path);
|
|
587
|
+
const normalizedFindingPath = path.normalize(findingPath);
|
|
588
|
+
// 2.1. Check if finding path falls under one of the subdirectories in folder artifact path
|
|
589
|
+
const folderSegments = normalizedFolderPath.split(path.sep);
|
|
590
|
+
// Find common suffix between folder path and finding path
|
|
591
|
+
let matchIndex = -1;
|
|
592
|
+
for (let i = folderSegments.length - 1; i >= 0; i--) {
|
|
593
|
+
const folderSuffix = folderSegments.slice(i).join(path.sep);
|
|
594
|
+
if (normalizedFindingPath.startsWith(folderSuffix + path.sep)) {
|
|
595
|
+
matchIndex = i;
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
// If common suffix is found, create the absolute path with it
|
|
600
|
+
if (matchIndex !== -1) {
|
|
601
|
+
const remainingPath = normalizedFindingPath.substring(folderSegments.slice(matchIndex).join(path.sep).length + 1);
|
|
602
|
+
const absolutePath = path.join(normalizedFolderPath, remainingPath);
|
|
603
|
+
if ((0, fs_1.existsSync)(absolutePath) && (0, fs_1.statSync)(absolutePath).isFile()) {
|
|
604
|
+
return absolutePath;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
// 2.2. Check if folder path + finding path gives the absolute file path
|
|
608
|
+
const filePath = path.join(folderArtifact.path, findingPath);
|
|
609
|
+
if ((0, fs_1.existsSync)(filePath) && (0, fs_1.statSync)(filePath).isFile()) {
|
|
610
|
+
return filePath;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
// 3. Check if finding already has absolute file path
|
|
614
|
+
const maybeAbsolutePath = path.normalize(findingPath);
|
|
615
|
+
if ((0, fs_1.existsSync)(maybeAbsolutePath) && (0, fs_1.statSync)(maybeAbsolutePath).isFile()) {
|
|
616
|
+
return maybeAbsolutePath;
|
|
617
|
+
}
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Checks if the operation has been cancelled by the user
|
|
622
|
+
* @param message Optional message to include in the cancellation error
|
|
623
|
+
* @throws Error if the operation has been cancelled
|
|
624
|
+
*/
|
|
625
|
+
checkCancellation(message = 'Command execution cancelled') {
|
|
626
|
+
qCodeReviewUtils_1.QCodeReviewUtils.checkCancellation(this.cancellationToken, this.logging, message);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
exports.QCodeReview = QCodeReview;
|
|
630
|
+
//# sourceMappingURL=qCodeReview.js.map
|