diff --git a/packages/amazonq/.changes/next-release/Feature-90704e88-3ab3-4030-8bc6-fdde95aa3923.json b/packages/amazonq/.changes/next-release/Feature-90704e88-3ab3-4030-8bc6-fdde95aa3923.json new file mode 100644 index 00000000000..10cc3d2f908 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Feature-90704e88-3ab3-4030-8bc6-fdde95aa3923.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "/review: Code reviews are now created with additional workspace context to enable grouping of related scans in the backend" +} diff --git a/packages/amazonq/test/unit/codewhisperer/service/securityScanHandler.test.ts b/packages/amazonq/test/unit/codewhisperer/service/securityScanHandler.test.ts index 41e3c1d42e2..704d5c5f2fb 100644 --- a/packages/amazonq/test/unit/codewhisperer/service/securityScanHandler.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/service/securityScanHandler.test.ts @@ -15,8 +15,9 @@ import { ListCodeScanFindingsResponse, pollScanJobStatus, SecurityScanTimedOutError, + generateScanName, } from 'aws-core-vscode/codewhisperer' -import { timeoutUtils } from 'aws-core-vscode/shared' +import { getStringHash, timeoutUtils } from 'aws-core-vscode/shared' import assert from 'assert' import sinon from 'sinon' import * as vscode from 'vscode' @@ -320,4 +321,37 @@ describe('securityScanHandler', function () { await assert.rejects(() => pollPromise, SecurityScanTimedOutError) }) }) + + describe('generateScanName', function () { + const clientId = 'ffffffff-ffff-ffff-ffff-ffffffffffff' + + it('generates scan name for FILE_AUTO scope', function () { + const result = generateScanName(['/some/root/path'], CodeAnalysisScope.FILE_AUTO, '/path/to/some/file') + assert.strictEqual(result, getStringHash(`${clientId}::/path/to/some/file::FILE_AUTO`)) + }) + + it('generates scan name for FILE_ON_DEMAND scope', function () { + const result = generateScanName(['/some/root/path'], CodeAnalysisScope.FILE_ON_DEMAND, '/path/to/some/file') + assert.strictEqual(result, getStringHash(`${clientId}::/path/to/some/file::FILE_ON_DEMAND`)) + }) + + it('generates scan name for PROJECT scope with a single project root', function () { + const result = generateScanName(['/some/root/path'], CodeAnalysisScope.PROJECT) + assert.strictEqual(result, getStringHash(`${clientId}::/some/root/path::PROJECT`)) + }) + + it('generates scan name for PROJECT scope with multiple project roots', function () { + const result = generateScanName(['/some/root/pathB', '/some/root/pathA'], CodeAnalysisScope.PROJECT) + assert.strictEqual(result, getStringHash(`${clientId}::/some/root/pathA,/some/root/pathB::PROJECT`)) + }) + + it('does not exceed 126 characters', function () { + let reallyDeepFilePath = '' + for (let i = 0; i < 100; i++) { + reallyDeepFilePath += '/some/deep/path' + } + const result = generateScanName(['/some/root/path'], CodeAnalysisScope.FILE_ON_DEMAND, reallyDeepFilePath) + assert.ok(result.length <= 126) + }) + }) }) diff --git a/packages/core/src/codewhisperer/commands/startSecurityScan.ts b/packages/core/src/codewhisperer/commands/startSecurityScan.ts index 5aef9184e2e..52f1fac5b4d 100644 --- a/packages/core/src/codewhisperer/commands/startSecurityScan.ts +++ b/packages/core/src/codewhisperer/commands/startSecurityScan.ts @@ -18,6 +18,7 @@ import { listScanResults, throwIfCancelled, getLoggerForScope, + generateScanName, } from '../service/securityScanHandler' import { runtimeLanguageContext } from '../util/runtimeLanguageContext' import { @@ -38,7 +39,6 @@ import path from 'path' import { ZipMetadata, ZipUtil } from '../util/zipUtil' import { debounce } from 'lodash' import { once } from '../../shared/utilities/functionUtils' -import { randomUUID } from '../../shared/crypto' import { CodeAnalysisScope, ProjectSizeExceededErrorMessage, SecurityScanStep } from '../models/constants' import { CodeScanJobFailedError, @@ -185,7 +185,7 @@ export async function startSecurityScan( } let artifactMap: ArtifactMap = {} const uploadStartTime = performance.now() - const scanName = randomUUID() + const scanName = generateScanName(projectPaths, scope, fileName) try { artifactMap = await getPresignedUrlAndUpload(client, zipMetadata, scope, scanName) } finally { diff --git a/packages/core/src/codewhisperer/index.ts b/packages/core/src/codewhisperer/index.ts index 565e9e3c238..3f62b3e9777 100644 --- a/packages/core/src/codewhisperer/index.ts +++ b/packages/core/src/codewhisperer/index.ts @@ -72,7 +72,12 @@ export { DocumentChangedSource, KeyStrokeHandler, DefaultDocumentChangedType } f export { ReferenceLogViewProvider } from './service/referenceLogViewProvider' export { LicenseUtil } from './util/licenseUtil' export { SecurityIssueProvider } from './service/securityIssueProvider' -export { listScanResults, mapToAggregatedList, pollScanJobStatus } from './service/securityScanHandler' +export { + listScanResults, + mapToAggregatedList, + pollScanJobStatus, + generateScanName, +} from './service/securityScanHandler' export { CodeWhispererCodeCoverageTracker } from './tracker/codewhispererCodeCoverageTracker' export { TelemetryHelper } from './util/telemetryHelper' export { LineSelection, LineTracker } from './tracker/lineTracker' diff --git a/packages/core/src/codewhisperer/service/securityScanHandler.ts b/packages/core/src/codewhisperer/service/securityScanHandler.ts index 2cf4b0d6227..23ee028d22f 100644 --- a/packages/core/src/codewhisperer/service/securityScanHandler.ts +++ b/packages/core/src/codewhisperer/service/securityScanHandler.ts @@ -46,6 +46,9 @@ import { runtimeLanguageContext } from '../util/runtimeLanguageContext' import { FeatureUseCase } from '../models/constants' import { UploadTestArtifactToS3Error } from '../../amazonqTest/error' import { ChatSessionManager } from '../../amazonqTest/chat/storages/chatSession' +import { getStringHash } from '../../shared/utilities/textUtilities' +import { getClientId } from '../../shared/telemetry/util' +import globals from '../../shared/extensionGlobals' export async function listScanResults( client: DefaultCodeWhispererClient, @@ -417,3 +420,21 @@ function getPollingTimeoutMsForScope(scope: CodeWhispererConstants.CodeAnalysisS ? CodeWhispererConstants.expressScanTimeoutMs : CodeWhispererConstants.standardScanTimeoutMs } + +/** + * Generates a scanName that unique identifies a user's workspace configuration for a Q code review. + * + * @param projectPaths List of project root paths + * @param scope {@link CodeWhispererConstants.CodeAnalysisScope} Scope of files included in the code review + * @param fileName File name of the file being reviewed, or pass undefined for workspace review + * @returns A string hash that uniquely identifies the workspace configuration + */ +export function generateScanName( + projectPaths: string[], + scope: CodeWhispererConstants.CodeAnalysisScope, + fileName?: string +) { + const clientId = getClientId(globals.globalState) + const projectId = fileName ?? projectPaths.sort((a, b) => a.localeCompare(b)).join(',') + return getStringHash(`${clientId}::${projectId}::${scope}`) +} diff --git a/packages/core/src/testE2E/codewhisperer/securityScan.test.ts b/packages/core/src/testE2E/codewhisperer/securityScan.test.ts index 730b9628290..62590cdfd7f 100644 --- a/packages/core/src/testE2E/codewhisperer/securityScan.test.ts +++ b/packages/core/src/testE2E/codewhisperer/securityScan.test.ts @@ -18,11 +18,11 @@ import { createScanJob, pollScanJobStatus, listScanResults, + generateScanName, } from '../../codewhisperer/service/securityScanHandler' import { makeTemporaryToolkitFolder } from '../../shared/filesystemUtilities' import fs from '../../shared/fs/fs' import { ZipUtil } from '../../codewhisperer/util/zipUtil' -import { randomUUID } from '../../shared/crypto' const filePromptWithSecurityIssues = `from flask import app @@ -95,7 +95,7 @@ describe('CodeWhisperer security scan', async function () { const projectPaths = zipUtil.getProjectPaths() const scope = CodeWhispererConstants.CodeAnalysisScope.PROJECT const zipMetadata = await zipUtil.generateZip(uri, scope) - const codeScanName = randomUUID() + const codeScanName = generateScanName(projectPaths, scope) let artifactMap try {