server/aws-lsp-codewhisperer/src/language-server/securityScan/dependencyGraph/dependencyGraph.ts (137 lines of code) (raw):

import { Logging, Workspace } from '@aws/language-server-runtimes/server-interface' import { gitIgnoreFilterUtils } from '@aws/lsp-core' import * as admZip from 'adm-zip' import * as path from 'path' import * as CodeWhispererConstants from './constants' export interface Truncation { rootDir: string zipFileBuffer: Buffer scannedFiles: Set<string> srcPayloadSizeInBytes: number buildPayloadSizeInBytes: number zipFileSizeInBytes: number lines: number } /** * Create a dependency graph to select all the dependent files associated with current active file. * @param uri * @returns */ export abstract class DependencyGraph { protected workspace: Workspace protected _sysPaths: Set<string> = new Set<string>() protected _parsedStatements: Set<string> = new Set<string>() protected _pickedSourceFiles: Set<string> = new Set<string>() protected _fetchedDirs: Set<string> = new Set<string>() protected _totalSize = 0 protected _tmpDir = '' protected _truncDir = '' protected _totalLines = 0 protected logging: Logging protected _workspaceFolderPath: string private _isProjectTruncated = false constructor(workspace: Workspace, logging: Logging, workspaceFolderPath: string) { this.workspace = workspace this.logging = logging this._tmpDir = this.workspace.fs.getTempDirPath() this._workspaceFolderPath = workspaceFolderPath } /** * Retrun project name if given uri is within project. If not then return last folder name where file is present. * @param uri file path to get project name * @returns project folder name */ public async getProjectName(uri: string) { const projectPath = await this.getProjectPath(uri) return path.basename(projectPath) } /** * Retrun project path if given uri is within project. If not then return current directory path where file is present. * @param uri file path to get project path * @returns project path uri if found within workspace otherwise current directory path of input uri */ public async getProjectPath(uri: string) { const workspaceFolder = this.workspace.getWorkspaceFolder(uri) if (workspaceFolder) { return workspaceFolder.uri } if (uri.includes(this._workspaceFolderPath)) { return this._workspaceFolderPath } return (await this.workspace.fs.isFile(uri)) ? path.dirname(uri) : uri } /** * Retrun readable size limit value in MB or KB */ public getReadableSizeLimit(): string { const totalBytesInMB = Math.pow(2, 20) const totalBytesInKB = Math.pow(2, 10) if (this.getPayloadSizeLimitInBytes() >= totalBytesInMB) { return `${this.getPayloadSizeLimitInBytes() / totalBytesInMB}MB` } else { return `${this.getPayloadSizeLimitInBytes() / totalBytesInKB}KB` } } /** * checks if the given size value exceeds the payload size limit or not */ public exceedsSizeLimit(size: number): boolean { return size >= this.getPayloadSizeLimitInBytes() } /** * get value of _isProjectTruncated */ get isProjectTruncated(): boolean { return this._isProjectTruncated } /** * set value for _isProjectTruncated */ set isProjectTruncated(value: boolean) { this._isProjectTruncated = value } /** * find all the files inside `dir` recursively. * @param dir folder path from where search for all the files. * @returns return a promise of list of absolute file paths */ async getFiles(dir: string): Promise<string[]> { const subdirs = await this.workspace.fs.readdir(dir) const files = await Promise.all( subdirs.map(async subdir => { const res = path.resolve(dir, subdir.name) return subdir.isDirectory() ? await this.getFiles(res) : [res] }) ) return files.reduce((a, f) => a.concat(f), []) } /** * @param rootPath root folder to look for .gitignore files * @returns list of files without those that are git ignored */ async filterOutGitIgnoredFiles(rootPath: string, files: string[]): Promise<string[]> { // Pattern to find .gitignore files with either windows or unix path styles const gitIgnorePattern = /.*[\/\\]\.gitignore$/ const gitIgnoreFiles = files.filter(file => gitIgnorePattern.test(file)) if (gitIgnoreFiles.length === 0) { return files } const gitIgnoreFilter = await gitIgnoreFilterUtils.GitIgnoreFilter.build( rootPath, gitIgnoreFiles, this.workspace ) return gitIgnoreFilter.filterFiles(files) } /** * copy list of file to temp dir */ protected async copyFilesToTmpDir(files: Set<string> | string[], dir: string) { // Convert Set to an array for compatibility with Promise.all const fileArray = Array.from(files) // Use Promise.all to asynchronously copy all files await Promise.all( fileArray.map(async filePath => { await this.copyFileToTmp(filePath, dir) }) ) } /** * copy input file in temp dir (destDir) with same relative path of srcFile in destDir. * @param srcFilePath file path to be copied * @param destDir destination directory path */ protected async copyFileToTmp(srcFilePath: string, destDir: string) { const sourceWorkspacePath = await this.getProjectPath(srcFilePath) const fileRelativePath = path.relative(sourceWorkspacePath, srcFilePath) const destinationFileAbsolutePath = path.join(destDir, fileRelativePath) await this.workspace.fs.copyFile(srcFilePath, destinationFileAbsolutePath, { ensureDir: true }) } /** * create a zip dir buffer of the given dir. * @param dir directory path to create zip of the directory * @returns zipped dir buffer object and its size in bytes */ createZip(dir: string) { const zip = new admZip() zip.addLocalFolder(dir) // writeZip uses `fs` under the hood and it wouldn't work in browsers // Instead of writeZip to write to disk. // The zip buffer can then be uploaded to s3 const zipBuffer = zip.toBuffer() return { zipFileBuffer: zipBuffer, zipFileSize: zipBuffer.byteLength } } /** * delete directory along with its sub-directories and files if it exists * @param dir directory path to remove */ protected async removeDir(dir: string) { if (await this.workspace.fs.exists(dir)) { await this.workspace.fs.rm(dir, { force: true, recursive: true }) } } /** * get or create truncation directory path * @returns truncation directory path */ protected getTruncDirPath() { if (this._truncDir === '') { this._truncDir = path.join( this._tmpDir, CodeWhispererConstants.codeScanTruncDirPrefix + '_' + Date.now().toString() ) } return this._truncDir } /** * Get total size of list of files * @param files list of file paths to find size * @returns total size of the input list of files */ protected async getFilesTotalSize(files: string[]) { const fileStatsPromises = files.map(file => this.workspace.fs.getFileSize(file)) const fileStats = await Promise.all(fileStatsPromises) const totalSize = fileStats.reduce((accumulator, { size }) => accumulator + size, 0) return totalSize } /** * remove all copied files from temp directory */ async removeTmpFiles() { await this.removeDir(this.getTruncDirPath()) } /** * This method will traverse throw the input file and its dependecy files to creates a list of files for the scan. * If the list does not exceeds the payload size limit then it will scan all the remaining files to add them into the list * until it reaches to the payload size limit. Then, it copies all the selected files to temp directory, creates a zip buffer. * @param uri file path for which truncation being created * @returns Truncation object */ abstract generateTruncation(uri: string): Promise<Truncation> /** * Search for all the dependecies for the given input file. Store the input filepath along with * the dependent filepaths until it does not exceeds the payload size limit. * @param uri file path to seach dependency of the file * @returns set of file paths found as dependecy of input uri */ abstract searchDependency(uri: string): Promise<Set<string>> /** * Traverse all the Csharp files in the given workspace and add each file along with its dependencies to * the list of files selected for security scan. */ abstract traverseDir(dirPath: string): void /** * Create a dependency file path set for given list of namespace. * @param imports list of import strings */ abstract getDependencies(imports: string[]): void /** * Get payload size limit in bytes for the given Language. */ abstract getPayloadSizeLimitInBytes(): number }