Merge pull request #172 from trailofbits/dev-usability

Improve usability
pull/179/head
Feist Josselin 6 years ago committed by GitHub
commit c6df387d7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      examples/printers/call_graph.sol.dot
  2. 185
      slither/__main__.py
  3. 163
      slither/core/slither_core.py
  4. 50
      slither/detectors/abstract_detector.py
  5. 4
      slither/detectors/all_detectors.py
  6. 4
      slither/detectors/attributes/const_functions.py
  7. 3
      slither/detectors/attributes/constant_pragma.py
  8. 3
      slither/detectors/attributes/incorrect_solc.py
  9. 3
      slither/detectors/attributes/locked_ether.py
  10. 5
      slither/detectors/examples/backdoor.py
  11. 5
      slither/detectors/functions/arbitrary_send.py
  12. 7
      slither/detectors/functions/external_function.py
  13. 4
      slither/detectors/functions/suicidal.py
  14. 15
      slither/detectors/naming_convention/naming_convention.py
  15. 5
      slither/detectors/operations/block_timestamp.py
  16. 8
      slither/detectors/operations/low_level_calls.py
  17. 3
      slither/detectors/operations/unused_return_values.py
  18. 2
      slither/detectors/reentrancy/reentrancy.py
  19. 5
      slither/detectors/reentrancy/reentrancy_benign.py
  20. 5
      slither/detectors/reentrancy/reentrancy_eth.py
  21. 5
      slither/detectors/reentrancy/reentrancy_read_before_write.py
  22. 3
      slither/detectors/shadowing/abstract.py
  23. 4
      slither/detectors/shadowing/builtin_symbols.py
  24. 4
      slither/detectors/shadowing/local.py
  25. 3
      slither/detectors/shadowing/state.py
  26. 7
      slither/detectors/statements/assembly.py
  27. 4
      slither/detectors/statements/calls_in_loop.py
  28. 3
      slither/detectors/statements/controlled_delegatecall.py
  29. 5
      slither/detectors/statements/incorrect_strict_equality.py
  30. 4
      slither/detectors/statements/tx_origin.py
  31. 3
      slither/detectors/variables/possible_const_state_variables.py
  32. 3
      slither/detectors/variables/uninitialized_local_variables.py
  33. 3
      slither/detectors/variables/uninitialized_state_variables.py
  34. 5
      slither/detectors/variables/uninitialized_storage_variables.py
  35. 6
      slither/detectors/variables/unused_state_variables.py
  36. 108
      slither/slither.py

