Skip to content

Commit 6be09e0

Browse files
authored
CodeWhisperer security scans support for Ruby files. (#4033)
* CodeWhisperer security scans support for Ruby files along with Test Cases. * CW: Code Refactoring * CW: Updates in Ruby Test Cases in Security scans * CW: Text changes --------- Co-authored-by: Laxman Reddy <[email protected]>
1 parent 314db55 commit 6be09e0

File tree

5 files changed

+412
-0
lines changed

5 files changed

+412
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type" : "feature",
3+
"description" : "CodeWhisperer security scans support ruby files."
4+
}

jetbrains-core/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/sessionconfig/CodeScanSessionConfig.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ sealed class CodeScanSessionConfig(
223223
CodewhispererLanguage.Json -> CloudFormationJsonCodeScanSessionConfig(file, project)
224224
CodewhispererLanguage.Tf,
225225
CodewhispererLanguage.Hcl -> TerraformCodeScanSessionConfig(file, project)
226+
CodewhispererLanguage.Ruby -> RubyCodeScanSessionConfig(file, project)
226227
else -> fileFormatNotSupported(file.extension ?: "")
227228
}
228229
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.services.codewhisperer.codescan.sessionconfig
5+
6+
import com.intellij.openapi.project.Project
7+
import com.intellij.openapi.roots.ProjectRootManager
8+
import com.intellij.openapi.vfs.VirtualFile
9+
import com.intellij.util.containers.addIfNotNull
10+
import software.aws.toolkits.core.utils.exists
11+
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
12+
import software.aws.toolkits.resources.message
13+
import java.io.IOException
14+
15+
internal class RubyCodeScanSessionConfig(
16+
private val selectedFile: VirtualFile,
17+
private val project: Project
18+
) : CodeScanSessionConfig(selectedFile, project) {
19+
20+
private val importRegex = Regex("^(require|require_relative|load|include|extend)\\s+('[^']+'|\"[^\"]+\"|\\w+)(\\s+as\\s+(\\w+))?")
21+
private val projectContentRoots = ProjectRootManager.getInstance(project).contentRoots
22+
override val sourceExt: List<String> = listOf(".rb")
23+
24+
override fun overallJobTimeoutInSeconds(): Long = CodeWhispererConstants.RUBY_CODE_SCAN_TIMEOUT_IN_SECONDS
25+
override fun getPayloadLimitInBytes(): Int = CodeWhispererConstants.RUBY_PAYLOAD_LIMIT_IN_BYTES
26+
27+
private fun generateModulePaths(inputPath: String): MutableSet<String> {
28+
val positionOfExt = inputPath.indexOf(sourceExt[0])
29+
val inputPathString = if (positionOfExt != -1) {
30+
inputPath.substring(0, positionOfExt).trim()
31+
} else {
32+
inputPath
33+
}
34+
val inputPaths = inputPathString.split('/')
35+
val outputPaths = mutableSetOf<String>()
36+
for (i in inputPaths.indices) {
37+
val outputPath = inputPaths.subList(0, i + 1).joinToString(FILE_SEPARATOR.toString())
38+
outputPaths.add(outputPath)
39+
}
40+
return outputPaths
41+
}
42+
43+
private fun getModulePath(modulePathStr: String): MutableSet<String> {
44+
val pos = modulePathStr.indexOf(" ${CodeWhispererConstants.AS} ")
45+
val modifiedModulePathStr = if (pos != -1) {
46+
modulePathStr.substring(0, pos)
47+
} else {
48+
modulePathStr
49+
}
50+
51+
return generateModulePaths(modifiedModulePathStr.replace(Regex("[\",'\\s()]"), "").trim())
52+
}
53+
54+
private fun extractModulePaths(importStr: String): Set<String> {
55+
val modulePaths = mutableSetOf<String>()
56+
val requireKeyword = CodeWhispererConstants.REQUIRE
57+
val requireRelativeKeyword = CodeWhispererConstants.REQUIRE_RELATIVE
58+
val includeKeyword = CodeWhispererConstants.INCLUDE
59+
val extendKeyword = CodeWhispererConstants.EXTEND
60+
val loadKeyword = CodeWhispererConstants.LOAD
61+
62+
var keyword: String? = null
63+
64+
when {
65+
importStr.startsWith(requireRelativeKeyword) -> {
66+
keyword = requireRelativeKeyword
67+
}
68+
importStr.startsWith(requireKeyword) -> {
69+
keyword = requireKeyword
70+
}
71+
importStr.startsWith(includeKeyword) -> {
72+
keyword = includeKeyword
73+
}
74+
importStr.startsWith(extendKeyword) -> {
75+
keyword = extendKeyword
76+
}
77+
importStr.startsWith(loadKeyword) -> {
78+
keyword = loadKeyword
79+
}
80+
}
81+
82+
if (keyword != null) {
83+
val modulePathStr = importStr
84+
.substring(keyword.length)
85+
.trim()
86+
.replace(Regex("\\s+"), "")
87+
modulePaths.addAll(getModulePath(modulePathStr))
88+
}
89+
90+
return modulePaths
91+
}
92+
93+
fun parseImports(file: VirtualFile): List<String> {
94+
val imports = mutableSetOf<String>()
95+
try {
96+
file.inputStream.use {
97+
it.bufferedReader().lines().forEach { line ->
98+
val importMatcher = importRegex.toPattern().matcher(line)
99+
if (importMatcher.find()) {
100+
val modulePathLine = line.replace(";", "")
101+
val goalImports = extractModulePaths(modulePathLine)
102+
imports.addAll(goalImports)
103+
}
104+
}
105+
}
106+
} catch (e: IOException) {
107+
error(message("codewhisperer.codescan.cannot_read_file", file.path))
108+
}
109+
return imports.toList()
110+
}
111+
112+
override fun getImportedFiles(file: VirtualFile, includedSourceFiles: Set<String>): List<String> {
113+
val importedFiles = mutableListOf<String>()
114+
val imports = parseImports(file)
115+
val importedFilePaths = mutableListOf<String>()
116+
117+
projectContentRoots.forEach { root ->
118+
imports.forEach { importPath ->
119+
val importedFilePath = getPath(root.path, importPath + sourceExt[0])
120+
if (importedFilePath?.exists() == true) {
121+
importedFilePaths.addIfNotNull(importedFilePath.toFile().toVirtualFile()?.path)
122+
}
123+
}
124+
}
125+
126+
val validSourceFiles = importedFilePaths.filter { it !in includedSourceFiles }
127+
validSourceFiles.forEach { validFile ->
128+
importedFiles.add(validFile)
129+
}
130+
return importedFiles
131+
}
132+
}

