Skip to content

Commit f6be15c

Browse files
committed
CW: Ruby language support for security scans
1 parent 3b503b7 commit f6be15c

File tree

9 files changed

+381
-5
lines changed

9 files changed

+381
-5
lines changed

src/codewhisperer/models/constants.ts

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

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

202+
export const codeScanRubyPayloadSizeLimitBytes = Math.pow(2, 20) // 1 MB
203+
202204
export const codeScanPythonPayloadSizeLimitBytes = 200 * Math.pow(2, 10) // 200 KB
203205

204206
export const codeScanCFPayloadSizeLimitBytes = 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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}
5965

6066
export abstract class DependencyGraph {

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

1617
const languageMap = {
1718
java: JavaDependencyGraph,
@@ -21,6 +22,7 @@ const languageMap = {
2122
csharp: CsharpDependencyGraph,
2223
cloudformation: cloudformationDependencyGraph,
2324
terraform: terraformDependencyGraph,
25+
ruby: RubyDependencyGraph,
2426
} as const
2527

2628
type LanguageMap = typeof languageMap
@@ -53,6 +55,8 @@ export class DependencyGraphFactory {
5355
return new languageMap['cloudformation']('yaml' satisfies CodeWhispererConstants.PlatformLanguageId)
5456
case 'json' satisfies CodeWhispererConstants.PlatformLanguageId:
5557
return new languageMap['cloudformation']('json' satisfies CodeWhispererConstants.PlatformLanguageId)
58+
case 'ruby' satisfies CodeWhispererConstants.PlatformLanguageId:
59+
return new languageMap['ruby']('ruby' satisfies CodeWhispererConstants.PlatformLanguageId)
5660
default:
5761
return this.getDependencyGraphFromFileExtensions(editor.document.fileName)
5862
}
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
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 path = require('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+
let indexOfKeyword: number | undefined
75+
76+
switch (true) {
77+
case importStr.startsWith(requireRelativeKeyword):
78+
keyword = requireRelativeKeyword
79+
indexOfKeyword = importStr.indexOf(requireRelativeKeyword)
80+
break
81+
case importStr.startsWith(requireKeyword):
82+
keyword = requireKeyword
83+
indexOfKeyword = importStr.indexOf(requireKeyword)
84+
break
85+
case importStr.startsWith(includeKeyword):
86+
keyword = includeKeyword
87+
indexOfKeyword = importStr.indexOf(includeKeyword)
88+
break
89+
case importStr.startsWith(extendKeyword):
90+
keyword = extendKeyword
91+
indexOfKeyword = importStr.indexOf(extendKeyword)
92+
break
93+
case importStr.startsWith(loadKeyword):
94+
keyword = loadKeyword
95+
indexOfKeyword = importStr.indexOf(loadKeyword)
96+
break
97+
default:
98+
break
99+
}
100+
101+
if (keyword && indexOfKeyword !== -1 && keyword !== undefined && indexOfKeyword !== undefined) {
102+
const modulePathStr = importStr
103+
.substring(indexOfKeyword + keyword.length)
104+
.trim()
105+
.replace(/\s+/g, '')
106+
modulePaths = this.getModulePath(modulePathStr)
107+
}
108+
109+
return modulePaths
110+
}
111+
112+
override parseImport(importStr: string, dirPaths: string[]) {
113+
if (this._parsedStatements.has(importStr)) {
114+
return []
115+
}
116+
117+
this._parsedStatements.add(importStr)
118+
const modulePaths = this.extractModulePaths(importStr)
119+
const dependencies = this.generateFilePaths(modulePaths, dirPaths)
120+
return dependencies
121+
}
122+
123+
override getDependencies(uri: vscode.Uri, imports: string[]) {
124+
const dependencies: string[] = []
125+
imports.forEach(importStr => {
126+
this.updateSysPaths(uri)
127+
const importString = importStr.replace(';', '')
128+
const findings = this.parseImport(importString, Array.from(this._sysPaths.values()))
129+
const validSourceFiles = findings.filter(finding => !this._pickedSourceFiles.has(finding))
130+
validSourceFiles.forEach(file => {
131+
if (existsSync(file) && !this.willReachSizeLimit(this._totalSize, statSync(file).size)) {
132+
dependencies.push(file)
133+
}
134+
})
135+
})
136+
return dependencies
137+
}
138+
private async readImports(content: string) {
139+
this._totalLines += content.split(DependencyGraphConstants.newlineRegex).length
140+
const regExp = new RegExp(importRegex)
141+
return content.match(regExp) ?? []
142+
}
143+
144+
override async searchDependency(uri: vscode.Uri): Promise<Set<string>> {
145+
const filePath = uri.fsPath
146+
const q: string[] = []
147+
q.push(filePath)
148+
while (q.length > 0) {
149+
let count: number = q.length
150+
while (count > 0) {
151+
if (this.reachSizeLimit(this._totalSize)) {
152+
return this._pickedSourceFiles
153+
}
154+
count -= 1
155+
const currentFilePath = q.shift()
156+
if (currentFilePath === undefined) {
157+
throw new Error('"undefined" is invalid for queued file.')
158+
}
159+
this._pickedSourceFiles.add(currentFilePath)
160+
this._totalSize += statSync(currentFilePath).size
161+
const uri = vscode.Uri.file(currentFilePath)
162+
const content: string = await readFileAsString(uri.fsPath)
163+
const imports = await this.readImports(content)
164+
const dependencies = this.getDependencies(uri, imports)
165+
dependencies.forEach(dependency => {
166+
q.push(dependency)
167+
})
168+
}
169+
}
170+
return this._pickedSourceFiles
171+
}
172+
173+
override async traverseDir(dirPath: string) {
174+
if (this.reachSizeLimit(this._totalSize)) {
175+
return
176+
}
177+
readdirSync(dirPath, { withFileTypes: true }).forEach(async file => {
178+
const absPath = path.join(dirPath, file.name)
179+
if (file.name.charAt(0) === '.' || !existsSync(absPath)) {
180+
return
181+
}
182+
if (file.isDirectory()) {
183+
await this.traverseDir(absPath)
184+
} else if (file.isFile()) {
185+
if (
186+
file.name.endsWith(DependencyGraphConstants.rubyExt) &&
187+
!this.reachSizeLimit(this._totalSize) &&
188+
!this.willReachSizeLimit(this._totalSize, statSync(absPath).size) &&
189+
!this._pickedSourceFiles.has(absPath)
190+
) {
191+
await this.searchDependency(vscode.Uri.file(absPath))
192+
}
193+
}
194+
})
195+
}
196+
197+
override async generateTruncation(uri: vscode.Uri): Promise<Truncation> {
198+
try {
199+
const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri)
200+
if (workspaceFolder === undefined) {
201+
this._pickedSourceFiles.add(uri.fsPath)
202+
} else {
203+
await this.searchDependency(uri)
204+
await this.traverseDir(this.getProjectPath(uri))
205+
}
206+
await sleep(1000)
207+
const truncDirPath = this.getTruncDirPath(uri)
208+
this.copyFilesToTmpDir(this._pickedSourceFiles, truncDirPath)
209+
const zipFilePath = this.zipDir(truncDirPath, CodeWhispererConstants.codeScanZipExt)
210+
const zipFileSize = statSync(zipFilePath).size
211+
return {
212+
rootDir: truncDirPath,
213+
zipFilePath: zipFilePath,
214+
scannedFiles: new Set(this._pickedSourceFiles),
215+
srcPayloadSizeInBytes: this._totalSize,
216+
zipFileSizeInBytes: zipFileSize,
217+
buildPayloadSizeInBytes: 0,
218+
lines: this._totalLines,
219+
}
220+
} catch (error) {
221+
getLogger().error(`${this._languageId} dependency graph error caused by:`, error)
222+
throw new Error(`${this._languageId} context processing failed.`)
223+
}
224+
}
225+
226+
async getSourceDependencies(uri: vscode.Uri, content: string): Promise<string[]> {
227+
return []
228+
}
229+
async getSamePackageFiles(uri: vscode.Uri, projectPath: string): Promise<string[]> {
230+
return []
231+
}
232+
async isTestFile(content: string): Promise<boolean> {
233+
return false
234+
}
235+
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +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'
9+
import { getTestWorkspaceFolder } from '../../../../testInteg/integrationTestsUtilities'
10+
import { DependencyGraphFactory } from '../../../../codewhisperer/util/dependencyGraph/dependencyGraphFactory'
11+
import { terraformDependencyGraph } from '../../../../codewhisperer/util/dependencyGraph/terraformDependencyGraph'
1212

1313
describe('DependencyGraphFactory', function () {
1414
const workspaceFolder = getTestWorkspaceFolder()

0 commit comments

Comments
 (0)