@ -11,11 +11,11 @@ label = "ContractA"
subgraph cluster_63_ContractB {
label = "ContractB"
"63_my_second_func_b" [label="my_second_func_b"]
"63_my_func_b" [label="my_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"
"63_my_func_b" -> "63_my_second_func_b"
}
subgraph cluster_solidity {
label = "[Solidity]"

@ -18,7 +18,7 @@ from slither.detectors.abstract_detector import (AbstractDetector,
from slither.printers import all_printers
from slither.printers.abstract_printer import AbstractPrinter
from slither.slither import Slither
from slither.utils.colors import red, set_colorization_enabled
from slither.utils.colors import red, yellow, set_colorization_enabled
from slither.utils.command_line import (output_detectors,
output_detectors_json, output_printers,
output_to_markdown, output_wiki)
@ -43,7 +43,13 @@ def process(filename, args, detector_classes, printer_classes):
ast = '--ast-compact-json'
if args.legacy_ast:
ast = '--ast-json'
slither = Slither(filename, args.solc, args.disable_solc_warnings, args.solc_args, ast)
slither = Slither(filename,
solc=args.solc,
disable_solc_warnings=args.disable_solc_warnings,
solc_arguments=args.solc_args,
ast_format=ast,
filter_paths=parse_filter_paths(args),
triage_mode=args.triage_mode)
return _process(slither, detector_classes, printer_classes)
@ -70,35 +76,39 @@ def _process(slither, detector_classes, printer_classes):
return results, analyzed_contracts_count
def process_truffle(dirname, args, detector_classes, printer_classes):
cmd = ['truffle', 'compile']
if args.truffle_version:
cmd = ['npx',args.truffle_version,'compile']
elif os.path.isfile('package.json'):
with open('package.json') as f:
package = json.load(f)
if 'devDependencies' in package:
if 'truffle' in package['devDependencies']:
version = package['devDependencies']['truffle']
if version.startswith('^'):
version = version[1:]
truffle_version = 'truffle@{}'.format(version)
cmd = ['npx', truffle_version,'compile']
logger.info("'{}' running (use --truffle-version truffle@x.x.x to use specific version)".format(' '.join(cmd)))
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = process.communicate()
stdout, stderr = stdout.decode(), stderr.decode() # convert bytestrings to unicode strings
logger.info(stdout)
if stderr:
logger.error(stderr)
if not args.ignore_truffle_compile:
cmd = ['truffle', 'compile']
if args.truffle_version:
cmd = ['npx',args.truffle_version,'compile']
elif os.path.isfile('package.json'):
with open('package.json') as f:
package = json.load(f)
if 'devDependencies' in package:
if 'truffle' in package['devDependencies']:
version = package['devDependencies']['truffle']
if version.startswith('^'):
version = version[1:]
truffle_version = 'truffle@{}'.format(version)
cmd = ['npx', truffle_version,'compile']
logger.info("'{}' running (use --truffle-version truffle@x.x.x to use specific version)".format(' '.join(cmd)))
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = process.communicate()
stdout, stderr = stdout.decode(), stderr.decode() # convert bytestrings to unicode strings
logger.info(stdout)
if stderr:
logger.error(stderr)
slither = Slither(dirname,
args.solc,
args.disable_solc_warnings,
args.solc_args,
is_truffle=True)
solc=args.solc,
disable_solc_warnings=args.disable_solc_warnings,
solc_arguments=args.solc_args,
is_truffle=True,
filter_paths=parse_filter_paths(args),
triage_mode=args.triage_mode)
return _process(slither, detector_classes, printer_classes)
@ -110,7 +120,13 @@ def process_files(filenames, args, detector_classes, printer_classes):
contract_loaded = json.load(f)
all_contracts.append(contract_loaded['ast'])
slither = Slither(all_contracts, args.solc, args.disable_solc_warnings, args.solc_args)
slither = Slither(all_contracts,
solc=args.solc,
disable_solc_warnings=args.disable_solc_warnings,
solc_arguments=args.solc_args,
filter_paths=parse_filter_paths(args),
triage_mode=args.triage_mode)
return _process(slither, detector_classes, printer_classes)
# endregion
@ -121,8 +137,11 @@ def process_files(filenames, args, detector_classes, printer_classes):
###################################################################################
def output_json(results, filename):
with open(filename, 'w', encoding='utf8') as f:
json.dump(results, f)
if os.path.isfile(filename):
logger.info(yellow(f'{filename} exists already, the overwrite is prevented'))
else:
with open(filename, 'w', encoding='utf8') as f:
json.dump(results, f)
# endregion
###################################################################################
@ -181,10 +200,11 @@ def choose_detectors(args, 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])
if args.detectors_to_exclude:
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:
@ -219,7 +239,7 @@ def choose_printers(args, all_printer_classes):
printers_to_run = []
# disable default printer
if args.printers_to_run == '':
if args.printers_to_run is None:
return []
printers = {p.ARGUMENT: p for p in all_printer_classes}
@ -237,6 +257,31 @@ def choose_printers(args, all_printer_classes):
###################################################################################
###################################################################################
def parse_filter_paths(args):
if args.filter_paths:
return args.filter_paths.split(',')
return []
# Those are the flags shared by the command line and the config file
defaults_flag_in_config = {
'detectors_to_run': 'all',
'printers_to_run': None,
'detectors_to_exclude': None,
'exclude_informational': False,
'exclude_low': False,
'exclude_medium': False,
'exclude_high': False,
'solc': 'solc',
'solc_args': None,
'disable_solc_warnings': False,
'json': None,
'truffle_version': None,
'disable_color': False,
'filter_paths': None,
'ignore_truffle_compile': False,
'legacy_ast': False
}
def parse_args(detector_classes, printer_classes):
parser = argparse.ArgumentParser(description='Slither',
usage="slither.py contract.sol [flag]")
@ -260,7 +305,7 @@ def parse_args(detector_classes, printer_classes):
', '.join(d.ARGUMENT for d in detector_classes)),
action='store',
dest='detectors_to_run',
default='all')
default=defaults_flag_in_config['detectors_to_run'])
group_printer.add_argument('--print',
help='Comma-separated list fo contract information printers, '
@ -268,7 +313,7 @@ def parse_args(detector_classes, printer_classes):
', '.join(d.ARGUMENT for d in printer_classes)),
action='store',
dest='printers_to_run',
default='')
default=defaults_flag_in_config['printers_to_run'])
group_detector.add_argument('--list-detectors',
help='List available detectors',
@ -282,48 +327,46 @@ def parse_args(detector_classes, printer_classes):
nargs=0,
default=False)
group_detector.add_argument('--exclude',
help='Comma-separated list of detectors that should be excluded',
action='store',
dest='detectors_to_exclude',
default='')
default=defaults_flag_in_config['detectors_to_exclude'])
group_detector.add_argument('--exclude-informational',
help='Exclude informational impact analyses',
action='store_true',
default=False)
default=defaults_flag_in_config['exclude_informational'])
group_detector.add_argument('--exclude-low',
help='Exclude low impact analyses',
action='store_true',
default=False)
default=defaults_flag_in_config['exclude_low'])
group_detector.add_argument('--exclude-medium',
help='Exclude medium impact analyses',
action='store_true',
default=False)
default=defaults_flag_in_config['exclude_medium'])
group_detector.add_argument('--exclude-high',
help='Exclude high impact analyses',
action='store_true',
default=False)
default=defaults_flag_in_config['exclude_high'])
group_solc.add_argument('--solc',
help='solc path',
action='store',
default='solc')
default=defaults_flag_in_config['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)
default=defaults_flag_in_config['solc_args'])
group_solc.add_argument('--disable-solc-warnings',
help='Disable solc warnings',
action='store_true',
default=False)
default=defaults_flag_in_config['disable_solc_warnings'])
group_solc.add_argument('--solc-ast',
help='Provide the ast solc file',
@ -333,17 +376,40 @@ def parse_args(detector_classes, printer_classes):
group_misc.add_argument('--json',
help='Export results as JSON',
action='store',
default=None)
default=defaults_flag_in_config['json'])
group_misc.add_argument('--truffle-version',
help='Use a local Truffle version (with npx)',
action='store',
default=False)
default=defaults_flag_in_config['truffle_version'])
group_misc.add_argument('--disable-color',
help='Disable output colorization',
action='store_true',
default=False)
default=defaults_flag_in_config['disable_color'])
group_misc.add_argument('--filter-paths',
help='Comma-separated list of paths for which results will be excluded',
action='store',
dest='filter_paths',
default=defaults_flag_in_config['filter_paths'])
group_misc.add_argument('--ignore-truffle-compile',
help='Do not run truffle compile',
action='store_true',
dest='ignore_truffle_compile',
default=defaults_flag_in_config['ignore_truffle_compile'])
group_misc.add_argument('--triage-mode',
help='Run triage mode (save results in slither.db.json)',
action='store_true',
dest='triage_mode',
default=False)
group_misc.add_argument('--config-file',
help='Provide a config file (default: slither.config.json)',
action='store',
dest='config_file',
default='slither.config.json')
# debugger command
parser.add_argument('--debug',
@ -370,7 +436,7 @@ def parse_args(detector_classes, printer_classes):
parser.add_argument('--legacy-ast',
help=argparse.SUPPRESS,
action='store_true',
default=False)
default=defaults_flag_in_config['legacy_ast'])
# if the json is splitted in different files
parser.add_argument('--splitted',
@ -384,6 +450,19 @@ def parse_args(detector_classes, printer_classes):
args = parser.parse_args()
if os.path.isfile(args.config_file):
try:
with open(args.config_file) as f:
config = json.load(f)
for key, elem in config.items():
if key not in defaults_flag_in_config:
logger.info(yellow('{} has an unknown key: {} : {}'.format(args.config_file, key, elem)))
continue
if getattr(args, key) == defaults_flag_in_config[key]:
setattr(args, key, elem)
except json.decoder.JSONDecodeError as e:
logger.error(red('Impossible to read {}, please check the file {}'.format(args.config_file, e)))
return args
class ListDetectors(argparse.Action):

@ -2,8 +2,14 @@
Main module
"""
import os
import logging
import json
from slither.core.context.context import Context
from slither.slithir.operations import InternalCall
from slither.utils.colors import red
logger = logging.getLogger("Slither")
logging.basicConfig()
class Slither(Context):
"""
@ -22,10 +28,62 @@ class Slither(Context):
self._all_functions = set()
self._all_modifiers = set()
self._previous_results_filename = 'slither.db.json'
self._results_to_hide = []
self._previous_results = []
self._paths_to_filter = set()
###################################################################################
###################################################################################
# region Source code
###################################################################################
###################################################################################
@property
def source_code(self):
""" {filename: source_code}: source code """
return self._raw_source_code
@property
def source_units(self):
return self._source_units
@property
def filename(self):
"""str: Filename."""
return self._filename
# endregion
###################################################################################
###################################################################################
# region Pragma attributes
###################################################################################
###################################################################################
@property
def solc_version(self):
"""str: Solidity version."""
return self._solc_version
@property
def pragma_directives(self):
""" list(list(str)): Pragma directives. Example [['solidity', '^', '0.4', '.24']]"""
return self._pragma_directives
@property
def import_directives(self):
""" list(str): Import directives"""
return self._import_directives
# endregion
###################################################################################
###################################################################################
# region Contracts
###################################################################################
###################################################################################
@property
def contracts(self):
"""list(Contract): List of contracts."""
@ -42,6 +100,23 @@ class Slither(Context):
"""list(dict(str: Contract): List of contracts as dict: name -> Contract."""
return self._contracts
def get_contract_from_name(self, contract_name):
"""
Return a contract from a name
Args:
contract_name (str): name of the contract
Returns:
Contract
"""
return next((c for c in self.contracts if c.name == contract_name), None)
# endregion
###################################################################################
###################################################################################
# region Functions and modifiers
###################################################################################
###################################################################################
@property
def functions(self):
return list(self._all_functions)
@ -60,31 +135,6 @@ class Slither(Context):
def functions_and_modifiers(self):
return self.functions + self.modifiers
@property
def filename(self):
"""str: Filename."""
return self._filename
@property
def solc_version(self):
"""str: Solidity version."""
return self._solc_version
@property
def pragma_directives(self):
""" list(list(str)): Pragma directives. Example [['solidity', '^', '0.4', '.24']]"""
return self._pragma_directives
@property
def import_directives(self):
""" list(str): Import directives"""
return self._import_directives
@property
def source_code(self):
""" {filename: source_code}: source code """
return self._raw_source_code
def _propagate_function_calls(self):
for f in self.functions_and_modifiers:
for node in f.nodes:
@ -92,15 +142,13 @@ class Slither(Context):
if isinstance(ir, InternalCall):
ir.function.add_reachable_from_node(node, ir)
def get_contract_from_name(self, contract_name):
"""
Return a contract from a name
Args:
contract_name (str): name of the contract
Returns:
Contract
"""
return next((c for c in self.contracts if c.name == contract_name), None)
# endregion
###################################################################################
###################################################################################
# region Export
###################################################################################
###################################################################################
def print_functions(self, d):
"""
@ -109,3 +157,50 @@ class Slither(Context):
for c in self.contracts:
for f in c.functions:
f.cfg_to_dot(os.path.join(d, '{}.{}.dot'.format(c.name, f.name)))
# endregion
###################################################################################
###################################################################################
# region Filtering results
###################################################################################
###################################################################################
def valid_result(self, r):
'''
Check if the result is valid
A result is invalid if:
- All its source paths belong to the source path filtered
- Or a similar result was reported and saved during a previous run
'''
if r['elements'] and all((any(path in elem['source_mapping']['filename'] for path in self._paths_to_filter) for elem in r['elements'])):
return False
return not r['description'] in [pr['description'] for pr in self._previous_results]
def load_previous_results(self):
filename = self._previous_results_filename
try:
if os.path.isfile(filename):
with open(filename) as f:
self._previous_results = json.load(f)
except json.decoder.JSONDecodeError:
logger.error(red('Impossible to decode {}. Consider removing the file'.format(filename)))
def write_results_to_hide(self):
if not self._results_to_hide:
return
filename = self._previous_results_filename
with open(filename, 'w', encoding='utf8') as f:
results = self._results_to_hide + self._previous_results
json.dump(results, f)
def save_results_to_hide(self, results):
self._results_to_hide += results
def add_path_to_filter(self, path):
'''
Add path to filter
Path are used through direct comparison (no regex)
'''
self._paths_to_filter.add(path)
# endregion

@ -86,18 +86,54 @@ class AbstractDetector(metaclass=abc.ABCMeta):
DetectorClassification.INFORMATIONAL]:
raise IncorrectDetectorInitialization('CONFIDENCE is not initialized {}'.format(self.__class__.__name__))
def log(self, info):
if self.logger:
info = "\n"+info
if self.WIKI != '':
info += 'Reference: {}'.format(self.WIKI)
self.logger.info(self.color(info))
# def log(self, info):
# if self.logger:
# info = "\n"+info
# if self.WIKI != '':
# info += 'Reference: {}'.format(self.WIKI)
# self.logger.info(self.color(info))
def _log(self, info):
self.logger.info(self.color(info))
@abc.abstractmethod
def detect(self):
def _detect(self):
"""TODO Documentation"""
return
def detect(self):
results = self._detect()
results = [r for r in results if self.slither.valid_result(r)]
if results:
if self.logger:
info = '\n'
for idx, result in enumerate(results):
if self.slither.triage_mode:
info += '{}: '.format(idx)
info += result['description']
info += 'Reference: {}'.format(self.WIKI)
self._log(info)
if results and self.slither.triage_mode:
while True:
indexes = input('Results to hide during next runs: "0,1,..." or "All" (enter to not hide results): '.format(len(results)))
if indexes == 'All':
self.slither.save_results_to_hide(results)
return []
if indexes == '':
return results
if indexes.startswith('['):
indexes = indexes[1:]
if indexes.endswith(']'):
indexes = indexes[:-1]
try:
indexes = [int(i) for i in indexes.split(',')]
self.slither.save_results_to_hide([r for (idx, r) in enumerate(results) if idx in indexes])
return [r for (idx, r) in enumerate(results) if idx not in indexes]
except ValueError:
self.logger.error(yellow('Malformed input. Example of valid input: 0,1,2,3'))
return results
@property
def color(self):
return classification_colors[self.IMPACT]

@ -28,5 +28,5 @@ from .shadowing.builtin_symbols import BuiltinSymbolShadowing
from .operations.block_timestamp import Timestamp
from .statements.calls_in_loop import MultipleCallsInLoop
from .statements.incorrect_strict_equality import IncorrectStrictEquality
#
#

@ -41,7 +41,7 @@ All the calls to `get` reverts, breaking Bob's smart contract execution.'''
WIKI_RECOMMENDATION = 'Ensure that the attributes of contracts compiled prior Solidity 0.5.0 are correct.'
def detect(self):
def _detect(self):
""" Detect the constant function changing the state
Recursively visit the calls
@ -58,7 +58,6 @@ All the calls to `get` reverts, breaking Bob's smart contract execution.'''
attr = 'view' if f.view else 'pure'
info = '{}.{} ({}) is declared {} but contains assembly code\n'
info = info.format(f.contract.name, f.name, f.source_mapping_str, attr)
self.log(info)
json = self.generate_json_result(info)
self.add_function_to_json(f, json)
json['elements'] = [{'type': 'info',
@ -73,7 +72,6 @@ All the calls to `get` reverts, breaking Bob's smart contract execution.'''
for variable_written in variables_written:
info += '\t- {}.{}\n'.format(variable_written.contract.name,
variable_written.name)
self.log(info)
json = self.generate_json_result(info)

@ -22,7 +22,7 @@ class ConstantPragma(AbstractDetector):
WIKI_DESCRIPTION = 'Detect if different Solidity versions are used.'
WIKI_RECOMMENDATION = 'Use one Solidity version.'
def detect(self):
def _detect(self):
results = []
pragma = self.slither.pragma_directives
versions = [p.version for p in pragma]
@ -33,7 +33,6 @@ class ConstantPragma(AbstractDetector):
info += "\t- Version used: {}\n".format([str(v) for v in versions])
for p in pragma:
info += "\t- {} declares {}\n".format(p.source_mapping_str, str(p))
self.log(info)
json = self.generate_json_result(info)
# follow the same format than add_nodes_to_json

@ -62,7 +62,7 @@ We recommend avoiding complex pragma statement.'''
return self._check_version(version_left)
else:
return self.COMPLEX_PRAGMA
def detect(self):
def _detect(self):
"""
Detects pragma statements that allow for outdated solc versions.
:return: Returns the relevant JSON data for the findings.
@ -88,7 +88,6 @@ We recommend avoiding complex pragma statement.'''
info = "Detected issues with version pragma in {}:\n".format(self.filename)
for (reason, p) in disallowed_pragmas:
info += "\t- {} ({}): {}\n".format(p, p.source_mapping_str, reason)
self.log(info)
json = self.generate_json_result(info)

@ -65,7 +65,7 @@ Every ethers send to `Locked` will be lost.'''
return True
def detect(self):
def _detect(self):
results = []
for contract in self.slither.contracts_derived:
@ -82,7 +82,6 @@ Every ethers send to `Locked` will be lost.'''
info = txt.format(self.filename,
contract.name,
[f.name for f in funcs_payable])
self.log(info)
json = self.generate_json_result(info)
self.add_functions_to_json(funcs_payable, json)

@ -18,7 +18,7 @@ class Backdoor(AbstractDetector):
WIKI_EXPLOIT_SCENARIO = '..'
WIKI_RECOMMENDATION = '..'
def detect(self):
def _detect(self):
results = []
for contract in self.slither.contracts_derived:
@ -28,12 +28,9 @@ class Backdoor(AbstractDetector):
# Info to be printed
info = 'Backdoor function found in {}.{} ({})\n'
info = info.format(contract.name, f.name, f.source_mapping_str)
# Print the info
self.log(info)
# Add the result in result
json = self.generate_json_result(info)
self.add_function_to_json(f, json)
results.append(json)
return results

@ -100,7 +100,7 @@ Bob calls `setDestination` and `withdraw`. As a result he withdraws the contract
ret.append((f, nodes))
return ret
def detect(self):
def _detect(self):
"""
"""
results = []
@ -117,9 +117,6 @@ Bob calls `setDestination` and `withdraw`. As a result he withdraws the contract
for node in nodes:
info += '\t- {} ({})\n'.format(node.expression, node.source_mapping_str)
self.log(info)
json = self.generate_json_result(info)
self.add_function_to_json(func, json)
self.add_nodes_to_json(nodes, json)

@ -96,11 +96,9 @@ class ExternalFunction(AbstractDetector):
for function in derived_contract.functions
if function.full_name == base_most_function.full_name]
def detect(self):
def _detect(self):
results = []
all_info = ''
# Create a set to track contracts with dynamic calls. All contracts with dynamic calls could potentially be
# calling functions internally, and thus we can't assume any function in such contracts isn't called by them.
dynamic_call_contracts = set()
@ -171,12 +169,9 @@ class ExternalFunction(AbstractDetector):
info = txt.format(function_definition.contract.name,
function_definition.name,
function_definition.source_mapping_str)
all_info += info
json = self.generate_json_result(info)
self.add_function_to_json(function_definition, json)
results.append(json)
if all_info != '':
self.log(all_info)
return results

@ -64,7 +64,7 @@ Bob calls `kill` and destruct the contract.'''
ret.append(f)
return ret
def detect(self):
def _detect(self):
""" Detect the suicidal functions
"""
results = []
@ -77,8 +77,6 @@ Bob calls `kill` and destruct the contract.'''
func.name,
func.source_mapping_str)
self.log(info)
json = self.generate_json_result(info)
self.add_function_to_json(func, json)
results.append(json)

@ -51,16 +51,14 @@ Solidity defines a [naming convention](https://solidity.readthedocs.io/en/v0.4.2
def should_avoid_name(name):
return re.search('^[lOI]$', name) is not None
def detect(self):
def _detect(self):
results = []
all_info = ''
for contract in self.contracts:
if not self.is_cap_words(contract.name):
info = "Contract '{}' ({}) is not in CapWords\n".format(contract.name,
contract.source_mapping_str)
all_info += info
json = self.generate_json_result(info)
elem = dict()
@ -78,7 +76,6 @@ Solidity defines a [naming convention](https://solidity.readthedocs.io/en/v0.4.2
if not self.is_cap_words(struct.name):
info = "Struct '{}.{}' ({}) is not in CapWords\n"
info = info.format(struct.contract.name, struct.name, struct.source_mapping_str)
all_info += info
json = self.generate_json_result(info)
elem = dict()
@ -95,7 +92,6 @@ Solidity defines a [naming convention](https://solidity.readthedocs.io/en/v0.4.2
if not self.is_cap_words(event.name):
info = "Event '{}.{}' ({}) is not in CapWords\n"
info = info.format(event.contract.name, event.name, event.source_mapping_str)
all_info += info
json = self.generate_json_result(info)
elem = dict()
@ -113,7 +109,6 @@ Solidity defines a [naming convention](https://solidity.readthedocs.io/en/v0.4.2
if not self.is_mixed_case(func.name):
info = "Function '{}.{}' ({}) is not in mixedCase\n"
info = info.format(func.contract.name, func.name, func.source_mapping_str)
all_info += info
json = self.generate_json_result(info)
elem = dict()
@ -135,7 +130,6 @@ Solidity defines a [naming convention](https://solidity.readthedocs.io/en/v0.4.2
argument.function.contract.name,
argument.function,
argument.source_mapping_str)
all_info += info
json = self.generate_json_result(info)
elem = dict()
@ -154,7 +148,6 @@ Solidity defines a [naming convention](https://solidity.readthedocs.io/en/v0.4.2
if not self.is_upper_case_with_underscores(var.name):
info = "Variable '{}.{}' ({}) used l, O, I, which should not be used\n"
info = info.format(var.contract.name, var.name, var.source_mapping_str)
all_info += info
json = self.generate_json_result(info)
elem = dict()
@ -173,7 +166,6 @@ Solidity defines a [naming convention](https://solidity.readthedocs.io/en/v0.4.2
if not self.is_upper_case_with_underscores(var.name):
info = "Constant '{}.{}' ({}) is not in UPPER_CASE_WITH_UNDERSCORES\n"
info = info.format(var.contract.name, var.name, var.source_mapping_str)
all_info += info
json = self.generate_json_result(info)
elem = dict()
@ -192,7 +184,6 @@ Solidity defines a [naming convention](https://solidity.readthedocs.io/en/v0.4.2
if not correct_naming:
info = "Variable '{}.{}' ({}) is not in mixedCase\n"
info = info.format(var.contract.name, var.name, var.source_mapping_str)
all_info += info
json = self.generate_json_result(info)
elem = dict()
@ -210,7 +201,6 @@ Solidity defines a [naming convention](https://solidity.readthedocs.io/en/v0.4.2
if not self.is_cap_words(enum.name):
info = "Enum '{}.{}' ({}) is not in CapWords\n"
info = info.format(enum.contract.name, enum.name, enum.source_mapping_str)
all_info += info
json = self.generate_json_result(info)
elem = dict()
@ -231,7 +221,6 @@ Solidity defines a [naming convention](https://solidity.readthedocs.io/en/v0.4.2
info = info.format(modifier.contract.name,
modifier.name,
modifier.source_mapping_str)
all_info += info
json = self.generate_json_result(info)
elem = dict()
@ -242,7 +231,5 @@ Solidity defines a [naming convention](https://solidity.readthedocs.io/en/v0.4.2
json['elements'] = [elem]
results.append(json)
if all_info != '':
self.log(all_info)
return results

@ -67,7 +67,7 @@ class Timestamp(AbstractDetector):
ret.append((f, nodes))
return ret
def detect(self):
def _detect(self):
"""
"""
results = []
@ -84,9 +84,6 @@ class Timestamp(AbstractDetector):
for node in nodes:
info += '\t- {} ({})\n'.format(node.expression, node.source_mapping_str)
self.log(info)
json = self.generate_json_result(info)
self.add_function_to_json(func, json)
self.add_nodes_to_json(nodes, json)

@ -41,11 +41,10 @@ class LowLevelCalls(AbstractDetector):
ret.append((f, assembly_nodes))
return ret
def detect(self):
def _detect(self):
""" Detect the functions that use low level calls
"""
results = []
all_info = ''
for c in self.contracts:
values = self.detect_low_level_calls(c)
for func, nodes in values:
@ -53,15 +52,10 @@ class LowLevelCalls(AbstractDetector):
info = info.format(func.contract.name, func.name, func.source_mapping_str)
for node in nodes:
info += "\t-{} {}\n".format(str(node.expression), node.source_mapping_str)
all_info += info
json = self.generate_json_result(info)
self.add_function_to_json(func, json)
self.add_nodes_to_json(nodes, json)
results.append(json)
if all_info != '':
self.log(all_info)
return results

@ -58,7 +58,7 @@ contract MyConc{
return [nodes_origin[value].node for value in values_returned]
def detect(self):
def _detect(self):
""" Detect unused high level calls that return a value but are never used
"""
results = []
@ -74,7 +74,6 @@ contract MyConc{
f.source_mapping_str)
for node in unused_return:
info += "\t-{} ({})\n".format(node.expression, node.source_mapping_str)
self.log(info)
json = self.generate_json_result(info)
self.add_function_to_json(f, json)

@ -188,7 +188,7 @@ class Reentrancy(AbstractDetector):
self._explore(function.entry_point, [])
function.context[self.KEY] = True
def detect(self):
def _detect(self):
"""
"""
# if a node was already visited by another path

@ -71,11 +71,11 @@ Only report reentrancy that acts as a double call (see `reentrancy-eth`, `reentr
result[finding_key] = list(set(result[finding_key] + finding_vars))
return result
def detect(self):
def _detect(self):
"""
"""
super().detect()
super()._detect()
reentrancies = self.find_reentrancies()
results = []
@ -96,7 +96,6 @@ Only report reentrancy that acts as a double call (see `reentrancy-eth`, `reentr
info += '\tState variables written after the call(s):\n'
for (v, node) in varsWritten:
info += '\t- {} ({})\n'.format(v, node.source_mapping_str)
self.log(info)
sending_eth_json = []
if calls != send_eth:

@ -73,10 +73,10 @@ Bob uses the re-entrancy bug to call `withdrawBalance` two times, and withdraw m
result[finding_key] = list(set(result[finding_key] + finding_vars))
return result
def detect(self):
def _detect(self):
"""
"""
super().detect()
super()._detect()
reentrancies = self.find_reentrancies()
@ -102,7 +102,6 @@ Bob uses the re-entrancy bug to call `withdrawBalance` two times, and withdraw m
info += '\tState variables written after the call(s):\n'
for (v, node) in varsWritten:
info += '\t- {} ({})\n'.format(v, node.source_mapping_str)
self.log(info)
sending_eth_json = []
if calls != send_eth:

@ -70,11 +70,11 @@ Do not report reentrancies that involve ethers (see `reentrancy-eth`)'''
result[finding_key] = list(set(result[finding_key] + finding_vars))
return result
def detect(self):
def _detect(self):
"""
"""
super().detect()
super()._detect()
reentrancies = self.find_reentrancies()
results = []
@ -90,7 +90,6 @@ Do not report reentrancies that involve ethers (see `reentrancy-eth`)'''
info += '\tState variables written after the call(s):\n'
for (v, node) in varsWritten:
info += '\t- {} ({})\n'.format(v, node.source_mapping_str)
self.log(info)
sending_eth_json = []

@ -50,7 +50,7 @@ contract DerivedContract is BaseContract{
return ret
def detect(self):
def _detect(self):
""" Detect shadowing
Recursively visit the calls
@ -72,7 +72,6 @@ contract DerivedContract is BaseContract{
info += "\t- {}.{} ({})\n".format(var.contract.name,
var.name,
var.source_mapping_str)
self.log(info)
json = self.generate_json_result(info)
self.add_variables_to_json(all_variables, json)

@ -111,7 +111,7 @@ contract Bug {
return result
def detect(self):
def _detect(self):
""" Detect shadowing of built-in symbols
Recursively visit the calls
@ -140,8 +140,6 @@ contract Bug {
shadow_type,
shadow_object.source_mapping_str,
shadow_object.name)
# Print relevant information to the log
self.log(info)
# Generate relevant JSON data for this shadowing definition.
json = self.generate_json_result(info)

@ -89,7 +89,7 @@ contract Bug {
return result
def detect(self):
def _detect(self):
""" Detect shadowing local variables
Recursively visit the calls
@ -116,8 +116,6 @@ contract Bug {
overshadowed_entry[0],
overshadowed_entry[2].source_mapping_str)
# Print relevant information to the log
self.log(info)
# Generate relevant JSON data for this shadowing definition.
json = self.generate_json_result(info)

@ -61,7 +61,7 @@ contract DerivedContract is BaseContract{
ret.append([var] + shadow)
return ret
def detect(self):
def _detect(self):
""" Detect shadowing
Recursively visit the calls
@ -83,7 +83,6 @@ contract DerivedContract is BaseContract{
info += "\t- {}.{} ({})\n".format(var.contract.name,
var.name,
var.source_mapping_str)
self.log(info)
json = self.generate_json_result(info)
self.add_variables_to_json(all_variables, json)

@ -44,11 +44,10 @@ class Assembly(AbstractDetector):
ret.append((f, assembly_nodes))
return ret
def detect(self):
def _detect(self):
""" Detect the functions that use inline assembly
"""
results = []
all_info = ''
for c in self.contracts:
values = self.detect_assembly(c)
for func, nodes in values:
@ -58,13 +57,9 @@ class Assembly(AbstractDetector):
for node in nodes:
info += "\t- {}\n".format(node.source_mapping_str)
all_info += info
json = self.generate_json_result(info)
self.add_function_to_json(func, json)
self.add_nodes_to_json(nodes, json)
results.append(json)
if all_info != '':
self.log(all_info)
return results

@ -78,7 +78,7 @@ If one of the destinations has a fallback function which reverts, `bad` will alw
return ret
def detect(self):
def _detect(self):
"""
"""
results = []
@ -91,8 +91,6 @@ If one of the destinations has a fallback function which reverts, `bad` will alw
info += "\t- {} ({})\n".format(node.expression, node.source_mapping_str)
self.log(info)
json = self.generate_json_result(info)
self.add_function_to_json(func, json)
self.add_nodes_to_json([node], json)

@ -37,7 +37,7 @@ Bob calls `delegate` and delegate the execution to its malicious contract. As a
ret.append(node)
return ret
def detect(self):
def _detect(self):
results = []
for contract in self.slither.contracts:
@ -50,7 +50,6 @@ Bob calls `delegate` and delegate the execution to its malicious contract. As a
info = info.format(contract.name, f.name, f.source_mapping_str)
for node in nodes:
info += '\t{} ({})\n'.format(node.expression, node.source_mapping_str)
self.log(info)
json = self.generate_json_result(info)
self.add_function_to_json(f, json)

@ -102,7 +102,7 @@ contract Crowdsale{
return results
def detect(self):
def _detect(self):
results = []
for c in self.slither.contracts_derived:
@ -126,7 +126,4 @@ contract Crowdsale{
self.add_nodes_to_json(nodes, json)
results.append(json)
if info:
self.log(info)
return results

@ -58,7 +58,7 @@ Bob is the owner of `TxOrigin`. Bob calls Eve's contract. Eve's contact calls `T
ret.append((f, bad_tx_nodes))
return ret
def detect(self):
def _detect(self):
""" Detect the functions that use tx.origin in a conditional node
"""
results = []
@ -71,8 +71,6 @@ Bob is the owner of `TxOrigin`. Bob calls Eve's contract. Eve's contact calls `T
for node in nodes:
info += "\t- {} ({})\n".format(node.expression, node.source_mapping_str)
self.log(info)
json = self.generate_json_result(info)
self.add_function_to_json(func, json)
self.add_nodes_to_json(nodes, json)

@ -63,7 +63,7 @@ class ConstCandidateStateVars(AbstractDetector):
def detect(self):
def _detect(self):
""" Detect state variables that could be const
"""
results = []
@ -93,5 +93,4 @@ class ConstCandidateStateVars(AbstractDetector):
json = self.generate_json_result(all_info)
self.add_variables_to_json(constable_variables, json)
results.append(json)
self.log(all_info)
return results

@ -76,7 +76,7 @@ Bob calls `transfer`. As a result, the ethers are sent to the address 0x0 and ar
self._detect_uninitialized(function, son, visited)
def detect(self):
def _detect(self):
""" Detect uninitialized state variables
Recursively visit the calls
@ -107,7 +107,6 @@ Bob calls `transfer`. As a result, the ethers are sent to the address 0x0 and ar
function.name,
uninitialized_local_variable.source_mapping_str)
self.log(info)
json = self.generate_json_result(info)
self.add_variable_to_json(uninitialized_local_variable, json)

@ -81,7 +81,7 @@ Initialize all the variables. If a variable is meant to be initialized to zero,
not variable.expression and\
variable in read_variables]
def detect(self):
def _detect(self):
""" Detect uninitialized state variables
Recursively visit the calls
@ -98,7 +98,6 @@ Initialize all the variables. If a variable is meant to be initialized to zero,
variable.source_mapping_str)
for f in functions:
info += "\t- {} ({})\n".format(f.name, f.source_mapping_str)
self.log(info)
source = [variable.source_mapping]
source += [f.source_mapping for f in functions]

@ -83,7 +83,7 @@ Bob calls `func`. As a result, `owner` is override to 0.
self._detect_uninitialized(function, son, visited)
def detect(self):
def _detect(self):
""" Detect uninitialized state variables
Recursively visit the calls
@ -108,9 +108,6 @@ Bob calls `func`. As a result, `owner` is override to 0.
info = "{} in {}.{} ({}) is a storage variable never initialiazed\n"
info = info.format(var_name, function.contract.name, function.name, uninitialized_storage_variable.source_mapping_str)
self.log(info)
json = self.generate_json_result(info)
self.add_variable_to_json(uninitialized_storage_variable, json)

@ -48,11 +48,10 @@ class UnusedStateVars(AbstractDetector):
return [x for x in contract.variables if
x not in variables_used and x.visibility != 'public']
def detect(self):
def _detect(self):
""" Detect unused state variables
"""
results = []
all_info = ''
for c in self.slither.contracts_derived:
unusedVars = self.detect_unused(c)
if unusedVars:
@ -63,12 +62,9 @@ class UnusedStateVars(AbstractDetector):
var.source_mapping_str,
c.name)
all_info += info
json = self.generate_json_result(info)
self.add_variables_to_json(unusedVars, json)
results.append(json)
if all_info != '':
self.log(all_info)
return results

@ -19,47 +19,86 @@ logger_printer = logging.getLogger("Printers")
class Slither(SlitherSolc):
def __init__(self, contract, solc='solc', disable_solc_warnings=False, solc_arguments='', ast_format='--ast-compact-json', is_truffle=False):
self._detectors = []
self._printers = []
def __init__(self, contract, **kwargs):
'''
Args:
contract (str| list(json))
Keyword Args:
solc (str): solc binary location (default 'solc')
disable_solc_warnings (bool): True to disable solc warnings (default false)
solc_argeuments (str): solc arguments (default '')
ast_format (str): ast format (default '--ast-compact-json')
is_truffle (bool): is a truffle directory (default false)
filter_paths (list(str)): list of path to filter (default [])
triage_mode (bool): if true, switch to triage mode (default false)
'''
is_truffle = kwargs.get('is_truffle', False)
# truffle directory
if is_truffle:
if not os.path.isdir(os.path.join(contract, 'build'))\
or not os.path.isdir(os.path.join(contract, 'build', 'contracts')):
logger.info(red('No truffle build directory found, did you run `truffle compile`?'))
sys.exit(-1)
super(Slither, self).__init__('')
filenames = glob.glob(os.path.join(contract, 'build', 'contracts', '*.json'))
for filename in filenames:
with open(filename, encoding='utf8') as f:
contract_loaded = json.load(f)
contract_loaded = contract_loaded['ast']
if 'absolutePath' in contract_loaded:
path = contract_loaded['absolutePath']
else:
path = contract_loaded['attributes']['absolutePath']
self._parse_contracts_from_loaded_json(contract_loaded, path)
self._init_from_truffle(contract)
# list of files provided (see --splitted option)
elif isinstance(contract, list):
super(Slither, self).__init__('')
for c in contract:
if 'absolutePath' in c:
path = c['absolutePath']
else:
path = c['attributes']['absolutePath']
self._parse_contracts_from_loaded_json(c, path)
self._init_from_list(contract)
# .json or .sol provided
else:
contracts_json = self._run_solc(contract, solc, disable_solc_warnings, solc_arguments, ast_format)
super(Slither, self).__init__(contract)
self._init_from_solc(contract, **kwargs)
self._detectors = []
self._printers = []
filter_paths = kwargs.get('filter_paths', [])
for p in filter_paths:
self.add_path_to_filter(p)
for c in contracts_json:
self._parse_contracts_from_json(c)
triage_mode = kwargs.get('triage_mode', False)
self._triage_mode = triage_mode
self._analyze_contracts()
def _init_from_truffle(self, contract):
if not os.path.isdir(os.path.join(contract, 'build'))\
or not os.path.isdir(os.path.join(contract, 'build', 'contracts')):
logger.info(red('No truffle build directory found, did you run `truffle compile`?'))
sys.exit(-1)
super(Slither, self).__init__('')
filenames = glob.glob(os.path.join(contract, 'build', 'contracts', '*.json'))
for filename in filenames:
with open(filename, encoding='utf8') as f:
contract_loaded = json.load(f)
contract_loaded = contract_loaded['ast']
if 'absolutePath' in contract_loaded:
path = contract_loaded['absolutePath']
else:
path = contract_loaded['attributes']['absolutePath']
self._parse_contracts_from_loaded_json(contract_loaded, path)
def _init_from_solc(self, contract, **kwargs):
solc = kwargs.get('solc', 'solc')
disable_solc_warnings = kwargs.get('disable_solc_warnings', False)
solc_arguments = kwargs.get('solc_arguments', '')
ast_format = kwargs.get('ast_format', '--ast-compact-json')
contracts_json = self._run_solc(contract,
solc,
disable_solc_warnings,
solc_arguments,
ast_format)
super(Slither, self).__init__(contract)
for c in contracts_json:
self._parse_contracts_from_json(c)
def _init_from_list(self, contract):
super(Slither, self).__init__('')
for c in contract:
if 'absolutePath' in c:
path = c['absolutePath']
else:
path = c['attributes']['absolutePath']
self._parse_contracts_from_loaded_json(c, path)
@property
def detectors(self):
return self._detectors
@ -103,7 +142,10 @@ class Slither(SlitherSolc):
:return: List of registered detectors results.
"""
return [d.detect() for d in self._detectors]
self.load_previous_results()
results = [d.detect() for d in self._detectors]
self.write_results_to_hide()
return results
def run_printers(self):
"""
@ -173,3 +215,7 @@ class Slither(SlitherSolc):
stdout = stdout.split('\n=')
return stdout
@property
def triage_mode(self):
return self._triage_mode

Loading…
Cancel
Save