From 5bef2bb1b02d403452a492ec2554c044face1cd6 Mon Sep 17 00:00:00 2001 From: Evgeniy Filatov Date: Tue, 23 Oct 2018 00:55:09 +0300 Subject: [PATCH 1/7] started implementation of call graph printer --- examples/printers/call_graph.sol | 25 ++++++++ examples/printers/call_graph.sol.dot | 20 ++++++ slither/__main__.py | 2 + slither/printers/call/__init__.py | 0 slither/printers/call/call_graph.py | 92 ++++++++++++++++++++++++++++ 5 files changed, 139 insertions(+) create mode 100644 examples/printers/call_graph.sol create mode 100644 examples/printers/call_graph.sol.dot create mode 100644 slither/printers/call/__init__.py create mode 100644 slither/printers/call/call_graph.py diff --git a/examples/printers/call_graph.sol b/examples/printers/call_graph.sol new file mode 100644 index 000000000..36cc7cbb5 --- /dev/null +++ b/examples/printers/call_graph.sol @@ -0,0 +1,25 @@ +contract ContractA { + function my_func_a() { + keccak256(0); + } +} + +contract ContractB { + ContractA a; + + constructor() { + a = new ContractA(); + } + + function my_func_b() { + a.my_func_a(); + my_second_func_b(); + } + + function my_func_a() { + my_second_func_b(); + } + + function my_second_func_b(){ + } +} \ No newline at end of file diff --git a/examples/printers/call_graph.sol.dot b/examples/printers/call_graph.sol.dot new file mode 100644 index 000000000..9e244e6ec --- /dev/null +++ b/examples/printers/call_graph.sol.dot @@ -0,0 +1,20 @@ +digraph { +subgraph "cluster_9_ContractA" { +label = "ContractA" +"9_my_func_a()" [label="my_func_a()"] +} +subgraph "cluster_45_ContractB" { +label = "ContractB" +"45_constructor()" [label="constructor()"] +"45_my_func_b()" [label="my_func_b()"] +"45_my_func_b()" -> "45_my_second_func_b()" +"45_my_func_a()" [label="my_func_a()"] +"45_my_func_a()" -> "45_my_second_func_b()" +"45_my_second_func_b()" [label="my_second_func_b()"] +} +subgraph cluster_solidity { +label = "[Solidity]" +"keccak256()" +} +"9_my_func_a()" -> "keccak256()" +} \ No newline at end of file diff --git a/slither/__main__.py b/slither/__main__.py index fedfbdfc1..085ef8e0f 100644 --- a/slither/__main__.py +++ b/slither/__main__.py @@ -157,6 +157,7 @@ def main(): from slither.printers.summary.contract import ContractSummary from slither.printers.inheritance.inheritance import PrinterInheritance from slither.printers.inheritance.inheritance_graph import PrinterInheritanceGraph + from slither.printers.call.call_graph import PrinterCallGraph from slither.printers.functions.authorization import PrinterWrittenVariablesAndAuthorization from slither.printers.summary.slithir import PrinterSlithIR @@ -164,6 +165,7 @@ def main(): ContractSummary, PrinterInheritance, PrinterInheritanceGraph, + PrinterCallGraph, PrinterWrittenVariablesAndAuthorization, PrinterSlithIR] diff --git a/slither/printers/call/__init__.py b/slither/printers/call/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/slither/printers/call/call_graph.py b/slither/printers/call/call_graph.py new file mode 100644 index 000000000..9c332f72e --- /dev/null +++ b/slither/printers/call/call_graph.py @@ -0,0 +1,92 @@ +""" + Module printing the call graph + + The call graph shows for each function, + what are the contracts/functions called. + The output is a dot file named filename.dot +""" + +from slither.printers.abstract_printer import AbstractPrinter +from slither.core.declarations.solidity_variables import SolidityFunction +from slither.core.declarations.function import Function + + +class PrinterCallGraph(AbstractPrinter): + ARGUMENT = 'call-graph' + HELP = 'the call graph' + + def __init__(self, slither, logger): + super(PrinterCallGraph, self).__init__(slither, logger) + + self.contracts = slither.contracts + + self.solidity_functions = set() + self.solidity_calls = set() + + @staticmethod + def _contract_subgraph_id(contract): + return f'"cluster_{contract.id}_{contract.name}"' + + @staticmethod + def _function_node_id(contract, function): + return f'"{contract.id}_{function.full_name}"' + + def _render_contract(self, contract): + result = f'subgraph {self._contract_subgraph_id(contract)} {{\n' + result += f'label = "{contract.name}"\n' + + for function in contract.functions: + result += self._render_internal_calls(contract, function) + + result += '}\n' + + return result + + def _render_internal_calls(self, contract, function): + result = '' + + # we need to define function nodes with unique ids, + # as it's possible that multiple contracts have same functions + result += f'{self._function_node_id(contract, function)} [label="{function.full_name}"]\n' + + for internal_call in function.internal_calls: + if isinstance(internal_call, (Function)): + result += f'{self._function_node_id(contract, function)} -> {self._function_node_id(contract, internal_call)}\n' + elif isinstance(internal_call, (SolidityFunction)): + self.solidity_functions.add(f'"{internal_call.full_name}"') + self.solidity_calls.add((self._function_node_id(contract, function), f'"{internal_call.full_name}"')) + + return result + + def _render_solidity_calls(self): + result = '' + + result = 'subgraph cluster_solidity {\n' + result += 'label = "[Solidity]"\n' + + for function in self.solidity_functions: + result += f'{function}\n' + + result += '}\n' + + for caller, callee in self.solidity_calls: + result += f'{caller} -> {callee}\n' + + return result + + def output(self, filename): + """ + Output the graph in filename + Args: + filename(string) + """ + if not filename.endswith('.dot'): + filename += ".dot" + info = 'Call Graph: ' + filename + self.info(info) + with open(filename, 'w') as f: + f.write('digraph {\n') + for contract in self.contracts: + f.write(self._render_contract(contract)) + f.write(self._render_solidity_calls()) + f.write('}') From 4a6d8f3495c50d8744fda999a98a09f4acda34c4 Mon Sep 17 00:00:00 2001 From: Evgeniy Filatov Date: Tue, 23 Oct 2018 17:10:05 +0300 Subject: [PATCH 2/7] implemented external call support, refactored code, added image example --- README.md | 1 + examples/printers/call_graph.sol.dot | 25 ++-- examples/printers/call_graph.sol.dot.png | Bin 0 -> 35587 bytes slither/printers/call/call_graph.py | 179 ++++++++++++++++------- 4 files changed, 144 insertions(+), 61 deletions(-) create mode 100644 examples/printers/call_graph.sol.dot.png diff --git a/README.md b/README.md index 03c8712b3..1f24121f2 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ If Slither is run on a directory, it will run on every `.sol` file of the direct * `--printer-quick-summary`: Print a quick summary of the contracts * `--printer-inheritance`: Print the inheritance relations * `--printer-inheritance-graph`: Print the inheritance graph in a file +* `--printer-call-graph`: Print the call graph in a file * `--printer-vars-and-auth`: Print the variables written and the check on `msg.sender` of each function ## Checks available diff --git a/examples/printers/call_graph.sol.dot b/examples/printers/call_graph.sol.dot index 9e244e6ec..a63b24b62 100644 --- a/examples/printers/call_graph.sol.dot +++ b/examples/printers/call_graph.sol.dot @@ -1,20 +1,21 @@ -digraph { -subgraph "cluster_9_ContractA" { +strict digraph { +subgraph cluster_9_ContractA { label = "ContractA" -"9_my_func_a()" [label="my_func_a()"] +"9_my_func_a" [label="my_func_a"] } -subgraph "cluster_45_ContractB" { +subgraph cluster_45_ContractB { label = "ContractB" -"45_constructor()" [label="constructor()"] -"45_my_func_b()" [label="my_func_b()"] -"45_my_func_b()" -> "45_my_second_func_b()" -"45_my_func_a()" [label="my_func_a()"] -"45_my_func_a()" -> "45_my_second_func_b()" -"45_my_second_func_b()" [label="my_second_func_b()"] +"45_my_func_a" [label="my_func_a"] +"45_my_second_func_b" [label="my_second_func_b"] +"45_my_func_b" [label="my_func_b"] +"45_constructor" [label="constructor"] +"45_my_func_b" -> "45_my_second_func_b" +"45_my_func_a" -> "45_my_second_func_b" } subgraph cluster_solidity { label = "[Solidity]" -"keccak256()" +"keccak256()" +"9_my_func_a" -> "keccak256()" } -"9_my_func_a()" -> "keccak256()" +"45_my_func_b" -> "9_my_func_a" } \ No newline at end of file diff --git a/examples/printers/call_graph.sol.dot.png b/examples/printers/call_graph.sol.dot.png new file mode 100644 index 0000000000000000000000000000000000000000..a679acbc488002e04bc3459499304d359b54b6dc GIT binary patch literal 35587 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 Vn4b9s zfc>X{0$Rc9Aw3N{Nhmgd_ds()bKbT#nwp4c?jq>Ci!s1$x*r&{`t{p4lrWHR&K>S) z*`Xe^1F%L6hmMEgdUrE2E>G02wx$cG^!fFFN1`1FZ~?j$nW34L)gcvIF%%k_e(M%$ zLtGf<)0s!{v%Q^`mLBkwL>HtU5WV)Q8;-@t&HzcT7sphM_s_Jgr&F-W4+U{pA9$)! z9GBAJD|dtm=lB^u`fJ$@)9zo{Tj__mHg*rx+KCgm~)nTX(T5l*}_-D)~YIE;cRiq89Ka67~9yc;M zM#?C1Y0&|2{R`SB6bb1(@*Cs%^9!6(n$}m{JgzI1Ljn1bOw$F#C4TqA>{G@zLyv_9 zw^YOk6-l`m=RLnVC|TeJsk3g~lEIrOro6Cvq-+5zR-sn|c5BR}5_>~iU?#HX2Tb0LJ%oE@ zKT-t0SR7{S#Z`YR4PEmpm#m2c`-V2yv>*T0zr(gYz};!c$ezGgW16nqeBviu5u~%` zaJK&r=0$QFNN=Ms8P|Jq)pT#RBgf+dzX;LN5WXgGDE*z$}2I-Iza0wAE)KbB7=hr`QGOBi1^ z%Q2KL!8c-jYSVrL()ac6C0c}{~M{|F;p&;`qJ7V zb8;T!nW&}-U$>c{F3c#btO&Y$mV_c)UHR%By zjT^A2=>WeWyq};Ihc_f`4!cb67%M1^QwZ}!SH=DWKE3PaP@6#PNGCDmWE}V(gT#C; zO1~JhHV&h;>}1lKC~BN@Mo5n=yl*@&l%HHz%)-y6P`|cp+qMy(AH=yMxq$qB5l&K{ z9OOkZ)XbNiV2273xxr0vRz;?)< z#Rmc8p>n{L#J>IdRmFaOcPe~K{~d*u11Anj>h5RStHrd#XE%Cn>^xX2-1y9seU^hY z-cERaEpt|vZ_|^KXWk6()|{lfr@&Y{uzjyl)3WQo4eLGqQnAhE71v*TPY!jR`u59} zJO8fvqoQcniV6EBjYo4c%OD^N1h=ic(QcWZ)D%2X7^^J_x> z#Yrdk@b&ENRkmp8q?0F5c2s=&bjGruqr3ZTw5oqC$zHcJa$wZDA6DcNS&tt5wP4{w zCQ6;8#RIR3Us?5%($bG>lXMy{zS5?J;yH5R^5xS}4t?}6`N0X(A2~9Z9GbbMlR`@S z?7jV(?ES{F{Eevp2M-(ex^(;ctze$qa>a;k4jn!`BYMtb_!8IZs?`LoiS$rUI{h#v zT}!RL%;sVmi|gr5=JSeLE6N*T8DYQMjKbeog~SI+WPRAevDmFakvuNO;eR@zyP=s2sN=8VDG6$t}7?mU0`Raq6T zcsX+$-*^!+`*Fol9O%!xne8vVQL@tD*^?)g#SebpU}Uu4{%mOIulwzL2TXNZwaOBO zS^35hhtv8TG^>v7+gAq5dbm;Ai$s^H1z4SXCI7RJv?jzrkaG5I`Yx@CSYnx z(>vUxtIM);a-gs0 z#*yW}yfIAbttd|(0)S@}hn1!1=0AQi*SbJa6x+e_MwDcE3dOeB|MrJGjdhCh`C{3u zf2&rv-thKso9&v4qV<+jdCubex}7Rjd9X_*@E*n4edSjwX6E^$S!6}8p$m-?ik+ "{to_node}"' + +# return dot language string to add graph node (with optional label) +def _node(node, label=None): + return ' '.join(( + f'"{node}"', + f'[label="{label}"]' if label is not None else '', + )) class PrinterCallGraph(AbstractPrinter): ARGUMENT = 'call-graph' @@ -18,61 +68,89 @@ class PrinterCallGraph(AbstractPrinter): def __init__(self, slither, logger): super(PrinterCallGraph, self).__init__(slither, logger) - self.contracts = slither.contracts - - self.solidity_functions = set() - self.solidity_calls = set() - - @staticmethod - def _contract_subgraph_id(contract): - return f'"cluster_{contract.id}_{contract.name}"' - - @staticmethod - def _function_node_id(contract, function): - return f'"{contract.id}_{function.full_name}"' + self.contract_functions = {} # contract -> contract functions nodes + self.contract_calls = {} # contract -> contract calls edges - def _render_contract(self, contract): - result = f'subgraph {self._contract_subgraph_id(contract)} {{\n' - result += f'label = "{contract.name}"\n' + self.solidity_functions = set() # solidity function nodes + self.solidity_calls = set() # solidity calls edges - for function in contract.functions: - result += self._render_internal_calls(contract, function) + self.external_calls = set() # external calls edges - result += '}\n' + self._process_contracts(slither.contracts) - return result + def _process_contracts(self, contracts): + for contract in contracts: + self.contract_functions[contract] = set() + self.contract_calls[contract] = set() - def _render_internal_calls(self, contract, function): - result = '' + for function in contract.functions: + self._process_function(contract, function) - # we need to define function nodes with unique ids, - # as it's possible that multiple contracts have same functions - result += f'{self._function_node_id(contract, function)} [label="{function.full_name}"]\n' + def _process_function(self, contract, function): + self.contract_functions[contract].add( + _node(_function_node(contract, function), function.name), + ) for internal_call in function.internal_calls: - if isinstance(internal_call, (Function)): - result += f'{self._function_node_id(contract, function)} -> {self._function_node_id(contract, internal_call)}\n' - elif isinstance(internal_call, (SolidityFunction)): - self.solidity_functions.add(f'"{internal_call.full_name}"') - self.solidity_calls.add((self._function_node_id(contract, function), f'"{internal_call.full_name}"')) - - return result + self._process_internal_call(contract, function, internal_call) + for external_call in function.external_calls: + self._process_external_call(contract, function, external_call) + + def _process_internal_call(self, contract, function, internal_call): + if isinstance(internal_call, (Function)): + self.contract_calls[contract].add(_edge( + _function_node(contract, function), + _function_node(contract, internal_call), + )) + elif isinstance(internal_call, (SolidityFunction)): + self.solidity_functions.add( + _node(_solidity_function_node(internal_call)), + ) + self.solidity_calls.add(_edge( + _function_node(contract, function), + _solidity_function_node(internal_call), + )) + + def _process_external_call(self, contract, function, external_call): + if isinstance(external_call.called, MemberAccess): + try: + self.external_calls.add(_edge( + _function_node(contract, function), + _external_function_node(external_call.called), + )) + except TypeError: + # cannot visualize call if we cannot get external contract + pass + + def _render_internal_calls(self): + lines = [] + + for contract in self.contract_functions: + lines.append(f'subgraph {_contract_subgraph(contract)} {{') + lines.append(f'label = "{contract.name}"') + + lines.extend(self.contract_functions[contract]) + lines.extend(self.contract_calls[contract]) + + lines.append('}') + + return '\n'.join(lines) def _render_solidity_calls(self): - result = '' + lines = [] - result = 'subgraph cluster_solidity {\n' - result += 'label = "[Solidity]"\n' + lines.append('subgraph cluster_solidity {') + lines.append('label = "[Solidity]"') - for function in self.solidity_functions: - result += f'{function}\n' + lines.extend(self.solidity_functions) + lines.extend(self.solidity_calls) - result += '}\n' + lines.append('}') - for caller, callee in self.solidity_calls: - result += f'{caller} -> {callee}\n' + return '\n'.join(lines) - return result + def _render_external_calls(self): + return '\n'.join(self.external_calls) def output(self, filename): """ @@ -81,12 +159,15 @@ class PrinterCallGraph(AbstractPrinter): filename(string) """ if not filename.endswith('.dot'): - filename += ".dot" - info = 'Call Graph: ' + filename - self.info(info) + filename += '.dot' + + self.info(f'Call Graph: {filename}') + with open(filename, 'w') as f: - f.write('digraph {\n') - for contract in self.contracts: - f.write(self._render_contract(contract)) - f.write(self._render_solidity_calls()) - f.write('}') + f.write('\n'.join([ + 'strict digraph {', + self._render_internal_calls(), + self._render_solidity_calls(), + self._render_external_calls(), + '}', + ])) From d50b0f01f3159ee42c9c264b8799f23bd341fdfc Mon Sep 17 00:00:00 2001 From: Evgeniy Filatov Date: Thu, 25 Oct 2018 15:17:13 +0300 Subject: [PATCH 3/7] improved support for external calls --- examples/printers/call_graph.sol | 9 +++++ examples/printers/call_graph.sol.dot | 29 +++++++++------ slither/printers/call/call_graph.py | 54 ++++++++++------------------ 3 files changed, 45 insertions(+), 47 deletions(-) diff --git a/examples/printers/call_graph.sol b/examples/printers/call_graph.sol index 36cc7cbb5..182ccbf52 100644 --- a/examples/printers/call_graph.sol +++ b/examples/printers/call_graph.sol @@ -1,6 +1,14 @@ +library Library { + function library_func() { + } +} + contract ContractA { + uint256 public val = 0; + function my_func_a() { keccak256(0); + Library.library_func(); } } @@ -21,5 +29,6 @@ contract ContractB { } function my_second_func_b(){ + a.val(); } } \ No newline at end of file diff --git a/examples/printers/call_graph.sol.dot b/examples/printers/call_graph.sol.dot index a63b24b62..5677b7a40 100644 --- a/examples/printers/call_graph.sol.dot +++ b/examples/printers/call_graph.sol.dot @@ -1,21 +1,28 @@ strict digraph { -subgraph cluster_9_ContractA { +subgraph cluster_5_Library { +label = "Library" +"5_library_func" [label="library_func"] +} +subgraph cluster_22_ContractA { label = "ContractA" -"9_my_func_a" [label="my_func_a"] +"22_my_func_a" [label="my_func_a"] +"22_val" [label="val"] } -subgraph cluster_45_ContractB { +subgraph cluster_63_ContractB { label = "ContractB" -"45_my_func_a" [label="my_func_a"] -"45_my_second_func_b" [label="my_second_func_b"] -"45_my_func_b" [label="my_func_b"] -"45_constructor" [label="constructor"] -"45_my_func_b" -> "45_my_second_func_b" -"45_my_func_a" -> "45_my_second_func_b" +"63_my_second_func_b" [label="my_second_func_b"] +"63_my_func_a" [label="my_func_a"] +"63_constructor" [label="constructor"] +"63_my_func_b" [label="my_func_b"] +"63_my_func_b" -> "63_my_second_func_b" +"63_my_func_a" -> "63_my_second_func_b" } subgraph cluster_solidity { label = "[Solidity]" "keccak256()" -"9_my_func_a" -> "keccak256()" +"22_my_func_a" -> "keccak256()" } -"45_my_func_b" -> "9_my_func_a" +"22_my_func_a" -> "5_library_func" +"63_my_func_b" -> "22_my_func_a" +"63_my_second_func_b" -> "22_val" } \ No newline at end of file diff --git a/slither/printers/call/call_graph.py b/slither/printers/call/call_graph.py index 6fed79c2c..18058c6c6 100644 --- a/slither/printers/call/call_graph.py +++ b/slither/printers/call/call_graph.py @@ -27,29 +27,6 @@ def _function_node(contract, function): def _solidity_function_node(solidity_function): return f'{solidity_function.name}' -# return node for externally called function -def _external_function_node(member_access): - # we have external function name, to get Contract we need to - # check that expression is variable and is of Contract type - expression = member_access.expression - if not isinstance(expression, (Identifier)): - raise TypeError - - value = expression.value - if not isinstance(value, (Variable)): - raise TypeError - - value_type = value.type - if not isinstance(value_type, (UserDefinedType)): - raise TypeError - - contract = value_type.type - if not isinstance(contract, (Contract)): - raise TypeError - - # at this point we have contract instance and function name - return f'{contract.id}_{member_access.member_name}' - # return dot language string to add graph edge def _edge(from_node, to_node): return f'"{from_node}" -> "{to_node}"' @@ -71,6 +48,10 @@ class PrinterCallGraph(AbstractPrinter): self.contract_functions = {} # contract -> contract functions nodes self.contract_calls = {} # contract -> contract calls edges + for contract in slither.contracts: + self.contract_functions[contract] = set() + self.contract_calls[contract] = set() + self.solidity_functions = set() # solidity function nodes self.solidity_calls = set() # solidity calls edges @@ -80,9 +61,6 @@ class PrinterCallGraph(AbstractPrinter): def _process_contracts(self, contracts): for contract in contracts: - self.contract_functions[contract] = set() - self.contract_calls[contract] = set() - for function in contract.functions: self._process_function(contract, function) @@ -93,7 +71,7 @@ class PrinterCallGraph(AbstractPrinter): for internal_call in function.internal_calls: self._process_internal_call(contract, function, internal_call) - for external_call in function.external_calls: + for external_call in function.high_level_calls: self._process_external_call(contract, function, external_call) def _process_internal_call(self, contract, function, internal_call): @@ -112,15 +90,19 @@ class PrinterCallGraph(AbstractPrinter): )) def _process_external_call(self, contract, function, external_call): - if isinstance(external_call.called, MemberAccess): - try: - self.external_calls.add(_edge( - _function_node(contract, function), - _external_function_node(external_call.called), - )) - except TypeError: - # cannot visualize call if we cannot get external contract - pass + external_contract, external_function = external_call + + # add variable as node to respective contract + if isinstance(external_function, (Variable)): + self.contract_functions[external_contract].add(_node( + _function_node(external_contract, external_function), + external_function.name + )) + + self.external_calls.add(_edge( + _function_node(contract, function), + _function_node(external_contract, external_function), + )) def _render_internal_calls(self): lines = [] From f6ed3ba68d1a152c5c0e9172b631157308b80464 Mon Sep 17 00:00:00 2001 From: redshark1802 Date: Mon, 22 Oct 2018 21:43:17 +0200 Subject: [PATCH 4/7] cli improvements - help is printed when no arguments are supplied - rework detector arguments: detectors are now a comma-separated list, defaults to all - rework printer arguments: printers are now a comma-separated list, defaults to contract-summary - add version command - add --list-detectors and --list-printers - update README --- README.md | 51 +++++++------ scripts/travis_test.sh | 30 ++++---- slither/__main__.py | 167 +++++++++++++++++++++++++++-------------- 3 files changed, 156 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 03c8712b3..dd26c2e98 100644 --- a/README.md +++ b/README.md @@ -36,32 +36,39 @@ If Slither is run on a directory, it will run on every `.sol` file of the direct ### Printers -* `--printer-summary`: Print a summary of the contracts -* `--printer-quick-summary`: Print a quick summary of the contracts -* `--printer-inheritance`: Print the inheritance relations -* `--printer-inheritance-graph`: Print the inheritance graph in a file -* `--printer-vars-and-auth`: Print the variables written and the check on `msg.sender` of each function +By default, the `contract-summary` printer is used. Use --printers comma-separated list of printers, +or `none` to disable the default printer. -## Checks available +Num | Printer | Description +--- | --- | --- +1 | `contract-summary` | a summary of the contract +2 | `function-summary` | the summary of the functions +3 | `inheritance` | the inheritance relation between contracts +4 | `inheritance-graph` | the inheritance graph +5 | `slithir` | the slithIR +6 | `vars-and-auth` | the state variables written and the authorization of the functions -By default, all the checks are run. Use --detect-_name-of-check_ to run one check at a time. +## Detectors -Num | Check | What it Detects | Impact | Confidence +By default, all the detectors are run. Use --detectors comma-separated list of detectors to run. + +Num | Detector | What it Detects | Impact | Confidence --- | --- | --- | --- | --- -1 | `suicidal` | Suicidal functions | High | High -2 | `uninitialized-state` | Uninitialized state variables | High | High -3 | `uninitialized-storage` | Uninitialized storage variables | High | High -4 | `arbitrary-send` | Functions that send ether to an arbitrary destination | High | Medium -5 | `reentrancy` | Reentrancy vulnerabilities | High | Medium -6 | `locked-ether` | Contracts that lock ether | Medium | High -7 | `tx-origin` | Dangerous usage of `tx.origin` | Medium | Medium -8 | `assembly` | Assembly usage | Informational | High -9 | `const-candidates-state` | State variables that could be declared constant | Informational | High -10 | `low-level-calls` | Low level calls | Informational | High -11 | `naming-convention` | Conformance to Solidity naming conventions | Informational | High -12 | `pragma` | If different pragma directives are used | Informational | High -13 | `solc-version` | If an old version of Solidity used (<0.4.23) | Informational | High -14 | `unused-state` | Unused state variables | Informational | High +1 | `backdoor` | Function named backdoor (detector example) | High | High +2 | `suicidal` | Suicidal functions | High | High +3 | `uninitialized-state` | Uninitialized state variables | High | High +4 | `uninitialized-storage` | Uninitialized storage variables | High | High +5 | `arbitrary-send` | Functions that send ether to an arbitrary destination | High | Medium +6 | `reentrancy` | Reentrancy vulnerabilities | High | Medium +7 | `locked-ether` | Contracts that lock ether | Medium | High +8 | `tx-origin` | Dangerous usage of `tx.origin` | Medium | Medium +9 | `assembly` | Assembly usage | Informational | High +10 | `const-candidates-state` | State variables that could be declared constant | Informational | High +11 | `low-level-calls` | Low level calls | Informational | High +12 | `naming-convention` | Conformance to Solidity naming conventions | Informational | High +13 | `pragma` | If different pragma directives are used | Informational | High +14 | `solc-version` | If an old version of Solidity used (<0.4.23) | Informational | High +15 | `unused-state` | Unused state variables | Informational | High [Contact us](https://www.trailofbits.com/contact/) to get access to additional detectors. diff --git a/scripts/travis_test.sh b/scripts/travis_test.sh index 0e4e4030a..0a8db8644 100755 --- a/scripts/travis_test.sh +++ b/scripts/travis_test.sh @@ -3,79 +3,79 @@ ### Test Detectors -slither tests/uninitialized.sol --disable-solc-warnings --detect-uninitialized-state +slither tests/uninitialized.sol --disable-solc-warnings --detectors uninitialized-state if [ $? -ne 1 ]; then exit 1 fi # contains also the test for the suicidal detector -slither tests/backdoor.sol --disable-solc-warnings --detect-backdoor +slither tests/backdoor.sol --disable-solc-warnings --detectors backdoor if [ $? -ne 1 ]; then exit 1 fi -slither tests/pragma.0.4.24.sol --disable-solc-warnings --detect-pragma +slither tests/pragma.0.4.24.sol --disable-solc-warnings --detectors pragma if [ $? -ne 1 ]; then exit 1 fi -slither tests/old_solc.sol.json --solc-ast --detect-solc-version +slither tests/old_solc.sol.json --solc-ast --detectors solc-version if [ $? -ne 1 ]; then exit 1 fi -slither tests/reentrancy.sol --disable-solc-warnings --detect-reentrancy +slither tests/reentrancy.sol --disable-solc-warnings --detectors reentrancy if [ $? -ne 1 ]; then exit 1 fi -slither tests/uninitialized_storage_pointer.sol --disable-solc-warnings --detect-uninitialized-storage +slither tests/uninitialized_storage_pointer.sol --disable-solc-warnings --detectors uninitialized-storage if [ $? -ne 1 ]; then exit 1 fi -slither tests/tx_origin.sol --disable-solc-warnings --detect-tx-origin +slither tests/tx_origin.sol --disable-solc-warnings --detectors tx-origin if [ $? -ne 2 ]; then exit 1 fi -slither tests/unused_state.sol --detect-unused-state +slither tests/unused_state.sol --detectors unused-state if [ $? -ne 1 ]; then exit 1 fi -slither tests/locked_ether.sol --detect-locked-ether +slither tests/locked_ether.sol --detectors locked-ether if [ $? -ne 1 ]; then exit 1 fi -slither tests/arbitrary_send.sol --disable-solc-warnings --detect-arbitrary-send +slither tests/arbitrary_send.sol --disable-solc-warnings --detectors arbitrary-send if [ $? -ne 2 ]; then exit 1 fi -slither tests/inline_assembly_contract.sol --disable-solc-warnings --detect-assembly +slither tests/inline_assembly_contract.sol --disable-solc-warnings --detectors assembly if [ $? -ne 1 ]; then exit 1 fi -slither tests/inline_assembly_library.sol --disable-solc-warnings --detect-assembly +slither tests/inline_assembly_library.sol --disable-solc-warnings --detectors assembly if [ $? -ne 2 ]; then exit 1 fi -slither tests/naming_convention.sol --disable-solc-warnings --detect-naming-convention +slither tests/naming_convention.sol --disable-solc-warnings --detectors naming-convention if [ $? -ne 10 ]; then exit 1 fi -slither tests/low_level_calls.sol --disable-solc-warnings --detect-low-level-calls +slither tests/low_level_calls.sol --disable-solc-warnings --detectors low-level-calls if [ $? -ne 1 ]; then exit 1 fi -slither tests/const_state_variables.sol --detect-const-candidates-state +slither tests/const_state_variables.sol --detectors const-candidates-state if [ $? -ne 2 ]; then exit 1 fi diff --git a/slither/__main__.py b/slither/__main__.py index 9c59f0205..72d83a331 100644 --- a/slither/__main__.py +++ b/slither/__main__.py @@ -8,7 +8,7 @@ import os import sys import traceback -from pkg_resources import iter_entry_points +from pkg_resources import iter_entry_points, require from slither.detectors.abstract_detector import (AbstractDetector, DetectorClassification, @@ -19,7 +19,8 @@ from slither.slither import Slither logging.basicConfig() logger = logging.getLogger("Slither") -def output_to_markdown(detector_classes): + +def output_detectors(detector_classes): """ Pretty print of the detectors to README.md """ @@ -27,8 +28,6 @@ def output_to_markdown(detector_classes): for detector in detector_classes: argument = detector.ARGUMENT # dont show the backdoor example - if argument == 'backdoor': - continue help_info = detector.HELP impact = detector.IMPACT confidence = classification_txt[detector.CONFIDENCE] @@ -43,7 +42,27 @@ def output_to_markdown(detector_classes): help_info, classification_txt[impact], confidence)) - idx = idx +1 + idx = idx + 1 + + +def output_printers(printer_classes): + """ + Pretty print of the printers to README.md + """ + printers_list = [] + for printer in printer_classes: + argument = printer.ARGUMENT + help_info = printer.HELP + printers_list.append((argument, help_info)) + + print(printers_list) + # Sort by name + printers_list = sorted(printers_list, key=lambda element: (element[0])) + idx = 1 + for (argument, help_info) in printers_list: + print('{} | `{}` | {} '.format(idx, argument, help_info)) + idx = idx + 1 + def process(filename, args, detector_classes, printer_classes): """ @@ -64,15 +83,13 @@ def process(filename, args, detector_classes, printer_classes): results = [] - if printer_classes: - slither.run_printers() # Currently printers does not return results + detector_results = slither.run_detectors() + detector_results = [x for x in detector_results if x] # remove empty results + detector_results = [item for sublist in detector_results for item in sublist] # flatten - elif detector_classes: - detector_results = slither.run_detectors() - detector_results = [x for x in detector_results if x] # remove empty results - detector_results = [item for sublist in detector_results for item in sublist] # flatten + results.extend(detector_results) - results.extend(detector_results) + slither.run_printers() # Currently printers does not return results return results, analyzed_contracts_count @@ -164,8 +181,12 @@ def main_impl(all_detector_classes, all_printer_classes): """ args = parse_args(all_detector_classes, all_printer_classes) - if args.markdown: - output_to_markdown(all_detector_classes) + if args.list_detectors: + output_detectors(all_detector_classes) + return + + if args.list_printers: + output_printers(all_printer_classes) return detector_classes = choose_detectors(args, all_detector_classes) @@ -205,9 +226,6 @@ def main_impl(all_detector_classes, all_printer_classes): (results_tmp, number_contracts_tmp) = process(filename, args, detector_classes, printer_classes) number_contracts += number_contracts_tmp results += results_tmp - # if args.json: - # output_json(results, args.json) - # exit(results) else: raise Exception("Unrecognised file/dir path: '#{filename}'".format(filename=filename)) @@ -229,17 +247,48 @@ def main_impl(all_detector_classes, all_printer_classes): def parse_args(detector_classes, printer_classes): parser = argparse.ArgumentParser(description='Slither', - usage="slither.py contract.sol [flag]", - formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=35)) + usage="slither.py contract.sol [flag]") parser.add_argument('filename', help='contract.sol file') + parser.add_argument('--version', + help='displays the current version', + version=require('slither-analyzer')[0].version, + action='version') + parser.add_argument('--solc', help='solc path', action='store', default='solc') + parser.add_argument('--detectors', + help='Comma-separated list of detectors, defaults to all, ' + 'available detectors: {}'.format(', '.join(d.ARGUMENT for d in detector_classes)), + action='store', + dest='detectors_to_run', + default='all') + + parser.add_argument('--printers', + help='Comma-separated list fo contract information printers, ' + 'defaults to contract-summary and can be disabled by using \'none\', ' + 'available printers: {}'.format(', '.join(d.ARGUMENT for d in printer_classes)), + action='store', + dest='printers_to_run', + default='contract-summary') + + parser.add_argument('--output', + help='Define output encoding', + action='store', + choices=['stdout', 'json'], + default='stdout') + + parser.add_argument('--exclude-detectors', + help='Comma-separated list of detectors that should be excluded', + action='store', + dest='detectors_to_exclude', + default='') + parser.add_argument('--solc-args', help='Add custom solc arguments. Example: --solc-args "--allow-path /tmp --evm-version byzantium".', action='store', @@ -280,34 +329,6 @@ def parse_args(detector_classes, printer_classes): action='store_true', default=False) - for detector_cls in detector_classes: - detector_arg = '--detect-{}'.format(detector_cls.ARGUMENT) - detector_help = '{}'.format(detector_cls.HELP) - parser.add_argument(detector_arg, - help=detector_help, - action="append_const", - dest="detectors_to_run", - const=detector_cls.ARGUMENT) - - # Second loop so that the --exclude are shown after all the detectors - for detector_cls in detector_classes: - exclude_detector_arg = '--exclude-{}'.format(detector_cls.ARGUMENT) - exclude_detector_help = 'Exclude {} detector'.format(detector_cls.ARGUMENT) - parser.add_argument(exclude_detector_arg, - help=exclude_detector_help, - action="append_const", - dest="detectors_to_exclude", - const=detector_cls.ARGUMENT) - - for printer_cls in printer_classes: - printer_arg = '--printer-{}'.format(printer_cls.ARGUMENT) - printer_help = 'Print {}'.format(printer_cls.HELP) - parser.add_argument(printer_arg, - help=printer_help, - action="append_const", - dest="printers_to_run", - const=printer_cls.ARGUMENT) - # debugger command parser.add_argument('--debug', help=argparse.SUPPRESS, @@ -319,15 +340,44 @@ def parse_args(detector_classes, printer_classes): action="store_true", default=False) - return parser.parse_args() + parser.add_argument('--list-detectors', + help='List available detectors', + action='store_true', + default=False) + + parser.add_argument('--list-printers', + help='List available printers', + action='store_true', + default=False) + + if len(sys.argv) == 1: + parser.print_help(sys.stderr) + sys.exit(1) + + args = parser.parse_args() + + return args def choose_detectors(args, all_detector_classes): # If detectors are specified, run only these ones - if args.detectors_to_run: - return [d for d in all_detector_classes if d.ARGUMENT in args.detectors_to_run] - detectors_to_run = all_detector_classes + detectors_to_run = [] + detectors = {d.ARGUMENT: d for d in all_detector_classes} + + if args.detectors_to_run == 'all': + detectors_to_run = all_detector_classes + detectors_excluded = args.detectors_to_exclude.split(',') + for d in detectors: + if d in detectors_excluded: + detectors_to_run.remove(detectors[d]) + else: + for d in args.detectors_to_run.split(','): + if d in detectors: + detectors_to_run.append(detectors[d]) + else: + raise Exception('Error: {} is not a detector'.format(d)) + return detectors_to_run if args.exclude_informational: detectors_to_run = [d for d in detectors_to_run if @@ -348,11 +398,18 @@ def choose_detectors(args, all_detector_classes): def choose_printers(args, all_printer_classes): - # by default, dont run any printer printers_to_run = [] - if args.printers_to_run: - printers_to_run = [p for p in all_printer_classes if - p.ARGUMENT in args.printers_to_run] + + # disable default printer + if args.printers_to_run == 'none': + return printers_to_run + + printers = {p.ARGUMENT: p for p in all_printer_classes} + for p in args.printers_to_run.split(','): + if p in printers: + printers_to_run.append(printers[p]) + else: + raise Exception('Error: {} is not a printer'.format(p)) return printers_to_run From d3aa4e069b9b10635a9377d9350863de5b72355c Mon Sep 17 00:00:00 2001 From: Josselin Date: Fri, 26 Oct 2018 10:28:50 +0100 Subject: [PATCH 5/7] - Group commands - Use PrettyTable for list-detectors/list-printers - Remove default printer - Remove --output --- slither/__main__.py | 257 ++++++++++++++++------------------ slither/utils/command_line.py | 67 +++++++++ 2 files changed, 187 insertions(+), 137 deletions(-) create mode 100644 slither/utils/command_line.py diff --git a/slither/__main__.py b/slither/__main__.py index 72d83a331..b2b075a35 100644 --- a/slither/__main__.py +++ b/slither/__main__.py @@ -11,59 +11,15 @@ import traceback from pkg_resources import iter_entry_points, require from slither.detectors.abstract_detector import (AbstractDetector, - DetectorClassification, - classification_txt) + DetectorClassification) from slither.printers.abstract_printer import AbstractPrinter from slither.slither import Slither +from slither.utils.command_line import output_to_markdown, output_detectors, output_printers logging.basicConfig() logger = logging.getLogger("Slither") -def output_detectors(detector_classes): - """ - Pretty print of the detectors to README.md - """ - detectors_list = [] - for detector in detector_classes: - argument = detector.ARGUMENT - # dont show the backdoor example - help_info = detector.HELP - impact = detector.IMPACT - confidence = classification_txt[detector.CONFIDENCE] - detectors_list.append((argument, help_info, impact, confidence)) - - # Sort by impact, confidence, and name - detectors_list = sorted(detectors_list, key=lambda element: (element[2], element[3], element[0])) - idx = 1 - for (argument, help_info, impact, confidence) in detectors_list: - print('{} | `{}` | {} | {} | {}'.format(idx, - argument, - help_info, - classification_txt[impact], - confidence)) - idx = idx + 1 - - -def output_printers(printer_classes): - """ - Pretty print of the printers to README.md - """ - printers_list = [] - for printer in printer_classes: - argument = printer.ARGUMENT - help_info = printer.HELP - printers_list.append((argument, help_info)) - - print(printers_list) - # Sort by name - printers_list = sorted(printers_list, key=lambda element: (element[0])) - idx = 1 - for (argument, help_info) in printers_list: - print('{} | `{}` | {} '.format(idx, argument, help_info)) - idx = idx + 1 - - def process(filename, args, detector_classes, printer_classes): """ The core high-level code for running Slither static analysis. @@ -105,7 +61,7 @@ def exit(results): sys.exit(len(results)) -def main(): +def get_detectors_and_printers(): """ NOTE: This contains just a few detectors and printers that we made public. """ @@ -171,6 +127,11 @@ def main(): detectors += list(plugin_detectors) printers += list(plugin_printers) + return detectors, printers + +def main(): + detectors, printers = get_detectors_and_printers() + main_impl(all_detector_classes=detectors, all_printer_classes=printers) @@ -181,16 +142,11 @@ def main_impl(all_detector_classes, all_printer_classes): """ args = parse_args(all_detector_classes, all_printer_classes) - if args.list_detectors: - output_detectors(all_detector_classes) - return - - if args.list_printers: - output_printers(all_printer_classes) - return - - detector_classes = choose_detectors(args, all_detector_classes) printer_classes = choose_printers(args, all_printer_classes) + if printer_classes: + detector_classes = [] + else: + detector_classes = choose_detectors(args, all_detector_classes) default_log = logging.INFO if not args.debug else logging.DEBUG @@ -250,84 +206,100 @@ def parse_args(detector_classes, printer_classes): usage="slither.py contract.sol [flag]") parser.add_argument('filename', - help='contract.sol file') + help='contract.sol') parser.add_argument('--version', help='displays the current version', version=require('slither-analyzer')[0].version, action='version') - parser.add_argument('--solc', - help='solc path', - action='store', - default='solc') - - parser.add_argument('--detectors', - help='Comma-separated list of detectors, defaults to all, ' - 'available detectors: {}'.format(', '.join(d.ARGUMENT for d in detector_classes)), - action='store', - dest='detectors_to_run', - default='all') - - parser.add_argument('--printers', - help='Comma-separated list fo contract information printers, ' - 'defaults to contract-summary and can be disabled by using \'none\', ' - 'available printers: {}'.format(', '.join(d.ARGUMENT for d in printer_classes)), - action='store', - dest='printers_to_run', - default='contract-summary') - - parser.add_argument('--output', - help='Define output encoding', - action='store', - choices=['stdout', 'json'], - default='stdout') - - parser.add_argument('--exclude-detectors', - help='Comma-separated list of detectors that should be excluded', - action='store', - dest='detectors_to_exclude', - default='') - - parser.add_argument('--solc-args', - help='Add custom solc arguments. Example: --solc-args "--allow-path /tmp --evm-version byzantium".', - action='store', - default=None) - - parser.add_argument('--disable-solc-warnings', - help='Disable solc warnings', - action='store_true', - default=False) + group_detector = parser.add_argument_group('Detectors') + group_printer = parser.add_argument_group('Printers') + group_solc = parser.add_argument_group('Solc options') + group_misc = parser.add_argument_group('Additional option') + + group_detector.add_argument('--detectors', + help='Comma-separated list of detectors, defaults to all, ' + 'available detectors: {}'.format( + ', '.join(d.ARGUMENT for d in detector_classes)), + action='store', + dest='detectors_to_run', + default='all') + + group_printer.add_argument('--printers', + help='Comma-separated list fo contract information printers, ' + 'available printers: {}'.format( + ', '.join(d.ARGUMENT for d in printer_classes)), + action='store', + dest='printers_to_run', + default='') + + group_detector.add_argument('--list-detectors', + help='List available detectors', + action=ListDetectors, + nargs=0, + default=False) + + group_printer.add_argument('--list-printers', + help='List available printers', + action=ListPrinters, + nargs=0, + default=False) + + + group_detector.add_argument('--exclude-detectors', + help='Comma-separated list of detectors that should be excluded', + action='store', + dest='detectors_to_exclude', + default='') + + group_detector.add_argument('--exclude-informational', + help='Exclude informational impact analyses', + action='store_true', + default=False) + + group_detector.add_argument('--exclude-low', + help='Exclude low impact analyses', + action='store_true', + default=False) + + group_detector.add_argument('--exclude-medium', + help='Exclude medium impact analyses', + action='store_true', + default=False) + + group_detector.add_argument('--exclude-high', + help='Exclude high impact analyses', + action='store_true', + default=False) + + + group_solc.add_argument('--solc', + help='solc path', + action='store', + default='solc') + + group_solc.add_argument('--solc-args', + help='Add custom solc arguments. Example: --solc-args "--allow-path /tmp --evm-version byzantium".', + action='store', + default=None) + + group_solc.add_argument('--disable-solc-warnings', + help='Disable solc warnings', + action='store_true', + default=False) + + group_solc.add_argument('--solc-ast', + help='Provide the ast solc file', + action='store_true', + default=False) + + group_misc.add_argument('--json', + help='Export results as JSON', + action='store', + default=None) - parser.add_argument('--solc-ast', - help='Provide the ast solc file', - action='store_true', - default=False) - parser.add_argument('--json', - help='Export results as JSON', - action='store', - default=None) - - parser.add_argument('--exclude-informational', - help='Exclude informational impact analyses', - action='store_true', - default=False) - - parser.add_argument('--exclude-low', - help='Exclude low impact analyses', - action='store_true', - default=False) - - parser.add_argument('--exclude-medium', - help='Exclude medium impact analyses', - action='store_true', - default=False) - - parser.add_argument('--exclude-high', - help='Exclude high impact analyses', - action='store_true', - default=False) # debugger command parser.add_argument('--debug', @@ -337,18 +309,10 @@ def parse_args(detector_classes, printer_classes): parser.add_argument('--markdown', help=argparse.SUPPRESS, - action="store_true", + action=OutputMarkdown, default=False) - parser.add_argument('--list-detectors', - help='List available detectors', - action='store_true', - default=False) - parser.add_argument('--list-printers', - help='List available printers', - action='store_true', - default=False) if len(sys.argv) == 1: parser.print_help(sys.stderr) @@ -358,6 +322,25 @@ def parse_args(detector_classes, printer_classes): return args +class ListDetectors(argparse.Action): + def __call__(self, parser, *args, **kwargs): + detectors, _ = get_detectors_and_printers() + output_detectors(detectors) + parser.exit() + +class ListPrinters(argparse.Action): + def __call__(self, parser, *args, **kwargs): + _, printers = get_detectors_and_printers() + output_printers(printers) + parser.exit() + +class OutputMarkdown(argparse.Action): + def __call__(self, parser, *args, **kwargs): + detectors, _ = get_detectors_and_printers() + output_to_markdown(detectors) + parser.exit() + + def choose_detectors(args, all_detector_classes): # If detectors are specified, run only these ones @@ -401,8 +384,8 @@ def choose_printers(args, all_printer_classes): printers_to_run = [] # disable default printer - if args.printers_to_run == 'none': - return printers_to_run + if args.printers_to_run == '': + return [] printers = {p.ARGUMENT: p for p in all_printer_classes} for p in args.printers_to_run.split(','): diff --git a/slither/utils/command_line.py b/slither/utils/command_line.py new file mode 100644 index 000000000..90a933fc3 --- /dev/null +++ b/slither/utils/command_line.py @@ -0,0 +1,67 @@ +from prettytable import PrettyTable + +from slither.detectors.abstract_detector import classification_txt + +def output_to_markdown(detector_classes): + detectors_list = [] + for detector in detector_classes: + argument = detector.ARGUMENT + # dont show the backdoor example + if argument == 'backdoor': + continue + help_info = detector.HELP + impact = detector.IMPACT + confidence = classification_txt[detector.CONFIDENCE] + detectors_list.append((argument, help_info, impact, confidence)) + + # Sort by impact, confidence, and name + detectors_list = sorted(detectors_list, key=lambda element: (element[2], element[3], element[0])) + idx = 1 + for (argument, help_info, impact, confidence) in detectors_list: + print('{} | `{}` | {} | {} | {}'.format(idx, + argument, + help_info, + classification_txt[impact], + confidence)) + idx = idx + 1 + + +def output_detectors(detector_classes): + detectors_list = [] + for detector in detector_classes: + argument = detector.ARGUMENT + help_info = detector.HELP + impact = detector.IMPACT + confidence = classification_txt[detector.CONFIDENCE] + detectors_list.append((argument, help_info, impact, confidence)) + table = PrettyTable(["Num", + "Check", + "What it Detects", + "Impact", + "Confidence"]) + + # Sort by impact, confidence, and name + detectors_list = sorted(detectors_list, key=lambda element: (element[2], element[3], element[0])) + idx = 1 + for (argument, help_info, impact, confidence) in detectors_list: + table.add_row([idx, argument, help_info, classification_txt[impact], confidence]) + idx = idx + 1 + print(table) + +def output_printers(printer_classes): + printers_list = [] + for printer in printer_classes: + argument = printer.ARGUMENT + help_info = printer.HELP + printers_list.append((argument, help_info)) + table = PrettyTable(["Num", + "Printer", + "What it Does"]) + + # Sort by impact, confidence, and name + printers_list = sorted(printers_list, key=lambda element: (element[0])) + idx = 1 + for (argument, help_info) in printers_list: + table.add_row([idx, argument, help_info]) + idx = idx + 1 + print(table) From b664ab0b2c0b9ca1a0364f8695bdc3eae7106eeb Mon Sep 17 00:00:00 2001 From: Feist Josselin Date: Fri, 26 Oct 2018 10:37:00 +0100 Subject: [PATCH 6/7] Update README.md --- README.md | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index dd26c2e98..b42ab440f 100644 --- a/README.md +++ b/README.md @@ -32,25 +32,10 @@ If Slither is run on a directory, it will run on every `.sol` file of the direct * `--disable-solc-warnings`: Do not print solc warnings * `--solc-ast`: Use the solc AST file as input (`solc file.sol --ast-json > file.ast.json`) * `--json FILE`: Export results as JSON -* `--exclude-name`: Excludes the detector `name` from analysis - -### Printers - -By default, the `contract-summary` printer is used. Use --printers comma-separated list of printers, -or `none` to disable the default printer. - -Num | Printer | Description ---- | --- | --- -1 | `contract-summary` | a summary of the contract -2 | `function-summary` | the summary of the functions -3 | `inheritance` | the inheritance relation between contracts -4 | `inheritance-graph` | the inheritance graph -5 | `slithir` | the slithIR -6 | `vars-and-auth` | the state variables written and the authorization of the functions ## Detectors -By default, all the detectors are run. Use --detectors comma-separated list of detectors to run. +By default, all the detectors are run. Use `--detectors` comma-separated list of detectors to run. Num | Detector | What it Detects | Impact | Confidence --- | --- | --- | --- | --- @@ -72,6 +57,20 @@ Num | Detector | What it Detects | Impact | Confidence [Contact us](https://www.trailofbits.com/contact/) to get access to additional detectors. +### Printers + +Use `--printers` comma-separated list of printers. + +Num | Printer | Description +--- | --- | --- +1 | `contract-summary` | a summary of the contract +2 | `function-summary` | the summary of the functions +3 | `inheritance` | the inheritance relation between contracts +4 | `inheritance-graph` | the inheritance graph +5 | `slithir` | the slithIR +6 | `vars-and-auth` | the state variables written and the authorization of the functions + + ## How to install Slither requires Python 3.6+ and [solc](https://github.com/ethereum/solidity/), the Solidity compiler. From 5e064be1e65176382612dc6934f24ae86890148c Mon Sep 17 00:00:00 2001 From: Josselin Date: Fri, 26 Oct 2018 10:53:34 +0100 Subject: [PATCH 7/7] Update readme Add printer to markdown generation --- README.md | 44 +++++++++++++++++------------------ slither/__main__.py | 5 ++-- slither/utils/command_line.py | 15 +++++++++++- 3 files changed, 39 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index b42ab440f..e5cfa6dd9 100644 --- a/README.md +++ b/README.md @@ -39,21 +39,21 @@ By default, all the detectors are run. Use `--detectors` comma-separated list of Num | Detector | What it Detects | Impact | Confidence --- | --- | --- | --- | --- -1 | `backdoor` | Function named backdoor (detector example) | High | High -2 | `suicidal` | Suicidal functions | High | High -3 | `uninitialized-state` | Uninitialized state variables | High | High -4 | `uninitialized-storage` | Uninitialized storage variables | High | High -5 | `arbitrary-send` | Functions that send ether to an arbitrary destination | High | Medium -6 | `reentrancy` | Reentrancy vulnerabilities | High | Medium -7 | `locked-ether` | Contracts that lock ether | Medium | High -8 | `tx-origin` | Dangerous usage of `tx.origin` | Medium | Medium -9 | `assembly` | Assembly usage | Informational | High -10 | `const-candidates-state` | State variables that could be declared constant | Informational | High -11 | `low-level-calls` | Low level calls | Informational | High -12 | `naming-convention` | Conformance to Solidity naming conventions | Informational | High -13 | `pragma` | If different pragma directives are used | Informational | High -14 | `solc-version` | If an old version of Solidity used (<0.4.23) | Informational | High -15 | `unused-state` | Unused state variables | Informational | High +1 | `suicidal` | Suicidal functions | High | High +2 | `uninitialized-state` | Uninitialized state variables | High | High +3 | `uninitialized-storage` | Uninitialized storage variables | High | High +4 | `arbitrary-send` | Functions that send ether to an arbitrary destination | High | Medium +5 | `reentrancy` | Reentrancy vulnerabilities | High | Medium +6 | `locked-ether` | Contracts that lock ether | Medium | High +7 | `tx-origin` | Dangerous usage of `tx.origin` | Medium | Medium +8 | `assembly` | Assembly usage | Informational | High +9 | `const-candidates-state` | State variables that could be declared constant | Informational | High +10 | `low-level-calls` | Low level calls | Informational | High +11 | `naming-convention` | Conformance to Solidity naming conventions | Informational | High +12 | `pragma` | If different pragma directives are used | Informational | High +13 | `solc-version` | If an old version of Solidity used (<0.4.23) | Informational | High +14 | `unused-state` | Unused state variables | Informational | High + [Contact us](https://www.trailofbits.com/contact/) to get access to additional detectors. @@ -63,13 +63,13 @@ Use `--printers` comma-separated list of printers. Num | Printer | Description --- | --- | --- -1 | `contract-summary` | a summary of the contract -2 | `function-summary` | the summary of the functions -3 | `inheritance` | the inheritance relation between contracts -4 | `inheritance-graph` | the inheritance graph -5 | `slithir` | the slithIR -6 | `vars-and-auth` | the state variables written and the authorization of the functions - +1 | `call-graph` | the call graph +2 | `contract-summary` | a summary of the contract +3 | `function-summary` | the summary of the functions +4 | `inheritance` | the inheritance relation between contracts +5 | `inheritance-graph` | the inheritance graph +6 | `slithir` | the slithIR +7 | `vars-and-auth` | the state variables written and the authorization of the functions ## How to install diff --git a/slither/__main__.py b/slither/__main__.py index 183727ec7..116747c88 100644 --- a/slither/__main__.py +++ b/slither/__main__.py @@ -345,6 +345,7 @@ def parse_args(detector_classes, printer_classes): parser.add_argument('--markdown', help=argparse.SUPPRESS, action=OutputMarkdown, + nargs=0, default=False) parser.add_argument('--compact-ast', @@ -374,8 +375,8 @@ class ListPrinters(argparse.Action): class OutputMarkdown(argparse.Action): def __call__(self, parser, *args, **kwargs): - detectors, _ = get_detectors_and_printers() - output_to_markdown(detectors) + detectors, printers = get_detectors_and_printers() + output_to_markdown(detectors, printers) parser.exit() diff --git a/slither/utils/command_line.py b/slither/utils/command_line.py index 90a933fc3..8ef40df9f 100644 --- a/slither/utils/command_line.py +++ b/slither/utils/command_line.py @@ -2,7 +2,7 @@ from prettytable import PrettyTable from slither.detectors.abstract_detector import classification_txt -def output_to_markdown(detector_classes): +def output_to_markdown(detector_classes, printer_classes): detectors_list = [] for detector in detector_classes: argument = detector.ARGUMENT @@ -25,6 +25,19 @@ def output_to_markdown(detector_classes): confidence)) idx = idx + 1 + print() + printers_list = [] + for printer in printer_classes: + argument = printer.ARGUMENT + help_info = printer.HELP + printers_list.append((argument, help_info)) + + # Sort by impact, confidence, and name + printers_list = sorted(printers_list, key=lambda element: (element[0])) + idx = 1 + for (argument, help_info) in printers_list: + print('{} | `{}` | {}'.format(idx, argument, help_info)) + idx = idx + 1 def output_detectors(detector_classes): detectors_list = []