slither plugin

pull/4925/head
Your Name 5 months ago
parent 68d9d7d8fa
commit da3801b7d7
  1. 9
      apps/remix-ide/src/app.js
  2. 3
      apps/remix-ide/src/app/panels/file-panel.js
  3. 13
      apps/remix-ide/src/app/plugins/electron/slitherPlugin.ts
  4. 2
      apps/remix-ide/src/remixAppManager.js
  5. 3
      apps/remixdesktop/src/engine.ts
  6. 36
      apps/remixdesktop/src/lib/remixd.ts
  7. 193
      apps/remixdesktop/src/lib/slither.ts
  8. 24
      apps/remixdesktop/src/lib/utils.ts
  9. 34
      apps/remixdesktop/src/plugins/slitherPlugin.ts
  10. 2
      apps/remixdesktop/src/preload.ts
  11. 95
      apps/remixdesktop/test/tests/app/slitherlinux.test.ts
  12. 38
      libs/remix-ui/static-analyser/src/lib/remix-ui-static-analyser.tsx

@ -56,6 +56,8 @@ import { xtermPlugin } from './app/plugins/electron/xtermPlugin'
import { ripgrepPlugin } from './app/plugins/electron/ripgrepPlugin'
import { compilerLoaderPlugin, compilerLoaderPluginDesktop } from './app/plugins/electron/compilerLoaderPlugin'
import { appUpdaterPlugin } from './app/plugins/electron/appUpdaterPlugin'
import { SlitherHandleDesktop } from './app/plugins/electron/slitherPlugin'
import { SlitherHandle } from './app/files/slither-handle'
import {SolCoder} from './app/plugins/solcoderAI'
const isElectron = require('is-electron')
@ -376,6 +378,10 @@ class AppComponent {
const compilerloader = isElectron()? new compilerLoaderPluginDesktop(): new compilerLoaderPlugin()
this.engine.register([compilerloader])
// slither analyzer plugin (remixd / desktop)
const slitherPlugin = isElectron() ? new SlitherHandleDesktop() : new SlitherHandle()
this.engine.register([slitherPlugin])
// LAYOUT & SYSTEM VIEWS
const appPanel = new MainPanel()
Registry.getInstance().put({api: this.mainview, name: 'mainview'})
@ -433,7 +439,6 @@ class AppComponent {
filePanel.hardhatHandle,
filePanel.foundryHandle,
filePanel.truffleHandle,
filePanel.slitherHandle,
linkLibraries,
deployLibraries,
openZeppelinProxy,
@ -500,7 +505,7 @@ class AppComponent {
await this.appManager.activatePlugin(['solidity-script', 'remix-templates'])
if (isElectron()){
await this.appManager.activatePlugin(['isogit', 'electronconfig', 'electronTemplates', 'xterm', 'ripgrep', 'appUpdater'])
await this.appManager.activatePlugin(['isogit', 'electronconfig', 'electronTemplates', 'xterm', 'ripgrep', 'appUpdater', 'slither'])
}
this.appManager.on(

@ -9,7 +9,6 @@ import {PluginViewWrapper} from '@remix-ui/helper'
const { HardhatHandle } = require('../files/hardhat-handle.js')
const { FoundryHandle } = require('../files/foundry-handle.js')
const { TruffleHandle } = require('../files/truffle-handle.js')
const { SlitherHandle } = require('../files/slither-handle.js')
/*
Overview of APIs:
@ -72,7 +71,7 @@ module.exports = class Filepanel extends ViewPlugin {
this.hardhatHandle = new HardhatHandle()
this.foundryHandle = new FoundryHandle()
this.truffleHandle = new TruffleHandle()
this.slitherHandle = new SlitherHandle()
this.workspaces = []
this.appManager = appManager
this.currentWorkspaceMetadata = null

@ -0,0 +1,13 @@
import { ElectronPlugin } from '@remixproject/engine-electron';
export class SlitherHandleDesktop extends ElectronPlugin {
constructor() {
super({
displayName: 'slither',
name: 'slither',
description: 'electron slither',
methods: ['analyse']
})
this.methods = ['analyse']
}
}

@ -158,7 +158,7 @@ export class RemixAppManager extends PluginManager {
this.pluginsDirectory = 'https://raw.githubusercontent.com/ethereum/remix-plugins-directory/master/build/metadata.json'
this.pluginLoader = new PluginLoader()
if (Registry.getInstance().get('platform').api.isDesktop()) {
requiredModules = [...requiredModules, 'fs', 'electronTemplates', 'isogit', 'remix-templates', 'electronconfig', 'xterm', 'compilerloader', 'ripgrep']
requiredModules = [...requiredModules, 'fs', 'electronTemplates', 'isogit', 'remix-templates', 'electronconfig', 'xterm', 'compilerloader', 'ripgrep', 'slither']
}
}

@ -9,6 +9,7 @@ import { ConfigPlugin } from './plugins/configPlugin';
import { TemplatesPlugin } from './plugins/templates';
import { RipgrepPlugin } from './plugins/ripgrepPlugin';
import { CompilerLoaderPlugin } from './plugins/compilerLoader';
import { SlitherPlugin } from './plugins/slitherPlugin';
import { AppUpdaterPlugin } from './plugins/appUpdater';
const engine = new Engine()
@ -20,6 +21,7 @@ const configPlugin = new ConfigPlugin()
const templatesPlugin = new TemplatesPlugin()
const ripgrepPlugin = new RipgrepPlugin()
const compilerLoaderPlugin = new CompilerLoaderPlugin()
const slitherPlugin = new SlitherPlugin()
const appUpdaterPlugin = new AppUpdaterPlugin()
engine.register(appManager)
@ -30,6 +32,7 @@ engine.register(configPlugin)
engine.register(templatesPlugin)
engine.register(ripgrepPlugin)
engine.register(compilerLoaderPlugin)
engine.register(slitherPlugin)
engine.register(appUpdaterPlugin)
appManager.activatePlugin('electronconfig')

@ -0,0 +1,36 @@
import { ElectronBasePluginClient } from "@remixproject/plugin-electron";
import { Profile } from "@remixproject/plugin-utils";
export class ElectronBasePluginRemixdClient extends ElectronBasePluginClient {
log: (...message: any) => void
error: (...message: any) => void
currentSharedFolder: string = ''
constructor(webContentsId: number, profile: Profile) {
super(webContentsId, profile);
this.log = (...message: any) => {
for(const m of message) {
this.call('terminal', 'log', {
type: 'log',
value: m
})
}
}
this.error = (...message: any) => {
for(const m of message) {
this.call('terminal', 'log', {
type: 'error',
value: m
})
}
}
this.onload(async () => {
this.on('fs' as any, 'workingDirChanged', async (path: string) => {
this.currentSharedFolder = path
})
this.currentSharedFolder = await this.call('fs' as any, 'getWorkingDir')
})
}
}

@ -0,0 +1,193 @@
import { existsSync, readFileSync, readdirSync, unlinkSync } from 'fs'
import * as utils from './utils'
const { spawn, execSync } = require('child_process') // eslint-disable-line
export interface OutputStandard {
description: string
title: string
confidence: string
severity: string
sourceMap: any
category?: string
reference?: string
example?: any
[key: string]: any
}
export const SlitherClientMixin = (Base) => class extends Base {
methods: Array<string>
currentSharedFolder: string
constructor(...args: any[]) {
super(...args); // Ensure the parent constructor is called
}
log(...message: any) {
if (this.log) {
this.log(...message)
} else {
console.log(...message)
}
}
error(...message: any) {
if (this.error) {
this.error(...message)
} else {
console.error(...message)
}
}
mapNpmDepsDir(list) {
const remixNpmDepsPath = utils.absolutePath('.deps/npm', this.currentSharedFolder)
const localNpmDepsPath = utils.absolutePath('node_modules', this.currentSharedFolder)
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<string, any>[]): 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<string, any>) {
return new Promise((resolve, reject) => {
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)
this.log(`[Slither Analysis]: Compiler version is ${versionString}`)
let solcOutput: Buffer
// Check solc current installed version
try {
solcOutput = execSync('solc --version', options)
} catch (err) {
this.error(err)
reject(new Error('Error in running solc command'))
}
if (!solcOutput.toString().includes(versionString)) {
this.log('[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)) {
this.log(`[Slither Analysis]: Installing ${version} using solc-select`)
// Install required version
execSync(`solc-select install ${version}`, options)
}
this.log(`[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) {
this.error(err)
reject(new Error('Error in running solc-select command'))
}
} else this.log('[Slither Analysis]: Compiler version is same as installed solc version')
}
// Allow paths and set solc remapping for import URLs
const fileContent = readFileSync(utils.absolutePath(filePath, this.currentSharedFolder), 'utf8')
const importsArr = fileContent.match(/import ['"][^.|..](.+?)['"];/g)
let remaps = ''
if (importsArr?.length) {
const { remapString } = this.mapNpmDepsDir(importsArr)
remaps = remapString.trim()
}
const optimizeOption: string = optimize ? '--optimize' : ''
const evmOption: string = evmVersion ? `--evm-version ${evmVersion}` : ''
let solcArgs = ''
if (optimizeOption) {
solcArgs += optimizeOption + ' '
}
if (evmOption) {
if (!solcArgs.endsWith(' ')) solcArgs += ' '
solcArgs += evmOption
}
if (solcArgs) {
solcArgs = `--solc-args "${solcArgs.trimStart()}"`
}
const solcRemaps = remaps ? `--solc-remaps "${remaps}"` : ''
const outputFile = 'remix-slither-report.json'
try {
// We don't keep the previous analysis
const outputFilePath = utils.absolutePath(outputFile, this.currentSharedFolder)
if (existsSync(outputFilePath)) unlinkSync(outputFilePath)
} catch (e) {
this.error('unable to remove the output file')
this.error(e.message)
}
const cmd = `slither ${filePath} ${solcArgs} ${solcRemaps} --json ${outputFile}`
this.log('[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 = utils.absolutePath(outputFile, this.currentSharedFolder)
// 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)
}
resolve(response)
} else {
this.log(report['error'])
reject(new Error('Error in running Slither Analysis.'))
}
} else {
this.error('Error in generating Slither Analysis Report. Make sure Slither is properly installed.')
reject(new Error('Error in generating Slither Analysis Report. Make sure Slither is properly installed.'))
}
})
})
}
}

@ -0,0 +1,24 @@
import * as pathModule from 'path'
/**
* returns the absolute path of the given @arg path
*
* @param {String} path - relative path (Unix style which is the one used by Remix IDE)
* @param {String} sharedFolder - absolute shared path. platform dependent representation.
* @return {String} platform dependent absolute path (/home/user1/.../... for unix, c:\user\...\... for windows)
*/
function absolutePath (path: string, sharedFolder:string): string {
path = normalizePath(path)
path = pathModule.resolve(sharedFolder, path)
return path
}
function normalizePath (path) {
if (path === '/') path = './'
if (process.platform === 'win32') {
return path.replace(/\//g, '\\')
}
return path
}
export { absolutePath }

@ -0,0 +1,34 @@
import { Profile } from "@remixproject/plugin-utils";
import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron"
import { ElectronBasePluginRemixdClient } from "../lib/remixd"
import { SlitherClientMixin } from "../lib/slither";
const profile: Profile = {
name: 'slither',
displayName: 'electron slither',
description: 'electron slither',
}
export class SlitherPlugin extends ElectronBasePlugin {
clients: any []
constructor() {
super(profile, clientProfile, SlitherClientMixin(SlitherPluginClient))
this.methods = [...super.methods]
}
}
const clientProfile: Profile = {
name: 'slither',
displayName: 'electron slither',
description: 'electron slither',
methods: ['analyse']
}
class SlitherPluginClient extends ElectronBasePluginRemixdClient {
constructor(webContentsId: number, profile: Profile) {
super(webContentsId, profile);
}
}

@ -6,7 +6,7 @@ console.log('preload.ts', new Date().toLocaleTimeString())
/* preload script needs statically defined API for each plugin */
const exposedPLugins = ['fs', 'git', 'xterm', 'isogit', 'electronconfig', 'electronTemplates', 'ripgrep', 'compilerloader', 'appUpdater']
const exposedPLugins = ['fs', 'git', 'xterm', 'isogit', 'electronconfig', 'electronTemplates', 'ripgrep', 'compilerloader', 'appUpdater', 'slither']
let webContentsId: number | undefined

@ -0,0 +1,95 @@
import {NightwatchBrowser} from 'nightwatch'
import { ChildProcess, spawn } from 'child_process'
import { homedir } from 'os'
const tests = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
done()
},
open: function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="openFolderButton"]', 10000).click('*[data-id="openFolderButton"]')
},
'open default template': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.waitForElementVisible('button[data-id="landingPageImportFromTemplate"]')
.click('button[data-id="landingPageImportFromTemplate"]')
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
.waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.pause(3000)
.windowHandles(function (result) {
console.log(result.value)
browser.switchWindow(result.value[1])
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]')
.click('*[data-id="treeViewLitreeViewItemtests"]')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemcontracts"]')
.click('*[data-id="treeViewLitreeViewItemcontracts"]')
.waitForElementVisible('[data-id="treeViewLitreeViewItemcontracts/1_Storage.sol"]')
.openFile('contracts/1_Storage.sol')
.waitForElementVisible('*[id="editorView"]', 10000)
.getEditorValue((content) => {
browser.assert.ok(content.includes('function retrieve() public view returns (uint256){'))
})
})
},
'Should install slither #group6': function (browser: NightwatchBrowser) {
browser.perform(async (done) => {
await installSlither()
done()
})
},
'run slither': function (browser: NightwatchBrowser) {
browser
.click('[data-id="verticalIconsKindpluginManager"]')
.scrollAndClick('[data-id="pluginManagerComponentActivateButtonsolidityStaticAnalysis"]')
.clickLaunchIcon('solidity').click('*[data-id="compilerContainerCompileBtn"]')
.pause(1000)
.clickLaunchIcon('solidityStaticAnalysis')
.useXpath()
.click('//*[@id="staticAnalysisRunBtn"]')
.waitForElementPresent('//*[@id="staticanalysisresult"]', 5000)
.waitForElementVisible({
selector: "//*[@data-id='nolibslitherwarnings'][contains(text(), '1')]",
locateStrategy: 'xpath',
timeout: 5000
})
.waitForElementVisible({
selector: "//div[@data-id='block']/span[contains(text(), '1 warnings found.')]",
locateStrategy: 'xpath',
timeout: 5000
})
},
after: function (browser: NightwatchBrowser) {
browser.end()
},
}
async function installSlither(): Promise<void> {
console.log('installSlither', process.cwd())
try {
const server = spawn('node', ['../../dist/libs/remixd/src/scripts/installSlither.js'], { cwd: process.cwd(), shell: true, detached: true })
return new Promise((resolve, reject) => {
server.stdout.on('data', function (data) {
console.log(data.toString())
if (
data.toString().includes("Slither is ready to use")
) {
console.log('resolving')
resolve()
}
})
server.stderr.on('err', function (data) {
console.log(data.toString())
reject(data.toString())
})
})
} catch (e) {
console.log(e)
}
}
module.exports = {
...process.platform.startsWith('linux')?tests:{}
}

