Skip to content

Commit 6e6ca6b

Browse files
authored
CodeWhisperer security scans support for Ruby files. (#4133)
* CW: Ruby language support for security scans * Change Log for Ruby Security scan * CW: Code Refactoring * CW: Code Refactoring * Changes in test case by adding import statement * CW: Code Change in import statement * Merge conflicts --------- Co-authored-by: Laxman Reddy <[email protected]>
1 parent de45a9b commit 6e6ca6b

File tree

10 files changed

+381
-16
lines changed

10 files changed

+381
-16
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+
}

src/codewhisperer/models/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ export const codeScanJavaPayloadSizeLimitBytes = Math.pow(2, 20) // 1 MB
200200

201201
export const codeScanCsharpPayloadSizeLimitBytes = Math.pow(2, 20) // 1 MB
202202

203+
export const codeScanRubyPayloadSizeLimitBytes = Math.pow(2, 20) // 1 MB
204+
203205
export const codeScanGoPayloadSizeLimitBytes = Math.pow(2, 20) // 1 MB
204206

205207
export const codeScanPythonPayloadSizeLimitBytes = 200 * Math.pow(2, 10) // 200 KB

src/codewhisperer/util/dependencyGraph/csharpDependencyGraph.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export class CsharpDependencyGraph extends DependencyGraph {
6262
? importStr.substring(indexOfStatic + staticKeyword.length).trim()
6363
: importStr.substring(importStr.indexOf(usingKeyword) + usingKeyword.length).trim()
6464

65-
modulePaths = this.getModulePath(modulePathStr.replaceAll(' ', ''))
65+
modulePaths = this.getModulePath(modulePathStr.replace(' ', ''))
6666
}
6767

