Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Amazon Q: Added telemetry metrics and modified customer facing messages for Q Codescans. #4442

Merged
merged 29 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e961765
Adding specific exception metrics and client messages for QCA
laileni-aws May 7, 2024
d413c31
Fixing typos
laileni-aws May 7, 2024
23a452e
Merge branch 'main' into bugfix/qca
laileni-aws May 7, 2024
957e394
Refactoring code
laileni-aws May 8, 2024
7541886
Removing redundant code
laileni-aws May 8, 2024
3452936
Merge branch 'main' into bugfix/qca
laileni-aws May 8, 2024
ca5b44b
minor changes
laileni-aws May 8, 2024
6ea7cab
Merge branch 'main' into bugfix/qca
laileni-aws May 8, 2024
b79c086
Merge branch 'main' into bugfix/qca
laileni-aws May 8, 2024
3ef86c0
Addressing comments
laileni-aws May 8, 2024
a971704
Adding null checks to error messages
laileni-aws May 9, 2024
681e492
Adding null checks to error messages
laileni-aws May 9, 2024
bd6b008
Removed redundant code in messagesBundle.props
laileni-aws May 9, 2024
8c228a9
Removed redundant functions
laileni-aws May 9, 2024
2b3ea12
Removing the LOG.error messages from codescans
laileni-aws May 9, 2024
ebec8b7
Added Change log
laileni-aws May 9, 2024
6ef786c
Updated Change log
laileni-aws May 9, 2024
f2d07e3
Adding UploadArtifactToS3 error message to CX
laileni-aws May 9, 2024
4fc83e6
Merge branch 'main' into bugfix/qca
laileni-aws May 9, 2024
6534934
Merge branch 'main' into bugfix/qca
laileni-aws May 13, 2024
0483f8b
Merge branch 'main' into bugfix/qca
laileni-aws May 13, 2024
e43ba48
Merge branch 'main' into bugfix/qca
laileni-aws May 13, 2024
8836eb2
Addressing comments
laileni-aws May 14, 2024
2227370
Merge branch 'main' into bugfix/qca
laileni-aws May 14, 2024
dc49bc1
Merge branch 'main' into bugfix/qca
laileni-aws May 14, 2024
8a5d1eb
Addressing comments
laileni-aws May 14, 2024
99903ee
Merge branch 'main' into bugfix/qca
laileni-aws May 15, 2024
b0c26fa
dry run CICD
laileni-aws May 15, 2024
dfe47d5
Merge branch 'main' into bugfix/qca
laileni-aws May 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type" : "bugfix",
"description" : "Security Scan: Improved error notifications"
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.cppFileName
import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.cppTestLeftContext
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.sessionconfig.CodeScanSessionConfig
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
import software.aws.toolkits.jetbrains.utils.rules.RunWithRealCredentials.RequiresRealCredentials
import software.aws.toolkits.resources.message

