slither.core.slither_core
Main module
1""" 2 Main module 3""" 4import json 5import logging 6import os 7import pathlib 8import posixpath 9import re 10from collections import defaultdict 11from typing import Optional, Dict, List, Set, Union, Tuple 12 13from crytic_compile import CryticCompile 14from crytic_compile.utils.naming import Filename 15 16from slither.core.declarations.contract_level import ContractLevel 17from slither.core.compilation_unit import SlitherCompilationUnit 18from slither.core.context.context import Context 19from slither.core.declarations import Contract, FunctionContract 20from slither.core.declarations.top_level import TopLevel 21from slither.core.source_mapping.source_mapping import SourceMapping, Source 22from slither.slithir.variables import Constant 23from slither.utils.colors import red 24from slither.utils.sarif import read_triage_info 25from slither.utils.source_mapping import get_definition, get_references, get_all_implementations 26 27logger = logging.getLogger("Slither") 28logging.basicConfig() 29 30 31def _relative_path_format(path: str) -> str: 32 """ 33 Strip relative paths of "." and ".." 34 """ 35 return path.split("..")[-1].strip(".").strip("/") 36 37 38# pylint: disable=too-many-instance-attributes,too-many-public-methods 39class SlitherCore(Context): 40 """ 41 Slither static analyzer 42 """ 43 44 def __init__(self) -> None: 45 super().__init__() 46 47 self._filename: Optional[str] = None 48 self._raw_source_code: Dict[str, str] = {} 49 self._source_code_to_line: Optional[Dict[str, List[str]]] = None 50 51 self._previous_results_filename: str = "slither.db.json" 52 53 # TODO: add cli flag to set these variables 54 self.sarif_input: str = "export.sarif" 55 self.sarif_triage: str = "export.sarif.sarifexplorer" 56 self._results_to_hide: List = [] 57 self._previous_results: List = [] 58 # From triaged result 59 self._previous_results_ids: Set[str] = set() 60 # Every slither object has a list of result from detector 61 # Because of the multiple compilation support, we might analyze 62 # Multiple time the same result, so we remove duplicates 63 self._currently_seen_resuts: Set[str] = set() 64 self._paths_to_filter: Set[str] = set() 65 self._paths_to_include: Set[str] = set() 66 67 self._crytic_compile: Optional[CryticCompile] = None 68 69 self._generate_patches = False 70 self._exclude_dependencies = False 71 72 self._markdown_root = "" 73 74 # If set to true, slither will not catch errors during parsing 75 self._disallow_partial: bool = False 76 self._skip_assembly: bool = False 77 78 self._show_ignored_findings = False 79 80 # Maps from file to detector name to the start/end ranges for that detector. 81 # Infinity is used to signal a detector has no end range. 82 self._ignore_ranges: Dict[str, Dict[str, List[Tuple[int, ...]]]] = defaultdict( 83 lambda: defaultdict(lambda: [(-1, -1)]) 84 ) 85 86 self._compilation_units: List[SlitherCompilationUnit] = [] 87 88 self._contracts: List[Contract] = [] 89 self._contracts_derived: List[Contract] = [] 90 91 self._offset_to_objects: Optional[Dict[Filename, Dict[int, Set[SourceMapping]]]] = None 92 self._offset_to_references: Optional[Dict[Filename, Dict[int, Set[Source]]]] = None 93 self._offset_to_implementations: Optional[Dict[Filename, Dict[int, Set[Source]]]] = None 94 self._offset_to_definitions: Optional[Dict[Filename, Dict[int, Set[Source]]]] = None 95 96 # Line prefix is used during the source mapping generation 97 # By default we generate file.sol#1 98 # But we allow to alter this (ex: file.sol:1) for vscode integration 99 self.line_prefix: str = "#" 100 101 # Use by the echidna printer 102 # If true, partial analysis is allowed 103 self.no_fail = False 104 105 self.skip_data_dependency = False 106 107 @property 108 def compilation_units(self) -> List[SlitherCompilationUnit]: 109 return list(self._compilation_units) 110 111 def add_compilation_unit(self, compilation_unit: SlitherCompilationUnit): 112 self._compilation_units.append(compilation_unit) 113 114 # endregion 115 ################################################################################### 116 ################################################################################### 117 # region Contracts 118 ################################################################################### 119 ################################################################################### 120 121 @property 122 def contracts(self) -> List[Contract]: 123 if not self._contracts: 124 all_contracts = [ 125 compilation_unit.contracts for compilation_unit in self._compilation_units 126 ] 127 self._contracts = [item for sublist in all_contracts for item in sublist] 128 return self._contracts 129 130 @property 131 def contracts_derived(self) -> List[Contract]: 132 if not self._contracts_derived: 133 all_contracts = [ 134 compilation_unit.contracts_derived for compilation_unit in self._compilation_units 135 ] 136 self._contracts_derived = [item for sublist in all_contracts for item in sublist] 137 return self._contracts_derived 138 139 def get_contract_from_name(self, contract_name: Union[str, Constant]) -> List[Contract]: 140 """ 141 Return a contract from a name 142 Args: 143 contract_name (str): name of the contract 144 Returns: 145 Contract 146 """ 147 contracts = [] 148 for compilation_unit in self._compilation_units: 149 contracts += compilation_unit.get_contract_from_name(contract_name) 150 return contracts 151 152 ################################################################################### 153 ################################################################################### 154 # region Source code 155 ################################################################################### 156 ################################################################################### 157 158 @property 159 def source_code(self) -> Dict[str, str]: 160 """{filename: source_code (str)}: source code""" 161 return self._raw_source_code 162 163 @property 164 def filename(self) -> Optional[str]: 165 """str: Filename.""" 166 return self._filename 167 168 @filename.setter 169 def filename(self, filename: str): 170 self._filename = filename 171 172 def add_source_code(self, path: str) -> None: 173 """ 174 :param path: 175 :return: 176 """ 177 if self.crytic_compile and path in self.crytic_compile.src_content: 178 self.source_code[path] = self.crytic_compile.src_content[path] 179 else: 180 with open(path, encoding="utf8", newline="") as f: 181 self.source_code[path] = f.read() 182 183 self.parse_ignore_comments(path) 184 185 @property 186 def markdown_root(self) -> str: 187 return self._markdown_root 188 189 def print_functions(self, d: str): 190 """ 191 Export all the functions to dot files 192 """ 193 for compilation_unit in self._compilation_units: 194 for c in compilation_unit.contracts: 195 for f in c.functions: 196 f.cfg_to_dot(os.path.join(d, f"{c.name}.{f.name}.dot")) 197 198 def offset_to_objects(self, filename_str: str, offset: int) -> Set[SourceMapping]: 199 if self._offset_to_objects is None: 200 self._compute_offsets_to_ref_impl_decl() 201 filename: Filename = self.crytic_compile.filename_lookup(filename_str) 202 return self._offset_to_objects[filename][offset] 203 204 def _compute_offsets_from_thing(self, thing: SourceMapping): 205 definition = get_definition(thing, self.crytic_compile) 206 references = get_references(thing) 207 implementations = get_all_implementations(thing, self.contracts) 208 209 for offset in range(definition.start, definition.end + 1): 210 if ( 211 isinstance(thing, (TopLevel, Contract)) 212 or ( 213 isinstance(thing, FunctionContract) 214 and thing.contract_declarer == thing.contract 215 ) 216 or (isinstance(thing, ContractLevel) and not isinstance(thing, FunctionContract)) 217 ): 218 219 self._offset_to_objects[definition.filename][offset].add(thing) 220 221 self._offset_to_definitions[definition.filename][offset].add(definition) 222 self._offset_to_implementations[definition.filename][offset].update(implementations) 223 self._offset_to_references[definition.filename][offset] |= set(references) 224 225 for ref in references: 226 for offset in range(ref.start, ref.end + 1): 227 is_declared_function = ( 228 isinstance(thing, FunctionContract) 229 and thing.contract_declarer == thing.contract 230 ) 231 if ( 232 isinstance(thing, TopLevel) 233 or is_declared_function 234 or ( 235 isinstance(thing, ContractLevel) and not isinstance(thing, FunctionContract) 236 ) 237 ): 238 self._offset_to_objects[definition.filename][offset].add(thing) 239 240 if is_declared_function: 241 # Only show the nearest lexical definition for declared contract-level functions 242 if ( 243 thing.contract.source_mapping.start 244 < offset 245 < thing.contract.source_mapping.end 246 ): 247 248 self._offset_to_definitions[ref.filename][offset].add(definition) 249 250 else: 251 self._offset_to_definitions[ref.filename][offset].add(definition) 252 253 self._offset_to_implementations[ref.filename][offset].update(implementations) 254 self._offset_to_references[ref.filename][offset] |= set(references) 255 256 def _compute_offsets_to_ref_impl_decl(self): # pylint: disable=too-many-branches 257 self._offset_to_references = defaultdict(lambda: defaultdict(lambda: set())) 258 self._offset_to_definitions = defaultdict(lambda: defaultdict(lambda: set())) 259 self._offset_to_implementations = defaultdict(lambda: defaultdict(lambda: set())) 260 self._offset_to_objects = defaultdict(lambda: defaultdict(lambda: set())) 261 262 for compilation_unit in self._compilation_units: 263 for contract in compilation_unit.contracts: 264 self._compute_offsets_from_thing(contract) 265 266 for function in contract.functions_declared: 267 self._compute_offsets_from_thing(function) 268 for variable in function.local_variables: 269 self._compute_offsets_from_thing(variable) 270 for modifier in contract.modifiers_declared: 271 self._compute_offsets_from_thing(modifier) 272 for variable in modifier.local_variables: 273 self._compute_offsets_from_thing(variable) 274 275 for var in contract.state_variables: 276 self._compute_offsets_from_thing(var) 277 278 for st in contract.structures: 279 self._compute_offsets_from_thing(st) 280 281 for enum in contract.enums: 282 self._compute_offsets_from_thing(enum) 283 284 for event in contract.events: 285 self._compute_offsets_from_thing(event) 286 287 for typ in contract.type_aliases: 288 self._compute_offsets_from_thing(typ) 289 290 for enum in compilation_unit.enums_top_level: 291 self._compute_offsets_from_thing(enum) 292 for event in compilation_unit.events_top_level: 293 self._compute_offsets_from_thing(event) 294 for function in compilation_unit.functions_top_level: 295 self._compute_offsets_from_thing(function) 296 for st in compilation_unit.structures_top_level: 297 self._compute_offsets_from_thing(st) 298 for var in compilation_unit.variables_top_level: 299 self._compute_offsets_from_thing(var) 300 for typ in compilation_unit.type_aliases.values(): 301 self._compute_offsets_from_thing(typ) 302 for err in compilation_unit.custom_errors: 303 self._compute_offsets_from_thing(err) 304 for event in compilation_unit.events_top_level: 305 self._compute_offsets_from_thing(event) 306 for import_directive in compilation_unit.import_directives: 307 self._compute_offsets_from_thing(import_directive) 308 for pragma in compilation_unit.pragma_directives: 309 self._compute_offsets_from_thing(pragma) 310 311 def offset_to_references(self, filename_str: str, offset: int) -> Set[Source]: 312 if self._offset_to_references is None: 313 self._compute_offsets_to_ref_impl_decl() 314 filename: Filename = self.crytic_compile.filename_lookup(filename_str) 315 return self._offset_to_references[filename][offset] 316 317 def offset_to_implementations(self, filename_str: str, offset: int) -> Set[Source]: 318 if self._offset_to_implementations is None: 319 self._compute_offsets_to_ref_impl_decl() 320 filename: Filename = self.crytic_compile.filename_lookup(filename_str) 321 return self._offset_to_implementations[filename][offset] 322 323 def offset_to_definitions(self, filename_str: str, offset: int) -> Set[Source]: 324 if self._offset_to_definitions is None: 325 self._compute_offsets_to_ref_impl_decl() 326 filename: Filename = self.crytic_compile.filename_lookup(filename_str) 327 return self._offset_to_definitions[filename][offset] 328 329 # endregion 330 ################################################################################### 331 ################################################################################### 332 # region Filtering results 333 ################################################################################### 334 ################################################################################### 335 336 def parse_ignore_comments(self, file: str) -> None: 337 # The first time we check a file, find all start/end ignore comments and memoize them. 338 line_number = 1 339 while True: 340 341 line_text = self.crytic_compile.get_code_from_line(file, line_number) 342 if line_text is None: 343 break 344 345 start_regex = r"^\s*//\s*slither-disable-start\s*([a-zA-Z0-9_,-]*)" 346 end_regex = r"^\s*//\s*slither-disable-end\s*([a-zA-Z0-9_,-]*)" 347 start_match = re.findall(start_regex, line_text.decode("utf8")) 348 end_match = re.findall(end_regex, line_text.decode("utf8")) 349 350 if start_match: 351 ignored = start_match[0].split(",") 352 if ignored: 353 for check in ignored: 354 vals = self._ignore_ranges[file][check] 355 if len(vals) == 0 or vals[-1][1] != float("inf"): 356 # First item in the array, or the prior item is fully populated. 357 self._ignore_ranges[file][check].append((line_number, float("inf"))) 358 else: 359 logger.error( 360 f"Consecutive slither-disable-starts without slither-disable-end in {file}#{line_number}" 361 ) 362 return 363 364 if end_match: 365 ignored = end_match[0].split(",") 366 if ignored: 367 for check in ignored: 368 vals = self._ignore_ranges[file][check] 369 if len(vals) == 0 or vals[-1][1] != float("inf"): 370 logger.error( 371 f"slither-disable-end without slither-disable-start in {file}#{line_number}" 372 ) 373 return 374 self._ignore_ranges[file][check][-1] = (vals[-1][0], line_number) 375 376 line_number += 1 377 378 def has_ignore_comment(self, r: Dict) -> bool: 379 """ 380 Check if the result has an ignore comment in the file or on the preceding line, in which 381 case, it is not valid 382 """ 383 if not self.crytic_compile: 384 return False 385 mapping_elements_with_lines = ( 386 ( 387 posixpath.normpath(elem["source_mapping"]["filename_absolute"]), 388 elem["source_mapping"]["lines"], 389 ) 390 for elem in r["elements"] 391 if "source_mapping" in elem 392 and "filename_absolute" in elem["source_mapping"] 393 and "lines" in elem["source_mapping"] 394 and len(elem["source_mapping"]["lines"]) > 0 395 ) 396 397 for file, lines in mapping_elements_with_lines: 398 399 # Check if result is within an ignored range. 400 ignore_ranges = self._ignore_ranges[file][r["check"]] + self._ignore_ranges[file]["all"] 401 for start, end in ignore_ranges: 402 # The full check must be within the ignore range to be ignored. 403 if start < lines[0] and end > lines[-1]: 404 return True 405 406 # Check for next-line matchers. 407 ignore_line_index = min(lines) - 1 408 ignore_line_text = self.crytic_compile.get_code_from_line(file, ignore_line_index) 409 if ignore_line_text: 410 match = re.findall( 411 r"^\s*//\s*slither-disable-next-line\s*([a-zA-Z0-9_,-]*)", 412 ignore_line_text.decode("utf8"), 413 ) 414 if match: 415 ignored = match[0].split(",") 416 if ignored and ("all" in ignored or any(r["check"] == c for c in ignored)): 417 return True 418 419 return False 420 421 def valid_result(self, r: Dict) -> bool: 422 """ 423 Check if the result is valid 424 A result is invalid if: 425 - All its source paths belong to the source path filtered 426 - Or a similar result was reported and saved during a previous run 427 - The --exclude-dependencies flag is set and results are only related to dependencies 428 - There is an ignore comment on the preceding line or in the file 429 """ 430 431 # Remove duplicate due to the multiple compilation support 432 if r["id"] in self._currently_seen_resuts: 433 return False 434 self._currently_seen_resuts.add(r["id"]) 435 436 source_mapping_elements = [ 437 elem["source_mapping"].get("filename_absolute", "unknown") 438 for elem in r["elements"] 439 if "source_mapping" in elem 440 ] 441 442 # Use POSIX-style paths so that filter_paths|include_paths works across different 443 # OSes. Convert to a list so elements don't get consumed and are lost 444 # while evaluating the first pattern 445 source_mapping_elements = list( 446 map(lambda x: pathlib.Path(x).resolve().as_posix() if x else x, source_mapping_elements) 447 ) 448 (matching, paths, msg_err) = ( 449 (True, self._paths_to_include, "--include-paths") 450 if self._paths_to_include 451 else (False, self._paths_to_filter, "--filter-paths") 452 ) 453 454 for path in paths: 455 try: 456 if any( 457 bool(re.search(_relative_path_format(path), src_mapping)) 458 for src_mapping in source_mapping_elements 459 ): 460 matching = not matching 461 break 462 except re.error: 463 logger.error( 464 f"Incorrect regular expression for {msg_err} {path}." 465 "\nSlither supports the Python re format" 466 ": https://docs.python.org/3/library/re.html" 467 ) 468 469 if r["elements"] and matching: 470 return False 471 472 if self._show_ignored_findings: 473 return True 474 if self.has_ignore_comment(r): 475 return False 476 if r["id"] in self._previous_results_ids: 477 return False 478 if r["elements"] and self._exclude_dependencies: 479 if all(element["source_mapping"]["is_dependency"] for element in r["elements"]): 480 return False 481 # Conserve previous result filtering. This is conserved for compatibility, but is meant to be removed 482 if r["description"] in [pr["description"] for pr in self._previous_results]: 483 return False 484 485 return True 486 487 def load_previous_results(self) -> None: 488 self.load_previous_results_from_sarif() 489 490 filename = self._previous_results_filename 491 try: 492 if os.path.isfile(filename): 493 with open(filename, encoding="utf8") as f: 494 self._previous_results = json.load(f) 495 if self._previous_results: 496 for r in self._previous_results: 497 if "id" in r: 498 self._previous_results_ids.add(r["id"]) 499 500 except json.decoder.JSONDecodeError: 501 logger.error(red(f"Impossible to decode {filename}. Consider removing the file")) 502 503 def load_previous_results_from_sarif(self) -> None: 504 sarif = pathlib.Path(self.sarif_input) 505 triage = pathlib.Path(self.sarif_triage) 506 507 if not sarif.exists(): 508 return 509 if not triage.exists(): 510 return 511 512 triaged = read_triage_info(sarif, triage) 513 514 for id_triaged in triaged: 515 self._previous_results_ids.add(id_triaged) 516 517 def write_results_to_hide(self) -> None: 518 if not self._results_to_hide: 519 return 520 filename = self._previous_results_filename 521 with open(filename, "w", encoding="utf8") as f: 522 results = self._results_to_hide + self._previous_results 523 json.dump(results, f) 524 525 def save_results_to_hide(self, results: List[Dict]) -> None: 526 self._results_to_hide += results 527 528 def add_path_to_filter(self, path: str): 529 """ 530 Add path to filter 531 Path are used through direct comparison (no regex) 532 """ 533 self._paths_to_filter.add(path) 534 535 def add_path_to_include(self, path: str): 536 """ 537 Add path to include 538 Path are used through direct comparison (no regex) 539 """ 540 self._paths_to_include.add(path) 541 542 # endregion 543 ################################################################################### 544 ################################################################################### 545 # region Crytic compile 546 ################################################################################### 547 ################################################################################### 548 549 @property 550 def crytic_compile(self) -> CryticCompile: 551 return self._crytic_compile # type: ignore 552 553 # endregion 554 ################################################################################### 555 ################################################################################### 556 # region Format 557 ################################################################################### 558 ################################################################################### 559 560 @property 561 def generate_patches(self) -> bool: 562 return self._generate_patches 563 564 @generate_patches.setter 565 def generate_patches(self, p: bool): 566 self._generate_patches = p 567 568 # endregion 569 ################################################################################### 570 ################################################################################### 571 # region Internals 572 ################################################################################### 573 ################################################################################### 574 575 @property 576 def disallow_partial(self) -> bool: 577 """ 578 Return true if partial analyses are disallowed 579 For example, codebase with duplicate names will lead to partial analyses 580 581 :return: 582 """ 583 return self._disallow_partial 584 585 @property 586 def skip_assembly(self) -> bool: 587 return self._skip_assembly 588 589 @property 590 def show_ignore_findings(self) -> bool: 591 return self._show_ignored_findings 592 593 # endregion
40class SlitherCore(Context): 41 """ 42 Slither static analyzer 43 """ 44 45 def __init__(self) -> None: 46 super().__init__() 47 48 self._filename: Optional[str] = None 49 self._raw_source_code: Dict[str, str] = {} 50 self._source_code_to_line: Optional[Dict[str, List[str]]] = None 51 52 self._previous_results_filename: str = "slither.db.json" 53 54 # TODO: add cli flag to set these variables 55 self.sarif_input: str = "export.sarif" 56 self.sarif_triage: str = "export.sarif.sarifexplorer" 57 self._results_to_hide: List = [] 58 self._previous_results: List = [] 59 # From triaged result 60 self._previous_results_ids: Set[str] = set() 61 # Every slither object has a list of result from detector 62 # Because of the multiple compilation support, we might analyze 63 # Multiple time the same result, so we remove duplicates 64 self._currently_seen_resuts: Set[str] = set() 65 self._paths_to_filter: Set[str] = set() 66 self._paths_to_include: Set[str] = set() 67 68 self._crytic_compile: Optional[CryticCompile] = None 69 70 self._generate_patches = False 71 self._exclude_dependencies = False 72 73 self._markdown_root = "" 74 75 # If set to true, slither will not catch errors during parsing 76 self._disallow_partial: bool = False 77 self._skip_assembly: bool = False 78 79 self._show_ignored_findings = False 80 81 # Maps from file to detector name to the start/end ranges for that detector. 82 # Infinity is used to signal a detector has no end range. 83 self._ignore_ranges: Dict[str, Dict[str, List[Tuple[int, ...]]]] = defaultdict( 84 lambda: defaultdict(lambda: [(-1, -1)]) 85 ) 86 87 self._compilation_units: List[SlitherCompilationUnit] = [] 88 89 self._contracts: List[Contract] = [] 90 self._contracts_derived: List[Contract] = [] 91 92 self._offset_to_objects: Optional[Dict[Filename, Dict[int, Set[SourceMapping]]]] = None 93 self._offset_to_references: Optional[Dict[Filename, Dict[int, Set[Source]]]] = None 94 self._offset_to_implementations: Optional[Dict[Filename, Dict[int, Set[Source]]]] = None 95 self._offset_to_definitions: Optional[Dict[Filename, Dict[int, Set[Source]]]] = None 96 97 # Line prefix is used during the source mapping generation 98 # By default we generate file.sol#1 99 # But we allow to alter this (ex: file.sol:1) for vscode integration 100 self.line_prefix: str = "#" 101 102 # Use by the echidna printer 103 # If true, partial analysis is allowed 104 self.no_fail = False 105 106 self.skip_data_dependency = False 107 108 @property 109 def compilation_units(self) -> List[SlitherCompilationUnit]: 110 return list(self._compilation_units) 111 112 def add_compilation_unit(self, compilation_unit: SlitherCompilationUnit): 113 self._compilation_units.append(compilation_unit) 114 115 # endregion 116 ################################################################################### 117 ################################################################################### 118 # region Contracts 119 ################################################################################### 120 ################################################################################### 121 122 @property 123 def contracts(self) -> List[Contract]: 124 if not self._contracts: 125 all_contracts = [ 126 compilation_unit.contracts for compilation_unit in self._compilation_units 127 ] 128 self._contracts = [item for sublist in all_contracts for item in sublist] 129 return self._contracts 130 131 @property 132 def contracts_derived(self) -> List[Contract]: 133 if not self._contracts_derived: 134 all_contracts = [ 135 compilation_unit.contracts_derived for compilation_unit in self._compilation_units 136 ] 137 self._contracts_derived = [item for sublist in all_contracts for item in sublist] 138 return self._contracts_derived 139 140 def get_contract_from_name(self, contract_name: Union[str, Constant]) -> List[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 contracts = [] 149 for compilation_unit in self._compilation_units: 150 contracts += compilation_unit.get_contract_from_name(contract_name) 151 return contracts 152 153 ################################################################################### 154 ################################################################################### 155 # region Source code 156 ################################################################################### 157 ################################################################################### 158 159 @property 160 def source_code(self) -> Dict[str, str]: 161 """{filename: source_code (str)}: source code""" 162 return self._raw_source_code 163 164 @property 165 def filename(self) -> Optional[str]: 166 """str: Filename.""" 167 return self._filename 168 169 @filename.setter 170 def filename(self, filename: str): 171 self._filename = filename 172 173 def add_source_code(self, path: str) -> None: 174 """ 175 :param path: 176 :return: 177 """ 178 if self.crytic_compile and path in self.crytic_compile.src_content: 179 self.source_code[path] = self.crytic_compile.src_content[path] 180 else: 181 with open(path, encoding="utf8", newline="") as f: 182 self.source_code[path] = f.read() 183 184 self.parse_ignore_comments(path) 185 186 @property 187 def markdown_root(self) -> str: 188 return self._markdown_root 189 190 def print_functions(self, d: str): 191 """ 192 Export all the functions to dot files 193 """ 194 for compilation_unit in self._compilation_units: 195 for c in compilation_unit.contracts: 196 for f in c.functions: 197 f.cfg_to_dot(os.path.join(d, f"{c.name}.{f.name}.dot")) 198 199 def offset_to_objects(self, filename_str: str, offset: int) -> Set[SourceMapping]: 200 if self._offset_to_objects is None: 201 self._compute_offsets_to_ref_impl_decl() 202 filename: Filename = self.crytic_compile.filename_lookup(filename_str) 203 return self._offset_to_objects[filename][offset] 204 205 def _compute_offsets_from_thing(self, thing: SourceMapping): 206 definition = get_definition(thing, self.crytic_compile) 207 references = get_references(thing) 208 implementations = get_all_implementations(thing, self.contracts) 209 210 for offset in range(definition.start, definition.end + 1): 211 if ( 212 isinstance(thing, (TopLevel, Contract)) 213 or ( 214 isinstance(thing, FunctionContract) 215 and thing.contract_declarer == thing.contract 216 ) 217 or (isinstance(thing, ContractLevel) and not isinstance(thing, FunctionContract)) 218 ): 219 220 self._offset_to_objects[definition.filename][offset].add(thing) 221 222 self._offset_to_definitions[definition.filename][offset].add(definition) 223 self._offset_to_implementations[definition.filename][offset].update(implementations) 224 self._offset_to_references[definition.filename][offset] |= set(references) 225 226 for ref in references: 227 for offset in range(ref.start, ref.end + 1): 228 is_declared_function = ( 229 isinstance(thing, FunctionContract) 230 and thing.contract_declarer == thing.contract 231 ) 232 if ( 233 isinstance(thing, TopLevel) 234 or is_declared_function 235 or ( 236 isinstance(thing, ContractLevel) and not isinstance(thing, FunctionContract) 237 ) 238 ): 239 self._offset_to_objects[definition.filename][offset].add(thing) 240 241 if is_declared_function: 242 # Only show the nearest lexical definition for declared contract-level functions 243 if ( 244 thing.contract.source_mapping.start 245 < offset 246 < thing.contract.source_mapping.end 247 ): 248 249 self._offset_to_definitions[ref.filename][offset].add(definition) 250 251 else: 252 self._offset_to_definitions[ref.filename][offset].add(definition) 253 254 self._offset_to_implementations[ref.filename][offset].update(implementations) 255 self._offset_to_references[ref.filename][offset] |= set(references) 256 257 def _compute_offsets_to_ref_impl_decl(self): # pylint: disable=too-many-branches 258 self._offset_to_references = defaultdict(lambda: defaultdict(lambda: set())) 259 self._offset_to_definitions = defaultdict(lambda: defaultdict(lambda: set())) 260 self._offset_to_implementations = defaultdict(lambda: defaultdict(lambda: set())) 261 self._offset_to_objects = defaultdict(lambda: defaultdict(lambda: set())) 262 263 for compilation_unit in self._compilation_units: 264 for contract in compilation_unit.contracts: 265 self._compute_offsets_from_thing(contract) 266 267 for function in contract.functions_declared: 268 self._compute_offsets_from_thing(function) 269 for variable in function.local_variables: 270 self._compute_offsets_from_thing(variable) 271 for modifier in contract.modifiers_declared: 272 self._compute_offsets_from_thing(modifier) 273 for variable in modifier.local_variables: 274 self._compute_offsets_from_thing(variable) 275 276 for var in contract.state_variables: 277 self._compute_offsets_from_thing(var) 278 279 for st in contract.structures: 280 self._compute_offsets_from_thing(st) 281 282 for enum in contract.enums: 283 self._compute_offsets_from_thing(enum) 284 285 for event in contract.events: 286 self._compute_offsets_from_thing(event) 287 288 for typ in contract.type_aliases: 289 self._compute_offsets_from_thing(typ) 290 291 for enum in compilation_unit.enums_top_level: 292 self._compute_offsets_from_thing(enum) 293 for event in compilation_unit.events_top_level: 294 self._compute_offsets_from_thing(event) 295 for function in compilation_unit.functions_top_level: 296 self._compute_offsets_from_thing(function) 297 for st in compilation_unit.structures_top_level: 298 self._compute_offsets_from_thing(st) 299 for var in compilation_unit.variables_top_level: 300 self._compute_offsets_from_thing(var) 301 for typ in compilation_unit.type_aliases.values(): 302 self._compute_offsets_from_thing(typ) 303 for err in compilation_unit.custom_errors: 304 self._compute_offsets_from_thing(err) 305 for event in compilation_unit.events_top_level: 306 self._compute_offsets_from_thing(event) 307 for import_directive in compilation_unit.import_directives: 308 self._compute_offsets_from_thing(import_directive) 309 for pragma in compilation_unit.pragma_directives: 310 self._compute_offsets_from_thing(pragma) 311 312 def offset_to_references(self, filename_str: str, offset: int) -> Set[Source]: 313 if self._offset_to_references is None: 314 self._compute_offsets_to_ref_impl_decl() 315 filename: Filename = self.crytic_compile.filename_lookup(filename_str) 316 return self._offset_to_references[filename][offset] 317 318 def offset_to_implementations(self, filename_str: str, offset: int) -> Set[Source]: 319 if self._offset_to_implementations is None: 320 self._compute_offsets_to_ref_impl_decl() 321 filename: Filename = self.crytic_compile.filename_lookup(filename_str) 322 return self._offset_to_implementations[filename][offset] 323 324 def offset_to_definitions(self, filename_str: str, offset: int) -> Set[Source]: 325 if self._offset_to_definitions is None: 326 self._compute_offsets_to_ref_impl_decl() 327 filename: Filename = self.crytic_compile.filename_lookup(filename_str) 328 return self._offset_to_definitions[filename][offset] 329 330 # endregion 331 ################################################################################### 332 ################################################################################### 333 # region Filtering results 334 ################################################################################### 335 ################################################################################### 336 337 def parse_ignore_comments(self, file: str) -> None: 338 # The first time we check a file, find all start/end ignore comments and memoize them. 339 line_number = 1 340 while True: 341 342 line_text = self.crytic_compile.get_code_from_line(file, line_number) 343 if line_text is None: 344 break 345 346 start_regex = r"^\s*//\s*slither-disable-start\s*([a-zA-Z0-9_,-]*)" 347 end_regex = r"^\s*//\s*slither-disable-end\s*([a-zA-Z0-9_,-]*)" 348 start_match = re.findall(start_regex, line_text.decode("utf8")) 349 end_match = re.findall(end_regex, line_text.decode("utf8")) 350 351 if start_match: 352 ignored = start_match[0].split(",") 353 if ignored: 354 for check in ignored: 355 vals = self._ignore_ranges[file][check] 356 if len(vals) == 0 or vals[-1][1] != float("inf"): 357 # First item in the array, or the prior item is fully populated. 358 self._ignore_ranges[file][check].append((line_number, float("inf"))) 359 else: 360 logger.error( 361 f"Consecutive slither-disable-starts without slither-disable-end in {file}#{line_number}" 362 ) 363 return 364 365 if end_match: 366 ignored = end_match[0].split(",") 367 if ignored: 368 for check in ignored: 369 vals = self._ignore_ranges[file][check] 370 if len(vals) == 0 or vals[-1][1] != float("inf"): 371 logger.error( 372 f"slither-disable-end without slither-disable-start in {file}#{line_number}" 373 ) 374 return 375 self._ignore_ranges[file][check][-1] = (vals[-1][0], line_number) 376 377 line_number += 1 378 379 def has_ignore_comment(self, r: Dict) -> bool: 380 """ 381 Check if the result has an ignore comment in the file or on the preceding line, in which 382 case, it is not valid 383 """ 384 if not self.crytic_compile: 385 return False 386 mapping_elements_with_lines = ( 387 ( 388 posixpath.normpath(elem["source_mapping"]["filename_absolute"]), 389 elem["source_mapping"]["lines"], 390 ) 391 for elem in r["elements"] 392 if "source_mapping" in elem 393 and "filename_absolute" in elem["source_mapping"] 394 and "lines" in elem["source_mapping"] 395 and len(elem["source_mapping"]["lines"]) > 0 396 ) 397 398 for file, lines in mapping_elements_with_lines: 399 400 # Check if result is within an ignored range. 401 ignore_ranges = self._ignore_ranges[file][r["check"]] + self._ignore_ranges[file]["all"] 402 for start, end in ignore_ranges: 403 # The full check must be within the ignore range to be ignored. 404 if start < lines[0] and end > lines[-1]: 405 return True 406 407 # Check for next-line matchers. 408 ignore_line_index = min(lines) - 1 409 ignore_line_text = self.crytic_compile.get_code_from_line(file, ignore_line_index) 410 if ignore_line_text: 411 match = re.findall( 412 r"^\s*//\s*slither-disable-next-line\s*([a-zA-Z0-9_,-]*)", 413 ignore_line_text.decode("utf8"), 414 ) 415 if match: 416 ignored = match[0].split(",") 417 if ignored and ("all" in ignored or any(r["check"] == c for c in ignored)): 418 return True 419 420 return False 421 422 def valid_result(self, r: Dict) -> bool: 423 """ 424 Check if the result is valid 425 A result is invalid if: 426 - All its source paths belong to the source path filtered 427 - Or a similar result was reported and saved during a previous run 428 - The --exclude-dependencies flag is set and results are only related to dependencies 429 - There is an ignore comment on the preceding line or in the file 430 """ 431 432 # Remove duplicate due to the multiple compilation support 433 if r["id"] in self._currently_seen_resuts: 434 return False 435 self._currently_seen_resuts.add(r["id"]) 436 437 source_mapping_elements = [ 438 elem["source_mapping"].get("filename_absolute", "unknown") 439 for elem in r["elements"] 440 if "source_mapping" in elem 441 ] 442 443 # Use POSIX-style paths so that filter_paths|include_paths works across different 444 # OSes. Convert to a list so elements don't get consumed and are lost 445 # while evaluating the first pattern 446 source_mapping_elements = list( 447 map(lambda x: pathlib.Path(x).resolve().as_posix() if x else x, source_mapping_elements) 448 ) 449 (matching, paths, msg_err) = ( 450 (True, self._paths_to_include, "--include-paths") 451 if self._paths_to_include 452 else (False, self._paths_to_filter, "--filter-paths") 453 ) 454 455 for path in paths: 456 try: 457 if any( 458 bool(re.search(_relative_path_format(path), src_mapping)) 459 for src_mapping in source_mapping_elements 460 ): 461 matching = not matching 462 break 463 except re.error: 464 logger.error( 465 f"Incorrect regular expression for {msg_err} {path}." 466 "\nSlither supports the Python re format" 467 ": https://docs.python.org/3/library/re.html" 468 ) 469 470 if r["elements"] and matching: 471 return False 472 473 if self._show_ignored_findings: 474 return True 475 if self.has_ignore_comment(r): 476 return False 477 if r["id"] in self._previous_results_ids: 478 return False 479 if r["elements"] and self._exclude_dependencies: 480 if all(element["source_mapping"]["is_dependency"] for element in r["elements"]): 481 return False 482 # Conserve previous result filtering. This is conserved for compatibility, but is meant to be removed 483 if r["description"] in [pr["description"] for pr in self._previous_results]: 484 return False 485 486 return True 487 488 def load_previous_results(self) -> None: 489 self.load_previous_results_from_sarif() 490 491 filename = self._previous_results_filename 492 try: 493 if os.path.isfile(filename): 494 with open(filename, encoding="utf8") as f: 495 self._previous_results = json.load(f) 496 if self._previous_results: 497 for r in self._previous_results: 498 if "id" in r: 499 self._previous_results_ids.add(r["id"]) 500 501 except json.decoder.JSONDecodeError: 502 logger.error(red(f"Impossible to decode {filename}. Consider removing the file")) 503 504 def load_previous_results_from_sarif(self) -> None: 505 sarif = pathlib.Path(self.sarif_input) 506 triage = pathlib.Path(self.sarif_triage) 507 508 if not sarif.exists(): 509 return 510 if not triage.exists(): 511 return 512 513 triaged = read_triage_info(sarif, triage) 514 515 for id_triaged in triaged: 516 self._previous_results_ids.add(id_triaged) 517 518 def write_results_to_hide(self) -> None: 519 if not self._results_to_hide: 520 return 521 filename = self._previous_results_filename 522 with open(filename, "w", encoding="utf8") as f: 523 results = self._results_to_hide + self._previous_results 524 json.dump(results, f) 525 526 def save_results_to_hide(self, results: List[Dict]) -> None: 527 self._results_to_hide += results 528 529 def add_path_to_filter(self, path: str): 530 """ 531 Add path to filter 532 Path are used through direct comparison (no regex) 533 """ 534 self._paths_to_filter.add(path) 535 536 def add_path_to_include(self, path: str): 537 """ 538 Add path to include 539 Path are used through direct comparison (no regex) 540 """ 541 self._paths_to_include.add(path) 542 543 # endregion 544 ################################################################################### 545 ################################################################################### 546 # region Crytic compile 547 ################################################################################### 548 ################################################################################### 549 550 @property 551 def crytic_compile(self) -> CryticCompile: 552 return self._crytic_compile # type: ignore 553 554 # endregion 555 ################################################################################### 556 ################################################################################### 557 # region Format 558 ################################################################################### 559 ################################################################################### 560 561 @property 562 def generate_patches(self) -> bool: 563 return self._generate_patches 564 565 @generate_patches.setter 566 def generate_patches(self, p: bool): 567 self._generate_patches = p 568 569 # endregion 570 ################################################################################### 571 ################################################################################### 572 # region Internals 573 ################################################################################### 574 ################################################################################### 575 576 @property 577 def disallow_partial(self) -> bool: 578 """ 579 Return true if partial analyses are disallowed 580 For example, codebase with duplicate names will lead to partial analyses 581 582 :return: 583 """ 584 return self._disallow_partial 585 586 @property 587 def skip_assembly(self) -> bool: 588 return self._skip_assembly 589 590 @property 591 def show_ignore_findings(self) -> bool: 592 return self._show_ignored_findings 593 594 # endregion
Slither static analyzer
131 @property 132 def contracts_derived(self) -> List[Contract]: 133 if not self._contracts_derived: 134 all_contracts = [ 135 compilation_unit.contracts_derived for compilation_unit in self._compilation_units 136 ] 137 self._contracts_derived = [item for sublist in all_contracts for item in sublist] 138 return self._contracts_derived
140 def get_contract_from_name(self, contract_name: Union[str, Constant]) -> List[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 contracts = [] 149 for compilation_unit in self._compilation_units: 150 contracts += compilation_unit.get_contract_from_name(contract_name) 151 return contracts
Return a contract from a name Args: contract_name (str): name of the contract Returns: Contract
159 @property 160 def source_code(self) -> Dict[str, str]: 161 """{filename: source_code (str)}: source code""" 162 return self._raw_source_code
{filename: source_code (str)}: source code
164 @property 165 def filename(self) -> Optional[str]: 166 """str: Filename.""" 167 return self._filename
str: Filename.
173 def add_source_code(self, path: str) -> None: 174 """ 175 :param path: 176 :return: 177 """ 178 if self.crytic_compile and path in self.crytic_compile.src_content: 179 self.source_code[path] = self.crytic_compile.src_content[path] 180 else: 181 with open(path, encoding="utf8", newline="") as f: 182 self.source_code[path] = f.read() 183 184 self.parse_ignore_comments(path)
Parameters
- path:
Returns
190 def print_functions(self, d: str): 191 """ 192 Export all the functions to dot files 193 """ 194 for compilation_unit in self._compilation_units: 195 for c in compilation_unit.contracts: 196 for f in c.functions: 197 f.cfg_to_dot(os.path.join(d, f"{c.name}.{f.name}.dot"))
Export all the functions to dot files
312 def offset_to_references(self, filename_str: str, offset: int) -> Set[Source]: 313 if self._offset_to_references is None: 314 self._compute_offsets_to_ref_impl_decl() 315 filename: Filename = self.crytic_compile.filename_lookup(filename_str) 316 return self._offset_to_references[filename][offset]
318 def offset_to_implementations(self, filename_str: str, offset: int) -> Set[Source]: 319 if self._offset_to_implementations is None: 320 self._compute_offsets_to_ref_impl_decl() 321 filename: Filename = self.crytic_compile.filename_lookup(filename_str) 322 return self._offset_to_implementations[filename][offset]
324 def offset_to_definitions(self, filename_str: str, offset: int) -> Set[Source]: 325 if self._offset_to_definitions is None: 326 self._compute_offsets_to_ref_impl_decl() 327 filename: Filename = self.crytic_compile.filename_lookup(filename_str) 328 return self._offset_to_definitions[filename][offset]
337 def parse_ignore_comments(self, file: str) -> None: 338 # The first time we check a file, find all start/end ignore comments and memoize them. 339 line_number = 1 340 while True: 341 342 line_text = self.crytic_compile.get_code_from_line(file, line_number) 343 if line_text is None: 344 break 345 346 start_regex = r"^\s*//\s*slither-disable-start\s*([a-zA-Z0-9_,-]*)" 347 end_regex = r"^\s*//\s*slither-disable-end\s*([a-zA-Z0-9_,-]*)" 348 start_match = re.findall(start_regex, line_text.decode("utf8")) 349 end_match = re.findall(end_regex, line_text.decode("utf8")) 350 351 if start_match: 352 ignored = start_match[0].split(",") 353 if ignored: 354 for check in ignored: 355 vals = self._ignore_ranges[file][check] 356 if len(vals) == 0 or vals[-1][1] != float("inf"): 357 # First item in the array, or the prior item is fully populated. 358 self._ignore_ranges[file][check].append((line_number, float("inf"))) 359 else: 360 logger.error( 361 f"Consecutive slither-disable-starts without slither-disable-end in {file}#{line_number}" 362 ) 363 return 364 365 if end_match: 366 ignored = end_match[0].split(",") 367 if ignored: 368 for check in ignored: 369 vals = self._ignore_ranges[file][check] 370 if len(vals) == 0 or vals[-1][1] != float("inf"): 371 logger.error( 372 f"slither-disable-end without slither-disable-start in {file}#{line_number}" 373 ) 374 return 375 self._ignore_ranges[file][check][-1] = (vals[-1][0], line_number) 376 377 line_number += 1
379 def has_ignore_comment(self, r: Dict) -> bool: 380 """ 381 Check if the result has an ignore comment in the file or on the preceding line, in which 382 case, it is not valid 383 """ 384 if not self.crytic_compile: 385 return False 386 mapping_elements_with_lines = ( 387 ( 388 posixpath.normpath(elem["source_mapping"]["filename_absolute"]), 389 elem["source_mapping"]["lines"], 390 ) 391 for elem in r["elements"] 392 if "source_mapping" in elem 393 and "filename_absolute" in elem["source_mapping"] 394 and "lines" in elem["source_mapping"] 395 and len(elem["source_mapping"]["lines"]) > 0 396 ) 397 398 for file, lines in mapping_elements_with_lines: 399 400 # Check if result is within an ignored range. 401 ignore_ranges = self._ignore_ranges[file][r["check"]] + self._ignore_ranges[file]["all"] 402 for start, end in ignore_ranges: 403 # The full check must be within the ignore range to be ignored. 404 if start < lines[0] and end > lines[-1]: 405 return True 406 407 # Check for next-line matchers. 408 ignore_line_index = min(lines) - 1 409 ignore_line_text = self.crytic_compile.get_code_from_line(file, ignore_line_index) 410 if ignore_line_text: 411 match = re.findall( 412 r"^\s*//\s*slither-disable-next-line\s*([a-zA-Z0-9_,-]*)", 413 ignore_line_text.decode("utf8"), 414 ) 415 if match: 416 ignored = match[0].split(",") 417 if ignored and ("all" in ignored or any(r["check"] == c for c in ignored)): 418 return True 419 420 return False
Check if the result has an ignore comment in the file or on the preceding line, in which case, it is not valid
422 def valid_result(self, r: Dict) -> bool: 423 """ 424 Check if the result is valid 425 A result is invalid if: 426 - All its source paths belong to the source path filtered 427 - Or a similar result was reported and saved during a previous run 428 - The --exclude-dependencies flag is set and results are only related to dependencies 429 - There is an ignore comment on the preceding line or in the file 430 """ 431 432 # Remove duplicate due to the multiple compilation support 433 if r["id"] in self._currently_seen_resuts: 434 return False 435 self._currently_seen_resuts.add(r["id"]) 436 437 source_mapping_elements = [ 438 elem["source_mapping"].get("filename_absolute", "unknown") 439 for elem in r["elements"] 440 if "source_mapping" in elem 441 ] 442 443 # Use POSIX-style paths so that filter_paths|include_paths works across different 444 # OSes. Convert to a list so elements don't get consumed and are lost 445 # while evaluating the first pattern 446 source_mapping_elements = list( 447 map(lambda x: pathlib.Path(x).resolve().as_posix() if x else x, source_mapping_elements) 448 ) 449 (matching, paths, msg_err) = ( 450 (True, self._paths_to_include, "--include-paths") 451 if self._paths_to_include 452 else (False, self._paths_to_filter, "--filter-paths") 453 ) 454 455 for path in paths: 456 try: 457 if any( 458 bool(re.search(_relative_path_format(path), src_mapping)) 459 for src_mapping in source_mapping_elements 460 ): 461 matching = not matching 462 break 463 except re.error: 464 logger.error( 465 f"Incorrect regular expression for {msg_err} {path}." 466 "\nSlither supports the Python re format" 467 ": https://docs.python.org/3/library/re.html" 468 ) 469 470 if r["elements"] and matching: 471 return False 472 473 if self._show_ignored_findings: 474 return True 475 if self.has_ignore_comment(r): 476 return False 477 if r["id"] in self._previous_results_ids: 478 return False 479 if r["elements"] and self._exclude_dependencies: 480 if all(element["source_mapping"]["is_dependency"] for element in r["elements"]): 481 return False 482 # Conserve previous result filtering. This is conserved for compatibility, but is meant to be removed 483 if r["description"] in [pr["description"] for pr in self._previous_results]: 484 return False 485 486 return True
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 - The --exclude-dependencies flag is set and results are only related to dependencies - There is an ignore comment on the preceding line or in the file
488 def load_previous_results(self) -> None: 489 self.load_previous_results_from_sarif() 490 491 filename = self._previous_results_filename 492 try: 493 if os.path.isfile(filename): 494 with open(filename, encoding="utf8") as f: 495 self._previous_results = json.load(f) 496 if self._previous_results: 497 for r in self._previous_results: 498 if "id" in r: 499 self._previous_results_ids.add(r["id"]) 500 501 except json.decoder.JSONDecodeError: 502 logger.error(red(f"Impossible to decode {filename}. Consider removing the file"))
504 def load_previous_results_from_sarif(self) -> None: 505 sarif = pathlib.Path(self.sarif_input) 506 triage = pathlib.Path(self.sarif_triage) 507 508 if not sarif.exists(): 509 return 510 if not triage.exists(): 511 return 512 513 triaged = read_triage_info(sarif, triage) 514 515 for id_triaged in triaged: 516 self._previous_results_ids.add(id_triaged)
529 def add_path_to_filter(self, path: str): 530 """ 531 Add path to filter 532 Path are used through direct comparison (no regex) 533 """ 534 self._paths_to_filter.add(path)
Add path to filter Path are used through direct comparison (no regex)
536 def add_path_to_include(self, path: str): 537 """ 538 Add path to include 539 Path are used through direct comparison (no regex) 540 """ 541 self._paths_to_include.add(path)
Add path to include Path are used through direct comparison (no regex)
576 @property 577 def disallow_partial(self) -> bool: 578 """ 579 Return true if partial analyses are disallowed 580 For example, codebase with duplicate names will lead to partial analyses 581 582 :return: 583 """ 584 return self._disallow_partial
Return true if partial analyses are disallowed For example, codebase with duplicate names will lead to partial analyses