├── .circleci └── config.yml ├── .editorconfig ├── .eslintrc.yml ├── .gitignore ├── LICENSE ├── README.md ├── contracts ├── ConvertLib.sol ├── ERC721.sol ├── MetaCoin.sol ├── Migrations.sol ├── OwnedToken.sol ├── Primality.sol ├── TokenSale.sol ├── empty.sol ├── error.sol ├── external-lib.sol ├── import.sol ├── no-contracts.sol ├── no-pragma.sol ├── token.sol ├── use-external-lib.sol └── vulnerable.sol ├── lib ├── client.js ├── compiler.js ├── controllers │ ├── analyze.js │ ├── api_version.js │ ├── help.js │ ├── list.js │ ├── status.js │ └── version.js ├── eslint.js ├── formatters │ ├── propertyCheckText.js │ └── text.js ├── releases.json ├── report.js └── utils.js ├── package.json ├── sabre.js ├── static ├── modes.png └── sabre_v2.jpg └── test ├── assets └── TokenSale.json ├── compile.test.js └── report.test.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | 7 | defaults: &defaults 8 | working_directory: ~/sabre 9 | docker: 10 | - image: circleci/node:lts 11 | 12 | jobs: 13 | test: 14 | <<: *defaults 15 | steps: 16 | - checkout 17 | 18 | # Download and cache dependencies 19 | - restore_cache: 20 | keys: 21 | - v1-dependencies-{{ checksum "package.json" }} 22 | # fallback to using the latest cache if no exact match is found 23 | - v1-dependencies- 24 | 25 | - run: npm install 26 | - run: 27 | name: Run tests 28 | command: npm test 29 | 30 | - save_cache: 31 | paths: 32 | - node_modules 33 | key: v1-dependencies-{{ checksum "package.json" }} 34 | 35 | - persist_to_workspace: 36 | root: ~/sabre 37 | paths: . 38 | 39 | deploy: 40 | <<: *defaults 41 | steps: 42 | - attach_workspace: 43 | at: ~/sabre 44 | - run: 45 | name: Authenticate with registry 46 | command: echo "//registry.npmjs.org/:_authToken=$npm_TOKEN" > ~/repo/.npmrc 47 | - run: 48 | name: Publish package 49 | command: npm publish 50 | 51 | workflows: 52 | version: 2 53 | test-deploy: 54 | jobs: 55 | - test: 56 | filters: 57 | tags: 58 | only: /^v.*/ 59 | - deploy: 60 | requires: 61 | - test 62 | filters: 63 | tags: 64 | only: /^v.*/ 65 | branches: 66 | ignore: /.*/ 67 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.json] 12 | indent_size = 2 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | node: true 3 | amd: true 4 | es6: true 5 | mocha: true 6 | extends: 'eslint:recommended' 7 | parser: 'babel-eslint' 8 | parserOptions: 9 | ecmaVersion: 8 10 | rules: 11 | indent: 12 | - error 13 | - 4 14 | linebreak-style: 15 | - error 16 | - unix 17 | quotes: 18 | - error 19 | - single 20 | semi: 21 | - error 22 | - always 23 | no-console: 0 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | config.js 4 | 5 | # IDE 6 | .idea 7 | .vscode 8 | 9 | # Ignore temp directory 10 | .temp 11 | 12 | # nyc 13 | .nyc_output 14 | 15 | package-lock.json 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-present Bernhard Mueller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sabre 2 | [![Discord](https://img.shields.io/discord/481002907366588416.svg)](https://discord.gg/E3YrVtG) 3 | 4 | Sabre is a security analysis tool for smart contracts written in Solidity. It uses the [MythX symbolic execution & fuzzing service](https://mythx.io) to: 5 | 6 | - Generically detect [a wide range of security issues](https://mythx.io/swc-coverage); 7 | - Check for assertion violations and produce counter-examples. 8 | 9 | **Warning: This is my own MythX client hobby implementation. Please use the official [MythX command line client](https://github.com/dmuhs/mythx-cli) in production environments .** 10 | 11 | ## Usage 12 | 13 | ### Installation 14 | 15 | ``` 16 | $ npm install -g sabre-mythx 17 | ``` 18 | 19 | ### Setting up an Account 20 | 21 | Sign up for an on the [MythX website](https://mythx.io) to generate an API key. Set the `MYTHX_API_KEY` enviroment variable by adding the following to your `.bashrc` or `.bash_profile`): 22 | 23 | ``` 24 | export MYTHX_API_KEY=eyJhbGciOiJI(...) 25 | ``` 26 | 27 | ### Generic bug detection 28 | 29 | Run `sabre analyze [contract-name]` to submit a smart contract for analysis. The default mode is "quick" analysis which returns results after approximately 2 minutes. You'll also get a dashboard link where you can monitor the progress and view the report later. 30 | 31 | ### Custom property checking 32 | 33 | To check specifically for assertion violations and print counter-examples for any violations found, run the following: 34 | 35 | ``` 36 | $ sabre check [contract-name] 37 | ``` 38 | 39 | #### Example 1: Primality test 40 | 41 | You're pretty sure that 973013 is a prime number. It ends with a "3" so why wouldn't it be?? 42 | 43 | 44 | ``` 45 | pragma solidity ^0.5.0; 46 | 47 | contract Primality { 48 | 49 | uint256 public largePrime = 973013; 50 | 51 | uint256 x; 52 | uint256 y; 53 | 54 | function setX(uint256 _x) external { 55 | x = _x; 56 | } 57 | 58 | function setY(uint256 _y) external { 59 | y = _y; 60 | } 61 | 62 | function verifyPrime() external view { 63 | require(x > 1 && x < largePrime); 64 | require(y > 1 && y < largePrime); 65 | assert(x*y != largePrime); 66 | } 67 | } 68 | ``` 69 | 70 | Surely the assertion in `verifyPrime()` will hold for all possible inputs? 71 | 72 | 73 | ``` 74 | $ sabre check primality.sol 75 | -------------------- 76 | ASSERTION VIOLATION! 77 | /Users/bernhardmueller/Desktop/primality.sol: from 21:8 to 21:33 78 | 79 | assert(x*y != largePrime) 80 | -------------------- 81 | Call sequence: 82 | 83 | 1: setY(1021) 84 | Sender: 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa [ USER ] 85 | Value: 0 86 | 87 | 2: setX(953) 88 | Sender: 0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe [ CREATOR ] 89 | Value: 0 90 | 91 | 3: verifyPrimeness() 92 | Sender: 0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe [ CREATOR ] 93 | Value: 0 94 | 95 | ``` 96 | 97 | Oh no! 1021 x 953 = 973013, better pick a different number 🙄 98 | 99 | #### Example 2: Integer precision bug 100 | 101 | Source: [Sigma Prime](https://blog.sigmaprime.io/solidity-security.html#precision-vuln) 102 | 103 | Here is a simple contract for buying and selling tokens. What could possibly go wrong? 104 | 105 | ``` 106 | pragma solidity ^0.5.0; 107 | 108 | contract FunWithNumbers { 109 | uint constant public tokensPerEth = 10; 110 | uint constant public weiPerEth = 1e18; 111 | mapping(address => uint) public balances; 112 | 113 | function buyTokens() public payable { 114 | uint tokens = msg.value/weiPerEth*tokensPerEth; // convert wei to eth, then multiply by token rate 115 | balances[msg.sender] += tokens; 116 | } 117 | 118 | function sellTokens(uint tokens) public { 119 | require(balances[msg.sender] >= tokens); 120 | uint eth = tokens/tokensPerEth; 121 | balances[msg.sender] -= tokens; 122 | msg.sender.transfer(eth*weiPerEth); 123 | } 124 | } 125 | ``` 126 | 127 | Better safe than sorry! Let's check some [contract invariants](https://gist.github.com/b-mueller/0916c3700c94e94b23dfa9aa650005e8) just to be 1,700% sure that everything works as expected. 128 | 129 | ``` 130 | $ sabre check funwithnumbers.sol 131 | -------------------- 132 | ASSERTION VIOLATION! 133 | /Users/bernhardmueller/Desktop/funwithnumbers.sol: from 47:17 to 47:131 134 | 135 | AssertionFailed("Invariant violation: Sender token balance must increase when contract account balance increases") 136 | -------------------- 137 | Call sequence: 138 | 139 | 1: buyTokens() 140 | Sender: 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa3 [ USER ] 141 | Value: 6 142 | 143 | -------------------- 144 | ASSERTION VIOLATION! 145 | /Users/bernhardmueller/Desktop/funwithnumbers.sol: from 56:17 to 56:131 146 | 147 | AssertionFailed("Invariant violation: Contract account balance must decrease when sender token balance decreases") 148 | -------------------- 149 | Call sequence: 150 | 151 | 1: buyTokens() 152 | Sender: 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0 [ USER ] 153 | Value: 1000000000000000000 154 | 155 | 2: sellTokens(6) 156 | Sender: 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0 [ USER ] 157 | Value: 0 158 | ``` 159 | 160 | Um what?? Fractional numbers are rounded down 😲 161 | 162 | #### Example 3: Arbitrary storage write 163 | 164 | Source: [Ethernaut](https://ethernaut.openzeppelin.com/level/0xe83cf387ddfd13a2db5493d014ba5b328589fb5f) (I made this [a bit more complex](https://gist.github.com/b-mueller/44a995aaf764051963802a061665b446)) 165 | 166 | This [smart contract](https://gist.github.com/b-mueller/44a995aaf764051963802a061665b446) has, and will always have, only one owner. There isn't even a `transferOwnership` function. But... can you be really sure? Don't you at least want to double-check with a high-level, catch-all invariant? 167 | 168 | ``` 169 | contract VerifyRegistrar is Registrar { 170 | 171 | modifier checkInvariants { 172 | address old_owner = owner; 173 | _; 174 | assert(owner == old_owner); 175 | } 176 | 177 | function register(bytes32 _name, address _mappedAddress) checkInvariants public { 178 | super.register(_name, _mappedAddress); 179 | } 180 | } 181 | ``` 182 | 183 | Let's check just to be 15,000% sure. 184 | 185 | 186 | ``` 187 | $ sabre check registrar.sol 188 | ✔ Loaded solc v0.4.25 from local cache 189 | ✔ Compiled with solc v0.4.25 successfully 190 | ✔ Analysis job submitted: https://dashboard.mythx.io/#/console/analyses/e98a345e-7418-4209-ab99-bffdc2535d9b 191 | -------------------- 192 | ASSERTION VIOLATION! 193 | /Users/bernhardmueller/Desktop/registrar.sol: from 40:8 to 40:34 194 | 195 | assert(owner == old_owner) 196 | -------------------- 197 | Call sequence: 198 | 199 | 1: register(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 0x0000000000000000000000000000000000000000) 200 | Sender: 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa [ USER ] 201 | Value: 0 202 | ``` 203 | 204 | Ooops... better initialize those structs before using them. 205 | 206 | #### Example 4: Pausable token 207 | 208 | Source: [TrailofBits](https://github.com/crytic/building-secure-contracts/tree/master/program-analysis/echidna/exercises/exercise1) 209 | 210 | Smart contracts get hacked all the time so it's always great to have a pause button, even if it's just a [simple token 211 | ](https://github.com/crytic/building-secure-contracts/tree/master/program-analysis/echidna/exercises/exercise1). This is even an off-switch if we pause the token and throw away the admin account? Or is it? 212 | 213 | Why not create an instance of the contract that's infinitely paused and check if there's any way to unpause it. 214 | 215 | ``` 216 | contract VerifyToken is Token { 217 | 218 | event AssertionFailed(string message); 219 | 220 | constructor() public { 221 | paused(); 222 | owner = address(0x0); // lose ownership 223 | } 224 | 225 | function transfer(address to, uint value) public { 226 | uint256 old_balance = balances[msg.sender]; 227 | 228 | super.transfer(to, value); 229 | 230 | if (balances[msg.sender] != old_balance) { 231 | emit AssertionFailed("Tokens transferred even though this contract instance was infinitely paused!!"); 232 | } 233 | } 234 | } 235 | ``` 236 | 237 | Given that this contract is forever paused, it should never be possible to transfer any tokens right? 238 | 239 | 240 | ``` 241 | $ sabre check token.sol 242 | ✔ Loaded solc v0.5.16 from local cache 243 | ✔ Compiled with solc v0.5.16 successfully 244 | ✔ Analysis job submitted: https://dashboard.mythx.io/#/console/analyses/8d4b0eb0-69d3-4d82-b6c6-bc90332a292c 245 | -------------------- 246 | ASSERTION VIOLATION! 247 | /Users/bernhardmueller/Desktop/token.sol: from 64:17 to 64:113 248 | 249 | AssertionFailed("Tokens transferred even though this contract instance was infinitely paused!!") 250 | -------------------- 251 | Call sequence: 252 | 253 | 1: Owner() 254 | Sender: 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef [ ATTACKER ] 255 | Value: 0 256 | 257 | 2: resume() 258 | Sender: 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef [ ATTACKER ] 259 | Value: 0 260 | 261 | 3: transfer(0x0008000002400240000200104000104080001000, 614153205830163099331592192) 262 | Sender: 0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe [ CREATOR ] 263 | Value: 0 264 | ``` 265 | 266 | Oh no 😵 Looks like somebody slipped up there when naming the constructor. 267 | 268 | ### Analysis mode 269 | 270 | ``` 271 | --mode 272 | ``` 273 | 274 | MythX integrates various analysis methods including static analysis, input fuzzing and symbolic execution. In the backend, each incoming analysis job is distributed to a number of workers that perform various tasks in parallel. There are two analysis modes, "quick", "standard" and "deep", that differ in the amount of resources dedicated to the analysis. 275 | 276 | 277 | ### Report format 278 | 279 | ``` 280 | --format 281 | ``` 282 | 283 | Select the report format. By default, Sabre outputs a verbose text report. Other options `stylish`, `compact`, `table`, `html` and `json`. Note that you can also view reports for past analyses on the [dashboard](http://dashboard.mythx.io). 284 | 285 | 286 | ### Other commands 287 | 288 | Besides `analyze` the following commands are available. 289 | 290 | ``` 291 | - list Get a list of submitted analyses. 292 | - status Get the status of an already submitted analysis 293 | - version Print Sabre Version 294 | - apiVersion Print MythX API version 295 | ``` 296 | 297 | ### Debugging 298 | 299 | ``` 300 | --debug 301 | ``` 302 | 303 | Dump the API request and reponse when submitting an analysis. 304 | 305 | # How it works 306 | 307 | Some articles and papers explaining the tech behind that runs in [MythX](https://mythx.io): 308 | 309 | - [Finding Vulnerabilities in Smart Contracts (Harvey Basics)](https://medium.com/consensys-diligence/finding-vulnerabilities-in-smart-contracts-175c56affe2) 310 | - [Fuzzing Smart Contracts Using Input Prediction](https://medium.com/consensys-diligence/fuzzing-smart-contracts-using-input-prediction-29b30ba8055c) 311 | - [Fuzzing Smart Contracts Using Multiple Transactions](https://medium.com/consensys-diligence/fuzzing-smart-contracts-using-multiple-transactions-51471e4b3c69) 312 | - [Learning Inputs in Greybox Fuzzing (Arxiv)](https://arxiv.org/pdf/1807.07875.pdf) 313 | - [Targeted Greybox Fuzzing using Lookahead Analysis (Arxiv)](https://arxiv.org/abs/1905.07147) 314 | - [Intro to Symbolic Execution in Mythril](https://medium.com/@joran.honig/introduction-to-mythril-classic-and-symbolic-execution-ef59339f259b) 315 | - [Advances in Smart Contract Vulnerability Detection (DEFCON 27)](https://github.com/b-mueller/smashing-smart-contracts/blob/master/DEFCON27-EVM-Smart-Contracts-Mueller-Luca.pdf) 316 | - [Smashing Smart Contracts (HITB GSEC 2018)](https://conference.hitb.org/hitbsecconf2018ams/materials/D1T2%20-%20Bernhard%20Mueller%20-%20Smashing%20Ethereum%20Smart%20Contracts%20for%20Fun%20and%20ACTUAL%20Profit.pdf) 317 | - [The Tech Behind MythX (high-level)](https://medium.com/consensys-diligence/the-tech-behind-mythx-smart-contract-security-analysis-32c849aedaef) 318 | - [Practical Mutation Testing in Smart Contracts](https://www.researchgate.net/publication/335937116_Practical_Mutation_Testing_for_Smart_Contracts) 319 | -------------------------------------------------------------------------------- /contracts/ConvertLib.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.4.25 <0.6.0; 2 | 3 | library ConvertLib{ 4 | function convert(uint amount,uint conversionRate) public pure returns (uint convertedAmount) 5 | { 6 | return amount * conversionRate; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /contracts/ERC721.sol: -------------------------------------------------------------------------------- 1 | /// @title Interface for contracts conforming to ERC-721: Non-Fungible Tokens 2 | /// @author Dieter Shirley (https://github.com/dete) 3 | 4 | contract ERC721 { 5 | // Required methods 6 | function totalSupply() public view returns (uint256 total); 7 | function balanceOf(address _owner) public view returns (uint256 balance); 8 | function ownerOf(uint256 _tokenId) external view returns (address owner); 9 | function approve(address _to, uint256 _tokenId) external; 10 | function transfer(address _to, uint256 _tokenId) external; 11 | function transferFrom(address _from, address _to, uint256 _tokenId) external; 12 | 13 | // Events 14 | event Transfer(address from, address to, uint256 tokenId); 15 | event Approval(address owner, address approved, uint256 tokenId); 16 | 17 | // Optional 18 | // function name() public view returns (string name); 19 | // function symbol() public view returns (string symbol); 20 | // function tokensOfOwner(address _owner) external view returns (uint256[] tokenIds); 21 | // function tokenMetadata(uint256 _tokenId, string _preferredTransport) public view returns (string infoUrl); 22 | 23 | // ERC-165 Compatibility (https://github.com/ethereum/EIPs/issues/165) 24 | function supportsInterface(bytes4 _interfaceID) external view returns (bool); 25 | } 26 | -------------------------------------------------------------------------------- /contracts/MetaCoin.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.4.25 <0.6.0; 2 | 3 | import "./ConvertLib.sol"; 4 | 5 | // This is just a simple example of a coin-like contract. 6 | // It is not standards compatible and cannot be expected to talk to other 7 | // coin/token contracts. If you want to create a standards-compliant 8 | // token, see: https://github.com/ConsenSys/Tokens. Cheers! 9 | 10 | contract MetaCoin { 11 | mapping (address => uint) balances; 12 | 13 | event Transfer(address indexed _from, address indexed _to, uint256 _value); 14 | 15 | constructor() public { 16 | balances[tx.origin] = 10000; 17 | } 18 | 19 | function sendCoin(address receiver, uint amount) public returns(bool sufficient) { 20 | if (balances[msg.sender] < amount) return false; 21 | balances[msg.sender] -= amount; 22 | balances[receiver] += amount; 23 | emit Transfer(msg.sender, receiver, amount); 24 | return true; 25 | } 26 | 27 | function getBalanceInEth(address addr) public view returns(uint){ 28 | return ConvertLib.convert(getBalance(addr),2); 29 | } 30 | 31 | function getBalance(address addr) public view returns(uint) { 32 | return balances[addr]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.4.25 <0.6.0; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | modifier restricted() { 8 | if (msg.sender == owner) _; 9 | } 10 | 11 | constructor() public { 12 | owner = msg.sender; 13 | } 14 | 15 | function setCompleted(uint completed) public restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) public restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /contracts/OwnedToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.4.22 <0.6.0; 2 | 3 | contract OwnedToken { 4 | // `TokenCreator` is a contract type that is defined below. 5 | // It is fine to reference it as long as it is not used 6 | // to create a new contract. 7 | TokenCreator creator; 8 | address owner; 9 | bytes32 name; 10 | 11 | // This is the constructor which registers the 12 | // creator and the assigned name. 13 | constructor(bytes32 _name) public { 14 | // State variables are accessed via their name 15 | // and not via e.g. `this.owner`. Functions can 16 | // be accessed directly or through `this.f`, 17 | // but the latter provides an external view 18 | // to the function. Especially in the constructor, 19 | // you should not access functions externally, 20 | // because the function does not exist yet. 21 | // See the next section for details. 22 | owner = msg.sender; 23 | 24 | // We do an explicit type conversion from `address` 25 | // to `TokenCreator` and assume that the type of 26 | // the calling contract is `TokenCreator`, there is 27 | // no real way to check that. 28 | creator = TokenCreator(msg.sender); 29 | name = _name; 30 | } 31 | 32 | function changeName(bytes32 newName) public { 33 | // Only the creator can alter the name -- 34 | // the comparison is possible since contracts 35 | // are explicitly convertible to addresses. 36 | if (msg.sender == address(creator)) 37 | name = newName; 38 | } 39 | 40 | function transfer(address newOwner) public { 41 | // Only the current owner can transfer the token. 42 | if (msg.sender != owner) return; 43 | 44 | // We ask the creator contract if the transfer 45 | // should proceed by using a function of the 46 | // `TokenCreator` contract defined below. If 47 | // the call fails (e.g. due to out-of-gas), 48 | // the execution also fails here. 49 | if (creator.isTokenTransferOK(owner, newOwner)) 50 | owner = newOwner; 51 | } 52 | } 53 | 54 | contract TokenCreator { 55 | function createToken(bytes32 name) 56 | public 57 | returns (OwnedToken tokenAddress) 58 | { 59 | // Create a new `Token` contract and return its address. 60 | // From the JavaScript side, the return type is 61 | // `address`, as this is the closest type available in 62 | // the ABI. 63 | return new OwnedToken(name); 64 | } 65 | 66 | function changeName(OwnedToken tokenAddress, bytes32 name) public { 67 | // Again, the external type of `tokenAddress` is 68 | // simply `address`. 69 | tokenAddress.changeName(name); 70 | } 71 | 72 | // Perform checks to determine if transferring a token to the 73 | // `OwnedToken` contract should proceed 74 | function isTokenTransferOK(address currentOwner, address newOwner) 75 | public 76 | pure 77 | returns (bool ok) 78 | { 79 | // Check an arbitrary condition to see if transfer should proceed 80 | return keccak256(abi.encodePacked(currentOwner, newOwner))[0] == 0x7f; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /contracts/Primality.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | contract Primality { 4 | 5 | uint256 public largePrime = 973013; 6 | 7 | uint256 x; 8 | uint256 y; 9 | 10 | function setX(uint256 _x) external { 11 | x = _x; 12 | } 13 | 14 | function setY(uint256 _y) external { 15 | y = _y; 16 | } 17 | 18 | function verifyPrime() external view { 19 | require(x > 1 && x < largePrime); 20 | require(y > 1 && y < largePrime); 21 | assert(x*y != largePrime); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /contracts/TokenSale.sol: -------------------------------------------------------------------------------- 1 | 2 | contract Tokensale { 3 | uint hardcap = 10000 ether; 4 | 5 | 6 | function fetchCap() public returns(uint) { 7 | return hardcap; 8 | } 9 | } 10 | 11 | contract Presale is Tokensale { 12 | uint hardcap = 1000 ether; 13 | 14 | 15 | } 16 | -------------------------------------------------------------------------------- /contracts/empty.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.7; 2 | 3 | contract Empty { 4 | } 5 | -------------------------------------------------------------------------------- /contracts/error.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | contract Err { 4 | constrictor () public { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /contracts/external-lib.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.7; 2 | 3 | library External { 4 | function increment(uint256 _n) public pure returns (uint256) { 5 | return (_n + 1); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /contracts/import.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.7; 2 | 3 | import "./vulnerable.sol"; 4 | 5 | contract Import is Vulnerable {} 6 | -------------------------------------------------------------------------------- /contracts/no-contracts.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | -------------------------------------------------------------------------------- /contracts/no-pragma.sol: -------------------------------------------------------------------------------- 1 | contract NoPragma { 2 | function f() public { 3 | selfdestruct(msg.sender); 4 | } 5 | } 6 | 7 | -------------------------------------------------------------------------------- /contracts/token.sol: -------------------------------------------------------------------------------- 1 | 2 | 3 | contract Token { 4 | 5 | mapping(address => uint) balances; 6 | uint public totalSupply; 7 | 8 | constructor(uint _initialSupply) public { 9 | balances[msg.sender] = totalSupply = _initialSupply; 10 | } 11 | 12 | function transfer(address _to, uint _value) public returns (bool) { 13 | balances[msg.sender] -= _value; 14 | balances[_to] += _value; 15 | return true; 16 | } 17 | 18 | function balanceOf(address _owner) public view returns (uint balance) { 19 | return balances[_owner]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /contracts/use-external-lib.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.7; 2 | 3 | import "./external-lib.sol"; 4 | 5 | contract UseExternalLib { 6 | function useExternal(uint256 _n) public pure returns (uint256) { 7 | return External.increment(_n); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /contracts/vulnerable.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.7; 2 | 3 | contract Vulnerable { 4 | uint256 public n = 2^250; 5 | 6 | function f() public { 7 | selfdestruct(msg.sender); 8 | } 9 | 10 | function a() public { 11 | n = n * 2; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | const mythx = require('mythxjs'); 2 | const utils = require('./utils'); 3 | 4 | const getRequestData = (input, compiledData, fileName, args) => { 5 | /* Format data for MythX API */ 6 | const data = { 7 | contractName: compiledData.contractName, 8 | bytecode: utils.replaceLinkedLibs(compiledData.contract.evm.bytecode.object), 9 | sourceMap: compiledData.contract.evm.bytecode.sourceMap, 10 | deployedBytecode: utils.replaceLinkedLibs(compiledData.contract.evm.deployedBytecode.object), 11 | deployedSourceMap: compiledData.contract.evm.deployedBytecode.sourceMap, 12 | sourceList: [], 13 | analysisMode: args.mode, 14 | toolName: args.clientToolName || 'sabre', 15 | noCacheLookup: args.noCacheLookup, 16 | sources: {} 17 | }; 18 | 19 | for (const key in compiledData.compiled.sources) { 20 | const ast = compiledData.compiled.sources[key].ast; 21 | const source = input.sources[key].content; 22 | 23 | data.sourceList.push(key); 24 | 25 | data.sources[key] = { ast, source }; 26 | } 27 | 28 | data.mainSource = fileName; 29 | 30 | return data; 31 | }; 32 | 33 | const failAnalysis = (reason, status) => { 34 | throw new Error( 35 | reason + 36 | ' ' + 37 | 'The analysis job state is ' + 38 | status.toLowerCase() + 39 | ' and the result may become available later.' 40 | ); 41 | }; 42 | 43 | const awaitAnalysisFinish = async (client, uuid, initialDelay, timeout) => { 44 | const statuses = [ 'Error', 'Finished' ]; 45 | 46 | let state = await client.getAnalysisStatus(uuid); 47 | 48 | if (statuses.includes(state.status)) { 49 | return state; 50 | } 51 | 52 | const timer = interval => new Promise(resolve => setTimeout(resolve, interval)); 53 | 54 | const maxRequests = 10; 55 | const start = Date.now(); 56 | const remaining = Math.max(timeout - initialDelay, 0); 57 | const inverted = Math.sqrt(remaining) / Math.sqrt(285); 58 | 59 | for (let r = 0; r < maxRequests; r++) { 60 | const idle = Math.min( 61 | r === 0 ? initialDelay : (inverted * r) ** 2, 62 | start + timeout - Date.now() 63 | ); 64 | 65 | await timer(idle); 66 | 67 | if (Date.now() - start >= timeout) { 68 | failAnalysis( 69 | `User or default timeout reached after ${timeout / 1000} sec(s).`, 70 | state.status 71 | ); 72 | } 73 | 74 | state = await client.getAnalysisStatus(uuid); 75 | 76 | if (statuses.includes(state.status)) { 77 | return state; 78 | } 79 | } 80 | 81 | failAnalysis( 82 | `Allowed number (${maxRequests}) of requests was reached.`, 83 | state.status 84 | ); 85 | }; 86 | 87 | const initialize = (apiUrl, apiKey) => { 88 | return new mythx.Client(undefined, undefined, undefined, apiUrl, apiKey); 89 | }; 90 | 91 | const authenticate = async (client) => { 92 | return await client.login(); 93 | }; 94 | 95 | const submitDataForAnalysis = async(client, data, isCheckProperty = false) => { 96 | return await client.analyze(data, isCheckProperty); 97 | }; 98 | 99 | const getReport = async (client, uuid) => { 100 | return await client.getDetectedIssues(uuid); 101 | }; 102 | 103 | const getApiVersion = async (client) => { 104 | return await client.getVersion(); 105 | }; 106 | 107 | const getAnalysesList = async (client) => { 108 | return await client.getAnalysesList(); 109 | }; 110 | 111 | const getAnalysisStatus = async(client, uuid) => { 112 | return await client.getAnalysisStatus(uuid); 113 | }; 114 | 115 | module.exports = { 116 | initialize, 117 | authenticate, 118 | awaitAnalysisFinish, 119 | submitDataForAnalysis, 120 | getApiVersion, 121 | getReport, 122 | getRequestData, 123 | getAnalysesList, 124 | getAnalysisStatus 125 | }; 126 | -------------------------------------------------------------------------------- /lib/compiler.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const axios = require('axios'); 3 | const path = require('path'); 4 | const requireFromString = require('require-from-string'); 5 | const solc = require('solc'); 6 | const parser = require('solidity-parser-antlr'); 7 | const releases = require('./releases'); 8 | 9 | /* Get solc-js input config for the compilation of contract */ 10 | 11 | const getSolcInput = sources => { 12 | return { 13 | language: 'Solidity', 14 | sources, 15 | settings: { 16 | outputSelection: { 17 | '*': { 18 | '*': ['*'], 19 | '': ['ast'] 20 | } 21 | }, 22 | optimizer: { 23 | enabled: true, 24 | runs: 200 25 | } 26 | } 27 | }; 28 | }; 29 | 30 | /** 31 | * Loads and initializes Solc of supplied version. 32 | * 33 | * @param {string} version Solc version string 34 | * 35 | * @returns {object} Loaded Solc version snapshot object 36 | * and indicator if it was loaded from local cache 37 | */ 38 | const loadSolcVersion = async version => { 39 | const tempDir = path.join(path.dirname(require.main.filename), '.temp'); 40 | const filePath = path.join(tempDir, version + '.js'); 41 | 42 | const fromCache = fs.existsSync(filePath); 43 | 44 | let solcString; 45 | 46 | if (fromCache) { 47 | solcString = fs.readFileSync(filePath).toString(); 48 | } else { 49 | const config = { 50 | method: 'get', 51 | url: 'https://ethereum.github.io/solc-bin/bin/soljson-' + version + '.js', 52 | responseType: 'stream' 53 | }; 54 | 55 | solcString = await axios(config).then(response => { 56 | /** 57 | * Create `.temp` directory if it doesn't exist 58 | */ 59 | if (!fs.existsSync(tempDir)) { 60 | fs.mkdirSync(tempDir); 61 | } 62 | 63 | const stream = fs.createWriteStream(filePath); 64 | 65 | response.data.pipe(stream); 66 | 67 | return new Promise((resolve, reject) => { 68 | stream.on( 69 | 'finish', 70 | () => resolve(fs.readFileSync(filePath).toString()) 71 | ).on( 72 | 'error', 73 | err => reject(err) 74 | ); 75 | }); 76 | }); 77 | } 78 | 79 | /** 80 | * NOTE: `solcSnapshot` has the same interface as the `solc`. 81 | */ 82 | const solcSnapshot = solc.setupMethods( 83 | requireFromString(solcString), 84 | 'soljson-' + releases[version] + '.js' 85 | ); 86 | 87 | return { solcSnapshot, fromCache }; 88 | }; 89 | 90 | /* Get solidity version specified in the contract */ 91 | 92 | const getSolidityVersion = content => { 93 | try { 94 | const ast = parser.parse(content, {}); 95 | let solidityVersion = releases.latest; 96 | let reg = RegExp(/[><=^]/, 'g'); 97 | 98 | for (let n of ast.children) { 99 | if ((n.name === 'solidity') && (n.type === 'PragmaDirective')) { 100 | solidityVersion = n.value; 101 | if (!reg.test(solidityVersion)) { 102 | return solidityVersion; 103 | } 104 | break; 105 | } 106 | } 107 | 108 | if (solidityVersion !== releases.latest) { 109 | solidityVersion = solidityVersion.replace(/[\^v]/g, ''); 110 | let upperLimit = 'latest'; 111 | 112 | if (solidityVersion.indexOf('<') !== -1) { 113 | if (solidityVersion.indexOf('<=') !== -1) { 114 | solidityVersion = solidityVersion.substring(solidityVersion.length - 5, solidityVersion.length); 115 | } else { 116 | upperLimit = solidityVersion.substring(solidityVersion.length - 5, solidityVersion.length); 117 | } 118 | } else if (solidityVersion.indexOf('>') !== -1) { 119 | solidityVersion = releases.latest; 120 | } else { 121 | upperLimit = '0.' + (parseInt(solidityVersion[2]) + 1).toString() + '.0'; 122 | } 123 | 124 | if (upperLimit !== 'latest') { 125 | if (upperLimit === '0.7.0') { 126 | solidityVersion = releases.latest; 127 | } else if (upperLimit === '0.6.0') { 128 | solidityVersion = '0.5.16'; 129 | } else if (upperLimit === '0.5.0') { 130 | solidityVersion = '0.4.25'; 131 | } else if (upperLimit === '0.4.0') { 132 | solidityVersion = '0.3.6'; 133 | } else if (upperLimit === '0.3.0') { 134 | solidityVersion = '0.2.2'; 135 | } else { 136 | let x = parseInt(upperLimit[upperLimit.length - 1], 10) - 1; 137 | solidityVersion = ''; 138 | for (let i = 0; i < upperLimit.length - 1; i++) { 139 | solidityVersion += upperLimit[i]; 140 | } 141 | solidityVersion += x.toString(); 142 | } 143 | } 144 | } 145 | 146 | return solidityVersion; 147 | } catch (error) { 148 | if (error instanceof parser.ParserError) { 149 | const messages = error.errors.map( 150 | e => `[line ${e.line}, column ${e.column}] - ${e.message}` 151 | ); 152 | 153 | throw new Error('Unable to parse input.\n' + messages.join('\n')); 154 | } else { 155 | throw error; 156 | } 157 | } 158 | }; 159 | 160 | /** 161 | * Returns dictionary of function signatures and their keccak256 hashes 162 | * for all contracts. 163 | * 164 | * Same function signatures will be overwritten 165 | * as there should be no distinction between their hashes, 166 | * even if such functions defined in different contracts. 167 | * 168 | * @param {object} contracts Compiler meta-data about contracts. 169 | * 170 | * @returns {object} Dictionary object where 171 | * key is a hex string first 4 bytes of keccak256 hash 172 | * and value is a corresponding function signature. 173 | */ 174 | const getFunctionHashes = contracts => { 175 | const hashes = {}; 176 | 177 | for (const fileName in contracts) { 178 | const fileContracts = contracts[fileName]; 179 | 180 | for (const contractName in fileContracts) { 181 | const contract = fileContracts[contractName]; 182 | 183 | const { methodIdentifiers } = contract.evm; 184 | 185 | for (const signature in methodIdentifiers) { 186 | const hash = methodIdentifiers[signature]; 187 | 188 | hashes[hash] = signature; 189 | } 190 | } 191 | } 192 | 193 | return hashes; 194 | }; 195 | 196 | /** 197 | * Extract compile errors from Solc compile error/warning messages combined array. 198 | * 199 | * @param {string[]|object[]} compileMessages Solc compile messages combined array 200 | * 201 | * @returns string[] Array with extracted error message strings 202 | */ 203 | const getCompileErrors = compileMessages => { 204 | const errors = []; 205 | 206 | for (const compileMessage of compileMessages) { 207 | if (compileMessage.severity === 'error') { 208 | errors.push(compileMessage.formattedMessage); 209 | } 210 | } 211 | 212 | return errors; 213 | }; 214 | 215 | /** 216 | * Safely extract compiled contract bytecode value if there is any. 217 | * 218 | * @param {object} contract Solc contract object 219 | * 220 | * @returns {string|undefined} Extracted bytecode string or undefined if there is no bytecode. 221 | */ 222 | const getByteCodeString = contract => { 223 | return ( 224 | contract && 225 | contract.evm && 226 | contract.evm.bytecode && 227 | contract.evm.bytecode.object 228 | ); 229 | }; 230 | 231 | /* 232 | * Compile contracts using solc snapshot 233 | */ 234 | 235 | const getCompiledContracts = (input, solcSnapshot, solidityFileName, compileContractName) => { 236 | const compiled = JSON.parse(solcSnapshot.compile(JSON.stringify(input))); 237 | 238 | if (compiled.errors) { 239 | const errors = getCompileErrors(compiled.errors); 240 | 241 | if (errors.length) { 242 | throw new Error('Unable to compile.\n' + errors.join('\n')); 243 | } 244 | } 245 | 246 | if (!compiled.contracts || !Object.keys(compiled.contracts).length) { 247 | throw new Error('No contracts detected after compiling'); 248 | } 249 | 250 | const inputFile = compiled.contracts[solidityFileName]; 251 | 252 | let contract, contractName; 253 | 254 | if (inputFile.length === 0) { 255 | throw new Error('No contracts found'); 256 | } else if (inputFile.length === 1) { 257 | contractName = Object.keys(inputFile)[0]; 258 | contract = inputFile[contractName]; 259 | } else { 260 | if (compileContractName && inputFile[compileContractName]) { 261 | contractName = compileContractName; 262 | contract = inputFile[compileContractName]; 263 | } else { 264 | /** 265 | * Get the contract with largest bytecode object to generate MythX analysis report. 266 | * If inheritance is used, the main contract is the largest as it contains the bytecode of all others. 267 | */ 268 | 269 | const byteCodes = {}; 270 | 271 | for (const key in inputFile) { 272 | if (inputFile.hasOwnProperty(key)) { 273 | const byteCode = getByteCodeString(inputFile[key]); 274 | 275 | if (byteCode) { 276 | byteCodes[byteCode.length] = key; 277 | } 278 | } 279 | } 280 | 281 | const largestByteCodeKey = Object.keys(byteCodes).reverse()[0]; 282 | 283 | contractName = byteCodes[largestByteCodeKey]; 284 | contract = inputFile[contractName]; 285 | } 286 | } 287 | 288 | const byteCode = getByteCodeString(contract); 289 | 290 | /** 291 | * Bytecode would be empty if contract is only an interface. 292 | */ 293 | if (!byteCode) { 294 | throw new Error( 295 | 'Compiling the Solidity code did not return any bytecode. Note that abstract contracts cannot be analyzed.' 296 | ); 297 | } 298 | 299 | const functionHashes = getFunctionHashes(compiled.contracts); 300 | 301 | return { 302 | compiled, 303 | contract, 304 | contractName, 305 | functionHashes 306 | }; 307 | }; 308 | 309 | module.exports = { 310 | getCompiledContracts, 311 | getSolcInput, 312 | getSolidityVersion, 313 | loadSolcVersion, 314 | }; 315 | -------------------------------------------------------------------------------- /lib/controllers/analyze.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const util = require('util'); 4 | const chalk = require('chalk'); 5 | const ora = require('ora'); 6 | const Profiler = require('@truffle/compile-solidity/profiler'); 7 | const Resolver = require('@truffle/resolver'); 8 | const client = require('../client'); 9 | const compiler = require('../compiler'); 10 | const report = require('../report'); 11 | const releases = require('../releases'); 12 | const Config = require("@truffle/config"); 13 | 14 | module.exports = async (env, args) => { 15 | let { apiUrl, apiKey } = env; 16 | 17 | const modes = ['quick', 'full', 'standard', 'deep']; 18 | 19 | if (args._.length < 2 || args._.length > 3) { 20 | console.log('Invalid command line. Use sabre analyze [options] '); 21 | 22 | process.exit(-1); 23 | 24 | } 25 | 26 | if (!modes.includes(args.mode)) { 27 | console.log('Invalid analysis mode. Available modes: quick, standard, deep'); 28 | 29 | process.exit(-1); 30 | } 31 | 32 | const formats = ['text', 'stylish', 'compact', 'table', 'html', 'json']; 33 | 34 | if (!formats.includes(args.format)) { 35 | console.log('Invalid output format. Available formats: ' + formats.join(', ') + '.'); 36 | 37 | process.exit(-1); 38 | } 39 | 40 | const solidityFilePath = path.resolve(process.cwd(), args._[1]); 41 | const solidityFileDir = path.dirname(solidityFilePath); 42 | 43 | const resolver = new Resolver({ 44 | working_directory: solidityFileDir, 45 | contracts_build_directory: solidityFileDir 46 | }); 47 | 48 | const spinner = ora({ 49 | color: 'yellow', 50 | spinner: 'bouncingBar' 51 | }); 52 | 53 | try { 54 | spinner.start('Reading input file'); 55 | 56 | const solidityCode = fs.readFileSync(solidityFilePath, 'utf8'); 57 | 58 | spinner.stop(); 59 | 60 | spinner.start('Detecting solidity version'); 61 | 62 | /* Get the version of the Solidity Compiler */ 63 | const version = compiler.getSolidityVersion(solidityCode); 64 | 65 | spinner.stop(); 66 | 67 | spinner.start(`Loading solc v${version}`); 68 | 69 | const { solcSnapshot, fromCache } = await compiler.loadSolcVersion( 70 | releases[version] 71 | ); 72 | 73 | spinner.succeed( 74 | fromCache 75 | ? `Loaded solc v${version} from local cache` 76 | : `Downloaded solc v${version} and saved to local cache` 77 | ); 78 | 79 | spinner.start('Resolving imports'); 80 | 81 | /** 82 | * Resolve imported sources and read source code for each file. 83 | */ 84 | const config = Config.default(); 85 | 86 | const resolvedSources = (await Profiler.required_sources(config.with({ 87 | paths: [solidityFilePath], 88 | resolver: resolver, 89 | base_path: solidityFileDir, 90 | contracts_directory: solidityFilePath 91 | }))).allSources; 92 | 93 | spinner.stop(); 94 | 95 | spinner.start('Compiling source(s)'); 96 | 97 | const allSources = {}; 98 | 99 | Object.keys(resolvedSources).forEach((file) => { 100 | allSources[file] = { content: resolvedSources[file] }; 101 | }) 102 | 103 | /* Get the input config for the Solidity Compiler */ 104 | const input = compiler.getSolcInput(allSources); 105 | 106 | const compiledData = compiler.getCompiledContracts( 107 | input, 108 | solcSnapshot, 109 | solidityFilePath, 110 | args._[2] 111 | ); 112 | 113 | spinner.succeed(`Compiled with solc v${version} successfully`); 114 | 115 | spinner.start('Authenticating user'); 116 | 117 | const mxClient = client.initialize(apiUrl, apiKey); 118 | 119 | if (!apiKey) { 120 | await client.authenticate(mxClient); 121 | } 122 | 123 | spinner.stop(); 124 | 125 | spinner.start('Submitting data for analysis'); 126 | 127 | const data = client.getRequestData( 128 | input, 129 | compiledData, 130 | solidityFilePath, 131 | args 132 | ); 133 | 134 | if (args.debug) { 135 | spinner.stop(); 136 | 137 | console.log('-------------------'); 138 | console.log('MythX Request Body:\n'); 139 | console.log(util.inspect(data, false, null, true)); 140 | 141 | spinner.start(); 142 | } 143 | 144 | const { uuid } = await client.submitDataForAnalysis(mxClient, data, args.isCheckProperty); 145 | 146 | spinner.succeed( 147 | 'Analysis job submitted: ' + 148 | chalk.yellow('https://dashboard.mythx.io/#/console/analyses/' + uuid) 149 | 150 | ); 151 | 152 | spinner.start('Analyzing ' + compiledData.contractName); 153 | 154 | let initialDelay; 155 | let timeout; 156 | 157 | if (args.mode === 'quick') { 158 | initialDelay = 20 * 1000; 159 | timeout = 300 * 1000; 160 | } else if (args.mode === 'standard' || args.mode === 'full') { 161 | initialDelay = 900 * 1000; 162 | timeout = 1800 * 1000; 163 | } else { 164 | initialDelay = 2700 * 1000; 165 | timeout = 5400 * 1000; 166 | } 167 | 168 | await client.awaitAnalysisFinish( 169 | mxClient, 170 | uuid, 171 | initialDelay, 172 | timeout 173 | ); 174 | 175 | spinner.stop(); 176 | 177 | spinner.start('Retrieving analysis results'); 178 | 179 | const issues = await client.getReport(mxClient, uuid); 180 | 181 | spinner.stop(); 182 | 183 | spinner.start('Rendering output'); 184 | 185 | /* Add all the imported contracts source code to the `data` to sourcemap the issue location */ 186 | data.sources = { ...input.sources }; 187 | 188 | /* Copy reference to compiled function hashes */ 189 | data.functionHashes = compiledData.functionHashes; 190 | 191 | if (args.debug) { 192 | spinner.stop(); 193 | 194 | console.log('-------------------'); 195 | console.log('MythX Response Body:\n'); 196 | console.log(util.inspect(issues, false, null, true)); 197 | console.log('-------------------'); 198 | 199 | spinner.start(); 200 | } 201 | 202 | const uniqueIssues = report.formatIssues(data, issues); 203 | 204 | if (uniqueIssues.length === 0) { 205 | spinner.stop(); 206 | 207 | console.log(chalk.green(`✔ No errors/warnings found in ${args._[0]} for contract: ${compiledData.contractName}`)); 208 | } else { 209 | // Custom text format for property checking 210 | const format = args.isCheckProperty && args.format === 'text' ? 'propertyCheckText' : args.format; 211 | const formatter = report.getFormatter(format); 212 | const output = formatter(uniqueIssues); 213 | 214 | spinner.stop(); 215 | 216 | console.log(output); 217 | } 218 | } catch (err) { 219 | if (spinner.isSpinning) { 220 | spinner.fail(); 221 | } 222 | 223 | console.log(chalk.red(err)); 224 | 225 | process.exit(1); 226 | } 227 | }; 228 | -------------------------------------------------------------------------------- /lib/controllers/api_version.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const ora = require('ora'); 3 | const util = require('util'); 4 | const client = require('../client'); 5 | 6 | module.exports = async (env, args) => { 7 | const spinnerConfig = { 8 | text: 'Obtaining API version', 9 | color: 'yellow', 10 | spinner: 'bouncingBar' 11 | }; 12 | 13 | const spinner = ora(spinnerConfig).start(); 14 | 15 | try { 16 | const mxClient = client.initialize( 17 | env.apiUrl 18 | ); 19 | 20 | const data = await client.getApiVersion(mxClient); 21 | 22 | spinner.stop(); 23 | 24 | if (args.debug) { 25 | console.log('MythX Response Body:\n'); 26 | console.log(util.inspect(data, { showHidden: false, depth: null })); 27 | console.log('-------------------'); 28 | } 29 | 30 | for (const key in data) { 31 | console.log(key + ': ' + data[key]); 32 | } 33 | } catch (err) { 34 | spinner.fail('Failed to obtain API version'); 35 | 36 | console.log(chalk.red(err)); 37 | 38 | process.exit(1); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /lib/controllers/help.js: -------------------------------------------------------------------------------- 1 | const helpText = `Minimum viable CLI for the MythX security analysis platform. 2 | 3 | USAGE: 4 | 5 | $ sabre 6 | 7 | COMMANDS: 8 | analyze [options] [contract_name] Generically test for ~50 security bugs 9 | check [options] [contract_name] Check for assertion violations and print counter-examples 10 | list Get a list of submitted analyses. 11 | status Get the status of an already submitted analysis 12 | version Print version 13 | help Print help message 14 | apiVersion Print MythX API version 15 | 16 | 17 | OPTIONS: 18 | --mode Analysis mode (default=quick) 19 | --format Output format (default=text) 20 | --clientToolName Override clientToolName 21 | --noCacheLookup Deactivate MythX cache lookups 22 | --debug Print MythX API request and response 23 | `; 24 | 25 | module.exports = async () => { 26 | console.log(helpText); 27 | }; 28 | -------------------------------------------------------------------------------- /lib/controllers/list.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const ora = require('ora'); 3 | const client = require('../client'); 4 | 5 | module.exports = async (env) => { 6 | let { apiUrl, apiKey } = env; 7 | 8 | const spinner = ora({ 9 | color: 'yellow', 10 | spinner: 'bouncingBar' 11 | }); 12 | 13 | try { 14 | spinner.start('Authenticating user'); 15 | 16 | const mxClient = client.initialize(apiUrl, apiKey); 17 | 18 | if (!apiKey) { 19 | await client.authenticate(mxClient); 20 | } 21 | 22 | spinner.stop(); 23 | console.log(chalk.green('✔ Authentication successful')); 24 | 25 | spinner.start('Retrieving submitted analyses'); 26 | 27 | let list = await client.getAnalysesList(mxClient); 28 | 29 | console.log(chalk.green('\n✔ Analyses retrieved')); 30 | 31 | list.analyses.forEach(analysis => { 32 | console.log('--------------------------------------------'); 33 | console.log(`API Version: ${analysis.apiVersion}`); 34 | console.log(`UUID: ${analysis.uuid}`); 35 | console.log(`Status: ${analysis.status}`); 36 | console.log(`API Version: ${analysis.apiVersion}`); 37 | console.log(`Submitted by: ${analysis.submittedBy}`); 38 | console.log(`Submitted at: ${analysis.submittedAt}`); 39 | console.log(`Report URL: https://dashboard.mythx.io/#/console/analyses/${analysis.uuid}`); 40 | console.log('---------------------------------------------'); 41 | }); 42 | 43 | spinner.stop(); 44 | } catch (err) { 45 | if (spinner.isSpinning) { 46 | spinner.fail(); 47 | } 48 | 49 | console.log(chalk.red(err)); 50 | 51 | process.exit(1); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /lib/controllers/status.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const ora = require('ora'); 3 | const client = require('../client'); 4 | 5 | module.exports = async (env, args) => { 6 | let { apiUrl, apiKey } = env; 7 | 8 | let uuid = args._[1]; 9 | 10 | const spinner = ora({ 11 | color: 'yellow', 12 | spinner: 'bouncingBar' 13 | }); 14 | if (uuid) { 15 | try { 16 | spinner.start('Authenticating user'); 17 | 18 | const mxClient = client.initialize(apiUrl, apiKey); 19 | 20 | if (!apiKey) { 21 | await client.authenticate(mxClient); 22 | } 23 | 24 | spinner.stop(); 25 | console.log(chalk.green('✔ Authentication successful')); 26 | 27 | spinner.start('Retrieving Status Analysis'); 28 | 29 | const status = await client.getAnalysisStatus(mxClient, uuid); 30 | 31 | console.log(chalk.green('\n✔ Analysis status retrieved')); 32 | console.log('--------------------------------------------'); 33 | console.log(`API Version: ${status.apiVersion}`); 34 | console.log(`UUID: ${status.uuid}`); 35 | console.log(`Status: ${status.status}`); 36 | console.log(`API Version: ${status.apiVersion}`); 37 | console.log(`Submitted by: ${status.submittedBy}`); 38 | console.log(`Submitted at: ${status.submittedAt}`); 39 | console.log('---------------------------------------------'); 40 | 41 | spinner.stop(); 42 | } catch (err) { 43 | if (spinner.isSpinning) { 44 | spinner.fail(); 45 | } 46 | 47 | console.log(chalk.red(err)); 48 | 49 | process.exit(1); 50 | } 51 | } else { 52 | console.log(chalk.red('No UUID was provided')); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /lib/controllers/version.js: -------------------------------------------------------------------------------- 1 | const { version } = require('../../package.json'); 2 | 3 | module.exports = async () => { 4 | console.log(version); 5 | }; 6 | -------------------------------------------------------------------------------- /lib/eslint.js: -------------------------------------------------------------------------------- 1 | const isFatal = (fatal, severity) => fatal || severity === 2; 2 | 3 | const getUniqueMessages = messages => { 4 | const jsonValues = messages.map(m => JSON.stringify(m)); 5 | const uniqueValues = jsonValues.reduce((acc, curr) => { 6 | if (acc.indexOf(curr) === -1) { 7 | acc.push(curr); 8 | } 9 | 10 | return acc; 11 | }, []); 12 | 13 | return uniqueValues.map(v => JSON.parse(v)); 14 | }; 15 | 16 | const calculateErrors = messages => messages.reduce( 17 | (acc, { fatal, severity }) => isFatal(fatal , severity) ? acc + 1 : acc, 18 | 0 19 | ); 20 | 21 | const calculateWarnings = messages => messages.reduce( 22 | (acc, { fatal, severity }) => !isFatal(fatal , severity) ? acc + 1: acc, 23 | 0 24 | ); 25 | 26 | const getUniqueIssues = issues => 27 | issues.map(({ messages, ...restProps }) => { 28 | const uniqueMessages = getUniqueMessages(messages); 29 | const warningCount = calculateWarnings(uniqueMessages); 30 | const errorCount = calculateErrors(uniqueMessages); 31 | 32 | return { 33 | ...restProps, 34 | messages: uniqueMessages, 35 | errorCount, 36 | warningCount, 37 | }; 38 | }); 39 | 40 | module.exports = { 41 | getUniqueIssues, 42 | getUniqueMessages, 43 | isFatal 44 | }; 45 | -------------------------------------------------------------------------------- /lib/formatters/propertyCheckText.js: -------------------------------------------------------------------------------- 1 | const separator = '-'.repeat(20); 2 | const indent = ' '.repeat(4); 3 | 4 | const roles = { 5 | creator: 'CREATOR', 6 | attacker: 'ATTACKER', 7 | other: 'USER' 8 | }; 9 | 10 | const textFormatter = {}; 11 | 12 | textFormatter.strToInt = str => parseInt(str, 10); 13 | 14 | textFormatter.guessAccountRoleByAddress = (address) => { 15 | const prefix = address.toLowerCase().substr(0, 10); 16 | 17 | if (prefix === '0xaffeaffe') { 18 | return roles.creator; 19 | } else if (prefix === '0xdeadbeef') { 20 | return roles.attacker; 21 | } 22 | 23 | return roles.other; 24 | }; 25 | 26 | textFormatter.stringifyValue = (value) => { 27 | const type = typeof value; 28 | 29 | if (type === 'number') { 30 | return String(value); 31 | } else if (type === 'string') { 32 | return value; 33 | } else if (value == null) { 34 | return 'null'; 35 | } 36 | 37 | return JSON.stringify(value); 38 | }; 39 | 40 | textFormatter.formatTestCaseSteps = (steps, /*fnHashes*/) => { 41 | const output = []; 42 | 43 | for (let s = 0, n = 0; s < steps.length; s++) { 44 | const step = steps[s]; 45 | 46 | /** 47 | * Empty address means "contract creation" transaction. 48 | * 49 | * Skip it to not spam. 50 | */ 51 | if (step.address === '') { 52 | continue; 53 | } 54 | 55 | n++; 56 | 57 | const type = textFormatter.guessAccountRoleByAddress(step.origin); 58 | 59 | // const fnHash = step.input.substr(2, 8); 60 | // const fnName = fnHashes[fnHash] || step.name || ''; 61 | // const fnDesc = `${fnName} [ ${fnHash} ]`; 62 | 63 | let decodedInput = 'DECODING FAILED :( RAW CALLDATA: ' + textFormatter.stringifyValue(step.input); 64 | 65 | if ('decodedInput' in step) { 66 | decodedInput = step.decodedInput; 67 | } 68 | 69 | output.push( 70 | indent + `${n}: ${decodedInput}`, 71 | indent + `Sender: ${step.origin} [ ${type} ]`, 72 | indent + `Value: ${parseInt(step.value)}`, 73 | '' 74 | ); 75 | 76 | } 77 | 78 | return output.join('\n').trimRight(); 79 | }; 80 | 81 | textFormatter.formatTestCase = (testCase, fnHashes) => { 82 | const output = []; 83 | 84 | if (testCase.steps) { 85 | const content = textFormatter.formatTestCaseSteps( 86 | testCase.steps, 87 | fnHashes 88 | ); 89 | 90 | if (content) { 91 | output.push('Call sequence:', '', content); 92 | } 93 | } 94 | 95 | return output.length ? output.join('\n') : undefined; 96 | }; 97 | 98 | textFormatter.getCodeSample = (source, src) => { 99 | const [start, length] = src.split(':').map(textFormatter.strToInt); 100 | 101 | return source.substr(start, length); 102 | }; 103 | 104 | textFormatter.formatLocation = message => { 105 | const start = message.line + ':' + message.column; 106 | const finish = message.endLine + ':' + message.endCol; 107 | 108 | return 'from ' + start + ' to ' + finish; 109 | }; 110 | 111 | textFormatter.formatMessage = (message, filePath, sourceCode, fnHashes) => { 112 | const { mythxIssue, mythxTextLocations } = message; 113 | const output = []; 114 | 115 | const code = mythxTextLocations.length 116 | ? textFormatter.getCodeSample( 117 | sourceCode, 118 | mythxTextLocations[0].sourceMap 119 | ) 120 | : undefined; 121 | 122 | output.push( 123 | separator, 124 | 'ASSERTION VIOLATION!', 125 | filePath + ': ' + textFormatter.formatLocation(message), 126 | '', 127 | code || '' 128 | ); 129 | 130 | const testCases = mythxIssue.extra && mythxIssue.extra.testCases; 131 | 132 | if (testCases) { 133 | for (const testCase of testCases) { 134 | const content = textFormatter.formatTestCase(testCase, fnHashes); 135 | 136 | if (content) { 137 | output.push(separator, content); 138 | } 139 | } 140 | } 141 | 142 | return output.join('\n'); 143 | }; 144 | 145 | textFormatter.formatResult = result => { 146 | const { filePath, sourceCode, functionHashes } = result; 147 | 148 | return result.messages 149 | .map( 150 | message => textFormatter.formatMessage( 151 | message, 152 | filePath, 153 | sourceCode, 154 | functionHashes 155 | ) 156 | ) 157 | .join('\n\n'); 158 | }; 159 | 160 | textFormatter.run = results => { 161 | return results 162 | .map(result => textFormatter.formatResult(result)) 163 | .join('\n\n'); 164 | }; 165 | 166 | module.exports = results => textFormatter.run(results); 167 | -------------------------------------------------------------------------------- /lib/formatters/text.js: -------------------------------------------------------------------------------- 1 | const separator = '-'.repeat(20); 2 | const indent = ' '.repeat(4); 3 | 4 | const roles = { 5 | creator: 'CREATOR', 6 | attacker: 'ATTACKER', 7 | other: 'USER' 8 | }; 9 | 10 | const textFormatter = {}; 11 | 12 | textFormatter.strToInt = str => parseInt(str, 10); 13 | 14 | textFormatter.guessAccountRoleByAddress = (address) => { 15 | const prefix = address.toLowerCase().substr(0, 10); 16 | 17 | if (prefix === '0xaffeaffe') { 18 | return roles.creator; 19 | } else if (prefix === '0xdeadbeef') { 20 | return roles.attacker; 21 | } 22 | 23 | return roles.other; 24 | }; 25 | 26 | textFormatter.stringifyValue = (value) => { 27 | const type = typeof value; 28 | 29 | if (type === 'number') { 30 | return String(value); 31 | } else if (type === 'string') { 32 | return value; 33 | } else if (value == null) { 34 | return 'null'; 35 | } 36 | 37 | return JSON.stringify(value); 38 | }; 39 | 40 | textFormatter.formatTestCaseSteps = (steps, fnHashes) => { 41 | const output = []; 42 | 43 | for (let s = 0, n = 0; s < steps.length; s++) { 44 | const step = steps[s]; 45 | 46 | /** 47 | * Empty address means "contract creation" transaction. 48 | * 49 | * Skip it to not spam. 50 | */ 51 | if (step.address === '') { 52 | continue; 53 | } 54 | 55 | n++; 56 | 57 | const type = textFormatter.guessAccountRoleByAddress(step.origin); 58 | 59 | const fnHash = step.input.substr(2, 8); 60 | const fnName = fnHashes[fnHash] || step.name || ''; 61 | const fnDesc = `${fnName} [ ${fnHash} ]`; 62 | 63 | 64 | output.push( 65 | `Tx #${n}:`, 66 | indent + `Origin: ${step.origin} [ ${type} ]`, 67 | indent + 'Function: ' + textFormatter.stringifyValue(fnDesc), 68 | indent + 'Calldata: ' + textFormatter.stringifyValue(step.input), 69 | ); 70 | 71 | if ('decodedInput' in step) { 72 | output.push(indent + 'Decoded Calldata: ' + step.decodedInput); 73 | } 74 | 75 | output.push( 76 | indent + 'Value: ' + textFormatter.stringifyValue(step.value), 77 | '' 78 | ); 79 | 80 | } 81 | 82 | return output.join('\n').trimRight(); 83 | }; 84 | 85 | textFormatter.formatTestCase = (testCase, fnHashes) => { 86 | const output = []; 87 | 88 | if (testCase.steps) { 89 | const content = textFormatter.formatTestCaseSteps( 90 | testCase.steps, 91 | fnHashes 92 | ); 93 | 94 | if (content) { 95 | output.push('Transaction Sequence:', '', content); 96 | } 97 | } 98 | 99 | return output.length ? output.join('\n') : undefined; 100 | }; 101 | 102 | textFormatter.getCodeSample = (source, src) => { 103 | const [start, length] = src.split(':').map(textFormatter.strToInt); 104 | 105 | return source.substr(start, length); 106 | }; 107 | 108 | textFormatter.formatLocation = message => { 109 | const start = message.line + ':' + message.column; 110 | const finish = message.endLine + ':' + message.endCol; 111 | 112 | return 'from ' + start + ' to ' + finish; 113 | }; 114 | 115 | textFormatter.formatMessage = (message, filePath, sourceCode, fnHashes) => { 116 | const { mythxIssue, mythxTextLocations } = message; 117 | const output = []; 118 | 119 | output.push( 120 | `==== ${mythxIssue.description.head || 'N/A'} ====`, 121 | ); 122 | 123 | if (message.ruleId !== 'N/A') { 124 | output.push(); 125 | } 126 | 127 | const code = mythxTextLocations.length 128 | ? textFormatter.getCodeSample( 129 | sourceCode, 130 | mythxTextLocations[0].sourceMap 131 | ) 132 | : undefined; 133 | 134 | output.push( 135 | separator, 136 | 'Severity: ' + mythxIssue.severity, 137 | 'Reference: ' + message.ruleId, 138 | 'File: ' + filePath, 139 | 'Location: ' + textFormatter.formatLocation(message), 140 | '', 141 | code || '' 142 | ); 143 | 144 | const testCases = mythxIssue.extra && mythxIssue.extra.testCases; 145 | 146 | if (testCases) { 147 | for (const testCase of testCases) { 148 | const content = textFormatter.formatTestCase(testCase, fnHashes); 149 | 150 | if (content) { 151 | output.push(separator, content); 152 | } 153 | } 154 | } 155 | 156 | return output.join('\n'); 157 | }; 158 | 159 | textFormatter.formatResult = result => { 160 | const { filePath, sourceCode, functionHashes } = result; 161 | 162 | return result.messages 163 | .map( 164 | message => textFormatter.formatMessage( 165 | message, 166 | filePath, 167 | sourceCode, 168 | functionHashes 169 | ) 170 | ) 171 | .join('\n\n'); 172 | }; 173 | 174 | textFormatter.run = results => { 175 | return results 176 | .map(result => textFormatter.formatResult(result)) 177 | .join('\n\n'); 178 | }; 179 | 180 | module.exports = results => textFormatter.run(results); 181 | -------------------------------------------------------------------------------- /lib/releases.json: -------------------------------------------------------------------------------- 1 | { 2 | "latest": "0.7.0", 3 | "0.7.0": "v0.7.0+commit.9e61f92b", 4 | "0.6.12": "v0.6.12+commit.27d51765", 5 | "0.6.11": "v0.6.11+commit.5ef660b1", 6 | "0.6.10": "v0.6.10+commit.00c0fcaf", 7 | "0.6.9": "v0.6.9+commit.3e3065ac", 8 | "0.6.8": "v0.6.8+commit.0bbfe453", 9 | "0.6.7": "v0.6.7+commit.b8d736ae", 10 | "0.6.6": "v0.6.6+commit.6c089d02", 11 | "0.6.5": "v0.6.5+commit.f956cc89", 12 | "0.6.4": "v0.6.4+commit.1dca32f3", 13 | "0.6.3": "v0.6.3+commit.8dda9521", 14 | "0.6.2": "v0.6.2+commit.bacdbe57", 15 | "0.6.1": "v0.6.1+commit.e6f7d5a4", 16 | "0.6.0": "v0.6.0+commit.26b70077", 17 | "0.5.17": "v0.5.17+commit.d19bba13", 18 | "0.5.16": "v0.5.16+commit.9c3226ce", 19 | "0.5.15": "v0.5.15+commit.6a57276f", 20 | "0.5.14": "v0.5.14+commit.1f1aaa4", 21 | "0.5.13": "v0.5.13+commit.5b0b510c", 22 | "0.5.12": "v0.5.12+commit.7709ece9", 23 | "0.5.11": "v0.5.11+commit.c082d0b4", 24 | "0.5.10": "v0.5.10+commit.5a6ea5b1", 25 | "0.5.9": "v0.5.9+commit.e560f70d", 26 | "0.5.8": "v0.5.8+commit.23d335f2", 27 | "0.5.7": "v0.5.7+commit.6da8b019", 28 | "0.5.6": "v0.5.6+commit.b259423e", 29 | "0.5.5": "v0.5.5+commit.47a71e8f", 30 | "0.5.4": "v0.5.4+commit.9549d8ff", 31 | "0.5.3": "v0.5.3+commit.10d17f24", 32 | "0.5.2": "v0.5.2+commit.1df8f40c", 33 | "0.5.1": "v0.5.1+commit.c8a2cb62", 34 | "0.5.0": "v0.5.0+commit.1d4f565a", 35 | "0.4.26": "v0.4.26+commit.4563c3fc", 36 | "0.4.25": "v0.4.25+commit.59dbf8f1", 37 | "0.4.24": "v0.4.24+commit.e67f0147", 38 | "0.4.23": "v0.4.23+commit.124ca40d", 39 | "0.4.22": "v0.4.22+commit.4cb486ee", 40 | "0.4.21": "v0.4.21+commit.dfe3193c", 41 | "0.4.20": "v0.4.20+commit.3155dd80", 42 | "0.4.19": "v0.4.19+commit.c4cbbb05", 43 | "0.4.18": "v0.4.18+commit.9cf6e910", 44 | "0.4.17": "v0.4.17+commit.bdeb9e52", 45 | "0.4.16": "v0.4.16+commit.d7661dd9", 46 | "0.4.15": "v0.4.15+commit.bbb8e64f", 47 | "0.4.14": "v0.4.14+commit.c2215d46", 48 | "0.4.13": "v0.4.13+commit.fb4cb1a", 49 | "0.4.12": "v0.4.12+commit.194ff033", 50 | "0.4.11": "v0.4.11+commit.68ef5810", 51 | "0.4.10": "v0.4.10+commit.f0d539ae", 52 | "0.4.9": "v0.4.9+commit.364da425", 53 | "0.4.8": "v0.4.8+commit.60cc1668", 54 | "0.4.7": "v0.4.7+commit.822622cf", 55 | "0.4.6": "v0.4.6+commit.2dabbdf0", 56 | "0.4.5": "v0.4.5+commit.b318366e", 57 | "0.4.4": "v0.4.4+commit.4633f3de", 58 | "0.4.3": "v0.4.3+commit.2353da71", 59 | "0.4.2": "v0.4.2+commit.af6afb04", 60 | "0.4.1": "v0.4.1+commit.4fc6fc2c", 61 | "0.4.0": "v0.4.0+commit.acd334c9", 62 | "0.3.6": "v0.3.6+commit.3fc68da", 63 | "0.3.5": "v0.3.5+commit.5f97274", 64 | "0.3.4": "v0.3.4+commit.7dab890", 65 | "0.3.3": "v0.3.3+commit.4dc1cb1", 66 | "0.3.2": "v0.3.2+commit.81ae2a7", 67 | "0.3.1": "v0.3.1+commit.c492d9b", 68 | "0.3.0": "v0.3.0+commit.11d6736", 69 | "0.2.2": "v0.2.2+commit.ef92f56", 70 | "0.2.1": "v0.2.1+commit.91a6b35", 71 | "0.2.0": "v0.2.0+commit.4dc2445", 72 | "0.1.7": "v0.1.7+commit.b4e666c", 73 | "0.1.6": "v0.1.6+commit.d41f8b7", 74 | "0.1.5": "v0.1.5+commit.23865e3", 75 | "0.1.4": "v0.1.4+commit.5f6c3cd", 76 | "0.1.3": "v0.1.3+commit.28f561", 77 | "0.1.2": "v0.1.2+commit.d0d36e3", 78 | "0.1.1": "v0.1.1+commit.6ff4cd6" 79 | } 80 | -------------------------------------------------------------------------------- /lib/report.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const eslintHelpers = require('./eslint'); 3 | const eslintCliEngine = require('eslint').CLIEngine; 4 | const SourceMappingDecoder = require('remix-lib/src/sourceMappingDecoder'); 5 | 6 | const mythx2Severity = { 7 | High: 2, 8 | Medium: 1, 9 | }; 10 | 11 | const decoder = new SourceMappingDecoder(); 12 | 13 | /** 14 | * @returns ESLint formatter module 15 | */ 16 | const getFormatter = (name) => { 17 | const custom = ['text', 'propertyCheckText']; 18 | 19 | if (custom.includes(name)) { 20 | name = path.join(__dirname, 'formatters/', name + '.js'); 21 | } 22 | 23 | return eslintCliEngine.getFormatter(name); 24 | }; 25 | 26 | /** 27 | * Turn a srcmap entry (the thing between semicolons) into a line and 28 | * column location. 29 | * We make use of this.sourceMappingDecoder of this class to make 30 | * the conversion. 31 | * 32 | * @param {string} srcEntry - a single entry of solc sourceMap 33 | * @param {Array} lineBreakPositions - array returned by the function 'mapLineBreakPositions' 34 | * @returns {line: number, column: number} 35 | */ 36 | const textSrcEntry2lineColumn = (srcEntry, lineBreakPositions) => { 37 | const ary = srcEntry.split(':'); 38 | const sourceLocation = { 39 | length: parseInt(ary[1], 10), 40 | start: parseInt(ary[0], 10), 41 | }; 42 | const loc = decoder.convertOffsetToLineColumn(sourceLocation, lineBreakPositions); 43 | // FIXME: note we are lossy in that we don't return the end location 44 | if (loc.start) { 45 | // Adjust because routines starts lines at 0 rather than 1. 46 | loc.start.line++; 47 | } 48 | if (loc.end) { 49 | loc.end.line++; 50 | } 51 | return [loc.start, loc.end]; 52 | }; 53 | 54 | /** 55 | * Convert a MythX issue into an ESLint-style issue. 56 | * The eslint report format which we use, has these fields: 57 | * 58 | * - column, 59 | * - endCol, 60 | * - endLine, 61 | * - fatal, 62 | * - line, 63 | * - message, 64 | * - ruleId, 65 | * - severity 66 | * 67 | * but a MythX JSON report has these fields: 68 | * 69 | * - description.head 70 | * - description.tail, 71 | * - locations 72 | * - severity 73 | * - swcId 74 | * - swcTitle 75 | * 76 | * @param {object} issue - the MythX issue we want to convert 77 | * @param {string} sourceCode - holds the contract code 78 | * @param {object[]} locations - array of text-only MythX API issue locations 79 | * @returns eslint -issue object 80 | */ 81 | const issue2EsLint = (issue, sourceCode, locations) => { 82 | const swcLink = issue.swcID 83 | ? 'https://swcregistry.io/SWC-registry/docs/' + issue.swcID 84 | : 'N/A'; 85 | 86 | const esIssue = { 87 | mythxIssue: issue, 88 | mythxTextLocations: locations, 89 | sourceCode: sourceCode, 90 | 91 | fatal: false, 92 | ruleId: swcLink, 93 | message: issue.description.head, 94 | severity: mythx2Severity[issue.severity] || 1, 95 | line: -1, 96 | column: 0, 97 | endLine: -1, 98 | endCol: 0 99 | }; 100 | 101 | let startLineCol, endLineCol; 102 | 103 | const lineBreakPositions = decoder.getLinebreakPositions(sourceCode); 104 | 105 | if (locations.length) { 106 | [startLineCol, endLineCol] = textSrcEntry2lineColumn( 107 | locations[0].sourceMap, 108 | lineBreakPositions 109 | ); 110 | } 111 | 112 | if (startLineCol) { 113 | esIssue.line = startLineCol.line; 114 | esIssue.column = startLineCol.column; 115 | 116 | esIssue.endLine = endLineCol.line; 117 | esIssue.endCol = endLineCol.column; 118 | } 119 | 120 | return esIssue; 121 | }; 122 | 123 | /** 124 | * Gets the source index from the issue sourcemap 125 | * 126 | * @param {object} location - MythX API issue location object 127 | * @returns {number} 128 | */ 129 | const getSourceIndex = location => { 130 | const sourceMapRegex = /(\d+):(\d+):(\d+)/g; 131 | const match = sourceMapRegex.exec(location.sourceMap); 132 | // Ignore `-1` source index for compiler generated code 133 | return match ? match[3] : 0; 134 | }; 135 | 136 | /** 137 | * Converts MythX analyze API output item to Eslint compatible object 138 | * @param {object} report - issue item from the collection MythX analyze API output 139 | * @param {object} data - Contains array of solidity contracts source code and the input filepath of contract 140 | * @returns {object} 141 | */ 142 | const convertMythXReport2EsIssue = (report, data) => { 143 | const { sources, functionHashes } = data; 144 | const results = {}; 145 | 146 | /** 147 | * Filters locations only for source files. 148 | * Other location types are not supported to detect code. 149 | * 150 | * @param {object} location 151 | */ 152 | const textLocationFilterFn = location => ( 153 | (location.sourceType === 'solidity-file') 154 | && 155 | (location.sourceFormat === 'text') 156 | ); 157 | 158 | report.issues.forEach(issue => { 159 | const locations = issue.locations.filter(textLocationFilterFn); 160 | const location = locations.length ? locations[0] : undefined; 161 | 162 | let sourceCode = ''; 163 | let sourcePath = ''; 164 | 165 | if (location) { 166 | const sourceList = location.sourceList || report.sourceList || []; 167 | const sourceIndex = getSourceIndex(location); 168 | const fileName = sourceList[sourceIndex]; 169 | 170 | if (fileName) { 171 | sourcePath = fileName; 172 | 173 | if (sources[fileName]) { 174 | sourceCode = sources[fileName].content; 175 | } 176 | } 177 | } 178 | 179 | if (!results[sourcePath]) { 180 | results[sourcePath] = { 181 | errorCount: 0, 182 | warningCount: 0, 183 | fixableErrorCount: 0, 184 | fixableWarningCount: 0, 185 | filePath: sourcePath, 186 | functionHashes, 187 | sourceCode, 188 | messages: [], 189 | }; 190 | } 191 | 192 | results[sourcePath].messages.push( 193 | issue2EsLint(issue, sourceCode, locations) 194 | ); 195 | }); 196 | 197 | for (const key in results) { 198 | const result = results[key]; 199 | 200 | for (const { fatal, severity } of result.messages) { 201 | if (eslintHelpers.isFatal(fatal, severity)) { 202 | result.errorCount++; 203 | } else { 204 | result.warningCount++; 205 | } 206 | } 207 | } 208 | 209 | return Object.values(results); 210 | }; 211 | 212 | const formatIssues = (data, issues) => { 213 | const eslintIssues = issues 214 | .map(report => convertMythXReport2EsIssue(report, data)) 215 | .reduce((acc, curr) => acc.concat(curr), []); 216 | 217 | return eslintHelpers.getUniqueIssues(eslintIssues); 218 | }; 219 | 220 | module.exports = { 221 | formatIssues, 222 | getFormatter 223 | }; 224 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const removeRelativePathFromUrl = url => url.replace(/^.+\.\//, '').replace('./', ''); 2 | 3 | /* Dynamic linking is not supported. */ 4 | 5 | const regex = new RegExp(/__\$\w+\$__/,'g'); 6 | const address = '0000000000000000000000000000000000000000'; 7 | const replaceLinkedLibs = byteCode => byteCode.replace(regex, address); 8 | 9 | module.exports = { 10 | removeRelativePathFromUrl, 11 | replaceLinkedLibs 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sabre-mythx", 3 | "version": "0.10.3", 4 | "description": "Client for the MythX smart contract security analysis service", 5 | "main": "sabre.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/b-mueller/sabre.git" 9 | }, 10 | "keywords": [ 11 | "ethereum", 12 | "sabre", 13 | "mythril", 14 | "mythX" 15 | ], 16 | "homepage": "https://github.com/b-mueller/sabre", 17 | "bugs": { 18 | "url": "https://github.com/b-mueller/sabre/issues" 19 | }, 20 | "author": "Bernhard Mueller", 21 | "license": "MIT", 22 | "dependencies": { 23 | "@truffle/compile-solidity": "4.3.15", 24 | "@truffle/contract": "4.2.15", 25 | "@truffle/resolver": "6.0.11", 26 | "axios": "^0.19.0", 27 | "babel-eslint": "^10.0.3", 28 | "eslint": "^5.16.0", 29 | "minimist": "^1.2.0", 30 | "mythxjs": "^1.3.11", 31 | "openzeppelin-solidity": "^2.2.0", 32 | "ora": "^3.4.0", 33 | "package.json": "^2.0.1", 34 | "remix-lib": "^0.4.12", 35 | "solidity-parser-antlr": "^0.4.11" 36 | }, 37 | "resolutions": { 38 | "@truffle/compile-common": "0.3.2", 39 | "@truffle/resolver": "6.0.11", 40 | "@truffle/provisioner": "0.2.0" 41 | }, 42 | "bin": { 43 | "sabre": "./sabre.js" 44 | }, 45 | "scripts": { 46 | "preinstall": "npx npm-force-resolutions", 47 | "lint": "eslint ./sabre.js ./lib ./test", 48 | "lint:fix": "eslint --fix ./sabre.js ./lib ./test", 49 | "test": "nyc mocha" 50 | }, 51 | "publishConfig": { 52 | "access": "public" 53 | }, 54 | "devDependencies": { 55 | "chai": "^4.2.0", 56 | "mocha": "^6.1.4", 57 | "nyc": "^14.1.1" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /sabre.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const env = { 4 | apiKey: process.env.MYTHX_API_KEY, 5 | apiUrl: process.env.MYTHX_API_URL 6 | }; 7 | 8 | let { apiKey } = env; 9 | 10 | if (!apiKey) { 11 | console.log('Unauthenticated use of MythX has been discontinued. Sign up for a account at https://mythx.io/ and set the MYTHX_API_KEY environment variable.'); 12 | 13 | process.exit(-1); 14 | } 15 | 16 | const args = require('minimist')(process.argv.slice(2), { 17 | boolean: [ 'help', 'noCacheLookup', 'debug' ], 18 | string: [ 'mode', 'format' ], 19 | default: { mode: 'quick', format: 'text' }, 20 | }); 21 | 22 | let command = args._[0]; 23 | 24 | let controller; 25 | 26 | switch (command) { 27 | case 'version': 28 | controller = require('./lib/controllers/version'); 29 | break; 30 | case 'status': 31 | controller = require('./lib/controllers/status'); 32 | break; 33 | case 'list': 34 | controller = require('./lib/controllers/list'); 35 | break; 36 | case 'analyze': 37 | controller = require('./lib/controllers/analyze'); 38 | break; 39 | case 'check': 40 | controller = require('./lib/controllers/analyze'); 41 | args.isCheckProperty = true; 42 | break; 43 | case 'apiVersion': 44 | controller = require('./lib/controllers/api_version'); 45 | break; 46 | default: 47 | controller = require('./lib/controllers/help'); 48 | break; 49 | } 50 | 51 | controller(env, args); 52 | -------------------------------------------------------------------------------- /static/modes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muellerberndt/sabre/2b39fabf3532aa7bed1b1a3e55808101a02af968/static/modes.png -------------------------------------------------------------------------------- /static/sabre_v2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muellerberndt/sabre/2b39fabf3532aa7bed1b1a3e55808101a02af968/static/sabre_v2.jpg -------------------------------------------------------------------------------- /test/assets/TokenSale.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "25275bc8-48d8-4224-90dc-edbc10041c84", 3 | "issues": [ 4 | { 5 | "issues": [ 6 | { 7 | "swcID": "SWC-103", 8 | "swcTitle": "Floating Pragma", 9 | "description": { 10 | "head": "No pragma is set.", 11 | "tail": "It is recommended to make a conscious choice on what version of Solidity is used for compilation. Currently no version is set in the Solidity file." 12 | }, 13 | "severity": "Low", 14 | "locations": [ 15 | { 16 | "sourceMap": "0:0:0", 17 | "sourceType": "solidity-file", 18 | "sourceFormat": "text", 19 | "sourceList": [ 20 | "TokenSale.sol" 21 | ] 22 | } 23 | ], 24 | "extra": { 25 | "discoveryTime": 46314763, 26 | "toolName": "maru" 27 | }, 28 | "decodedLocations": null 29 | }, 30 | { 31 | "swcID": "SWC-119", 32 | "swcTitle": "Shadowing State Variables", 33 | "description": { 34 | "head": "State variable shadows another state variable.", 35 | "tail": "The state variable \"hardcap\" in contract \"Presale\" shadows another state variable with the same name \"hardcap\" in contract \"Tokensale\"." 36 | }, 37 | "severity": "Low", 38 | "locations": [ 39 | { 40 | "sourceMap": "172:25:0", 41 | "sourceType": "solidity-file", 42 | "sourceFormat": "text", 43 | "sourceList": [ 44 | "TokenSale.sol" 45 | ] 46 | } 47 | ], 48 | "extra": { 49 | "discoveryTime": 49669989, 50 | "toolName": "maru" 51 | }, 52 | "decodedLocations": null 53 | } 54 | ], 55 | "sourceType": "solidity-file", 56 | "sourceFormat": "text", 57 | "sourceList": [ 58 | "TokenSale.sol" 59 | ], 60 | "meta": { 61 | "selectedCompiler": "Unknown" 62 | } 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /test/compile.test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const Profiler = require('@truffle/compile-solidity/profiler'); 5 | const Resolver = require('@truffle/resolver'); 6 | const solc = require('solc'); 7 | const compiler = require('../lib/compiler'); 8 | const Config = require("@truffle/config"); 9 | 10 | const assert = chai.assert; 11 | 12 | const workingDir = process.cwd(); 13 | const contractsDir = path.resolve(workingDir, 'contracts'); 14 | 15 | const resolver = new Resolver({ 16 | working_directory: workingDir, 17 | contracts_build_directory: contractsDir 18 | }); 19 | 20 | describe('Compile test', () => { 21 | fs.readdirSync(contractsDir).forEach(file => { 22 | it(`Compile contract "${file}"`, async () => { 23 | const filePath = path.join(contractsDir, file); 24 | 25 | 26 | const config = Config.default(); 27 | 28 | const resolvedSources = (await Profiler.required_sources(config.with({ 29 | paths: [filePath], 30 | resolver: resolver, 31 | base_path: contractsDir, 32 | contracts_directory: filePath 33 | }))).allSources; 34 | 35 | const sources = {}; 36 | 37 | Object.keys(resolvedSources).forEach(source => { 38 | sources[source] = { content: resolvedSources[source] }; 39 | }); 40 | 41 | /* Get the input config for the Solidity Compiler */ 42 | const input = compiler.getSolcInput(sources); 43 | 44 | let data, error; 45 | 46 | try { 47 | data = compiler.getCompiledContracts(input, solc, filePath); 48 | } catch (e) { 49 | error = e; 50 | } 51 | 52 | if (data) { 53 | assert.isObject(data.compiled); 54 | assert.isObject(data.compiled.contracts); 55 | assert.isObject(data.compiled.sources); 56 | assert.isObject(data.compiled.sources[filePath]); 57 | 58 | assert.isString(data.contractName); 59 | assert.isObject(data.functionHashes); 60 | 61 | assert.isObject(data.contract); 62 | assert.isObject(data.contract.evm); 63 | 64 | assert.isObject(data.contract.evm.bytecode); 65 | assert.isString(data.contract.evm.bytecode.object); 66 | 67 | assert.isObject(data.contract.evm.deployedBytecode); 68 | assert.isString(data.contract.evm.deployedBytecode.object); 69 | 70 | assert.isTrue( 71 | data.contract.evm.bytecode.object.endsWith( 72 | data.contract.evm.deployedBytecode.object 73 | ) 74 | ); 75 | } else if (error) { 76 | const prefixes = [ 77 | 'Compiling the Solidity code did not return any bytecode', 78 | 'Unable to compile', 79 | 'No contracts detected after compiling', 80 | 'No contracts found' 81 | ]; 82 | 83 | assert.isTrue(prefixes.some(prefix => error.message.startsWith(prefix))); 84 | } else { 85 | assert.fail( 86 | 'None of compile data or compile error were detected' 87 | ); 88 | } 89 | }).timeout(20000); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /test/report.test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const Profiler = require('@truffle/compile-solidity/profiler'); 5 | const Resolver = require('@truffle/resolver'); 6 | const solc = require('solc'); 7 | const compiler = require('../lib/compiler'); 8 | const report = require('../lib/report'); 9 | const Config = require("@truffle/config"); 10 | 11 | const assert = chai.assert; 12 | 13 | const workingDir = process.cwd(); 14 | const contractsDir = path.resolve(workingDir, 'contracts'); 15 | 16 | const resolver = new Resolver({ 17 | working_directory: workingDir, 18 | contracts_build_directory: contractsDir 19 | }); 20 | 21 | const correctMessages = [ 22 | { 23 | message: 'No pragma is set.', 24 | severity: 1, 25 | line: 1, 26 | column: 0 27 | }, 28 | { 29 | message: 'State variable shadows another state variable.', 30 | severity: 1, 31 | line: 12, 32 | column: 4 33 | } 34 | ]; 35 | 36 | describe('Report test', () => { 37 | it('Contract TokenSale.sol', async () => { 38 | const filePath = path.resolve(contractsDir, 'TokenSale.sol'); 39 | 40 | const config = Config.default(); 41 | 42 | const resolvedSources = (await Profiler.required_sources(config.with({ 43 | paths: [filePath], 44 | resolver: resolver, 45 | base_path: contractsDir, 46 | contracts_directory: filePath 47 | }))).allSources; 48 | 49 | const sources = {}; 50 | 51 | Object.keys(resolvedSources).forEach(source => { 52 | const baseName = path.basename(source); 53 | 54 | sources[baseName] = { content: resolvedSources[source] }; 55 | }); 56 | 57 | /* Get the input config for the Solidity Compiler */ 58 | const input = compiler.getSolcInput(sources); 59 | 60 | /* Add all the imported contracts source code to the `data` to sourcemap the issue location */ 61 | const data = { sources: { ...input.sources } }; 62 | 63 | /* Get the issues from file to mock the API response */ 64 | const { issues } = JSON.parse( 65 | fs.readFileSync('test/assets/TokenSale.json', 'utf8').toString() 66 | ); 67 | 68 | const reports = report.formatIssues(data, issues); 69 | 70 | assert.equal(reports.length, 1); 71 | 72 | const fileReport = reports[0]; 73 | 74 | assert.equal(fileReport.messages.length, correctMessages.length); 75 | assert.equal(fileReport.errorCount, 0); 76 | assert.equal(fileReport.warningCount, 2); 77 | 78 | fileReport.messages.forEach((message, index) => { 79 | assert.include(message, correctMessages[index]); 80 | }); 81 | }); 82 | }); 83 | --------------------------------------------------------------------------------