Expand Down Expand Up @@ -58,14 +56,7 @@ class CodeWhispererCodeScanIntegrationTest : CodeWhispererIntegrationTestBase()
projectRule.fixture.openFileInEditor(file.virtualFile)
}
testCodeScanWithErrorMessage(
message(
"codewhisperer.codescan.file_too_large",
CodeScanSessionConfig.create(
file.virtualFile,
projectRule.project,
CodeWhispererConstants.CodeAnalysisScope.PROJECT
).getPresentablePayloadLimit()
)
message("codewhisperer.codescan.file_too_large")
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import software.aws.toolkits.resources.message

open class CodeWhispererCodeScanException(override val message: String?) : RuntimeException()

open class UploadCodeScanException(override val message: String?) : Exception()

internal fun noFileOpenError(): Nothing =
throw CodeWhispererCodeScanException(message("codewhisperer.codescan.no_file_open"))

internal fun codeScanFailed(): Nothing =
throw CodeWhispererCodeScanException(message("codewhisperer.codescan.service_error"))
internal fun codeScanFailed(errorMessage: String): Nothing =
throw Exception(errorMessage)

internal fun cannotFindFile(file: String?): Nothing =
error(message("codewhisperer.codescan.file_not_found", file ?: ""))
Expand All @@ -22,11 +24,14 @@ internal fun cannotFindBuildArtifacts(): Nothing =
internal fun fileFormatNotSupported(format: String): Nothing =
throw CodeWhispererCodeScanException(message("codewhisperer.codescan.file_ext_not_supported", format))

internal fun fileTooLarge(presentableSize: String): Nothing =
throw CodeWhispererCodeScanException(message("codewhisperer.codescan.file_too_large", presentableSize))
internal fun fileTooLarge(): Nothing =
throw CodeWhispererCodeScanException(message("codewhisperer.codescan.file_too_large"))

internal fun uploadArtifactFailedError(errorMessage: String): Nothing =
throw UploadCodeScanException(errorMessage)

internal fun uploadArtifactFailedError(): Nothing =
throw CodeWhispererCodeScanException(message("codewhisperer.codescan.upload_to_s3_failed"))
internal fun invalidSourceZipError(): Nothing =
throw CodeWhispererCodeScanException(message("codewhisperer.codescan.invalid_source_zip_telemetry"))

internal fun noSupportedFilesError(): Nothing =
throw CodeWhispererCodeScanException(message("codewhisperer.codescan.unsupported_language_error"))
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ class CodeWhispererCodeScanManager(val project: Project) {
if (e.cause?.message?.contains("com.intellij.openapi.compiler.CompilerPaths") == true) {
message("codewhisperer.codescan.java_module_not_found")
} else {
message("codewhisperer.codescan.service_error")
ashishrp-aws marked this conversation as resolved.
Show resolved Hide resolved
null
}
}
else -> null
Expand All @@ -324,10 +324,14 @@ class CodeWhispererCodeScanManager(val project: Project) {

fun handleException(coroutineContext: CoroutineContext, e: Exception, scope: CodeWhispererConstants.CodeAnalysisScope): String {
val errorMessage = when (e) {
is CodeWhispererException -> e.awsErrorDetails().errorMessage() ?: message("codewhisperer.codescan.service_error")
is CodeWhispererCodeScanException -> e.message
is CodeWhispererException -> e.awsErrorDetails().errorMessage() ?: message("codewhisperer.codescan.run_scan_error")
is CodeWhispererCodeScanException -> when (e.message) {
message("codewhisperer.codescan.invalid_source_zip_telemetry") -> message("codewhisperer.codescan.run_scan_error")
else -> e.message
}
is UploadCodeScanException -> message("codewhisperer.codescan.upload_to_s3_failed")
is WaiterTimeoutException, is TimeoutCancellationException -> message("codewhisperer.codescan.scan_timed_out")
is CancellationException -> "Code scan job cancelled by user."
is CancellationException -> message("codewhisperer.codescan.cancelled_by_user_exception")
else -> null
} ?: message("codewhisperer.codescan.run_scan_error")

Expand Down Expand Up @@ -358,7 +362,22 @@ class CodeWhispererCodeScanManager(val project: Project) {
"stacktrace: ${e.stackTrace.contentDeepToString()}"
}
}
return errorMessage

val telemetryErrorMessage = when (e) {
is CodeWhispererException -> e.awsErrorDetails().errorMessage() ?: message("codewhisperer.codescan.run_scan_error_telemetry")
is CodeWhispererCodeScanException -> when (e.message) {
message("codewhisperer.codescan.no_file_open") -> message("codewhisperer.codescan.no_file_open_telemetry")
laileni-aws marked this conversation as resolved.
Show resolved Hide resolved
message("codewhisperer.codescan.unsupported_language_error") -> message("codewhisperer.codescan.unsupported_language_error_telemetry")
message("codewhisperer.codescan.file_too_large") -> message("codewhisperer.codescan.file_too_large_telemetry")
else -> e.message
}
is UploadCodeScanException -> e.message
is WaiterTimeoutException, is TimeoutCancellationException -> message("codewhisperer.codescan.scan_timed_out")
is CancellationException -> message("codewhisperer.codescan.cancelled_by_user_exception")
else -> e.message
ashishrp-aws marked this conversation as resolved.
Show resolved Hide resolved
} ?: message("codewhisperer.codescan.run_scan_error_telemetry")

return telemetryErrorMessage
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhisperer
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.TOTAL_MILLIS_IN_SECOND
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.notifyErrorCodeWhispererUsageLimit
import software.aws.toolkits.jetbrains.utils.assertIsNonDispatchThread
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CodewhispererLanguage
import java.io.File
import java.io.FileInputStream
Expand Down Expand Up @@ -152,7 +153,8 @@ class CodeWhispererCodeScanSession(val sessionContext: CodeScanSessionContext) {
"Status: ${createCodeScanResponse.status()} for request id: ${createCodeScanResponse.responseMetadata().requestId()}"
}
}
codeScanFailed()
val errorMessage = createCodeScanResponse.errorMessage()?.let { it } ?: message("codewhisperer.codescan.run_scan_error_telemetry")
codeScanFailed(errorMessage)
}
val jobId = createCodeScanResponse.jobId()
codeScanResponseContext = codeScanResponseContext.copy(codeScanJobId = jobId)
Expand Down Expand Up @@ -188,7 +190,8 @@ class CodeWhispererCodeScanSession(val sessionContext: CodeScanSessionContext) {
"Status: ${getCodeScanResponse.status()} for request id: ${getCodeScanResponse.responseMetadata().requestId()}"
}
}
codeScanFailed()
val errorMessage = getCodeScanResponse.errorMessage()?.let { it } ?: message("codewhisperer.codescan.run_scan_error_telemetry")
codeScanFailed(errorMessage)
}
}

