diff --git a/apps/remix-ide/src/app.js b/apps/remix-ide/src/app.js index 8e8570b249..aa17788170 100644 --- a/apps/remix-ide/src/app.js +++ b/apps/remix-ide/src/app.js @@ -448,7 +448,8 @@ Please make a backup of your contracts and start using http://remix.ethereum.org test, filePanel.remixdHandle, filePanel.gitHandle, - filePanel.hardhatHandle + filePanel.hardhatHandle, + filePanel.slitherHandle ]) if (isElectron()) { diff --git a/apps/remix-ide/src/app/editor/sourceHighlighter.js b/apps/remix-ide/src/app/editor/sourceHighlighter.js index e25e2c948f..22f705001e 100644 --- a/apps/remix-ide/src/app/editor/sourceHighlighter.js +++ b/apps/remix-ide/src/app/editor/sourceHighlighter.js @@ -37,7 +37,7 @@ class SourceHighlighter { this.statementMarker = null this.fullLineMarker = null this.source = null - if (lineColumnPos) { + if (lineColumnPos && lineColumnPos.start && lineColumnPos.end) { this.source = filePath this.style = style || 'var(--info)' // if (!this.source) this.source = this._deps.fileManager.currentFile() diff --git a/apps/remix-ide/src/app/files/fileManager.js b/apps/remix-ide/src/app/files/fileManager.js index 9b618a9fe9..f2d83d3915 100644 --- a/apps/remix-ide/src/app/files/fileManager.js +++ b/apps/remix-ide/src/app/files/fileManager.js @@ -155,9 +155,9 @@ class FileManager extends Plugin { * @param {string} path path of the directory * @returns {boolean} true if path is a directory. */ - isDirectory (path) { + async isDirectory (path) { const provider = this.fileProviderOf(path) - const result = provider.isDirectory(path) + const result = await provider.isDirectory(path) return result } @@ -362,7 +362,6 @@ class FileManager extends Plugin { path = this.limitPluginScope(path) await this._handleExists(path, `Cannot remove file or directory ${path}`) const provider = this.fileProviderOf(path) - return await provider.remove(path) } catch (e) { throw new Error(e) diff --git a/apps/remix-ide/src/app/files/remixDProvider.js b/apps/remix-ide/src/app/files/remixDProvider.js index bc59bfe90b..04b0217df1 100644 --- a/apps/remix-ide/src/app/files/remixDProvider.js +++ b/apps/remix-ide/src/app/files/remixDProvider.js @@ -38,7 +38,7 @@ module.exports = class RemixDProvider extends FileProvider { }) this._appManager.on('remixd', 'fileRenamed', (oldPath, newPath) => { - this.event.emit('fileRemoved', oldPath, newPath) + this.event.emit('fileRenamed', oldPath, newPath) }) this._appManager.on('remixd', 'rootFolderChanged', () => { @@ -141,7 +141,6 @@ module.exports = class RemixDProvider extends FileProvider { this._appManager.call('remixd', 'remove', { path: unprefixedpath }) .then(result => { const path = unprefixedpath - delete this.filesContent[path] resolve(true) this.init() diff --git a/apps/remix-ide/src/app/files/remixd-handle.js b/apps/remix-ide/src/app/files/remixd-handle.js index abea437b64..6f06d8773b 100644 --- a/apps/remix-ide/src/app/files/remixd-handle.js +++ b/apps/remix-ide/src/app/files/remixd-handle.js @@ -43,6 +43,7 @@ export class RemixdHandle extends WebsocketPlugin { if (super.socket) super.deactivate() // this.appManager.deactivatePlugin('git') // plugin call doesn't work.. see issue https://github.com/ethereum/remix-plugin/issues/342 if (this.appManager.actives.includes('hardhat')) this.appManager.deactivatePlugin('hardhat') + if (this.appManager.actives.includes('slither')) this.appManager.deactivatePlugin('slither') this.localhostProvider.close((error) => { if (error) console.log(error) }) @@ -88,6 +89,7 @@ export class RemixdHandle extends WebsocketPlugin { this.call('filePanel', 'setWorkspace', { name: LOCALHOST, isLocalhost: true }, true) }) this.call('manager', 'activatePlugin', 'hardhat') + this.call('manager', 'activatePlugin', 'slither') } } if (this.localhostProvider.isConnected()) { diff --git a/apps/remix-ide/src/app/files/slither-handle.js b/apps/remix-ide/src/app/files/slither-handle.js new file mode 100644 index 0000000000..e8ff1e66c7 --- /dev/null +++ b/apps/remix-ide/src/app/files/slither-handle.js @@ -0,0 +1,18 @@ +import { WebsocketPlugin } from '@remixproject/engine-web' +import * as packageJson from '../../../../../package.json' + +const profile = { + name: 'slither', + displayName: 'Slither', + url: 'ws://127.0.0.1:65523', + methods: ['analyse'], + description: 'Using Remixd daemon, run slither static analysis', + kind: 'other', + version: packageJson.version +} + +export class SlitherHandle extends WebsocketPlugin { + constructor () { + super(profile) + } +} diff --git a/apps/remix-ide/src/app/panels/file-panel.js b/apps/remix-ide/src/app/panels/file-panel.js index 0d1523fa08..4ed0e6c8c9 100644 --- a/apps/remix-ide/src/app/panels/file-panel.js +++ b/apps/remix-ide/src/app/panels/file-panel.js @@ -9,6 +9,7 @@ import { checkSpecialChars, checkSlash } from '../../lib/helper' const { RemixdHandle } = require('../files/remixd-handle.js') const { GitHandle } = require('../files/git-handle.js') const { HardhatHandle } = require('../files/hardhat-handle.js') +const { SlitherHandle } = require('../files/slither-handle.js') const globalRegistry = require('../../global/registry') const examples = require('../editor/examples') const GistHandler = require('../../lib/gist-handler') @@ -59,6 +60,7 @@ module.exports = class Filepanel extends ViewPlugin { this.remixdHandle = new RemixdHandle(this._deps.fileProviders.localhost, appManager) this.gitHandle = new GitHandle() this.hardhatHandle = new HardhatHandle() + this.slitherHandle = new SlitherHandle() this.registeredMenuItems = [] this.removedMenuItems = [] this.request = {} diff --git a/apps/remix-ide/src/app/panels/tab-proxy.js b/apps/remix-ide/src/app/panels/tab-proxy.js index 24edfa1da6..dba54aef61 100644 --- a/apps/remix-ide/src/app/panels/tab-proxy.js +++ b/apps/remix-ide/src/app/panels/tab-proxy.js @@ -44,7 +44,6 @@ export class TabProxy extends Plugin { fileManager.events.on('fileRemoved', (name) => { const workspace = this.fileManager.currentWorkspace() - workspace ? this.removeTab(workspace + '/' + name) : this.removeTab(this.fileManager.mode + '/' + name) }) diff --git a/apps/remix-ide/src/app/tabs/compileTab/compileTab.js b/apps/remix-ide/src/app/tabs/compileTab/compileTab.js index b357f8a8aa..a9c17192cc 100644 --- a/apps/remix-ide/src/app/tabs/compileTab/compileTab.js +++ b/apps/remix-ide/src/app/tabs/compileTab/compileTab.js @@ -7,6 +7,7 @@ const profile = { name: 'solidity-logic', displayName: 'Solidity compiler logic', description: 'Compile solidity contracts - Logic', + methods: ['getCompilerState'], version: packageJson.version } @@ -68,6 +69,10 @@ class CompileTab extends Plugin { this.compiler.set('language', lang) } + getCompilerState () { + return this.compiler.state + } + /** * Compile a specific file of the file manager * @param {string} target the path to the file to compile diff --git a/apps/remix-ide/src/remixEngine.js b/apps/remix-ide/src/remixEngine.js index abf20340f6..2836d974b2 100644 --- a/apps/remix-ide/src/remixEngine.js +++ b/apps/remix-ide/src/remixEngine.js @@ -10,6 +10,7 @@ export class RemixEngine extends Engine { setPluginOption ({ name, kind }) { if (kind === 'provider') return { queueTimeout: 60000 * 2 } if (name === 'LearnEth') return { queueTimeout: 60000 } + if (name === 'slither') return { queueTimeout: 60000 * 4 } // Requires when a solc version is installed return { queueTimeout: 10000 } } diff --git a/libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.tsx b/libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.tsx index 5535a05971..95913e533b 100644 --- a/libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.tsx +++ b/libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.tsx @@ -1,4 +1,4 @@ -import React from 'react' //eslint-disable-line +import React, { CSSProperties } from 'react' //eslint-disable-line import './remix-ui-checkbox.css' /* eslint-disable-next-line */ @@ -12,6 +12,8 @@ export interface RemixUiCheckboxProps { id?: string itemName?: string categoryId?: string + visibility?: string + display?: string } export const RemixUiCheckbox = ({ @@ -23,10 +25,12 @@ export const RemixUiCheckbox = ({ checked, onChange, itemName, - categoryId + categoryId, + visibility, + display = 'flex' }: RemixUiCheckboxProps) => { return ( -
+
{ try { await fileManager.remove(p) } catch (e) { - const isDir = state.fileManager.isDirectory(p) + const isDir = await state.fileManager.isDirectory(p) toast(`Failed to remove ${isDir ? 'folder' : 'file'} ${p}.`) } } diff --git a/libs/remix-ui/solidity-compiler/src/lib/css/style.css b/libs/remix-ui/solidity-compiler/src/lib/css/style.css index 3846ca0f0f..d2bbe9e606 100644 --- a/libs/remix-ui/solidity-compiler/src/lib/css/style.css +++ b/libs/remix-ui/solidity-compiler/src/lib/css/style.css @@ -103,7 +103,6 @@ } .remixui_container { margin: 0; - margin-bottom: 2%; } .remixui_optimizeContainer { display: flex; diff --git a/libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts b/libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts index 50fc98410a..83b688f104 100644 --- a/libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts +++ b/libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts @@ -7,6 +7,7 @@ const profile = { name: 'solidity-logic', displayName: 'Solidity compiler logic', description: 'Compile solidity contracts - Logic', + methods: ['getCompilerState'], version: packageJson.version } export class CompileTab extends Plugin { @@ -60,6 +61,10 @@ export class CompileTab extends Plugin { this.compiler.set('evmVersion', this.evmVersion) } + getCompilerState () { + return this.compiler.state + } + /** * Set the compiler to using Solidity or Yul (default to Solidity) * @params lang {'Solidity' | 'Yul'} ... diff --git a/libs/remix-ui/static-analyser/src/lib/remix-ui-static-analyser.tsx b/libs/remix-ui/static-analyser/src/lib/remix-ui-static-analyser.tsx index 8060310c46..a4419dc6f8 100644 --- a/libs/remix-ui/static-analyser/src/lib/remix-ui-static-analyser.tsx +++ b/libs/remix-ui/static-analyser/src/lib/remix-ui-static-analyser.tsx @@ -55,10 +55,12 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => { return indexOfCategory } const [autoRun, setAutoRun] = useState(true) + const [slitherEnabled, setSlitherEnabled] = useState(false) + const [showSlither, setShowSlither] = useState('hidden') const [categoryIndex, setCategoryIndex] = useState(groupedModuleIndex(groupedModules)) const warningContainer = React.useRef(null) - const [warningState, setWarningState] = useState([]) + const [warningState, setWarningState] = useState({}) const [state, dispatch] = useReducer(analysisReducer, initialState) useEffect(() => { @@ -66,7 +68,7 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => { }, []) useEffect(() => { - setWarningState([]) + setWarningState({}) if (autoRun) { if (state.data !== null) { run(state.data, state.source, state.file) @@ -78,13 +80,15 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => { }, [state]) useEffect(() => { - props.analysisModule.on('filePanel', 'setWorkspace', () => { + props.analysisModule.on('filePanel', 'setWorkspace', (currentWorkspace) => { // Reset warning state setWarningState([]) // Reset badge props.event.trigger('staticAnaysisWarning', []) // Reset state dispatch({ type: '', payload: {} }) + // Show 'Enable Slither Analysis' checkbox + if (currentWorkspace && currentWorkspace.isLocalhost === true) setShowSlither('visible') }) return () => { } }, [props.analysisModule]) @@ -103,12 +107,35 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => { ) } + const showWarnings = (warningMessage, groupByKey) => { + const resultArray = [] + warningMessage.map(x => { + resultArray.push(x) + }) + function groupBy (objectArray, property) { + return objectArray.reduce((acc, obj) => { + const key = obj[property] + if (!acc[key]) { + acc[key] = [] + } + // Add object to list for given key's value + acc[key].push(obj) + return acc + }, {}) + } + + const groupedCategory = groupBy(resultArray, groupByKey) + setWarningState(groupedCategory) + } + const run = (lastCompilationResult, lastCompilationSource, currentFile) => { if (state.data !== null) { - if (lastCompilationResult && categoryIndex.length > 0) { + if (lastCompilationResult && (categoryIndex.length > 0 || slitherEnabled)) { let warningCount = 0 const warningMessage = [] + const warningErrors = [] + // Remix Analysis runner.run(lastCompilationResult, categoryIndex, results => { results.map((result) => { let moduleName @@ -119,7 +146,6 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => { } }) }) - const warningErrors = [] result.report.map((item) => { let location: any = {} let locationString = 'not available' @@ -163,28 +189,73 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => { warningMessage.push({ msg, options, hasWarning: true, warningModuleName: moduleName }) }) }) - const resultArray = [] - warningMessage.map(x => { - resultArray.push(x) - }) - function groupBy (objectArray, property) { - return objectArray.reduce((acc, obj) => { - const key = obj[property] - if (!acc[key]) { - acc[key] = [] - } - // Add object to list for given key's value - acc[key].push(obj) - return acc - }, {}) - } + // Slither Analysis + if (slitherEnabled) { + props.analysisModule.call('solidity-logic', 'getCompilerState').then((compilerState) => { + const { currentVersion, optimize, evmVersion } = compilerState + props.analysisModule.call('terminal', 'log', { type: 'info', value: '[Slither Analysis]: Running...' }) + props.analysisModule.call('slither', 'analyse', state.file, { currentVersion, optimize, evmVersion }).then((result) => { + if (result.status) { + props.analysisModule.call('terminal', 'log', { type: 'info', value: `[Slither Analysis]: Analysis Completed!! ${result.count} warnings found.` }) + const report = result.data + report.map((item) => { + let location: any = {} + let locationString = 'not available' + let column = 0 + let row = 0 + let fileName = currentFile - const groupedCategory = groupBy(resultArray, 'warningModuleName') - setWarningState(groupedCategory) + if (item.sourceMap && item.sourceMap.length) { + const fileIndex = Object.keys(lastCompilationResult.sources).indexOf(item.sourceMap[0].source_mapping.filename_relative) + if (fileIndex >= 0) { + location = { + start: item.sourceMap[0].source_mapping.start, + length: item.sourceMap[0].source_mapping.length + } + location = props.analysisModule._deps.offsetToLineColumnConverter.offsetToLineColumn( + location, + fileIndex, + lastCompilationSource.sources, + lastCompilationResult.sources + ) + row = location.start.line + column = location.start.column + locationString = row + 1 + ':' + column + ':' + fileName = Object.keys(lastCompilationResult.sources)[fileIndex] + } + } + warningCount++ + const msg = message(item.title, item.description, item.more, fileName, locationString) + const options = { + type: 'warning', + useSpan: true, + errFile: fileName, + fileName, + errLine: row, + errCol: column, + item: { warning: item.description }, + name: item.title, + locationString, + more: item.more, + location: location + } + warningErrors.push(options) + warningMessage.push({ msg, options, hasWarning: true, warningModuleName: 'Slither Analysis' }) + }) + showWarnings(warningMessage, 'warningModuleName') + props.event.trigger('staticAnaysisWarning', [warningCount]) + } + }).catch((error) => { + console.log('Error found:', error) // This should be removed once testing done + props.analysisModule.call('terminal', 'log', { type: 'error', value: '[Slither Analysis]: Error occured! See remixd console for details.' }) + showWarnings(warningMessage, 'warningModuleName') + }) + }) + } else { + showWarnings(warningMessage, 'warningModuleName') + props.event.trigger('staticAnaysisWarning', [warningCount]) + } }) - if (categoryIndex.length > 0) { - props.event.trigger('staticAnaysisWarning', [warningCount]) - } } else { if (categoryIndex.length) { warningContainer.current.innerText = 'No compiled AST available' @@ -220,6 +291,14 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => { } } + const handleSlitherEnabled = () => { + if (slitherEnabled) { + setSlitherEnabled(false) + } else { + setSlitherEnabled(true) + } + } + const handleAutoRun = () => { if (autoRun) { setAutoRun(false) @@ -317,7 +396,18 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => { label="Autorun" onChange={() => {}} /> -
+
+ {}} + visibility = {showSlither} + />
@@ -330,7 +420,7 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => { }
- last results for: + Last results for: { {state.file}
+
{Object.entries(warningState).length > 0 &&
@@ -345,9 +436,9 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => { (Object.entries(warningState).map((element, index) => (
{element[0]} - {element[1].map((x, i) => ( - x.hasWarning ? ( -
+ {element[1]['map']((x, i) => ( // eslint-disable-line dot-notation + x.hasWarning ? ( // eslint-disable-next-line dot-notation +
diff --git a/libs/remixd/src/bin/remixd.ts b/libs/remixd/src/bin/remixd.ts index 2085735b8c..623fa5ef97 100644 --- a/libs/remixd/src/bin/remixd.ts +++ b/libs/remixd/src/bin/remixd.ts @@ -25,6 +25,7 @@ async function warnLatestVersion () { const services = { git: (readOnly: boolean) => new servicesList.GitClient(readOnly), hardhat: (readOnly: boolean) => new servicesList.HardhatClient(readOnly), + slither: (readOnly: boolean) => new servicesList.SlitherClient(readOnly), folder: (readOnly: boolean) => new servicesList.Sharedfolder(readOnly) } @@ -32,11 +33,12 @@ const services = { const ports = { git: 65521, hardhat: 65522, + slither: 65523, folder: 65520 } const killCallBack: Array = [] -function startService (service: S, callback: (ws: WS, sharedFolderClient: servicesList.Sharedfolder, error?:Error) => void) { +function startService (service: S, callback: (ws: WS, sharedFolderClient: servicesList.Sharedfolder, error?:Error) => void) { const socket = new WebSocket(ports[service], { remixIdeUrl: program.remixIde }, () => services[service](program.readOnly || false)) socket.start(callback) killCallBack.push(socket.close.bind(socket)) @@ -94,6 +96,10 @@ function errorHandler (error: any, service: string) { sharedFolderClient.setupNotifications(program.sharedFolder) sharedFolderClient.sharedFolder(program.sharedFolder) }) + startService('slither', (ws: WS, sharedFolderClient: servicesList.Sharedfolder) => { + sharedFolderClient.setWebSocket(ws) + sharedFolderClient.sharedFolder(program.sharedFolder) + }) // Run hardhat service if a hardhat project is shared as folder const hardhatConfigFilePath = absolutePath('./', program.sharedFolder) + '/hardhat.config.js' const isHardhatProject = fs.existsSync(hardhatConfigFilePath) diff --git a/libs/remixd/src/index.ts b/libs/remixd/src/index.ts index 849f35b6fa..8b35cea6dd 100644 --- a/libs/remixd/src/index.ts +++ b/libs/remixd/src/index.ts @@ -2,6 +2,7 @@ import { RemixdClient as sharedFolder } from './services/remixdClient' import { GitClient } from './services/gitClient' import { HardhatClient } from './services/hardhatClient' +import { SlitherClient } from './services/slitherClient' import Websocket from './websocket' import * as utils from './utils' @@ -11,6 +12,7 @@ module.exports = { services: { sharedFolder, GitClient, - HardhatClient + HardhatClient, + SlitherClient } } diff --git a/libs/remixd/src/serviceList.ts b/libs/remixd/src/serviceList.ts index 19d613b7c2..ec8c1f374d 100644 --- a/libs/remixd/src/serviceList.ts +++ b/libs/remixd/src/serviceList.ts @@ -1,3 +1,4 @@ export { RemixdClient as Sharedfolder } from './services/remixdClient' export { GitClient } from './services/gitClient' export { HardhatClient } from './services/hardhatClient' +export { SlitherClient } from './services/slitherClient' diff --git a/libs/remixd/src/services/remixdClient.ts b/libs/remixd/src/services/remixdClient.ts index 31588628ea..8645caf4ac 100644 --- a/libs/remixd/src/services/remixdClient.ts +++ b/libs/remixd/src/services/remixdClient.ts @@ -180,12 +180,34 @@ export class RemixdClient extends PluginClient { if (!fs.existsSync(path)) return reject(new Error('File not found ' + path)) if (!isRealPath(path)) return + // Saving the content of the item{folder} before removing it + const ls = [] + try { + const resolveList = (path) => { + if (!this._isFile(path)) { + const list = utils.resolveDirectory(path, this.currentSharedFolder) + Object.keys(list).forEach(itemPath => { + if (list[itemPath].isDirectory) { + resolveList(`${this.currentSharedFolder}/${itemPath}`) + } + ls.push(itemPath) + }) + } + } + resolveList(path) + ls.push(args.path) + } catch (e) { + throw new Error(e) + } return fs.remove(path, (error: Error) => { if (error) { console.log(error) return reject(new Error('Failed to remove file/directory: ' + error)) } - this.emit('fileRemoved', args.path) + for (const file in ls) { + this.emit('fileRemoved', ls[file]) + } + resolve(true) }) }) @@ -194,10 +216,17 @@ export class RemixdClient extends PluginClient { } } + _isFile (path: string): boolean { + try { + return fs.statSync(path).isFile() + } catch (error) { + throw new Error(error) + } + } + isDirectory (args: SharedFolderArgs): boolean { try { const path = utils.absolutePath(args.path, this.currentSharedFolder) - return fs.statSync(path).isDirectory() } catch (error) { throw new Error(error) @@ -207,7 +236,6 @@ export class RemixdClient extends PluginClient { isFile (args: SharedFolderArgs): boolean { try { const path = utils.absolutePath(args.path, this.currentSharedFolder) - return fs.statSync(path).isFile() } catch (error) { throw new Error(error) diff --git a/libs/remixd/src/services/slitherClient.ts b/libs/remixd/src/services/slitherClient.ts new file mode 100644 index 0000000000..e2d9a281bb --- /dev/null +++ b/libs/remixd/src/services/slitherClient.ts @@ -0,0 +1,162 @@ +/* eslint dot-notation: "off" */ + +import * as WS from 'ws' // eslint-disable-line +import { PluginClient } from '@remixproject/plugin' +import { existsSync, readFileSync, readdirSync } from 'fs' +import { OutputStandard } from '../types' // eslint-disable-line +const { spawn, execSync } = require('child_process') + +export class SlitherClient extends PluginClient { + methods: Array + websocket: WS + currentSharedFolder: string + + constructor (private readOnly = false) { + super() + this.methods = ['analyse'] + } + + setWebSocket (websocket: WS): void { + this.websocket = websocket + } + + sharedFolder (currentSharedFolder: string): void { + this.currentSharedFolder = currentSharedFolder + } + + mapNpmDepsDir (list) { + const remixNpmDepsPath = `${this.currentSharedFolder}/.deps/npm` + const localNpmDepsPath = `${this.currentSharedFolder}/node_modules` + const npmDepsExists = existsSync(remixNpmDepsPath) + const nodeModulesExists = existsSync(localNpmDepsPath) + let isLocalDep = false + let isRemixDep = false + let allowPathString = '' + let remapString = '' + + for (const e of list) { + const importPath = e.replace(/import ['"]/g, '').trim() + const packageName = importPath.split('/')[0] + if (nodeModulesExists && readdirSync(localNpmDepsPath).includes(packageName)) { + isLocalDep = true + remapString += `${packageName}=./node_modules/${packageName} ` + } else if (npmDepsExists && readdirSync(remixNpmDepsPath).includes(packageName)) { + isRemixDep = true + remapString += `${packageName}=./.deps/npm/${packageName} ` + } + } + if (isLocalDep) allowPathString += './node_modules,' + if (isRemixDep) allowPathString += './.deps/npm,' + + return { remapString, allowPathString } + } + + transform (detectors: Record[]): OutputStandard[] { + const standardReport: OutputStandard[] = [] + for (const e of detectors) { + const obj = {} as OutputStandard + obj.description = e.description + obj.title = e.check + obj.confidence = e.confidence + obj.severity = e.impact + obj.sourceMap = e.elements.map((element) => { + delete element.source_mapping.filename_used + delete element.source_mapping.filename_absolute + return element + }) + standardReport.push(obj) + } + return standardReport + } + + analyse (filePath: string, compilerConfig: Record) { + return new Promise((resolve, reject) => { + if (this.readOnly) { + const errMsg: string = '[Slither Analysis]: Cannot analyse in read-only mode' + return reject(new Error(errMsg)) + } + const options = { cwd: this.currentSharedFolder, shell: true } + const { currentVersion, optimize, evmVersion } = compilerConfig + if (currentVersion && currentVersion.includes('+commit')) { + // Get compiler version with commit id e.g: 0.8.2+commit.661d110 + const versionString: string = currentVersion.substring(0, currentVersion.indexOf('+commit') + 16) + console.log('\x1b[32m%s\x1b[0m', `[Slither Analysis]: Compiler version is ${versionString}`) + let solcOutput: Buffer + // Check solc current installed version + try { + solcOutput = execSync('solc --version', options) + } catch (err) { + console.log(err) + reject(new Error('Error in running solc command')) + } + if (!solcOutput.toString().includes(versionString)) { + console.log('\x1b[32m%s\x1b[0m', '[Slither Analysis]: Compiler version is different from installed solc version') + // Get compiler version without commit id e.g: 0.8.2 + const version: string = versionString.substring(0, versionString.indexOf('+commit')) + // List solc versions installed using solc-select + try { + const solcSelectInstalledVersions: Buffer = execSync('solc-select versions', options) + // Check if required version is already installed + if (!solcSelectInstalledVersions.toString().includes(version)) { + console.log('\x1b[32m%s\x1b[0m', `[Slither Analysis]: Installing ${version} using solc-select`) + // Install required version + execSync(`solc-select install ${version}`, options) + } + console.log('\x1b[32m%s\x1b[0m', `[Slither Analysis]: Setting ${version} as current solc version using solc-select`) + // Set solc current version as required version + execSync(`solc-select use ${version}`, options) + } catch (err) { + console.log(err) + reject(new Error('Error in running solc-select command')) + } + } else console.log('\x1b[32m%s\x1b[0m', '[Slither Analysis]: Compiler version is same as installed solc version') + } + // Allow paths and set solc remapping for import URLs + const fileContent = readFileSync(`${this.currentSharedFolder}/${filePath}`, 'utf8') + const importsArr = fileContent.match(/import ['"][^.|..](.+?)['"];/g) + let allowPaths = ''; let remaps = '' + if (importsArr?.length) { + const { remapString, allowPathString } = this.mapNpmDepsDir(importsArr) + allowPaths = allowPathString + remaps = remapString.trim() + } + const allowPathsOption: string = allowPaths ? `--allow-paths ${allowPaths}` : '' + const optimizeOption: string = optimize ? ' --optimize ' : '' + const evmOption: string = evmVersion ? ` --evm-version ${evmVersion}` : '' + const solcArgs: string = optimizeOption || evmOption || allowPathsOption ? `--solc-args '${allowPathsOption}${optimizeOption}${evmOption}'` : '' + const solcRemaps = remaps ? `--solc-remaps "${remaps}"` : '' + + const outputFile: string = 'remix-slitherReport_' + Math.floor(Date.now() / 1000) + '.json' + const cmd: string = `slither ${filePath} ${solcArgs} ${solcRemaps} --json ${outputFile}` + console.log('\x1b[32m%s\x1b[0m', '[Slither Analysis]: Running Slither...') + // Added `stdio: 'ignore'` as for contract with NPM imports analysis which is exported in 'stderr' + // get too big and hangs the process. We process analysis from the report file only + const child = spawn(cmd, { cwd: this.currentSharedFolder, shell: true, stdio: 'ignore' }) + + const response = {} + child.on('close', () => { + const outputFileAbsPath: string = `${this.currentSharedFolder}/${outputFile}` + // Check if slither report file exists + if (existsSync(outputFileAbsPath)) { + let report = readFileSync(outputFileAbsPath, 'utf8') + report = JSON.parse(report) + if (report['success']) { + response['status'] = true + if (!report['results'] || !report['results'].detectors || !report['results'].detectors.length) { + response['count'] = 0 + } else { + const { detectors } = report['results'] + response['count'] = detectors.length + response['data'] = this.transform(detectors) + } + console.log('\x1b[32m%s\x1b[0m', `[Slither Analysis]: Analysis Completed!! ${response['count']} warnings found.`) + resolve(response) + } else { + console.log(report['error']) + reject(new Error('Error in running Slither Analysis.')) + } + } else reject(new Error('Error in generating Slither Analysis Report. Make sure Slither is properly installed.')) + }) + }) + } +} diff --git a/libs/remixd/src/types/index.ts b/libs/remixd/src/types/index.ts index cde06fcf07..66818dff48 100644 --- a/libs/remixd/src/types/index.ts +++ b/libs/remixd/src/types/index.ts @@ -1,6 +1,18 @@ import * as ServiceList from '../serviceList' import * as Websocket from 'ws' +export interface OutputStandard { + description: string + title: string + confidence: string + severity: string + sourceMap: any + category?: string + reference?: string + example?: any + [key: string]: any +} + type ServiceListKeys = keyof typeof ServiceList; export type Service = typeof ServiceList[ServiceListKeys] diff --git a/libs/remixd/src/websocket.ts b/libs/remixd/src/websocket.ts index 370dc48abb..324fa3332b 100644 --- a/libs/remixd/src/websocket.ts +++ b/libs/remixd/src/websocket.ts @@ -19,7 +19,8 @@ export default class WebSocket { const listeners = { 65520: 'remixd', 65521: 'git', - 65522: 'hardhat' + 65522: 'hardhat', + 65523: 'slither' } this.server.on('error', (error: Error) => {