Skip to content

Commit e22a0a9

Browse files
Merge master into feature/q-transform-staging
2 parents f0789f0 + 25dee3f commit e22a0a9

File tree

7 files changed

+130
-48
lines changed

7 files changed

+130
-48
lines changed

packages/core/src/codewhisperer/commands/startSecurityScan.ts

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as vscode from 'vscode'
77
import * as nls from 'vscode-nls'
88
import { ArtifactMap, DefaultCodeWhispererClient } from '../client/codewhisperer'
99
import { isCloud9 } from '../../shared/extensionUtilities'
10-
import { initSecurityScanRender, securityScanRender } from '../service/diagnosticsProvider'
10+
import { initSecurityScanRender } from '../service/diagnosticsProvider'
1111
import { SecurityPanelViewProvider } from '../views/securityPanelViewProvider'
1212
import { getLogger } from '../../shared/logger'
1313
import { makeLogger } from '../../shared/logger/activation'
@@ -21,7 +21,13 @@ import {
2121
getLoggerForScope,
2222
} from '../service/securityScanHandler'
2323
import { runtimeLanguageContext } from '../util/runtimeLanguageContext'
24-
import { AggregatedCodeScanIssue, CodeScansState, codeScanState, CodeScanTelemetryEntry } from '../models/model'
24+
import {
25+
AggregatedCodeScanIssue,
26+
CodeScansState,
27+
codeScanState,
28+
CodeScanStoppedError,
29+
CodeScanTelemetryEntry,
30+
} from '../models/model'
2531
import { cancel, ok } from '../../shared/localizedText'
2632
import { getFileExt } from '../util/commonUtil'
2733
import { getDirSize } from '../../shared/filesystemUtilities'
@@ -96,6 +102,9 @@ export async function startSecurityScan(
96102
* Step 0: Initial Code Scan telemetry
97103
*/
98104
const codeScanStartTime = performance.now()
105+
if (scope === CodeAnalysisScope.FILE) {
106+
CodeScansState.instance.setLatestScanTime(codeScanStartTime)
107+
}
99108
let serviceInvocationStartTime = 0
100109
const codeScanTelemetryEntry: CodeScanTelemetryEntry = {
101110
codewhispererLanguage: runtimeLanguageContext.getLanguageContext(
@@ -120,7 +129,7 @@ export async function startSecurityScan(
120129
/**
121130
* Step 1: Generate zip
122131
*/
123-
throwIfCancelled(scope)
132+
throwIfCancelled(scope, codeScanStartTime)
124133
const zipUtil = new ZipUtil()
125134
const zipMetadata = await zipUtil.generateZip(editor.document.uri, scope)
126135
const projectPath = zipUtil.getProjectPath(editor.document.uri)
@@ -139,7 +148,7 @@ export async function startSecurityScan(
139148
/**
140149
* Step 2: Get presigned Url, upload and clean up
141150
*/
142-
throwIfCancelled(scope)
151+
throwIfCancelled(scope, codeScanStartTime)
143152
let artifactMap: ArtifactMap = {}
144153
const uploadStartTime = performance.now()
145154
const scanName = randomUUID()
@@ -153,7 +162,7 @@ export async function startSecurityScan(
153162
/**
154163
* Step 3: Create scan job
155164
*/
156-
throwIfCancelled(scope)
165+
throwIfCancelled(scope, codeScanStartTime)
157166
serviceInvocationStartTime = performance.now()
158167
const scanJob = await createScanJob(
159168
client,
@@ -171,16 +180,16 @@ export async function startSecurityScan(
171180
/**
172181
* Step 4: Polling mechanism on scan job status
173182
*/
174-
throwIfCancelled(scope)
175-
const jobStatus = await pollScanJobStatus(client, scanJob.jobId, scope)
183+
throwIfCancelled(scope, codeScanStartTime)
184+
const jobStatus = await pollScanJobStatus(client, scanJob.jobId, scope, codeScanStartTime)
176185
if (jobStatus === 'Failed') {
177186
throw new Error('Security scan job failed.')
178187
}
179188

180189
/**
181190
* Step 5: Process and render scan results
182191
*/
183-
throwIfCancelled(scope)
192+
throwIfCancelled(scope, codeScanStartTime)
184193
logger.verbose(`Security scan job succeeded and start processing result.`)
185194
const securityRecommendationCollection = await listScanResults(
186195
client,
@@ -198,27 +207,22 @@ export async function startSecurityScan(
198207
)
199208
codeScanTelemetryEntry.codewhispererCodeScanTotalIssues = total
200209
codeScanTelemetryEntry.codewhispererCodeScanIssuesWithFixes = withFixes
201-
throwIfCancelled(scope)
210+
throwIfCancelled(scope, codeScanStartTime)
202211
logger.verbose(`Security scan totally found ${total} issues. ${withFixes} of them have fixes.`)
203-
if (codeScanStartTime > securityScanRender.lastUpdated) {
204-
showSecurityScanResults(
205-
securityPanelViewProvider,
206-
securityRecommendationCollection,
207-
editor,
208-
context,
209-
scope,
210-
zipMetadata,
211-
total,
212-
codeScanStartTime
213-
)
214-
} else {
215-
logger.verbose('Received issues from older scan, discarding the results')
216-
}
212+
showSecurityScanResults(
213+
securityPanelViewProvider,
214+
securityRecommendationCollection,
215+
editor,
216+
context,
217+
scope,
218+
zipMetadata,
219+
total
220+
)
217221

218222
logger.verbose(`Security scan completed.`)
219223
} catch (error) {
220224
getLogger().error('Security scan failed.', error)
221-
if (codeScanState.isCancelling()) {
225+
if (error instanceof CodeScanStoppedError) {
222226
codeScanTelemetryEntry.result = 'Cancelled'
223227
} else {
224228
errorPromptHelper(error as Error, scope)
@@ -257,14 +261,13 @@ export function showSecurityScanResults(
257261
context: vscode.ExtensionContext,
258262
scope: CodeWhispererConstants.CodeAnalysisScope,
259263
zipMetadata: ZipMetadata,
260-
totalIssues: number,
261-
codeScanStartTime: number
264+
totalIssues: number
262265
) {
263266
if (isCloud9()) {
264267
securityPanelViewProvider.addLines(securityRecommendationCollection, editor)
265268
void vscode.commands.executeCommand('workbench.view.extension.aws-codewhisperer-security-panel')
266269
} else {
267-
initSecurityScanRender(securityRecommendationCollection, context, editor, scope, codeScanStartTime)
270+
initSecurityScanRender(securityRecommendationCollection, context, editor, scope)
268271
if (scope === CodeWhispererConstants.CodeAnalysisScope.PROJECT) {
269272
void vscode.commands.executeCommand('workbench.action.problems.focus')
270273
}

packages/core/src/codewhisperer/models/constants.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,13 +232,17 @@ export const codeFileScanJobTimeoutSeconds = 60 //1 minute
232232

233233
export const projectSizeCalculateTimeoutSeconds = 10
234234

235-
export const codeScanJobPollingIntervalSeconds = 1
235+
export const codeScanJobPollingIntervalSeconds = 5
236+
237+
export const fileScanPollingDelaySeconds = 10
238+
239+
export const projectScanPollingDelaySeconds = 30
236240

237241
export const artifactTypeSource = 'SourceCode'
238242

239243
export const codeScanFindingsSchema = 'codescan/findings/1.0'
240244

241-
export const autoScanDebounceDelaySeconds = 2
245+
export const autoScanDebounceDelaySeconds = 5
242246

243247
export const codewhispererDiagnosticSourceLabel = 'Amazon Q '
244248

packages/core/src/codewhisperer/models/model.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export class CodeScansState {
121121
onDidChangeState = this.#onDidChangeState.event
122122

123123
private exceedsMonthlyQuota = false
124+
private latestScanTime: number | undefined = undefined
124125

125126
static #instance: CodeScansState
126127
static get instance() {
@@ -158,6 +159,14 @@ export class CodeScansState {
158159
isMonthlyQuotaExceeded() {
159160
return this.exceedsMonthlyQuota
160161
}
162+
163+
setLatestScanTime(time: number) {
164+
this.latestScanTime = time
165+
}
166+
167+
getLatestScanTime() {
168+
return this.latestScanTime
169+
}
161170
}
162171

163172
export interface AcceptedSuggestionEntry {

packages/core/src/codewhisperer/service/diagnosticsProvider.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,18 @@ import { CodeAnalysisScope, codewhispererDiagnosticSourceLabel } from '../models
1212
interface SecurityScanRender {
1313
securityDiagnosticCollection: vscode.DiagnosticCollection | undefined
1414
initialized: boolean
15-
lastUpdated: number
1615
}
1716

1817
export const securityScanRender: SecurityScanRender = {
1918
securityDiagnosticCollection: undefined,
2019
initialized: false,
21-
lastUpdated: 0,
2220
}
2321

2422
export function initSecurityScanRender(
2523
securityRecommendationList: AggregatedCodeScanIssue[],
2624
context: vscode.ExtensionContext,
2725
editor: vscode.TextEditor,
28-
scope: CodeAnalysisScope,
29-
codeScanStartTime: number
26+
scope: CodeAnalysisScope
3027
) {
3128
securityScanRender.initialized = false
3229
if (scope === CodeAnalysisScope.FILE) {
@@ -39,7 +36,6 @@ export function initSecurityScanRender(
3936
updateSecurityIssueHoverAndCodeActions(securityRecommendation)
4037
})
4138
securityScanRender.initialized = true
42-
securityScanRender.lastUpdated = codeScanStartTime
4339
}
4440

4541
function updateSecurityIssueHoverAndCodeActions(securityRecommendation: AggregatedCodeScanIssue) {

packages/core/src/codewhisperer/service/securityScanHandler.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,18 @@ function mapToAggregatedList(codeScanIssueMap: Map<string, RawCodeScanIssue[]>,
9999
export async function pollScanJobStatus(
100100
client: DefaultCodeWhispererClient,
101101
jobId: string,
102-
scope: CodeWhispererConstants.CodeAnalysisScope
102+
scope: CodeWhispererConstants.CodeAnalysisScope,
103+
codeScanStartTime: number
103104
) {
105+
// We don't expect to get results immediately, so sleep for some time initially to not make unnecessary calls
106+
await sleep(getPollingDelayForScope(scope))
107+
104108
const logger = getLoggerForScope(scope)
105109
logger.verbose(`Polling scan job status...`)
106110
let status: string = 'Pending'
107111
let timer: number = 0
108112
while (true) {
109-
throwIfCancelled(scope)
113+
throwIfCancelled(scope, codeScanStartTime)
110114
const req: codewhispererClient.GetCodeScanRequest = {
111115
jobId: jobId,
112116
}
@@ -118,7 +122,7 @@ export async function pollScanJobStatus(
118122
logger.verbose(`Complete Polling scan job status.`)
119123
break
120124
}
121-
throwIfCancelled(scope)
125+
throwIfCancelled(scope, codeScanStartTime)
122126
await sleep(CodeWhispererConstants.codeScanJobPollingIntervalSeconds * 1000)
123127
timer += CodeWhispererConstants.codeScanJobPollingIntervalSeconds
124128
const timeoutSeconds =
@@ -208,18 +212,23 @@ function getMd5(fileName: string) {
208212
return hasher.digest('base64')
209213
}
210214

211-
export function throwIfCancelled(scope: CodeWhispererConstants.CodeAnalysisScope) {
215+
export function throwIfCancelled(scope: CodeWhispererConstants.CodeAnalysisScope, codeScanStartTime: number) {
212216
switch (scope) {
213217
case CodeWhispererConstants.CodeAnalysisScope.PROJECT:
214218
if (codeScanState.isCancelling()) {
215219
throw new CodeScanStoppedError()
216220
}
217221
break
218-
case CodeWhispererConstants.CodeAnalysisScope.FILE:
219-
if (!CodeScansState.instance.isScansEnabled()) {
222+
case CodeWhispererConstants.CodeAnalysisScope.FILE: {
223+
const latestCodeScanStartTime = CodeScansState.instance.getLatestScanTime()
224+
if (
225+
!CodeScansState.instance.isScansEnabled() ||
226+
(latestCodeScanStartTime && latestCodeScanStartTime > codeScanStartTime)
227+
) {
220228
throw new CodeScanStoppedError()
221229
}
222230
break
231+
}
223232
default:
224233
getLogger().warn(`Unknown code analysis scope: ${scope}`)
225234
break
@@ -263,3 +272,9 @@ export async function uploadArtifactToS3(
263272
export function getLoggerForScope(scope: CodeWhispererConstants.CodeAnalysisScope) {
264273
return scope === CodeWhispererConstants.CodeAnalysisScope.FILE ? getNullLogger() : getLogger()
265274
}
275+
276+
function getPollingDelayForScope(scope: CodeWhispererConstants.CodeAnalysisScope) {
277+
return scope === CodeWhispererConstants.CodeAnalysisScope.FILE
278+
? CodeWhispererConstants.fileScanPollingDelaySeconds
279+
: CodeWhispererConstants.projectScanPollingDelaySeconds
280+
}

packages/core/src/test/codewhisperer/commands/startSecurityScan.test.ts

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -296,21 +296,64 @@ describe('startSecurityScan', function () {
296296
} as CodewhispererSecurityScan)
297297
})
298298

299-
it('Should not show security scan results if a later scan already finished', async function () {
299+
it('Should cancel a scan if a newer one has started', async function () {
300300
getFetchStubWithResponse({ status: 200, statusText: 'testing stub' })
301-
const commandSpy = sinon.spy(vscode.commands, 'executeCommand')
302-
const securityScanRenderSpy = sinon.spy(diagnosticsProvider, 'initSecurityScanRender')
303-
diagnosticsProvider.securityScanRender.lastUpdated = Date.now() + 60
301+
await model.CodeScansState.instance.setScansEnabled(true)
302+
303+
const scanPromise = startSecurityScan.startSecurityScan(
304+
mockSecurityPanelViewProvider,
305+
editor,
306+
createClient(),
307+
extensionContext,
308+
CodeAnalysisScope.FILE
309+
)
310+
await startSecurityScan.startSecurityScan(
311+
mockSecurityPanelViewProvider,
312+
editor,
313+
createClient(),
314+
extensionContext,
315+
CodeAnalysisScope.FILE
316+
)
317+
await scanPromise
318+
assertTelemetry('codewhisperer_securityScan', [
319+
{
320+
result: 'Cancelled',
321+
reason: 'Security scan stopped by user.',
322+
},
323+
{
324+
result: 'Succeeded',
325+
},
326+
])
327+
})
304328

329+
it('Should not cancel a project scan if a file scan has started', async function () {
330+
getFetchStubWithResponse({ status: 200, statusText: 'testing stub' })
305331
await model.CodeScansState.instance.setScansEnabled(true)
332+
333+
const scanPromise = startSecurityScan.startSecurityScan(
334+
mockSecurityPanelViewProvider,
335+
editor,
336+
createClient(),
337+
extensionContext,
338+
CodeAnalysisScope.PROJECT
339+
)
306340
await startSecurityScan.startSecurityScan(
307341
mockSecurityPanelViewProvider,
308342
editor,
309343
createClient(),
310344
extensionContext,
311345
CodeAnalysisScope.FILE
312346
)
313-
assert.ok(commandSpy.neverCalledWith('workbench.action.problems.focus'))
314-
assert.ok(securityScanRenderSpy.notCalled)
347+
await scanPromise
348+
assertTelemetry('codewhisperer_securityScan', [
349+
{
350+
result: 'Succeeded',
351+
codewhispererCodeScanScope: 'FILE',
352+
},
353+
{
354+
result: 'Succeeded',
355+
codewhispererCodeScanScope: 'PROJECT',
356+
},
357+
])
315358
})
316359
})

packages/core/src/testE2E/codewhisperer/securityScan.test.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ describe('CodeWhisperer security scan', async function () {
8888
returns artifactMap, projectPath and codeScanName
8989
*/
9090
async function securityJobSetup(editor: vscode.TextEditor) {
91+
const codeScanStartTime = performance.now()
9192
const zipUtil = new ZipUtil()
9293
const uri = editor.document.uri
9394

@@ -106,6 +107,7 @@ describe('CodeWhisperer security scan', async function () {
106107
artifactMap: artifactMap,
107108
projectPath: projectPath,
108109
codeScanName: codeScanName,
110+
codeScanStartTime: codeScanStartTime,
109111
}
110112
}
111113

@@ -130,7 +132,12 @@ describe('CodeWhisperer security scan', async function () {
130132
scope,
131133
securityJobSetupResult.codeScanName
132134
)
133-
const jobStatus = await pollScanJobStatus(client, scanJob.jobId, scope)
135+
const jobStatus = await pollScanJobStatus(
136+
client,
137+
scanJob.jobId,
138+
scope,
139+
securityJobSetupResult.codeScanStartTime
140+
)
134141
const securityRecommendationCollection = await listScanResults(
135142
client,
136143
scanJob.jobId,
@@ -165,7 +172,12 @@ describe('CodeWhisperer security scan', async function () {
165172
)
166173

167174
//get job status and result
168-
const jobStatus = await pollScanJobStatus(client, scanJob.jobId, scope)
175+
const jobStatus = await pollScanJobStatus(
176+
client,
177+
scanJob.jobId,
178+
scope,
179+
securityJobSetupResult.codeScanStartTime
180+
)
169181
const securityRecommendationCollection = await listScanResults(
170182
client,
171183
scanJob.jobId,

0 commit comments

Comments
 (0)