Expand Down Expand Up @@ -250,7 +253,11 @@ class CodeWhispererCodeScanSession(val sessionContext: CodeScanSessionContext) {
/**
* Creates an upload URL and uplaods the zip file to the presigned URL
*/
fun createUploadUrlAndUpload(zipFile: File, artifactType: String, codeScanName: String): CreateUploadUrlResponse = try {
fun createUploadUrlAndUpload(zipFile: File, artifactType: String, codeScanName: String): CreateUploadUrlResponse {
// Throw error if zipFile is invalid.
if (!zipFile.exists()) {
invalidSourceZipError()
laileni-aws marked this conversation as resolved.
Show resolved Hide resolved
}
val fileMd5: String = Base64.getEncoder().encodeToString(DigestUtils.md5(FileInputStream(zipFile)))
val createUploadUrlResponse = createUploadUrl(fileMd5, artifactType, codeScanName)
val url = createUploadUrlResponse.uploadUrl()
Expand All @@ -265,20 +272,21 @@ class CodeWhispererCodeScanSession(val sessionContext: CodeScanSessionContext) {
createUploadUrlResponse.kmsKeyArn(),
createUploadUrlResponse.requestHeaders()
)
createUploadUrlResponse
} catch (e: Exception) {
LOG.error { "Security scan failed. Something went wrong uploading artifacts: ${e.message}" }
uploadArtifactFailedError()
return createUploadUrlResponse
}

fun createUploadUrl(md5Content: String, artifactType: String, codeScanName: String): CreateUploadUrlResponse = clientAdaptor.createUploadUrl(
CreateUploadUrlRequest.builder()
.contentMd5(md5Content)
.artifactType(artifactType)
.uploadIntent(getUploadIntent(sessionContext.codeAnalysisScope))
.uploadContext(UploadContext.fromCodeAnalysisUploadContext(CodeAnalysisUploadContext.builder().codeScanName(codeScanName).build()))
.build()
)
fun createUploadUrl(md5Content: String, artifactType: String, codeScanName: String): CreateUploadUrlResponse = try {
clientAdaptor.createUploadUrl(
CreateUploadUrlRequest.builder()
.contentMd5(md5Content)
.artifactType(artifactType)
.uploadIntent(getUploadIntent(sessionContext.codeAnalysisScope))
.uploadContext(UploadContext.fromCodeAnalysisUploadContext(CodeAnalysisUploadContext.builder().codeScanName(codeScanName).build()))
.build()
)
} catch (e: Exception) {
throw e
}

private fun getUploadIntent(scope: CodeWhispererConstants.CodeAnalysisScope): UploadIntent = when (scope) {
CodeWhispererConstants.CodeAnalysisScope.FILE -> UploadIntent.AUTOMATIC_FILE_SECURITY_SCAN
Expand All @@ -287,25 +295,30 @@ class CodeWhispererCodeScanSession(val sessionContext: CodeScanSessionContext) {

@Throws(IOException::class)
fun uploadArtifactToS3(url: String, uploadId: String, fileToUpload: File, md5: String, kmsArn: String?, requestHeaders: Map<String, String>?) {
val uploadIdJson = """{"uploadId":"$uploadId"}"""
HttpRequests.put(url, "application/zip").userAgent(AwsClientManager.getUserAgent()).tuner {
if (requestHeaders.isNullOrEmpty()) {
it.setRequestProperty(CONTENT_MD5, md5)
it.setRequestProperty(CONTENT_TYPE, APPLICATION_ZIP)
it.setRequestProperty(SERVER_SIDE_ENCRYPTION, AWS_KMS)
if (kmsArn?.isNotEmpty() == true) {
it.setRequestProperty(SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID, kmsArn)
}
it.setRequestProperty(SERVER_SIDE_ENCRYPTION_CONTEXT, Base64.getEncoder().encodeToString(uploadIdJson.toByteArray()))
} else {
requestHeaders.forEach { entry ->
it.setRequestProperty(entry.key, entry.value)
try {
val uploadIdJson = """{"uploadId":"$uploadId"}"""
HttpRequests.put(url, "application/zip").userAgent(AwsClientManager.getUserAgent()).tuner {
if (requestHeaders.isNullOrEmpty()) {
it.setRequestProperty(CONTENT_MD5, md5)
it.setRequestProperty(CONTENT_TYPE, APPLICATION_ZIP)
it.setRequestProperty(SERVER_SIDE_ENCRYPTION, AWS_KMS)
if (kmsArn?.isNotEmpty() == true) {
it.setRequestProperty(SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID, kmsArn)
}
it.setRequestProperty(SERVER_SIDE_ENCRYPTION_CONTEXT, Base64.getEncoder().encodeToString(uploadIdJson.toByteArray()))
} else {
requestHeaders.forEach { entry ->
it.setRequestProperty(entry.key, entry.value)
}
}
}.connect {
val connection = it.connection as HttpURLConnection
connection.setFixedLengthStreamingMode(fileToUpload.length())
IoUtils.copy(fileToUpload.inputStream(), connection.outputStream)
}
}.connect {
val connection = it.connection as HttpURLConnection
connection.setFixedLengthStreamingMode(fileToUpload.length())
IoUtils.copy(fileToUpload.inputStream(), connection.outputStream)
} catch (e: Exception) {
val errorMessage = e.message?.let { it } ?: message("codewhisperer.codescan.run_scan_error_telemetry")
throw uploadArtifactFailedError(errorMessage)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhisperer
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.DEFAULT_PAYLOAD_LIMIT_IN_BYTES
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.FILE_SCAN_PAYLOAD_SIZE_LIMIT_IN_BYTES
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.FILE_SCAN_TIMEOUT_IN_SECONDS
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.TOTAL_BYTES_IN_KB
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.TOTAL_BYTES_IN_MB
import software.aws.toolkits.telemetry.CodewhispererLanguage
import java.io.File
import java.nio.file.Files
Expand Down Expand Up @@ -87,7 +85,7 @@ class CodeScanSessionConfig(

// Fail fast if the selected file size is greater than the payload limit.
if (selectedFile != null && selectedFile.length > getPayloadLimitInBytes()) {
fileTooLarge(getPresentablePayloadLimit())
ashishrp-aws marked this conversation as resolved.
Show resolved Hide resolved
fileTooLarge()
}

val start = Instant.now().toEpochMilli()
Expand Down Expand Up @@ -131,11 +129,6 @@ class CodeScanSessionConfig(
*/
fun createPayloadTimeoutInSeconds(): Long = CODE_SCAN_CREATE_PAYLOAD_TIMEOUT_IN_SECONDS

fun getPresentablePayloadLimit(): String = when (getPayloadLimitInBytes() >= TOTAL_BYTES_IN_MB) {
true -> "${getPayloadLimitInBytes() / TOTAL_BYTES_IN_MB}MB"
false -> "${getPayloadLimitInBytes() / TOTAL_BYTES_IN_KB}KB"
}

private fun countLinesInVirtualFile(virtualFile: VirtualFile): Int {
val bufferedReader = virtualFile.inputStream.bufferedReader()
return bufferedReader.useLines { lines -> lines.count() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,21 +274,21 @@ class CodeWhispererCodeFileScanTest : CodeWhispererCodeScanTestBase(PythonCodeIn
val codeScanResponse = codeScanSessionSpy.run()
assertThat(codeScanResponse).isInstanceOf<CodeScanResponse.Failure>()
assertThat(codeScanResponse.responseContext.payloadContext).isEqualTo(payloadContext)
assertThat((codeScanResponse as CodeScanResponse.Failure).failureReason).isInstanceOf<CodeWhispererCodeScanException>()
assertThat((codeScanResponse as CodeScanResponse.Failure).failureReason).isInstanceOf<Exception>()
}
}

@Test
fun `test run() - createCodeScan error`() {
mockClient.stub {
onGeneric { createCodeScan(any(), any()) }.thenThrow(CodeWhispererException::class.java)
onGeneric { createCodeScan(any(), any()) }.thenThrow(CodeWhispererCodeScanException::class.java)
}

runBlocking {
val codeScanResponse = codeScanSessionSpy.run()
assertThat(codeScanResponse).isInstanceOf<CodeScanResponse.Failure>()
assertThat(codeScanResponse.responseContext.payloadContext).isEqualTo(payloadContext)
assertThat((codeScanResponse as CodeScanResponse.Failure).failureReason).isInstanceOf<CodeWhispererException>()
assertThat((codeScanResponse as CodeScanResponse.Failure).failureReason).isInstanceOf<CodeWhispererCodeScanException>()
}
}

Expand All @@ -302,7 +302,7 @@ class CodeWhispererCodeFileScanTest : CodeWhispererCodeScanTestBase(PythonCodeIn
val codeScanResponse = codeScanSessionSpy.run()
assertThat(codeScanResponse).isInstanceOf<CodeScanResponse.Failure>()
assertThat(codeScanResponse.responseContext.payloadContext).isEqualTo(payloadContext)
assertThat((codeScanResponse as CodeScanResponse.Failure).failureReason).isInstanceOf<CodeWhispererCodeScanException>()
assertThat((codeScanResponse as CodeScanResponse.Failure).failureReason).isInstanceOf<Exception>()
}
}

Expand Down