Merge branch 'dev' into feature/rewrite-uninitialized-state-with-ir

pull/49/head
Feist Josselin 6 years ago committed by GitHub
commit c6afe6a8d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 30
      README.md
  2. 34
      examples/printers/call_graph.sol
  3. 28
      examples/printers/call_graph.sol.dot
  4. BIN
      examples/printers/call_graph.sol.dot.png
  5. 38
      scripts/travis_test.sh
  6. 294
      slither/__main__.py
  7. 0
      slither/printers/call/__init__.py
  8. 155
      slither/printers/call/call_graph.py
  9. 80
      slither/utils/command_line.py

@ -32,21 +32,12 @@ 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
* `--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
## Detectors
## Checks available
By default, all the detectors are run. Use `--detectors` comma-separated list of detectors to run.
By default, all the checks are run. Use --detect-_name-of-check_ to run one check at a time.
Num | Check | What it Detects | Impact | Confidence
Num | Detector | What it Detects | Impact | Confidence
--- | --- | --- | --- | ---
1 | `suicidal` | Suicidal functions | High | High
2 | `uninitialized-state` | Uninitialized state variables | High | High
@ -63,8 +54,23 @@ Num | Check | What it Detects | Impact | Confidence
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.
### Printers
Use `--printers` comma-separated list of printers.
Num | Printer | Description
--- | --- | ---
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
Slither requires Python 3.6+ and [solc](https://github.com/ethereum/solidity/), the Solidity compiler.

@ -0,0 +1,34 @@
library Library {
function library_func() {
}
}
contract ContractA {
uint256 public val = 0;
function my_func_a() {
keccak256(0);
Library.library_func();
}
}
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(){
a.val();
}
}

@ -0,0 +1,28 @@
strict digraph {
subgraph cluster_5_Library {
label = "Library"
"5_library_func" [label="library_func"]
}
subgraph cluster_22_ContractA {
label = "ContractA"
"22_my_func_a" [label="my_func_a"]
"22_val" [label="val"]
}
subgraph cluster_63_ContractB {
label = "ContractB"
"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()"
"22_my_func_a" -> "keccak256()"
}
"22_my_func_a" -> "5_library_func"
"63_my_func_b" -> "22_my_func_a"
"63_my_second_func_b" -> "22_val"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

@ -2,35 +2,35 @@
### Test Detectors
# test_slither file.sol --detect-detectors number_results
# test_slither file.sol detectors number_results
test_slither(){
slither "$1" --disable-solc-warnings "$2"
slither "$1" --disable-solc-warnings --detectors "$2"
if [ $? -ne "$3" ]; then
exit 1
fi
slither "$1" --disable-solc-warnings "$2" --compact-ast
slither "$1" --disable-solc-warnings --detectors "$2" --compact-ast
if [ $? -ne "$3" ]; then
exit 1
fi
}
test_slither tests/uninitialized.sol "--detect-uninitialized-state" 3
test_slither tests/backdoor.sol "--detect-backdoor" 1
test_slither tests/backdoor.sol "--detect-suicidal" 1
test_slither tests/pragma.0.4.24.sol "--detect-pragma" 1
test_slither tests/old_solc.sol.json "--detect-solc-version" 1
test_slither tests/reentrancy.sol "--detect-reentrancy" 1
test_slither tests/uninitialized_storage_pointer.sol "--detect-uninitialized-storage" 1
test_slither tests/tx_origin.sol "--detect-tx-origin" 2
test_slither tests/unused_state.sol "--detect-unused-state" 1
test_slither tests/locked_ether.sol "--detect-locked-ether" 1
test_slither tests/arbitrary_send.sol "--detect-arbitrary-send" 2
test_slither tests/inline_assembly_contract.sol "--detect-assembly" 1
test_slither tests/inline_assembly_library.sol "--detect-assembly" 2
test_slither tests/naming_convention.sol "--detect-naming-convention" 10
test_slither tests/low_level_calls.sol "--detect-low-level-calls" 1
test_slither tests/const_state_variables.sol "--detect-const-candidates-state" 2
test_slither tests/uninitialized.sol "uninitialized-state" 3
test_slither tests/backdoor.sol "backdoor" 1
test_slither tests/backdoor.sol "suicidal" 1
test_slither tests/pragma.0.4.24.sol "pragma" 1
test_slither tests/old_solc.sol.json "solc-version" 1
test_slither tests/reentrancy.sol "reentrancy" 1
test_slither tests/uninitialized_storage_pointer.sol "uninitialized-storage" 1
test_slither tests/tx_origin.sol "tx-origin" 2
test_slither tests/unused_state.sol "unused-state" 1
test_slither tests/locked_ether.sol "locked-ether" 1
test_slither tests/arbitrary_send.sol "arbitrary-send" 2
test_slither tests/inline_assembly_contract.sol "assembly" 1
test_slither tests/inline_assembly_library.sol "assembly" 2
test_slither tests/naming_convention.sol "naming-convention" 10
test_slither tests/low_level_calls.sol "low-level-calls" 1
test_slither tests/const_state_variables.sol "const-candidates-state" 2
### Test scripts

@ -8,43 +8,18 @@ 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,
classification_txt)
DetectorClassification)
from slither.printers.abstract_printer import AbstractPrinter
from slither.slither import Slither
from slither.utils.colors import red
from slither.utils.command_line import output_to_markdown, output_detectors, output_printers
logging.basicConfig()
logger = logging.getLogger("Slither")
def output_to_markdown(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
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 process(filename, args, detector_classes, printer_classes):
"""
@ -71,15 +46,13 @@ def _process(slither, 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
@ -117,7 +90,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.
"""
@ -157,6 +130,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 +138,7 @@ def main():
ContractSummary,
PrinterInheritance,
PrinterInheritanceGraph,
PrinterCallGraph,
PrinterWrittenVariablesAndAuthorization,
PrinterSlithIR]
@ -183,6 +158,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)
@ -193,12 +173,11 @@ 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)
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
@ -237,9 +216,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:
@ -262,84 +238,103 @@ 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('--solc',
help='solc path',
action='store',
default='solc')
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)
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)
help='contract.sol')
parser.add_argument('--version',
help='displays the current version',
version=require('slither-analyzer')[0].version,
action='version')
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('--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)
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',
@ -349,7 +344,8 @@ def parse_args(detector_classes, printer_classes):
parser.add_argument('--markdown',
help=argparse.SUPPRESS,
action="store_true",
action=OutputMarkdown,
nargs=0,
default=False)
parser.add_argument('--compact-ast',
@ -357,15 +353,52 @@ def parse_args(detector_classes, printer_classes):
action='store_true',
default=False)
return parser.parse_args()
if len(sys.argv) == 1:
parser.print_help(sys.stderr)
sys.exit(1)
args = parser.parse_args()
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, printers = get_detectors_and_printers()
output_to_markdown(detectors, printers)
parser.exit()
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
@ -386,11 +419,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 == '':
return []
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

@ -0,0 +1,155 @@
"""
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
from slither.core.declarations.contract import Contract
from slither.core.expressions.member_access import MemberAccess
from slither.core.expressions.identifier import Identifier
from slither.core.variables.variable import Variable
from slither.core.solidity_types.user_defined_type import UserDefinedType
# return unique id for contract to use as subgraph name
def _contract_subgraph(contract):
return f'cluster_{contract.id}_{contract.name}'
# return unique id for contract function to use as node name
def _function_node(contract, function):
return f'{contract.id}_{function.name}'
# return unique id for solidity function to use as node name
def _solidity_function_node(solidity_function):
return f'{solidity_function.name}'
# return dot language string to add graph edge
def _edge(from_node, to_node):
return f'"{from_node}" -> "{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'
HELP = 'the call graph'
def __init__(self, slither, logger):
super(PrinterCallGraph, self).__init__(slither, logger)
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
self.external_calls = set() # external calls edges
self._process_contracts(slither.contracts)
def _process_contracts(self, contracts):
for contract in contracts:
for function in contract.functions:
self._process_function(contract, function)
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:
self._process_internal_call(contract, function, internal_call)
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):
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):
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 = []
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):
lines = []
lines.append('subgraph cluster_solidity {')
lines.append('label = "[Solidity]"')
lines.extend(self.solidity_functions)
lines.extend(self.solidity_calls)
lines.append('}')
return '\n'.join(lines)
def _render_external_calls(self):
return '\n'.join(self.external_calls)
def output(self, filename):
"""
Output the graph in filename
Args:
filename(string)
"""
if not filename.endswith('.dot'):
filename += '.dot'
self.info(f'Call Graph: {filename}')
with open(filename, 'w') as f:
f.write('\n'.join([
'strict digraph {',
self._render_internal_calls(),
self._render_solidity_calls(),
self._render_external_calls(),
'}',
]))

@ -0,0 +1,80 @@
from prettytable import PrettyTable
from slither.detectors.abstract_detector import classification_txt
def output_to_markdown(detector_classes, printer_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
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 = []
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)
Loading…
Cancel
Save