jetbrains-core/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererConstants.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ object CodeWhispererConstants {
3939
const val USING: String = "using"
4040
const val GLOBAL_USING: String = "global using"
4141
const val STATIC: String = "static"
42+
const val REQUIRE: String = "require"
43+
const val REQUIRE_RELATIVE: String = "require_relative"
44+
const val LOAD: String = "load"
45+
const val INCLUDE: String = "include"
46+
const val EXTEND: String = "extend"
47+
const val AS: String = " as "
4248

4349
// TODO: this is currently set to 2050 to account for the server side 0.5 TPS and and extra 50 ms buffer to
4450
// avoid ThrottlingException as much as possible.
@@ -63,6 +69,8 @@ object CodeWhispererConstants {
6369
const val JAVA_PAYLOAD_LIMIT_IN_BYTES = 1024 * 1024 // 1MB
6470
const val CSHARP_CODE_SCAN_TIMEOUT_IN_SECONDS: Long = 60
6571
const val CSHARP_PAYLOAD_LIMIT_IN_BYTES = 1024 * 1024 // 1MB
72+
const val RUBY_CODE_SCAN_TIMEOUT_IN_SECONDS: Long = 60
73+
const val RUBY_PAYLOAD_LIMIT_IN_BYTES = 1024 * 1024 // 1MB
6674
const val CLOUDFORMATION_CODE_SCAN_TIMEOUT_IN_SECONDS: Long = 60
6775
const val CLOUDFORMATION_PAYLOAD_LIMIT_IN_BYTES = 1024 * 200 // 200KB
6876
const val TERRAFORM_CODE_SCAN_TIMEOUT_IN_SECONDS: Long = 60

0 commit comments

Comments
 (0)