diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index c8893d445ea..1dea43e9993 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -442,17 +442,22 @@ }, { "command": "aws.amazonq.openSecurityIssuePanel", + "when": "false && view == aws.amazonq.SecurityIssuesTree && (viewItem == issueWithoutFix || viewItem == issueWithFix || viewItem == issueWithFixDisabled)", + "group": "inline@4" + }, + { + "command": "aws.amazonq.security.explain", "when": "view == aws.amazonq.SecurityIssuesTree && (viewItem == issueWithoutFix || viewItem == issueWithFix || viewItem == issueWithFixDisabled)", "group": "inline@4" }, { - "command": "aws.amazonq.security.ignore", + "command": "aws.amazonq.security.generateFix", "when": "view == aws.amazonq.SecurityIssuesTree && (viewItem == issueWithoutFix || viewItem == issueWithFix || viewItem == issueWithFixDisabled)", "group": "inline@5" }, { - "command": "aws.amazonq.security.generateFix", - "when": "view == aws.amazonq.SecurityIssuesTree && viewItem == issueWithoutFix", + "command": "aws.amazonq.security.ignore", + "when": "view == aws.amazonq.SecurityIssuesTree && (viewItem == issueWithoutFix || viewItem == issueWithFix || viewItem == issueWithFixDisabled)", "group": "inline@6" }, { @@ -535,16 +540,17 @@ "aws.amazonq.submenu.securityIssueMoreActions": [ { "command": "aws.amazonq.security.explain", + "when": "false", "group": "1_more@1" }, { "command": "aws.amazonq.applySecurityFix", - "when": "view == aws.amazonq.SecurityIssuesTree && viewItem == issueWithFix", + "when": "false && view == aws.amazonq.SecurityIssuesTree && viewItem == issueWithFix", "group": "1_more@3" }, { "command": "aws.amazonq.security.regenerateFix", - "when": "view == aws.amazonq.SecurityIssuesTree && viewItem == issueWithFix", + "when": "false && view == aws.amazonq.SecurityIssuesTree && viewItem == issueWithFix", "group": "1_more@4" }, { @@ -778,6 +784,7 @@ { "command": "aws.amazonq.security.explain", "title": "%AWS.command.amazonq.explainIssue%", + "icon": "$(search)", "enablement": "view == aws.amazonq.SecurityIssuesTree" }, { @@ -1325,26 +1332,40 @@ "fontCharacter": "\\f1de" } }, - "aws-schemas-registry": { + "aws-sagemaker-code-editor": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1df" } }, - "aws-schemas-schema": { + "aws-sagemaker-jupyter-lab": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1e0" } }, - "aws-stepfunctions-preview": { + "aws-schemas-registry": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1e1" } + }, + "aws-schemas-schema": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e2" + } + }, + "aws-stepfunctions-preview": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1e3" + } } }, "walkthroughs": [ diff --git a/packages/amazonq/src/lsp/chat/commands.ts b/packages/amazonq/src/lsp/chat/commands.ts index 9135be6d8d4..2876bf83b33 100644 --- a/packages/amazonq/src/lsp/chat/commands.ts +++ b/packages/amazonq/src/lsp/chat/commands.ts @@ -7,7 +7,8 @@ import { Commands, globals } from 'aws-core-vscode/shared' import { window } from 'vscode' import { AmazonQChatViewProvider } from './webviewProvider' import { CodeScanIssue } from 'aws-core-vscode/codewhisperer' -import { EditorContextExtractor } from 'aws-core-vscode/codewhispererChat' +import * as vscode from 'vscode' +import * as path from 'path' /** * TODO: Re-enable these once we can figure out which path they're going to live in @@ -21,30 +22,63 @@ export function registerCommands(provider: AmazonQChatViewProvider) { registerGenericCommand('aws.amazonq.optimizeCode', 'Optimize', provider), registerGenericCommand('aws.amazonq.generateUnitTests', 'Generate Tests', provider), - Commands.register('aws.amazonq.explainIssue', async (issue: CodeScanIssue) => { + Commands.register('aws.amazonq.explainIssue', async (issue: CodeScanIssue, filePath: string) => { void focusAmazonQPanel().then(async () => { - const editorContextExtractor = new EditorContextExtractor() - const extractedContext = await editorContextExtractor.extractContextForTrigger('ContextMenu') - const selectedCode = - extractedContext?.activeFileContext?.fileText - ?.split('\n') - .slice(issue.startLine, issue.endLine) - .join('\n') ?? '' - - // The message that gets sent to the UI - const uiMessage = [ - 'Explain the ', - issue.title, - ' issue in the following code:', - '\n```\n', - selectedCode, - '\n```', - ].join('') + if (issue && filePath) { + const range = new vscode.Range(issue.startLine, 0, issue.endLine, 0) + await vscode.workspace.openTextDocument(filePath).then((doc) => { + void vscode.window.showTextDocument(doc, { + selection: range, + viewColumn: vscode.ViewColumn.One, + preview: true, + }) + }) + } + + const lineRange = + issue.startLine === issue.endLine - 1 + ? `[${issue.startLine + 1}]` + : `[${issue.startLine + 1}, ${issue.endLine}]` + const visibleMessageInChat = `_Explain **${issue.title}** issue in **${path.basename(filePath)}** at \`${lineRange}\`_` + + // The message that gets sent to the backend + const contextMessage = `Provide a small description of the issue. You must not attempt to fix the issue. You should only give a small summary of it to the user. Code issue - ${JSON.stringify(issue)}` + + void provider.webview?.postMessage({ + command: 'sendToPrompt', + params: { + selection: '', + triggerType: 'contextMenu', + prompt: { + prompt: visibleMessageInChat, // what gets sent to the user + escapedPrompt: contextMessage, // what gets sent to the backend + }, + autoSubmit: true, + }, + }) + }) + }), + Commands.register('aws.amazonq.generateFix', async (issue: CodeScanIssue, filePath: string) => { + void focusAmazonQPanel().then(async () => { + if (issue && filePath) { + const range = new vscode.Range(issue.startLine, 0, issue.endLine, 0) + await vscode.workspace.openTextDocument(filePath).then((doc) => { + void vscode.window.showTextDocument(doc, { + selection: range, + viewColumn: vscode.ViewColumn.One, + preview: true, + }) + }) + } + + const lineRange = + issue.startLine === issue.endLine - 1 + ? `[${issue.startLine + 1}]` + : `[${issue.startLine + 1}, ${issue.endLine}]` + const visibleMessageInChat = `_Fix **${issue.title}** issue in **${path.basename(filePath)}** at \`${lineRange}\`_` // The message that gets sent to the backend - const contextMessage = `Explain the issue "${issue.title}" (${JSON.stringify( - issue - )}) and generate code demonstrating the fix` + const contextMessage = `Generate a fix for the following code issue. You must not explain the issue, just generate and explain the fix. The user should have the option to accept or reject the fix before any code is changed. Code issue - ${JSON.stringify(issue)}` void provider.webview?.postMessage({ command: 'sendToPrompt', @@ -52,7 +86,7 @@ export function registerCommands(provider: AmazonQChatViewProvider) { selection: '', triggerType: 'contextMenu', prompt: { - prompt: uiMessage, // what gets sent to the user + prompt: visibleMessageInChat, // what gets sent to the user escapedPrompt: contextMessage, // what gets sent to the backend }, autoSubmit: true, diff --git a/packages/amazonq/src/lsp/chat/messages.ts b/packages/amazonq/src/lsp/chat/messages.ts index b44ada0a134..bd6ee29b06d 100644 --- a/packages/amazonq/src/lsp/chat/messages.ts +++ b/packages/amazonq/src/lsp/chat/messages.ts @@ -70,7 +70,16 @@ import { v4 as uuidv4 } from 'uuid' import * as vscode from 'vscode' import { Disposable, LanguageClient, Position, TextDocumentIdentifier } from 'vscode-languageclient' import { AmazonQChatViewProvider } from './webviewProvider' -import { AuthUtil, ReferenceLogViewProvider } from 'aws-core-vscode/codewhisperer' +import { + AggregatedCodeScanIssue, + AuthUtil, + CodeAnalysisScope, + CodeWhispererSettings, + initSecurityScanRender, + ReferenceLogViewProvider, + SecurityIssueTreeViewProvider, + CodeWhispererConstants, +} from 'aws-core-vscode/codewhisperer' import { amazonQDiffScheme, AmazonQPromptSettings, messages, openUrl, isTextEditor } from 'aws-core-vscode/shared' import { DefaultAmazonQAppInitContext, @@ -84,6 +93,7 @@ import { isValidResponseError } from './error' import { decryptResponse, encryptRequest } from '../encryption' import { getCursorState } from '../utils' import { focusAmazonQPanel } from './commands' +import { ChatMessage } from '@aws/language-server-runtimes/server-interface' export function registerActiveEditorChangeListener(languageClient: LanguageClient) { let debounceTimer: NodeJS.Timeout | undefined @@ -298,7 +308,8 @@ export function registerMessageListeners( encryptionKey, provider, chatParams.tabId, - chatDisposable + chatDisposable, + languageClient ) } catch (e) { const errorMsg = `Error occurred during chat request: ${e}` @@ -314,7 +325,8 @@ export function registerMessageListeners( encryptionKey, provider, chatParams.tabId, - chatDisposable + chatDisposable, + languageClient ) } finally { chatStreamTokens.delete(chatParams.tabId) @@ -358,7 +370,8 @@ export function registerMessageListeners( encryptionKey, provider, message.params.tabId, - quickActionDisposable + quickActionDisposable, + languageClient ) break } @@ -610,6 +623,11 @@ async function handlePartialResult( ) { const decryptedMessage = await decryptResponse(partialResult, encryptionKey) + decryptedMessage.additionalMessages = decryptedMessage.additionalMessages?.filter( + (message) => + !(message.messageId !== undefined && message.messageId.endsWith(CodeWhispererConstants.findingsSuffix)) + ) + if (decryptedMessage.body !== undefined) { void provider.webview?.postMessage({ command: chatRequestType.method, @@ -630,10 +648,13 @@ async function handleCompleteResult( encryptionKey: Buffer | undefined, provider: AmazonQChatViewProvider, tabId: string, - disposable: Disposable + disposable: Disposable, + languageClient: LanguageClient ) { const decryptedMessage = await decryptResponse(result, encryptionKey) + handleSecurityFindings(decryptedMessage, languageClient) + void provider.webview?.postMessage({ command: chatRequestType.method, params: decryptedMessage, @@ -647,6 +668,37 @@ async function handleCompleteResult( disposable.dispose() } +function handleSecurityFindings( + decryptedMessage: { additionalMessages?: ChatMessage[] }, + languageClient: LanguageClient +): void { + if (decryptedMessage.additionalMessages === undefined || decryptedMessage.additionalMessages.length === 0) { + return + } + for (let i = decryptedMessage.additionalMessages.length - 1; i >= 0; i--) { + const message = decryptedMessage.additionalMessages[i] + if (message.messageId !== undefined && message.messageId.endsWith(CodeWhispererConstants.findingsSuffix)) { + if (message.body !== undefined) { + try { + const aggregatedCodeScanIssues: AggregatedCodeScanIssue[] = JSON.parse(message.body) + for (const aggregatedCodeScanIssue of aggregatedCodeScanIssues) { + for (const issue of aggregatedCodeScanIssue.issues) { + issue.visible = !CodeWhispererSettings.instance + .getIgnoredSecurityIssues() + .includes(issue.title) + } + } + initSecurityScanRender(aggregatedCodeScanIssues, undefined, CodeAnalysisScope.PROJECT) + SecurityIssueTreeViewProvider.focus() + } catch (e) { + languageClient.info('Failed to parse findings') + } + } + decryptedMessage.additionalMessages.splice(i, 1) + } + } +} + async function resolveChatResponse( requestMethod: string, params: any, diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index e9066678698..3fb3f70c523 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -167,6 +167,7 @@ export async function startLanguageServer( reroute: true, modelSelection: true, workspaceFilePath: vscode.workspace.workspaceFile?.fsPath, + agenticReviewer: true, }, window: { notifications: true, diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json index b3cf958c980..c8c091a609e 100644 --- a/packages/core/package.nls.json +++ b/packages/core/package.nls.json @@ -146,7 +146,7 @@ "AWS.command.amazonq.generateUnitTests": "Generate Tests", "AWS.command.amazonq.security.scan": "Run Project Review", "AWS.command.amazonq.security.fileScan": "Run File Review", - "AWS.command.amazonq.generateFix": "Generate Fix", + "AWS.command.amazonq.generateFix": "Fix", "AWS.command.amazonq.viewDetails": "View Details", "AWS.command.amazonq.explainIssue": "Explain", "AWS.command.amazonq.ignoreIssue": "Ignore Issue", diff --git a/packages/core/src/codewhisperer/activation.ts b/packages/core/src/codewhisperer/activation.ts index d6dd7fdc61d..11bbce5a817 100644 --- a/packages/core/src/codewhisperer/activation.ts +++ b/packages/core/src/codewhisperer/activation.ts @@ -476,6 +476,7 @@ export async function activate(context: ExtContext): Promise { CodeWhispererConstants.amazonqIgnoreNextLine ) ) + // SecurityIssueProvider.instance.cleanOldFiles() }), vscode.window.createTreeView(SecurityIssueTreeViewProvider.viewType, { treeDataProvider: SecurityIssueTreeViewProvider.instance, diff --git a/packages/core/src/codewhisperer/commands/basicCommands.ts b/packages/core/src/codewhisperer/commands/basicCommands.ts index f2b67c49593..c7df25bf824 100644 --- a/packages/core/src/codewhisperer/commands/basicCommands.ts +++ b/packages/core/src/codewhisperer/commands/basicCommands.ts @@ -12,7 +12,6 @@ import { DefaultCodeWhispererClient } from '../client/codewhisperer' import { confirmStopSecurityScan, startSecurityScan } from './startSecurityScan' import { SecurityPanelViewProvider } from '../views/securityPanelViewProvider' import { - codeFixState, CodeScanIssue, CodeScansState, codeScanState, @@ -50,7 +49,7 @@ import { once } from '../../shared/utilities/functionUtils' import { focusAmazonQPanel } from '../../codewhispererChat/commands/registerCommands' import { removeDiagnostic } from '../service/diagnosticsProvider' import { SsoAccessTokenProvider } from '../../auth/sso/ssoAccessTokenProvider' -import { ToolkitError, getErrorMsg, getTelemetryReason, getTelemetryReasonDesc } from '../../shared/errors' +import { ToolkitError, getTelemetryReason, getTelemetryReasonDesc } from '../../shared/errors' import { isRemoteWorkspace } from '../../shared/vscode/env' import { isBuilderIdConnection } from '../../auth/connection' import globals from '../../shared/extensionGlobals' @@ -61,7 +60,6 @@ import { SecurityIssueProvider } from '../service/securityIssueProvider' import { CodeWhispererSettings } from '../util/codewhispererSettings' import { closeDiff, getPatchedCode } from '../../shared/utilities/diffUtils' import { insertCommentAboveLine } from '../../shared/utilities/commentUtils' -import { startCodeFixGeneration } from './startCodeFixGeneration' import { DefaultAmazonQAppInitContext } from '../../amazonq/apps/initContext' import path from 'path' import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' @@ -369,9 +367,9 @@ export const openSecurityIssuePanel = Commands.declare( const targetFilePath: string = issue instanceof IssueItem ? issue.filePath : filePath await showSecurityIssueWebview(context.extensionContext, targetIssue, targetFilePath) - if (targetIssue.suggestedFixes.length === 0) { - await generateFix.execute(targetIssue, targetFilePath, 'webview', true, false) - } + // if (targetIssue.suggestedFixes.length === 0) { + // await generateFix.execute(targetIssue, targetFilePath, 'webview', true, false) + // } telemetry.codewhisperer_codeScanIssueViewDetails.emit({ findingId: targetIssue.findingId, detectorId: targetIssue.detectorId, @@ -685,116 +683,13 @@ export const generateFix = Commands.declare( { id: 'aws.amazonq.security.generateFix' }, (client: DefaultCodeWhispererClient, context: ExtContext) => async ( - issue: CodeScanIssue | IssueItem | undefined, + issueItem: IssueItem, filePath: string, source: Component, refresh: boolean = false, shouldOpenSecurityIssuePanel: boolean = true ) => { - const targetIssue: CodeScanIssue | undefined = issue instanceof IssueItem ? issue.issue : issue - const targetFilePath: string = issue instanceof IssueItem ? issue.filePath : filePath - const targetSource: Component = issue instanceof IssueItem ? 'tree' : source - if (!targetIssue) { - return - } - if (targetIssue.ruleId === CodeWhispererConstants.sasRuleId) { - getLogger().warn('GenerateFix is not available for SAS findings.') - return - } - await telemetry.codewhisperer_codeScanIssueGenerateFix.run(async () => { - try { - if (shouldOpenSecurityIssuePanel) { - await vscode.commands - .executeCommand('aws.amazonq.openSecurityIssuePanel', targetIssue, targetFilePath) - .then(undefined, (e) => { - getLogger().error('Failed to open security issue panel: %s', e.message) - }) - } - await updateSecurityIssueWebview({ - isGenerateFixLoading: true, - // eslint-disable-next-line unicorn/no-null - generateFixError: null, - context: context.extensionContext, - filePath: targetFilePath, - shouldRefreshView: false, - }) - - codeFixState.setToRunning() - let hasSuggestedFix = false - const { suggestedFix, jobId } = await startCodeFixGeneration( - client, - targetIssue, - targetFilePath, - targetIssue.findingId - ) - // redact the fix if the user disabled references and there is a reference - if ( - // TODO: enable references later for scans - // !CodeWhispererSettings.instance.isSuggestionsWithCodeReferencesEnabled() && - suggestedFix?.references && - suggestedFix?.references?.length > 0 - ) { - getLogger().debug( - `Received fix with reference and user settings disallow references. Job ID: ${jobId}` - ) - // TODO: re-enable notifications once references published - // void vscode.window.showInformationMessage( - // 'Your settings do not allow code generation with references.' - // ) - hasSuggestedFix = false - } else { - hasSuggestedFix = suggestedFix !== undefined - } - telemetry.record({ includesFix: hasSuggestedFix }) - const updatedIssue: CodeScanIssue = { - ...targetIssue, - fixJobId: jobId, - suggestedFixes: - hasSuggestedFix && suggestedFix - ? [ - { - code: suggestedFix.codeDiff, - description: suggestedFix.description ?? '', - references: suggestedFix.references, - }, - ] - : [], - } - await updateSecurityIssueWebview({ - issue: updatedIssue, - isGenerateFixLoading: false, - filePath: targetFilePath, - context: context.extensionContext, - shouldRefreshView: true, - }) - - SecurityIssueProvider.instance.updateIssue(updatedIssue, targetFilePath) - SecurityIssueTreeViewProvider.instance.refresh() - } catch (err) { - const error = err instanceof Error ? err : new TypeError('Unexpected error') - await updateSecurityIssueWebview({ - issue: targetIssue, - isGenerateFixLoading: false, - generateFixError: getErrorMsg(error, true), - filePath: targetFilePath, - context: context.extensionContext, - shouldRefreshView: false, - }) - SecurityIssueProvider.instance.updateIssue(targetIssue, targetFilePath) - SecurityIssueTreeViewProvider.instance.refresh() - throw err - } finally { - telemetry.record({ - component: targetSource, - detectorId: targetIssue.detectorId, - findingId: targetIssue.findingId, - ruleId: targetIssue.ruleId, - variant: refresh ? 'refresh' : undefined, - autoDetected: targetIssue.autoDetected, - codewhispererCodeScanJobId: targetIssue.scanJobId, - }) - } - }) + await vscode.commands.executeCommand('aws.amazonq.generateFix', issueItem.issue, issueItem.filePath) } ) @@ -825,18 +720,18 @@ export const rejectFix = Commands.declare( export const regenerateFix = Commands.declare( { id: 'aws.amazonq.security.regenerateFix' }, () => async (issue: CodeScanIssue | IssueItem | undefined, filePath: string, source: Component) => { - const targetIssue: CodeScanIssue | undefined = issue instanceof IssueItem ? issue.issue : issue - const targetFilePath: string = issue instanceof IssueItem ? issue.filePath : filePath - const targetSource: Component = issue instanceof IssueItem ? 'tree' : source - const updatedIssue = await rejectFix.execute(targetIssue, targetFilePath) - await generateFix.execute(updatedIssue, targetFilePath, targetSource, true) + // const targetIssue: CodeScanIssue | undefined = issue instanceof IssueItem ? issue.issue : issue + // const targetFilePath: string = issue instanceof IssueItem ? issue.filePath : filePath + // const targetSource: Component = issue instanceof IssueItem ? 'tree' : source + // const updatedIssue = await rejectFix.execute(targetIssue, targetFilePath) + // await generateFix.execute(updatedIssue, targetFilePath, targetSource, true) } ) export const explainIssue = Commands.declare( { id: 'aws.amazonq.security.explain' }, () => async (issueItem: IssueItem) => { - await vscode.commands.executeCommand('aws.amazonq.explainIssue', issueItem.issue) + await vscode.commands.executeCommand('aws.amazonq.explainIssue', issueItem.issue, issueItem.filePath) } ) diff --git a/packages/core/src/codewhisperer/commands/startSecurityScan.ts b/packages/core/src/codewhisperer/commands/startSecurityScan.ts index d04fe6effc3..5ff6d13bd91 100644 --- a/packages/core/src/codewhisperer/commands/startSecurityScan.ts +++ b/packages/core/src/codewhisperer/commands/startSecurityScan.ts @@ -401,7 +401,7 @@ export function showSecurityScanResults( zipMetadata: ZipMetadata, totalIssues: number ) { - initSecurityScanRender(securityRecommendationCollection, context, editor, scope) + initSecurityScanRender(securityRecommendationCollection, editor, scope) if (scope === CodeWhispererConstants.CodeAnalysisScope.PROJECT) { populateCodeScanLogStream(zipMetadata.scannedFiles) @@ -439,7 +439,7 @@ export function showScanResultsInChat( break } - initSecurityScanRender(securityRecommendationCollection, context, editor, scope) + initSecurityScanRender(securityRecommendationCollection, editor, scope) if (totalIssues > 0) { SecurityIssueTreeViewProvider.focus() } diff --git a/packages/core/src/codewhisperer/models/constants.ts b/packages/core/src/codewhisperer/models/constants.ts index 2a095de2f3e..eca88915e3f 100644 --- a/packages/core/src/codewhisperer/models/constants.ts +++ b/packages/core/src/codewhisperer/models/constants.ts @@ -903,3 +903,5 @@ export const predictionTrackerDefaultConfig = { maxAgeMs: 30000, maxSupplementalContext: 15, } + +export const findingsSuffix = '_qCodeReviewFindings' diff --git a/packages/core/src/codewhisperer/service/diagnosticsProvider.ts b/packages/core/src/codewhisperer/service/diagnosticsProvider.ts index 72407cd80f5..e110f37c121 100644 --- a/packages/core/src/codewhisperer/service/diagnosticsProvider.ts +++ b/packages/core/src/codewhisperer/service/diagnosticsProvider.ts @@ -25,7 +25,6 @@ export const securityScanRender: SecurityScanRender = { export function initSecurityScanRender( securityRecommendationList: AggregatedCodeScanIssue[], - context: vscode.ExtensionContext, editor: vscode.TextEditor | undefined, scope: CodeAnalysisScope ) { @@ -40,6 +39,7 @@ export function initSecurityScanRender( updateSecurityIssuesForProviders(securityRecommendation, scope === CodeAnalysisScope.FILE_AUTO) } securityScanRender.initialized = true + // SecurityIssueProvider.instance.cleanOldFiles() } function updateSecurityIssuesForProviders(securityRecommendation: AggregatedCodeScanIssue, isAutoScope?: boolean) { diff --git a/packages/core/src/codewhisperer/service/securityIssueCodeActionProvider.ts b/packages/core/src/codewhisperer/service/securityIssueCodeActionProvider.ts index f1d01494d54..4dc7bceebe7 100644 --- a/packages/core/src/codewhisperer/service/securityIssueCodeActionProvider.ts +++ b/packages/core/src/codewhisperer/service/securityIssueCodeActionProvider.ts @@ -66,7 +66,7 @@ export class SecurityIssueCodeActionProvider implements vscode.CodeActionProvide `Amazon Q: Explain "${issue.title}"`, vscode.CodeActionKind.QuickFix ) - const explainWithQArgs = [issue] + const explainWithQArgs = [issue, group.filePath] explainWithQ.command = { title: 'Explain with Amazon Q', command: 'aws.amazonq.explainIssue', diff --git a/packages/core/src/codewhisperer/service/securityIssueHoverProvider.ts b/packages/core/src/codewhisperer/service/securityIssueHoverProvider.ts index b82c10063e6..95ea3869cb0 100644 --- a/packages/core/src/codewhisperer/service/securityIssueHoverProvider.ts +++ b/packages/core/src/codewhisperer/service/securityIssueHoverProvider.ts @@ -10,7 +10,6 @@ import path from 'path' import { AuthUtil } from '../util/authUtil' import { TelemetryHelper } from '../util/telemetryHelper' import { SecurityIssueProvider } from './securityIssueProvider' -import { amazonqCodeIssueDetailsTabTitle } from '../models/constants' export class SecurityIssueHoverProvider implements vscode.HoverProvider { static #instance: SecurityIssueHoverProvider @@ -79,23 +78,23 @@ export class SecurityIssueHoverProvider implements vscode.HoverProvider { `${suggestedFix?.code && suggestedFix.description !== '' ? suggestedFix.description : issue.recommendation.text}\n\n` ) - const viewDetailsCommand = this._getCommandMarkdown( - 'aws.amazonq.openSecurityIssuePanel', - [issue, filePath], - 'eye', - 'View Details', - `Open "${amazonqCodeIssueDetailsTabTitle}"` - ) - markdownString.appendMarkdown(viewDetailsCommand) - const explainWithQCommand = this._getCommandMarkdown( 'aws.amazonq.explainIssue', - [issue], + [issue, filePath], 'comment', 'Explain', 'Explain with Amazon Q' ) - markdownString.appendMarkdown(' | ' + explainWithQCommand) + markdownString.appendMarkdown(explainWithQCommand) + + const generateFixCommand = this._getCommandMarkdown( + 'aws.amazonq.generateFix', + [issue, filePath], + 'comment', + 'Fix', + 'Generate Fix for Issue' + ) + markdownString.appendMarkdown(' | ' + generateFixCommand) const ignoreIssueCommand = this._getCommandMarkdown( 'aws.amazonq.security.ignore', @@ -115,22 +114,6 @@ export class SecurityIssueHoverProvider implements vscode.HoverProvider { ) markdownString.appendMarkdown(' | ' + ignoreSimilarIssuesCommand) - if (suggestedFix && suggestedFix.code) { - const applyFixCommand = this._getCommandMarkdown( - 'aws.amazonq.applySecurityFix', - [issue, filePath, 'hover'], - 'wrench', - 'Fix', - 'Fix with Amazon Q' - ) - markdownString.appendMarkdown(' | ' + applyFixCommand) - - markdownString.appendMarkdown('### Suggested Fix Preview\n') - markdownString.appendMarkdown( - `${this._makeCodeBlock(suggestedFix.code, issue.detectorId.split('/').shift())}\n` - ) - } - return markdownString } @@ -145,60 +128,4 @@ export class SecurityIssueHoverProvider implements vscode.HoverProvider { } return `![${severity}](severity-${severity.toLowerCase()}.svg)` } - - /** - * Creates a markdown string to render a code diff block for a given code block. Lines - * that are highlighted red indicate deletion while lines highlighted in green indicate - * addition. An optional language can be provided for syntax highlighting on lines which are - * not additions or deletions. - * - * @param code The code containing the diff - * @param language The language for syntax highlighting - * @returns The markdown string - */ - private _makeCodeBlock(code: string, language?: string) { - const lines = code - .replaceAll('\n\\ No newline at end of file', '') - .replaceAll('--- buggyCode\n', '') - .replaceAll('+++ fixCode\n', '') - .split('\n') - const maxLineChars = lines.reduce((acc, curr) => Math.max(acc, curr.length), 0) - const paddedLines = lines.map((line) => line.padEnd(maxLineChars + 2)) - - // Group the lines into sections so consecutive lines of the same type can be placed in - // the same span below - const sections = [paddedLines[0]] - let i = 1 - while (i < paddedLines.length) { - if (paddedLines[i][0] === sections[sections.length - 1][0]) { - sections[sections.length - 1] += '\n' + paddedLines[i] - } else { - sections.push(paddedLines[i]) - } - i++ - } - - // Return each section with the correct syntax highlighting and background color - return sections - .map( - (section) => ` - - -\`\`\`${section.startsWith('-') || section.startsWith('+') ? 'diff' : section.startsWith('@@') ? undefined : language} -${section} -\`\`\` - - -` - ) - .join('
') - } } diff --git a/packages/core/src/codewhisperer/service/securityIssueProvider.ts b/packages/core/src/codewhisperer/service/securityIssueProvider.ts index 61957e6eca5..4727aa43caa 100644 --- a/packages/core/src/codewhisperer/service/securityIssueProvider.ts +++ b/packages/core/src/codewhisperer/service/securityIssueProvider.ts @@ -5,6 +5,9 @@ import * as vscode from 'vscode' import { AggregatedCodeScanIssue, CodeScanIssue, SuggestedFix } from '../models/model' +import { randomUUID } from '../../shared/crypto' +import fs = require('fs') +import path from 'path' export class SecurityIssueProvider { static #instance: SecurityIssueProvider public static get instance() { @@ -20,6 +23,15 @@ export class SecurityIssueProvider { this._issues = issues } + private _id: string = randomUUID() + public get id() { + return this._id + } + + public set id(id: string) { + this._id = id + } + public handleDocumentChange(event: vscode.TextDocumentChangeEvent) { // handleDocumentChange function may be triggered while testing by our own code generation. if (!event.contentChanges || event.contentChanges.length === 0) { @@ -157,4 +169,28 @@ export class SecurityIssueProvider { (i) => i.title === issue.title && i.startLine === issue.startLine && i.endLine === issue.endLine ) } + + public cleanOldFiles(maxfiles = 100): void { + const issuesJson = JSON.stringify(this._issues) + const dirPath = path.join(__dirname, '..', '..', '..', '..', '..', '..', 'findings') + fs.existsSync(dirPath) || fs.mkdirSync(dirPath) + fs.writeFileSync(path.join(dirPath, `SecurityIssues-${this._id}.json`), issuesJson, 'utf8') + const files = fs.readdirSync(dirPath) + if (files.length > maxfiles) { + type Stat = { fileName: string; mtime: number } + const stats: Stat[] = [] + for (const file of files) { + const stat = fs.statSync(path.join(dirPath, file[0])) + stats.push({ + fileName: file[0], + mtime: stat.mtime.getTime(), + }) + } + const sortedStats = stats.sort((a: Stat, b: Stat) => a.mtime - b.mtime) + const numberToDelete = files.length - maxfiles + for (let i = 0; i < numberToDelete; i++) { + fs.rmSync(path.join(dirPath, sortedStats[i].fileName)) + } + } + } } diff --git a/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts b/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts index d7c93f70423..b1f7f73907b 100644 --- a/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts +++ b/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts @@ -189,11 +189,8 @@ export class IssueItem extends vscode.TreeItem { } private getDescription() { - const positionStr = `[Ln ${this.issue.startLine + 1}, Col 1]` const groupingStrategy = CodeIssueGroupingStrategyState.instance.getState() - return groupingStrategy !== CodeIssueGroupingStrategy.FileLocation - ? `${path.basename(this.filePath)} ${positionStr}` - : positionStr + return groupingStrategy !== CodeIssueGroupingStrategy.FileLocation ? `${path.basename(this.filePath)}` : '' } private getContextValue() { diff --git a/packages/core/src/codewhisperer/views/securityIssue/securityIssueWebview.ts b/packages/core/src/codewhisperer/views/securityIssue/securityIssueWebview.ts index d511bd9a5f6..632283215ab 100644 --- a/packages/core/src/codewhisperer/views/securityIssue/securityIssueWebview.ts +++ b/packages/core/src/codewhisperer/views/securityIssue/securityIssueWebview.ts @@ -109,7 +109,10 @@ export class SecurityIssueWebview extends VueWebview { } public generateFix() { - void vscode.commands.executeCommand('aws.amazonq.security.generateFix', this.issue, this.filePath, 'webview') + const args = [this.issue] + void this.navigateToFile()?.then(() => { + void vscode.commands.executeCommand('aws.amazonq.generateFix', ...args) + }) } public regenerateFix() {