slither_core.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. """
  2. Main module
  3. """
  4. import os
  5. import logging
  6. import json
  7. import re
  8. import math
  9. from collections import defaultdict
  10. from typing import Optional, Dict, List, Set, Union, Tuple
  11. from crytic_compile import CryticCompile
  12. from slither.core.context.context import Context
  13. from slither.core.declarations import (
  14. Contract,
  15. Pragma,
  16. Import,
  17. Function,
  18. Modifier,
  19. Structure,
  20. Enum,
  21. )
  22. from slither.core.variables.state_variable import StateVariable
  23. from slither.slithir.operations import InternalCall
  24. from slither.slithir.variables import Constant
  25. from slither.utils.colors import red
  26. logger = logging.getLogger("Slither")
  27. logging.basicConfig()
  28. def _relative_path_format(path: str) -> str:
  29. """
  30. Strip relative paths of "." and ".."
  31. """
  32. return path.split("..")[-1].strip(".").strip("/")
  33. class SlitherCore(Context): # pylint: disable=too-many-instance-attributes,too-many-public-methods
  34. """
  35. Slither static analyzer
  36. """
  37. def __init__(self):
  38. super().__init__()
  39. self._contracts: Dict[str, Contract] = {}
  40. self._filename: Optional[str] = None
  41. self._source_units: Dict[int, str] = {}
  42. self._solc_version: Optional[str] = None # '0.3' or '0.4':!
  43. self._pragma_directives: List[Pragma] = []
  44. self._import_directives: List[Import] = []
  45. self._raw_source_code: Dict[str, str] = {}
  46. self._all_functions: Set[Function] = set()
  47. self._all_modifiers: Set[Modifier] = set()
  48. # Memoize
  49. self._all_state_variables: Optional[Set[StateVariable]] = None
  50. self._previous_results_filename: str = "slither.db.json"
  51. self._results_to_hide: List = []
  52. self._previous_results: List = []
  53. self._previous_results_ids: Set[str] = set()
  54. self._paths_to_filter: Set[str] = set()
  55. self._crytic_compile: Optional[CryticCompile] = None
  56. self._generate_patches = False
  57. self._exclude_dependencies = False
  58. self._markdown_root = ""
  59. self._contract_name_collisions = defaultdict(list)
  60. self._contract_with_missing_inheritance = set()
  61. self._storage_layouts: Dict[str, Dict[str, Tuple[int, int]]] = {}
  62. # If set to true, slither will not catch errors during parsing
  63. self._disallow_partial: bool = False
  64. self._skip_assembly: bool = False
  65. ###################################################################################
  66. ###################################################################################
  67. # region Source code
  68. ###################################################################################
  69. ###################################################################################
  70. @property
  71. def source_code(self) -> Dict[str, str]:
  72. """ {filename: source_code (str)}: source code """
  73. return self._raw_source_code
  74. @property
  75. def source_units(self) -> Dict[int, str]:
  76. return self._source_units
  77. @property
  78. def filename(self) -> Optional[str]:
  79. """str: Filename."""
  80. return self._filename
  81. @filename.setter
  82. def filename(self, filename: str):
  83. self._filename = filename
  84. def add_source_code(self, path):
  85. """
  86. :param path:
  87. :return:
  88. """
  89. if self.crytic_compile and path in self.crytic_compile.src_content:
  90. self.source_code[path] = self.crytic_compile.src_content[path]
  91. else:
  92. with open(path, encoding="utf8", newline="") as f:
  93. self.source_code[path] = f.read()
  94. @property
  95. def markdown_root(self) -> str:
  96. return self._markdown_root
  97. # endregion
  98. ###################################################################################
  99. ###################################################################################
  100. # region Pragma attributes
  101. ###################################################################################
  102. ###################################################################################
  103. @property
  104. def solc_version(self) -> str:
  105. """str: Solidity version."""
  106. if self.crytic_compile:
  107. return self.crytic_compile.compiler_version.version
  108. return self._solc_version
  109. @solc_version.setter
  110. def solc_version(self, version: str):
  111. self._solc_version = version
  112. @property
  113. def pragma_directives(self) -> List[Pragma]:
  114. """ list(core.declarations.Pragma): Pragma directives."""
  115. return self._pragma_directives
  116. @property
  117. def import_directives(self) -> List[Import]:
  118. """ list(core.declarations.Import): Import directives"""
  119. return self._import_directives
  120. # endregion
  121. ###################################################################################
  122. ###################################################################################
  123. # region Contracts
  124. ###################################################################################
  125. ###################################################################################
  126. @property
  127. def contracts(self) -> List[Contract]:
  128. """list(Contract): List of contracts."""
  129. return list(self._contracts.values())
  130. @property
  131. def contracts_derived(self) -> List[Contract]:
  132. """list(Contract): List of contracts that are derived and not inherited."""
  133. inheritance = (x.inheritance for x in self.contracts)
  134. inheritance = [item for sublist in inheritance for item in sublist]
  135. return [c for c in self._contracts.values() if c not in inheritance and not c.is_top_level]
  136. @property
  137. def contracts_as_dict(self) -> Dict[str, Contract]:
  138. """list(dict(str: Contract): List of contracts as dict: name -> Contract."""
  139. return self._contracts
  140. def get_contract_from_name(self, contract_name: Union[str, Constant]) -> Optional[Contract]:
  141. """
  142. Return a contract from a name
  143. Args:
  144. contract_name (str): name of the contract
  145. Returns:
  146. Contract
  147. """
  148. return next((c for c in self.contracts if c.name == contract_name), None)
  149. # endregion
  150. ###################################################################################
  151. ###################################################################################
  152. # region Functions and modifiers
  153. ###################################################################################
  154. ###################################################################################
  155. @property
  156. def functions(self) -> List[Function]:
  157. return list(self._all_functions)
  158. def add_function(self, func: Function):
  159. self._all_functions.add(func)
  160. @property
  161. def modifiers(self) -> List[Modifier]:
  162. return list(self._all_modifiers)
  163. def add_modifier(self, modif: Modifier):
  164. self._all_modifiers.add(modif)
  165. @property
  166. def functions_and_modifiers(self) -> List[Function]:
  167. return self.functions + self.modifiers
  168. def propagate_function_calls(self):
  169. for f in self.functions_and_modifiers:
  170. for node in f.nodes:
  171. for ir in node.irs_ssa:
  172. if isinstance(ir, InternalCall):
  173. ir.function.add_reachable_from_node(node, ir)
  174. # endregion
  175. ###################################################################################
  176. ###################################################################################
  177. # region Variables
  178. ###################################################################################
  179. ###################################################################################
  180. @property
  181. def state_variables(self) -> List[StateVariable]:
  182. if self._all_state_variables is None:
  183. state_variables = [c.state_variables for c in self.contracts]
  184. state_variables = [item for sublist in state_variables for item in sublist]
  185. self._all_state_variables = set(state_variables)
  186. return list(self._all_state_variables)
  187. # endregion
  188. ###################################################################################
  189. ###################################################################################
  190. # region Top level
  191. ###################################################################################
  192. ###################################################################################
  193. @property
  194. def top_level_structures(self) -> List[Structure]:
  195. top_level_structures = [c.structures for c in self.contracts if c.is_top_level]
  196. return [st for sublist in top_level_structures for st in sublist]
  197. @property
  198. def top_level_enums(self) -> List[Enum]:
  199. top_level_enums = [c.enums for c in self.contracts if c.is_top_level]
  200. return [st for sublist in top_level_enums for st in sublist]
  201. # endregion
  202. ###################################################################################
  203. ###################################################################################
  204. # region Export
  205. ###################################################################################
  206. ###################################################################################
  207. def print_functions(self, d: str):
  208. """
  209. Export all the functions to dot files
  210. """
  211. for c in self.contracts:
  212. for f in c.functions:
  213. f.cfg_to_dot(os.path.join(d, "{}.{}.dot".format(c.name, f.name)))
  214. # endregion
  215. ###################################################################################
  216. ###################################################################################
  217. # region Filtering results
  218. ###################################################################################
  219. ###################################################################################
  220. def valid_result(self, r: Dict) -> bool:
  221. """
  222. Check if the result is valid
  223. A result is invalid if:
  224. - All its source paths belong to the source path filtered
  225. - Or a similar result was reported and saved during a previous run
  226. - The --exclude-dependencies flag is set and results are only related to dependencies
  227. """
  228. source_mapping_elements = [
  229. elem["source_mapping"].get("filename_absolute", "unknown")
  230. for elem in r["elements"]
  231. if "source_mapping" in elem
  232. ]
  233. source_mapping_elements = map(
  234. lambda x: os.path.normpath(x) if x else x, source_mapping_elements
  235. )
  236. matching = False
  237. for path in self._paths_to_filter:
  238. try:
  239. if any(
  240. bool(re.search(_relative_path_format(path), src_mapping))
  241. for src_mapping in source_mapping_elements
  242. ):
  243. matching = True
  244. break
  245. except re.error:
  246. logger.error(
  247. f"Incorrect regular expression for --filter-paths {path}."
  248. "\nSlither supports the Python re format"
  249. ": https://docs.python.org/3/library/re.html"
  250. )
  251. if r["elements"] and matching:
  252. return False
  253. if r["elements"] and self._exclude_dependencies:
  254. return not all(element["source_mapping"]["is_dependency"] for element in r["elements"])
  255. if r["id"] in self._previous_results_ids:
  256. return False
  257. # Conserve previous result filtering. This is conserved for compatibility, but is meant to be removed
  258. return not r["description"] in [pr["description"] for pr in self._previous_results]
  259. def load_previous_results(self):
  260. filename = self._previous_results_filename
  261. try:
  262. if os.path.isfile(filename):
  263. with open(filename) as f:
  264. self._previous_results = json.load(f)
  265. if self._previous_results:
  266. for r in self._previous_results:
  267. if "id" in r:
  268. self._previous_results_ids.add(r["id"])
  269. except json.decoder.JSONDecodeError:
  270. logger.error(
  271. red("Impossible to decode {}. Consider removing the file".format(filename))
  272. )
  273. def write_results_to_hide(self):
  274. if not self._results_to_hide:
  275. return
  276. filename = self._previous_results_filename
  277. with open(filename, "w", encoding="utf8") as f:
  278. results = self._results_to_hide + self._previous_results
  279. json.dump(results, f)
  280. def save_results_to_hide(self, results: List[Dict]):
  281. self._results_to_hide += results
  282. def add_path_to_filter(self, path: str):
  283. """
  284. Add path to filter
  285. Path are used through direct comparison (no regex)
  286. """
  287. self._paths_to_filter.add(path)
  288. # endregion
  289. ###################################################################################
  290. ###################################################################################
  291. # region Crytic compile
  292. ###################################################################################
  293. ###################################################################################
  294. @property
  295. def crytic_compile(self) -> Optional[CryticCompile]:
  296. return self._crytic_compile
  297. # endregion
  298. ###################################################################################
  299. ###################################################################################
  300. # region Format
  301. ###################################################################################
  302. ###################################################################################
  303. @property
  304. def generate_patches(self) -> bool:
  305. return self._generate_patches
  306. @generate_patches.setter
  307. def generate_patches(self, p: bool):
  308. self._generate_patches = p
  309. # endregion
  310. ###################################################################################
  311. ###################################################################################
  312. # region Internals
  313. ###################################################################################
  314. ###################################################################################
  315. @property
  316. def contract_name_collisions(self) -> Dict:
  317. return self._contract_name_collisions
  318. @property
  319. def contracts_with_missing_inheritance(self) -> Set:
  320. return self._contract_with_missing_inheritance
  321. @property
  322. def disallow_partial(self) -> bool:
  323. """
  324. Return true if partial analyses are disallowed
  325. For example, codebase with duplicate names will lead to partial analyses
  326. :return:
  327. """
  328. return self._disallow_partial
  329. @property
  330. def skip_assembly(self) -> bool:
  331. return self._skip_assembly
  332. # endregion
  333. ###################################################################################
  334. ###################################################################################
  335. # region Storage Layouts
  336. ###################################################################################
  337. ###################################################################################
  338. def compute_storage_layout(self):
  339. for contract in self.contracts_derived:
  340. self._storage_layouts[contract.name] = {}
  341. slot = 0
  342. offset = 0
  343. for var in contract.state_variables_ordered:
  344. if var.is_constant:
  345. continue
  346. size, new_slot = var.type.storage_size
  347. if new_slot:
  348. if offset > 0:
  349. slot += 1
  350. offset = 0
  351. elif size + offset > 32:
  352. slot += 1
  353. offset = 0
  354. self._storage_layouts[contract.name][var.canonical_name] = (
  355. slot,
  356. offset,
  357. )
  358. if new_slot:
  359. slot += math.ceil(size / 32)
  360. else:
  361. offset += size
  362. def storage_layout_of(self, contract, var) -> Tuple[int, int]:
  363. return self._storage_layouts[contract.name][var.canonical_name]
  364. # endregion