From 6a88043e9c17a71c01c217a511d0dd123500459a Mon Sep 17 00:00:00 2001 From: Sebastian Mueller Date: Mon, 22 Apr 2019 21:15:40 +0200 Subject: [PATCH] Enabled parameters: mode, debug, cache-lookup, limit and contracts! --- README.md | 24 ++++++++++ index.js | 110 ++++++++++++++++++++++++++++++++++++------- lib/issues2eslint.js | 15 ++---- mythx.js | 90 +++++++++++++++++++++++++++++++---- package-lock.json | 34 +++++++++++++ package.json | 1 + 6 files changed, 238 insertions(+), 36 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..c276df8 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +Status Embark plugin for MythX. + +# QuickStart + +Run `verify` in the Embark console. When the call returns, it will look something like this: + +``` +Embark (development) > verify +embark-mythx: Running MythX analysis in background. +embark-mythx: Submitting 'ERC20' for analysis... +embark-mythx: Submitting 'SafeMath' for analysis... +embark-mythx: Submitting 'Ownable' for analysis... + +embark-mythx: +/home/flex/mythx-plugin/testToken/.embark/contracts/ERC20.sol + 1:0 warning A floating pragma is set SWC-103 + +✖ 1 problem (0 errors, 1 warning) + +embark-mythx: MythX analysis found vulnerabilities. +``` + +# Options + diff --git a/index.js b/index.js index 6c483e6..05ad1f4 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,5 @@ const mythx = require('./mythx') +const commandLineArgs = require('command-line-args') module.exports = function(embark) { @@ -13,33 +14,47 @@ module.exports = function(embark) { embark.registerConsoleCommand({ description: "Run MythX analysis", matches: (cmd) => { - embark.logger.info('cmd', cmd) + //embark.logger.info('cmd', cmd) const cmdName = cmd.match(/".*?"|\S+/g) - embark.logger.info("cmd === 'verify': " + cmd === 'verify') + //embark.logger.info("cmdName", cmdName) //embark.logger.info('cmdName.length === 1', cmdName.length === 1) - return (cmd === 'verify' && cmdName.length === 1) + //embark.logger.info("eh?") + return (Array.isArray(cmdName) && + cmdName[0] === 'verify' && + cmdName[1] != 'help' && + cmdName[1] != 'status' && + cmdName.length >= 1) }, usage: "verify [options]", process: async (cmd, callback) => { + //embark.logger.info("cmd", cmd) + //embark.logger.info("verifying...") + + const cmdName = cmd.match(/".*?"|\S+/g) + // Remove first element, as we know it's the command + cmdName.shift() //embark.logger.info("embark.logger", JSON.stringify(embark.logger)) - let cfg = parseOptions(cmd) - embark.logger.info('cmd', cmd) - embark.logger.info('cfg', JSON.stringify(cfg)) + //console.log("option object", JSON.stringify({ "argv": cmdName })) + let cfg = parseOptions({ "argv": cmdName }) + + //embark.logger.info('cmd', cmdName) + //embark.logger.info('cfg', JSON.stringify(cfg)) + try { embark.logger.info("Running MythX analysis in background.") - const result = await mythx(contracts, cfg, embark) + const returnCode = await mythx.analyse(contracts, cfg, embark) //embark.logger.info("result", result) if (returnCode === 0) { return callback(null, "returnCode: " + returnCode) } else if (returnCode === 1) { - embark.logger.error("Analysis returned with exit code 1.") + embark.logger.error("\nMythX analysis found vulnerabilities.") return callback() } else { //TODO: Figure out how to use error with callback properly. - return callback(new Error("Unexpected Error: return value of `analyze` should be either 0 or 1."), null) + return callback(new Error("\nUnexpected Error: return value of `analyze` should be either 0 or 1."), null) } } catch (e) { @@ -52,16 +67,17 @@ module.exports = function(embark) { embark.registerConsoleCommand({ description: "Help", matches: (cmd) => { - embark.logger.info('cmd', cmd) + //embark.logger.info('cmd', cmd) const cmdName = cmd.match(/".*?"|\S+/g) - embark.logger.info('cmdName', cmdName) + //embark.logger.info('cmdName', cmdName) //embark.logger.info("cmdName[0] === 'verify' && cmdName[1] === 'help'", cmdName[0] === 'verify' && cmdName[1] === 'help') return (Array.isArray(cmdName) && - (cmdName[0] === 'verify' && - cmdName[1] === 'help')) + (cmdName[0] === 'verify' && + cmdName[1] === 'help')) }, usage: "verify help", process: (cmd, callback) => { + embark.logger.info("verify help running") return callback(null, help()) } }) @@ -75,9 +91,71 @@ module.exports = function(embark) { ) } + embark.registerConsoleCommand({ + description: "Check MythX analysis status", + matches: (cmd) => { + //embark.logger.info('cmd', cmd) + const cmdName = cmd.match(/".*?"|\S+/g) + //embark.logger.info("cmdName", cmdName) + //embark.logger.info('cmdName.length === 1', cmdName.length === 1) + return (Array.isArray(cmdName) && + cmdName[0] === 'verify' && + cmdName[1] != 'status' && + cmdName.length == 3) + }, + usage: "verify status ", + process: async (cmd, callback) => { + //embark.logger.info("verify status running") + //embark.logger.info("embark.logger", JSON.stringify(embark.logger)) + + let cfg = parseOptions(cmd) + //embark.logger.info('cmd', cmd) + //embark.logger.info('cfg', JSON.stringify(cfg)) + try { + const returnCode = await mythx.getStatus(cfg, embark) + //embark.logger.info("result", result) + + if (returnCode === 0) { + return callback(null, "returnCode: " + returnCode) + } else if (returnCode === 1) { + embark.logger.error("MythX analysis found vulnerabilities.") + return callback() + } else { + //TODO: Figure out how to use error with callback properly. + return callback(new Error("Unexpected Error: return value of `analyze` should be either 0 or 1."), null) + } + + } catch (e) { + embark.logger.error("error", e) + return callback(e, "ERR: " + e.message) + } + } + }) + function parseOptions(options) { - let config = {} - //TODO - return config + + //console.log("options", JSON.stringify(options)) + + const optionDefinitions = [ + { name: 'full', alias: 'f', type: Boolean }, + { name: 'debug', alias: 'd', type: Boolean }, + { name: 'no-cache-lookup', alias: 'c', type: Boolean }, + { name: 'limit', alias: 'l', type: Number }, + { name: 'contracts', type: String, multiple: true, defaultOption: true } + ] + + const parsed = commandLineArgs(optionDefinitions, options) + + //console.log("parsed", JSON.stringify(parsed)) + //console.log("parsed.contracts", parsed.contracts) + //console.log("parsed.full", parsed.full) + + if(parsed.full) { + parsed.analysisMode = "full" + } else { + parsed.analysisMode = "full" + } + + return parsed } } \ No newline at end of file diff --git a/lib/issues2eslint.js b/lib/issues2eslint.js index d22ed8e..62ce6a6 100644 --- a/lib/issues2eslint.js +++ b/lib/issues2eslint.js @@ -260,7 +260,7 @@ function doReport(config, objects, errors, notAnalyzedContracts) { return config.debug || (log.level !== 'info'); } - // Return 1 if some vulenrabilities were found. + // Return 1 if vulnerabilities were found. objects.forEach(ele => { ele.issues.forEach(ele => { ret = ele.issues.length > 0 ? 1 : ret; @@ -287,15 +287,8 @@ function doReport(config, objects, errors, notAnalyzedContracts) { const eslintIssuesByBaseName = groupEslintIssuesByBasename(eslintIssues); const uniqueIssues = getUniqueIssues(eslintIssuesByBaseName); - - console.log("uniqueIssues", JSON.stringify(uniqueIssues)) - - console.log("config.style", config.style) const formatter = getFormatter(config.style); - - config.logger.info("config.logger", JSON.stringify(config.logger)) const report = formatter(uniqueIssues); - console.log("report", report) config.logger.info(report); } @@ -315,7 +308,7 @@ function doReport(config, objects, errors, notAnalyzedContracts) { if (haveLogs) { ret = 1; - config.logger.info('MythX Logs:'.yellow); + config.logger.info('MythX Logs:'); logGroups.forEach(logGroup => { config.logger.info(`\n${logGroup.sourcePath}`.yellow); config.logger.info(`UUID: ${logGroup.uuid}`.yellow); @@ -329,9 +322,9 @@ function doReport(config, objects, errors, notAnalyzedContracts) { if (errors.length > 0) { ret = 1; - console.error('Internal MythX errors encountered:'.red); + config.logger.error('Internal MythX errors encountered:'.red); errors.forEach(err => { - console.error(err.error || err); + config.logger.error(err.error || err); if (config.debug > 1 && err.stack) { config.logger.info(err.stack); } diff --git a/mythx.js b/mythx.js index dd7b282..e5d45fb 100644 --- a/mythx.js +++ b/mythx.js @@ -8,7 +8,7 @@ const { MythXIssues, doReport } = require('./lib/issues2eslint'); const defaultAnalyzeRateLimit = 4 -module.exports = async function analyse(contracts, cfg, embark) { +async function analyse(contracts, cfg, embark) { //embark.logger.debug("embark.config", embark.config) @@ -39,14 +39,34 @@ module.exports = async function analyse(contracts, cfg, embark) { }) - //TODO: Check contract names provided in options are respected - //const contractNames = cfg._.length > 1 ? cfg._.slice(1, cfg._.length) : null - + //Check contract names provided in options are respected //embark.logger.info("contracts", contracts) + //embark.logger.info("cfg.contracts", cfg.contracts) - const submitObjects = mythXUtil.buildRequestData(contracts) + // Filter contracts based on parameter choice + let toSubmit = {}; + if(cfg.contracts) { + toSubmit.sources = contracts.sources + toSubmit.contracts = {} + for (let [filename, contractObjects] of Object.entries(contracts.contracts)) { + for (let [contractName, contract] of Object.entries(contractObjects)) { + if (cfg.contracts.indexOf(contractName) >= 0) { + //console.log("Adding to submit", contractName, contractObjects) + if(!toSubmit.contracts[filename]) { + toSubmit.contracts[filename] = {} + } + toSubmit.contracts[filename][contractName] = contract ; + } + } + } + } else { + toSubmit = contracts + } + + //embark.logger.info("toSubmit", toSubmit) + const submitObjects = mythXUtil.buildRequestData(toSubmit) - process.exit(0) + //return 0 const { objects, errors } = await doAnalysis(armletClient, cfg, submitObjects, null, limit) //console.log("objects", JSON.stringify(objects)) @@ -57,14 +77,42 @@ module.exports = async function analyse(contracts, cfg, embark) { return result } +async function getStatus(cfg, embark) { + + //embark.logger.debug("embark.config", embark.config) + + //console.log("embark.logger", embark.logger) + //console.log("JSON.stringify(embark.logger)", JSON.stringify(embark.logger)) + //embark.logger.info("typeof embark.logger", typeof embark.logger) + cfg.logger = embark.logger + //embark.logger.info("embark", JSON.stringify(embark)) + + // Connect to MythX via armlet + const armletClient = new armlet.Client( + { + clientToolName: "embark-mythx", + password: process.env.MYTHX_PASSWORD, + ethAddress: process.env.MYTHX_ETH_ADDRESS, + }) + + if (cfg.uuid) { + try { + const results = await armletClient.getIssues(config.uuid); + return ghettoReport(embark.logger.info, results); + } catch (err) { + embark.logger.warn(err); + return 1; + } + } +} + const doAnalysis = async (armletClient, config, contracts, contractNames = null, limit) => { //config.logger.info("\ncontracts", contracts) const timeout = (config.timeout || 300) * 1000; const initialDelay = ('initial-delay' in config) ? config['initial-delay'] * 1000 : undefined; - //const cacheLookup = ('cache-lookup' in config) ? config['cache-lookup'] : true; - const cacheLookup = false + const cacheLookup = ('cache-lookup' in config) ? config['cache-lookup'] : true; const results = await asyncPool(limit, contracts, async buildObj => { @@ -85,7 +133,7 @@ const doAnalysis = async (armletClient, config, contracts, contractNames = null, // request analysis to armlet. try { - //config.logger.info("analyzeOpts", JSON.stringify(analyzeOpts)) + config.logger.info("Submitting '" + obj.contractName + "' for analysis...") const armletResult = await armletClient.analyzeWithStatus(analyzeOpts); //config.logger.info("armletResult", JSON.stringify(armletResult)) const {issues, status} = armletResult @@ -143,3 +191,27 @@ const doAnalysis = async (armletClient, config, contracts, contractNames = null, return accum; }, { errors: [], objects: [] }); }; + +function ghettoReport(logger, results) { + let issuesCount = 0; + results.forEach(ele => { + issuesCount += ele.issues.length; + }); + + if (issuesCount === 0) { + logger('No issues found'); + return 0; + } + for (const group of results) { + logger(group.sourceList.join(', ').underline); + for (const issue of group.issues) { + logger(yaml.safeDump(issue, {'skipInvalid': true})); + } + } + return 1; +} + +module.exports = { + analyse, + getStatus +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5c1ab3c..ea15639 100644 --- a/package-lock.json +++ b/package-lock.json @@ -116,6 +116,11 @@ "request": "^2.88.0" } }, + "array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==" + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -356,6 +361,17 @@ "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.8.tgz", "integrity": "sha512-PM54PkseWbiiD/mMsbvW351/u+dafwTJ0ye2qB60G1aGQP9j3xK2gmMDc+R34L3nDtx4qMCitXT75mkbkGJDLw==" }, + "command-line-args": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.1.1.tgz", + "integrity": "sha512-hL/eG8lrll1Qy1ezvkant+trihbGnaKaeEjj6Scyr3DN+RC7iQ5Rz84IeLERfAWDGo0HBSNAakczwgCilDXnWg==", + "requires": { + "array-back": "^3.0.1", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1073,6 +1089,14 @@ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" }, + "find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "requires": { + "array-back": "^3.0.1" + } + }, "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", @@ -1717,6 +1741,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2586,6 +2615,11 @@ "prelude-ls": "~1.1.2" } }, + "typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==" + }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", diff --git a/package.json b/package.json index f646ccc..f6781d8 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "license": "ISC", "dependencies": { "armlet": "^2.3.0", + "command-line-args": "^5.1.1", "dotenv": "^7.0.0", "eslint": "^5.16.0", "minimist": "^1.2.0",