6868
return modulePaths
@@ -89,7 +89,7 @@ export class CsharpDependencyGraph extends DependencyGraph {
8989
const dependencies: string[] = []
9090
imports.forEach(importStr => {
9191
this.updateSysPaths(uri)
92-
const importString = importStr.replaceAll(';', '')
92+
const importString = importStr.replace(';', '')
9393
const findings = this.parseImport(importString, Array.from(this._sysPaths.values()))
9494
const validSourceFiles = findings.filter(finding => !this._pickedSourceFiles.has(finding))
9595
validSourceFiles.forEach(file => {

src/codewhisperer/util/dependencyGraph/dependencyGraph.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as vscode from 'vscode'
88
import admZip from 'adm-zip'
99
import { existsSync, statSync } from 'fs'
1010
import { asyncCallWithTimeout } from '../commonUtil'
11-
import path = require('path')
11+
import * as path from 'path'
1212
import { tempDirPath } from '../../../shared/filesystemUtilities'
1313
import * as CodeWhispererConstants from '../../models/constants'
1414
import { getLogger } from '../../../shared/logger'
@@ -35,6 +35,11 @@ export const DependencyGraphConstants = {
3535
globalusing: 'global using',
3636
semicolon: ';',
3737
equals: '=',
38+
require: 'require',
39+
require_relative: 'require_relative',
40+
load: 'load',
41+
include: 'include',
42+
extend: 'extend',
3843

3944
/**
4045
* Regex
@@ -55,6 +60,7 @@ export const DependencyGraphConstants = {
5560
ymlExt: '.yml',
5661
tfExt: '.tf',
5762
hclExt: '.hcl',
63+
rubyExt: '.rb',
5864
goExt: '.go',
5965
}
6066

src/codewhisperer/util/dependencyGraph/dependencyGraphFactory.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { cloudformationDependencyGraph } from './cloudformationDependencyGraph'
1212
import { DependencyGraphConstants } from './dependencyGraph'
1313
import * as vscode from 'vscode'
1414
import { terraformDependencyGraph } from './terraformDependencyGraph'
15+
import { RubyDependencyGraph } from './rubyDependencyGraph'
1516
import { GoDependencyGraph } from './goDependencyGraph'
1617

1718
const languageMap = {
@@ -22,6 +23,7 @@ const languageMap = {
2223
csharp: CsharpDependencyGraph,
2324
cloudformation: cloudformationDependencyGraph,
2425
terraform: terraformDependencyGraph,
26+
ruby: RubyDependencyGraph,
2527
go: GoDependencyGraph,
2628
} as const
2729

@@ -55,6 +57,8 @@ export class DependencyGraphFactory {
5557
return new languageMap['csharp']('csharp' satisfies CodeWhispererConstants.PlatformLanguageId)
5658
case 'yaml' satisfies CodeWhispererConstants.PlatformLanguageId:
5759
return new languageMap['cloudformation']('yaml' satisfies CodeWhispererConstants.PlatformLanguageId)
60+
case 'ruby' satisfies CodeWhispererConstants.PlatformLanguageId:
61+
return new languageMap['ruby']('ruby' satisfies CodeWhispererConstants.PlatformLanguageId)
5862
case 'go' satisfies CodeWhispererConstants.PlatformLanguageId:
5963
return new languageMap['go']('go' satisfies CodeWhispererConstants.PlatformLanguageId)
6064
default:
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import * as vscode from 'vscode'
6+
import { existsSync, statSync, readdirSync } from 'fs'
7+
import { getLogger } from '../../../shared/logger'
8+
import * as CodeWhispererConstants from '../../models/constants'
9+
import { readFileAsString } from '../../../shared/filesystemUtilities'
10+
import { sleep } from '../../../shared/utilities/timeoutUtils'
11+
import { DependencyGraphConstants, DependencyGraph, Truncation } from './dependencyGraph'
12+
import * as path from 'path'
13+
14+
export const importRegex = /(require|require_relative|load|include|extend)\s+('[^']+'|"[^"]+"|\w+)(\s+as\s+(\w+))?/gm
15+
16+
export class RubyDependencyGraph extends DependencyGraph {
17+
// Payload Size for Ruby: 1MB
18+
getPayloadSizeLimitInBytes(): number {
19+
return CodeWhispererConstants.codeScanRubyPayloadSizeLimitBytes
20+
}
21+
override updateSysPaths(uri: vscode.Uri) {
22+
this.getDirPaths(uri).forEach(dirPath => {
23+
this._sysPaths.add(dirPath)
24+
})
25+
}
26+
27+
private generateFilePath(modulePath: string, dirPath: string) {
28+
const filePath = path.join(dirPath, modulePath + DependencyGraphConstants.rubyExt)
29+
return existsSync(filePath) ? filePath : ''
30+
}
31+
32+
//For Generating File Paths
33+
private generateFilePaths(modulePaths: string[], dirPaths: string[]) {
34+
return modulePaths
35+
.flatMap(modulePath => dirPaths.map(dirPath => this.generateFilePath(modulePath, dirPath)))
36+
.filter(filePath => filePath !== '')
37+
}
38+
39+
//Generate the combinations for module paths
40+
private generateModulePaths(inputPath: string): string[] {
41+
const positionOfExt = inputPath.indexOf(DependencyGraphConstants.rubyExt) //To remove imports having .rb
42+
if (positionOfExt !== -1) {
43+
inputPath = inputPath.substring(0, positionOfExt).trim()
44+
}
45+
46+
const inputPaths = inputPath.split('/')
47+
let outputPath = ''
48+
return inputPaths.map(pathSegment => {
49+
outputPath += (outputPath ? '/' : '') + pathSegment
50+
return path.join(...outputPath.split('/'))
51+
})
52+
}
53+
54+
private getModulePath(modulePathStr: string) {
55+
const pos = modulePathStr.indexOf(DependencyGraphConstants.as)
56+
if (pos !== -1) {
57+
modulePathStr = modulePathStr.substring(0, pos)
58+
}
59+
60+
return this.generateModulePaths(modulePathStr.replace(/[",'\s()]/g, '').trim())
61+
}
62+
63+
private extractModulePaths(importStr: string) {
64+
let modulePaths: string[] = []
65+
const {
66+
require: requireKeyword,
67+
require_relative: requireRelativeKeyword,
68+
include: includeKeyword,
69+
extend: extendKeyword,
70+
load: loadKeyword,
71+
} = DependencyGraphConstants
72+
73+
let keyword: string | undefined
74+
75+
switch (true) {
76+
case importStr.startsWith(requireRelativeKeyword):
77+
keyword = requireRelativeKeyword
78+
break
79+
case importStr.startsWith(requireKeyword):
80+
keyword = requireKeyword
81+
break
82+
case importStr.startsWith(includeKeyword):
83+
keyword = includeKeyword
84+
break
85+
case importStr.startsWith(extendKeyword):
86+
keyword = extendKeyword
87+
break
88+
case importStr.startsWith(loadKeyword):
89+
keyword = loadKeyword
90+
break
91+
default:
92+
break
93+
}
94+
95+
if (keyword !== undefined) {
96+
const modulePathStr = importStr.substring(keyword.length).trim().replace(/\s+/g, '')
97+
modulePaths = this.getModulePath(modulePathStr)
98+
}
99+
100+
return modulePaths
101+
}
102+
103+
override parseImport(importStr: string, dirPaths: string[]) {
104+
if (this._parsedStatements.has(importStr)) {
105+
return []
106+
}
107+
108+
this._parsedStatements.add(importStr)
109+
const modulePaths = this.extractModulePaths(importStr)
110+
const dependencies = this.generateFilePaths(modulePaths, dirPaths)
111+
return dependencies
112+
}
113+
114+
override getDependencies(uri: vscode.Uri, imports: string[]) {
115+
const dependencies: string[] = []
116+
imports.forEach(importStr => {
117+
this.updateSysPaths(uri)
118+
const importString = importStr.replace(';', '')
119+
const findings = this.parseImport(importString, Array.from(this._sysPaths.values()))
120+
const validSourceFiles = findings.filter(finding => !this._pickedSourceFiles.has(finding))
121+
validSourceFiles.forEach(file => {
122+
if (existsSync(file) && !this.willReachSizeLimit(this._totalSize, statSync(file).size)) {
123+
dependencies.push(file)
124+
}
125+
})
126+
})
127+
return dependencies
128+
}
129+
private async readImports(content: string) {
130+
this._totalLines += content.split(DependencyGraphConstants.newlineRegex).length
131+
const regExp = new RegExp(importRegex)
132+
return content.match(regExp) ?? []
133+
}
134+
135+
override async searchDependency(uri: vscode.Uri): Promise<Set<string>> {
136+
const filePath = uri.fsPath
137+
const q: string[] = []
138+
q.push(filePath)
139+
while (q.length > 0) {
140+
let count: number = q.length
141+
while (count > 0) {
142+
if (this.reachSizeLimit(this._totalSize)) {
143+
return this._pickedSourceFiles
144+
}
145+
count -= 1
146+
const currentFilePath = q.shift()
147+
if (currentFilePath === undefined) {
148+
throw new Error('"undefined" is invalid for queued file.')
149+
}
150+
this._pickedSourceFiles.add(currentFilePath)
151+
this._totalSize += statSync(currentFilePath).size
152+
const uri = vscode.Uri.file(currentFilePath)
153+
const content: string = await readFileAsString(uri.fsPath)
154+
const imports = await this.readImports(content)
155+
const dependencies = this.getDependencies(uri, imports)
156+
for (const dependency of dependencies) {
157+
q.push(dependency)
158+
}
159+
}
160+
}
161+
return this._pickedSourceFiles
162+
}
163+
164+
override async traverseDir(dirPath: string) {
165+
if (this.reachSizeLimit(this._totalSize)) {
166+
return
167+
}
168+
readdirSync(dirPath, { withFileTypes: true }).forEach(async file => {
169+
const absPath = path.join(dirPath, file.name)
170+
if (file.name.charAt(0) === '.' || !existsSync(absPath)) {
171+
return
172+
}
173+
if (file.isDirectory()) {
174+
await this.traverseDir(absPath)
175+
} else if (file.isFile()) {
176+
if (
177+
file.name.endsWith(DependencyGraphConstants.rubyExt) &&
178+
!this.reachSizeLimit(this._totalSize) &&
179+
!this.willReachSizeLimit(this._totalSize, statSync(absPath).size) &&
180+
!this._pickedSourceFiles.has(absPath)
181+
) {
182+
await this.searchDependency(vscode.Uri.file(absPath))
183+
}
184+
}
185+
})
186+
}
187+
188+
override async generateTruncation(uri: vscode.Uri): Promise<Truncation> {
189+
try {
190+
const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri)
191+
if (workspaceFolder === undefined) {
192+
this._pickedSourceFiles.add(uri.fsPath)
193+
} else {
194+
await this.searchDependency(uri)
195+
await this.traverseDir(this.getProjectPath(uri))
196+
}
197+
await sleep(1000)
198+
const truncDirPath = this.getTruncDirPath(uri)
199+
this.copyFilesToTmpDir(this._pickedSourceFiles, truncDirPath)
200+
const zipFilePath = this.zipDir(truncDirPath, CodeWhispererConstants.codeScanZipExt)
201+
const zipFileSize = statSync(zipFilePath).size
202+
return {
203+
rootDir: truncDirPath,
204+
zipFilePath: zipFilePath,
205+
scannedFiles: new Set(this._pickedSourceFiles),
206+
srcPayloadSizeInBytes: this._totalSize,
207+
zipFileSizeInBytes: zipFileSize,
208+
buildPayloadSizeInBytes: 0,
209+
lines: this._totalLines,
210+
}
211+
} catch (error) {
212+
getLogger().error(`${this._languageId} dependency graph error caused by:`, error)
213+
throw new Error(`${this._languageId} context processing failed.`)
214+
}
215+
}
216+
217+
async getSourceDependencies(uri: vscode.Uri, content: string): Promise<string[]> {
218+
return []
219+
}
220+
async getSamePackageFiles(uri: vscode.Uri, projectPath: string): Promise<string[]> {
221+
return []
222+
}
223+
async isTestFile(content: string): Promise<boolean> {
224+
return false
225+
}
226+
}

src/test/codewhisperer/util/dependencyGraphFactory.test.ts renamed to src/test/codewhisperer/util/dependencyGraph/dependencyGraphFactory.test.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@
66
import assert from 'assert'
77
import vscode from 'vscode'
88
import { join } from 'path'
9-
import { getTestWorkspaceFolder } from '../../../testInteg/integrationTestsUtilities'
10-
import { DependencyGraphFactory } from '../../../codewhisperer/util/dependencyGraph/dependencyGraphFactory'
11-
import { terraformDependencyGraph } from './../../../codewhisperer/util/dependencyGraph/terraformDependencyGraph'
12-
import { cloudformationDependencyGraph } from '../../../codewhisperer/util/dependencyGraph/cloudformationDependencyGraph'
9+
import { getTestWorkspaceFolder } from '../../../../testInteg/integrationTestsUtilities'
10+
import { DependencyGraphFactory } from '../../../../codewhisperer/util/dependencyGraph/dependencyGraphFactory'
11+
import { terraformDependencyGraph } from '../../../../codewhisperer/util/dependencyGraph/terraformDependencyGraph'
1312

1413
describe('DependencyGraphFactory', function () {
1514
const workspaceFolder = getTestWorkspaceFolder()
@@ -28,13 +27,4 @@ describe('DependencyGraphFactory', function () {
2827
const isTerraformDependencyGraph = dependencyGraph instanceof terraformDependencyGraph
2928
assert.ok(isTerraformDependencyGraph)
3029
})
31-
32-
it('codescan request for file in supported json language find generate dependency graph using file extension', async function () {
33-
const appRoot = join(workspaceFolder, 'cloudformation-plain-sam-app')
34-
const appCodePath = join(appRoot, 'src', 'app.json')
35-
const editor = await openTestFile(appCodePath)
36-
const dependencyGraph = DependencyGraphFactory.getDependencyGraph(editor)
37-
const isCloudFormationDependencyGraph = dependencyGraph instanceof cloudformationDependencyGraph
38-
assert.ok(isCloudFormationDependencyGraph)
39-
})
4030
})

0 commit comments

Comments
 (0)