@ -1,23 +1,24 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unused-vars */
import React, {useEffect, useState, useReducer, useRef, Fragment} from 'react' // eslint-disable-line
import React, {useEffect, useState, useReducer, useRef, Fragment, useContext} from 'react' // eslint-disable-line
import Button from './Button/StaticAnalyserButton' // eslint-disable-line
import { util } from '@remix-project/remix-lib'
import {util} from '@remix-project/remix-lib'
import _ from 'lodash'
import * as semver from 'semver'
import {TreeView, TreeViewItem} from '@remix-ui/tree-view' // eslint-disable-line
import {RemixUiCheckbox} from '@remix-ui/checkbox' // eslint-disable-line
import ErrorRenderer from './ErrorRenderer' // eslint-disable-line
import { compilation } from './actions/staticAnalysisActions'
import { initialState, analysisReducer } from './reducers/staticAnalysisReducer'
import { CodeAnalysis } from '@remix-project/remix-analyzer'
import {compilation} from './actions/staticAnalysisActions'
import {initialState, analysisReducer} from './reducers/staticAnalysisReducer'
import {CodeAnalysis} from '@remix-project/remix-analyzer'
import Tab from 'react-bootstrap/Tab'
import Tabs from 'react-bootstrap/Tabs'
import { AnalysisTab, SolHintReport } from '../staticanalyser'
import { run } from './actions/staticAnalysisActions'
import { BasicTitle, calculateWarningStateEntries } from './components/BasicTitle'
import { Nav, TabContainer } from 'react-bootstrap'
import { CustomTooltip } from '@remix-ui/helper'
import {AnalysisTab, SolHintReport} from '../staticanalyser'
import {run} from './actions/staticAnalysisActions'
import {BasicTitle, calculateWarningStateEntries} from './components/BasicTitle'
import {Nav, TabContainer} from 'react-bootstrap'
import {CustomTooltip} from '@remix-ui/helper'
import { appPlatformTypes, platformContext } from '@remix-ui/app'
declare global {
interface Window {
@ -37,6 +38,7 @@ type tabSelectionType = 'remix' | 'solhint' | 'slither' | 'none'
export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
const [runner] = useState(new CodeAnalysis())
const platform = useContext(platformContext)
const preProcessModules = (arr: any) => {
return arr.map((Item, i) => {
@ -130,7 +132,7 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
useEffect(() => {
const checkRemixdActive = async () => {
const remixdActive = await props.analysisModule.call('manager', 'isActive', 'remixd')
const remixdActive = await props.analysisModule.call('manager', 'isActive', 'remixd') || platform === appPlatformTypes.desktop
if (remixdActive) {
setSlitherEnabled(true)
setShowSlither(true)
@ -147,12 +149,12 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
// Reset badge
// Reset state
dispatch({ type: '', payload: initialState })
dispatch({type: '', payload: initialState})
setHints([])
setSlitherWarnings([])
setSsaWarnings([])
// Show 'Enable Slither Analysis' checkbox
if (currentWorkspace && currentWorkspace.isLocalhost === true) {
if ((currentWorkspace && currentWorkspace.isLocalhost === true) || platform === appPlatformTypes.desktop) {
setShowSlither(true)
setSlitherEnabled(true)
} else {
@ -175,7 +177,7 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
// Reset badge
props.event.trigger('staticAnalysisWarning', [-1])
// Reset state
dispatch({ type: '', payload: initialState })
dispatch({type: '', payload: initialState})
setShowSlither(false)
}
})
@ -319,7 +321,7 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
label={
<label
htmlFor={`heading${categoryId}`}
style={{ cursor: 'pointer' }}
style={{cursor: 'pointer'}}
className="pl-3 card-header h6 d-flex justify-content-between font-weight-bold px-1 py-2 w-100"
data-bs-toggle="collapse"
data-bs-expanded="false"
@ -523,7 +525,7 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
},
state.file,
'',
{ focus: true }
{focus: true}
)
}}
>
@ -563,7 +565,7 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
},
state.file,
'',
{ focus: true }
{focus: true}
)
}}
>
@ -602,7 +604,7 @@ export const RemixUiStaticAnalyser = (props: RemixUiStaticAnalyserProps) => {
},
state.file,
'',
{ focus: true }
{focus: true}
)
}}
>

Loading…
Cancel
Save