slither.utils.output
1import hashlib 2import json 3import logging 4import os 5import zipfile 6from collections import OrderedDict 7from importlib import metadata 8from typing import Any, TYPE_CHECKING 9from zipfile import ZipFile 10 11 12from slither.core.cfg.node import Node 13from slither.core.declarations import ( 14 Contract, 15 Function, 16 Enum, 17 Event, 18 Structure, 19 Pragma, 20 FunctionContract, 21 CustomError, 22) 23from slither.core.source_mapping.source_mapping import SourceMapping 24from slither.core.variables.local_variable import LocalVariable 25from slither.core.variables.variable import Variable 26from slither.exceptions import SlitherError 27from slither.utils.colors import yellow 28from slither.utils.myprettytable import MyPrettyTable 29 30if TYPE_CHECKING: 31 from slither.core.compilation_unit import SlitherCompilationUnit 32 from slither.detectors.abstract_detector import AbstractDetector 33 34logger = logging.getLogger("Slither") 35 36 37class OutputConfig: 38 """Configuration settings for output formatting.""" 39 40 EXCLUDE_LOCATION: bool = False 41 42 43def set_exclude_location(exclude: bool) -> None: 44 """Set whether to exclude location info from detector messages.""" 45 OutputConfig.EXCLUDE_LOCATION = exclude 46 47 48def get_exclude_location() -> bool: 49 """Get whether location info should be excluded from detector messages.""" 50 return OutputConfig.EXCLUDE_LOCATION 51 52 53################################################################################### 54################################################################################### 55# region Output 56################################################################################### 57################################################################################### 58 59 60def output_to_json(filename: str | None, error, results: dict) -> None: 61 """ 62 63 :param filename: Filename where the json will be written. If None or "-", write to stdout 64 :param error: Error to report 65 :param results: Results to report 66 :param logger: Logger where to log potential info 67 :return: 68 """ 69 # Create our encapsulated JSON result. 70 json_result = {"success": error is None, "error": error, "results": results} 71 72 if filename == "-": 73 filename = None 74 75 # Determine if we should output to stdout 76 if filename is None: 77 # Write json to console 78 print(json.dumps(json_result)) 79 else: 80 # Write json to file 81 if os.path.isfile(filename): 82 logger.info(yellow(f"{filename} exists already, the overwrite is prevented")) 83 else: 84 with open(filename, "w", encoding="utf8") as f: 85 json.dump(json_result, f, indent=2) 86 87 88def _output_result_to_sarif( 89 detector: dict, detectors_classes: list["AbstractDetector"], sarif: dict 90) -> None: 91 confidence = "very-high" 92 if detector["confidence"] == "Medium": 93 confidence = "high" 94 elif detector["confidence"] == "Low": 95 confidence = "medium" 96 elif detector["confidence"] == "Informational": 97 confidence = "low" 98 99 risk = "0.0" 100 if detector["impact"] == "High": 101 risk = "8.0" 102 elif detector["impact"] == "Medium": 103 risk = "4.0" 104 elif detector["impact"] == "Low": 105 risk = "3.0" 106 107 detector_class = next(d for d in detectors_classes if detector["check"] == d.ARGUMENT) 108 check_id = ( 109 str(detector_class.IMPACT.value) 110 + "-" 111 + str(detector_class.CONFIDENCE.value) 112 + "-" 113 + detector["check"] 114 ) 115 116 rule = { 117 "id": check_id, 118 "name": detector["check"], 119 "properties": {"precision": confidence, "security-severity": risk}, 120 "shortDescription": {"text": detector_class.WIKI_TITLE}, 121 "help": {"text": detector_class.WIKI_RECOMMENDATION}, 122 } 123 # Add the rule if does not exist yet 124 if len([x for x in sarif["runs"][0]["tool"]["driver"]["rules"] if x["id"] == check_id]) == 0: 125 sarif["runs"][0]["tool"]["driver"]["rules"].append(rule) 126 127 if not detector["elements"]: 128 logger.info(yellow("Cannot generate Github security alert for finding without location")) 129 logger.info(yellow(detector["description"])) 130 logger.info(yellow("This will be supported in a future Slither release")) 131 return 132 133 # From 3.19.10 (http://docs.oasis-open.org/sarif/sarif/v2.0/csprd01/sarif-v2.0-csprd01.html) 134 # The locations array SHALL NOT contain more than one element unless the condition indicated by the result, 135 # if any, can only be corrected by making a change at every location specified in the array. 136 finding = detector["elements"][0] 137 path = finding["source_mapping"]["filename_relative"] 138 start_line = finding["source_mapping"]["lines"][0] 139 end_line = finding["source_mapping"]["lines"][-1] 140 141 sarif["runs"][0]["results"].append( 142 { 143 "ruleId": check_id, 144 "message": {"text": detector["description"], "markdown": detector["markdown"]}, 145 "level": "warning", 146 "locations": [ 147 { 148 "physicalLocation": { 149 "artifactLocation": {"uri": path}, 150 "region": {"startLine": start_line, "endLine": end_line}, 151 } 152 } 153 ], 154 "partialFingerprints": {"id": detector["id"]}, 155 } 156 ) 157 158 159def output_to_sarif( 160 filename: str | None, results: dict, detectors_classes: list[type["AbstractDetector"]] 161) -> None: 162 """ 163 164 :param filename: 165 :type filename: 166 :param results: 167 :type results: 168 :return: 169 :rtype: 170 """ 171 172 sarif: dict[str, Any] = { 173 "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", 174 "version": "2.1.0", 175 "runs": [ 176 { 177 "tool": { 178 "driver": { 179 "name": "Slither", 180 "informationUri": "https://github.com/crytic/slither", 181 "version": metadata.version("slither-analyzer"), 182 "rules": [], 183 } 184 }, 185 "results": [], 186 } 187 ], 188 } 189 190 for detector in results.get("detectors", []): 191 _output_result_to_sarif(detector, detectors_classes, sarif) 192 193 if filename == "-": 194 filename = None 195 196 # Determine if we should output to stdout 197 if filename is None: 198 # Write json to console 199 print(json.dumps(sarif)) 200 else: 201 # Write json to file 202 if os.path.isfile(filename): 203 logger.info(yellow(f"{filename} exists already, the overwrite is prevented")) 204 else: 205 with open(filename, "w", encoding="utf8") as f: 206 json.dump(sarif, f, indent=2) 207 208 209# https://docs.python.org/3/library/zipfile.html#zipfile-objects 210ZIP_TYPES_ACCEPTED = { 211 "lzma": zipfile.ZIP_LZMA, 212 "stored": zipfile.ZIP_STORED, 213 "deflated": zipfile.ZIP_DEFLATED, 214 "bzip2": zipfile.ZIP_BZIP2, 215} 216 217 218def output_to_zip(filename: str, error: str | None, results: dict, zip_type: str = "lzma"): 219 """ 220 Output the results to a zip 221 The file in the zip is named slither_results.json 222 Note: the json file will not have indentation, as a result the resulting json file will be smaller 223 :param zip_type: 224 :param filename: 225 :param error: 226 :param results: 227 :return: 228 """ 229 json_result = {"success": error is None, "error": error, "results": results} 230 if os.path.isfile(filename): 231 logger.info(yellow(f"{filename} exists already, the overwrite is prevented")) 232 else: 233 with ZipFile( 234 filename, 235 "w", 236 compression=ZIP_TYPES_ACCEPTED.get(zip_type, zipfile.ZIP_LZMA), 237 ) as file_desc: 238 file_desc.writestr("slither_results.json", json.dumps(json_result).encode("utf8")) 239 240 241# endregion 242################################################################################### 243################################################################################### 244# region Json generation 245################################################################################### 246################################################################################### 247 248 249def _format_with_location(name: str, source_mapping: SourceMapping) -> str: 250 """Format name with optional location based on OutputConfig.EXCLUDE_LOCATION.""" 251 if OutputConfig.EXCLUDE_LOCATION: 252 return name 253 return f"{name} ({source_mapping})" 254 255 256def _convert_to_description(d: str) -> str: 257 if isinstance(d, str): 258 return d 259 260 if not isinstance(d, SourceMapping): 261 raise SlitherError(f"{d} does not inherit from SourceMapping, conversion impossible") 262 263 if isinstance(d, Node): 264 if d.expression: 265 return _format_with_location(str(d.expression), d.source_mapping) 266 return _format_with_location(str(d), d.source_mapping) 267 268 if hasattr(d, "canonical_name"): 269 return _format_with_location(d.canonical_name, d.source_mapping) 270 271 if hasattr(d, "name"): 272 return _format_with_location(d.name, d.source_mapping) 273 274 raise SlitherError(f"{type(d)} cannot be converted (no name, or canonical_name") 275 276 277def _convert_to_markdown(d: str, markdown_root: str) -> str: 278 if isinstance(d, str): 279 return d 280 281 if not isinstance(d, SourceMapping): 282 raise SlitherError(f"{d} does not inherit from SourceMapping, conversion impossible") 283 284 if isinstance(d, Node): 285 if d.expression: 286 return f"[{d.expression}]({d.source_mapping.to_markdown(markdown_root)})" 287 return f"[{d!s}]({d.source_mapping.to_markdown(markdown_root)})" 288 289 if hasattr(d, "canonical_name"): 290 return f"[{d.canonical_name}]({d.source_mapping.to_markdown(markdown_root)})" 291 292 if hasattr(d, "name"): 293 return f"[{d.name}]({d.source_mapping.to_markdown(markdown_root)})" 294 295 raise SlitherError(f"{type(d)} cannot be converted (no name, or canonical_name") 296 297 298def _convert_to_id(d: str) -> str: 299 """ 300 Id keeps the source mapping of the node, otherwise we risk to consider two different node as the same 301 :param d: 302 :return: 303 """ 304 if isinstance(d, str): 305 return d 306 307 if not isinstance(d, SourceMapping): 308 raise SlitherError(f"{d} does not inherit from SourceMapping, conversion impossible") 309 310 if isinstance(d, Node): 311 if d.expression: 312 return f"{d.expression} ({d.source_mapping})" 313 return f"{d!s} ({d.source_mapping})" 314 315 if isinstance(d, Pragma): 316 return f"{d} ({d.source_mapping})" 317 318 if hasattr(d, "canonical_name"): 319 return f"{d.canonical_name}" 320 321 if hasattr(d, "name"): 322 return f"{d.name}" 323 324 raise SlitherError(f"{type(d)} cannot be converted (no name, or canonical_name") 325 326 327# endregion 328################################################################################### 329################################################################################### 330# region Internal functions 331################################################################################### 332################################################################################### 333 334 335def _create_base_element( 336 custom_type: str, 337 name: str, 338 source_mapping: dict, 339 type_specific_fields: dict[ 340 str, 341 dict[ 342 str, 343 str 344 | dict[str, int | str | bool | list[int]] 345 | dict[str, dict[str, str | dict[str, int | str | bool | list[int]]] | str], 346 ] 347 | dict[str, str | dict[str, int | str | bool | list[int]]] 348 | str 349 | list[str], 350 ] 351 | None = None, 352 additional_fields: dict[str, str] | None = None, 353) -> dict[str, Any]: 354 if additional_fields is None: 355 additional_fields = {} 356 if type_specific_fields is None: 357 type_specific_fields = {} 358 element = {"type": custom_type, "name": name, "source_mapping": source_mapping} 359 if type_specific_fields: 360 element["type_specific_fields"] = type_specific_fields 361 if additional_fields: 362 element["additional_fields"] = additional_fields 363 return element 364 365 366def _create_parent_element( 367 element: SourceMapping, 368) -> dict[ 369 str, 370 str 371 | dict[str, int | str | bool | list[int]] 372 | dict[str, dict[str, str | dict[str, int | str | bool | list[int]]] | str], 373]: 374 from slither.core.declarations.contract_level import ContractLevel 375 376 if isinstance(element, FunctionContract): 377 if element.contract_declarer: 378 contract = Output("") 379 contract.add_contract(element.contract_declarer) 380 return contract.data["elements"][0] 381 elif isinstance(element, ContractLevel): 382 if element.contract: 383 contract = Output("") 384 contract.add_contract(element.contract) 385 return contract.data["elements"][0] 386 elif isinstance(element, (LocalVariable, Node)): 387 if element.function: 388 function = Output("") 389 function.add_function(element.function) 390 return function.data["elements"][0] 391 return None 392 393 394SupportedOutput = Variable | Contract | Function | Enum | Event | Structure | Pragma | Node 395AllSupportedOutput = str | SupportedOutput 396 397 398class Output: 399 def __init__( 400 self, 401 info_: str | list[str | SupportedOutput], 402 additional_fields: dict | None = None, 403 markdown_root: str = "", 404 standard_format: bool = True, 405 ) -> None: 406 if additional_fields is None: 407 additional_fields = {} 408 409 # Allow info to be a string to simplify the API 410 info: list[str | SupportedOutput] 411 if isinstance(info_, str): 412 info = [info_] 413 else: 414 info = info_ 415 416 self._data = OrderedDict() 417 self._data["elements"] = [] 418 self._data["description"] = "".join(_convert_to_description(d) for d in info) 419 self._data["markdown"] = "".join(_convert_to_markdown(d, markdown_root) for d in info) 420 self._data["first_markdown_element"] = "" 421 self._markdown_root = markdown_root 422 423 id_txt = "".join(_convert_to_id(d) for d in info) 424 self._data["id"] = hashlib.sha3_256(id_txt.encode("utf8")).hexdigest() 425 426 if standard_format: 427 to_add = [i for i in info if not isinstance(i, str)] 428 429 for add in to_add: 430 self.add(add) 431 432 if additional_fields: 433 self._data["additional_fields"] = additional_fields 434 435 def add(self, add: SupportedOutput, additional_fields: dict | None = None) -> None: 436 if not self._data["first_markdown_element"]: 437 self._data["first_markdown_element"] = add.source_mapping.to_markdown( 438 self._markdown_root 439 ) 440 if isinstance(add, Variable): 441 self.add_variable(add, additional_fields=additional_fields) 442 elif isinstance(add, Contract): 443 self.add_contract(add, additional_fields=additional_fields) 444 elif isinstance(add, Function): 445 self.add_function(add, additional_fields=additional_fields) 446 elif isinstance(add, Enum): 447 self.add_enum(add, additional_fields=additional_fields) 448 elif isinstance(add, Event): 449 self.add_event(add, additional_fields=additional_fields) 450 elif isinstance(add, Structure): 451 self.add_struct(add, additional_fields=additional_fields) 452 elif isinstance(add, CustomError): 453 self.add_custom_error(add, additional_fields=additional_fields) 454 elif isinstance(add, Pragma): 455 self.add_pragma(add, additional_fields=additional_fields) 456 elif isinstance(add, Node): 457 self.add_node(add, additional_fields=additional_fields) 458 else: 459 raise SlitherError(f"Impossible to add {type(add)} to the json") 460 461 @property 462 def data(self) -> dict: 463 return self._data 464 465 @property 466 def elements(self) -> list[dict]: 467 return self._data["elements"] 468 469 # endregion 470 ################################################################################### 471 ################################################################################### 472 # region Variables 473 ################################################################################### 474 ################################################################################### 475 476 def add_variable(self, variable: Variable, additional_fields: dict | None = None) -> None: 477 if additional_fields is None: 478 additional_fields = {} 479 type_specific_fields = {"parent": _create_parent_element(variable)} 480 element = _create_base_element( 481 "variable", 482 variable.name, 483 variable.source_mapping.to_json(), 484 type_specific_fields, 485 additional_fields, 486 ) 487 self._data["elements"].append(element) 488 489 def add_variables(self, variables: list[Variable]): 490 for variable in sorted(variables, key=lambda x: x.name): 491 self.add_variable(variable) 492 493 # endregion 494 ################################################################################### 495 ################################################################################### 496 # region Contract 497 ################################################################################### 498 ################################################################################### 499 500 def add_contract(self, contract: Contract, additional_fields: dict | None = None) -> None: 501 if additional_fields is None: 502 additional_fields = {} 503 element = _create_base_element( 504 "contract", contract.name, contract.source_mapping.to_json(), {}, additional_fields 505 ) 506 self._data["elements"].append(element) 507 508 # endregion 509 ################################################################################### 510 ################################################################################### 511 # region Functions 512 ################################################################################### 513 ################################################################################### 514 515 def add_function(self, function: Function, additional_fields: dict | None = None) -> None: 516 if additional_fields is None: 517 additional_fields = {} 518 type_specific_fields = { 519 "parent": _create_parent_element(function), 520 "signature": function.full_name, 521 } 522 element = _create_base_element( 523 "function", 524 function.name, 525 function.source_mapping.to_json(), 526 type_specific_fields, 527 additional_fields, 528 ) 529 self._data["elements"].append(element) 530 531 def add_functions(self, functions: list[Function], additional_fields: dict | None = None): 532 if additional_fields is None: 533 additional_fields = {} 534 for function in sorted(functions, key=lambda x: x.name): 535 self.add_function(function, additional_fields) 536 537 # endregion 538 ################################################################################### 539 ################################################################################### 540 # region Enum 541 ################################################################################### 542 ################################################################################### 543 544 def add_enum(self, enum: Enum, additional_fields: dict | None = None) -> None: 545 if additional_fields is None: 546 additional_fields = {} 547 type_specific_fields = {"parent": _create_parent_element(enum)} 548 element = _create_base_element( 549 "enum", 550 enum.name, 551 enum.source_mapping.to_json(), 552 type_specific_fields, 553 additional_fields, 554 ) 555 self._data["elements"].append(element) 556 557 # endregion 558 ################################################################################### 559 ################################################################################### 560 # region Structures 561 ################################################################################### 562 ################################################################################### 563 564 def add_struct(self, struct: Structure, additional_fields: dict | None = None) -> None: 565 if additional_fields is None: 566 additional_fields = {} 567 type_specific_fields = {"parent": _create_parent_element(struct)} 568 element = _create_base_element( 569 "struct", 570 struct.name, 571 struct.source_mapping.to_json(), 572 type_specific_fields, 573 additional_fields, 574 ) 575 self._data["elements"].append(element) 576 577 # endregion 578 ################################################################################### 579 ################################################################################### 580 # region Events 581 ################################################################################### 582 ################################################################################### 583 584 def add_event(self, event: Event, additional_fields: dict | None = None) -> None: 585 if additional_fields is None: 586 additional_fields = {} 587 type_specific_fields = { 588 "parent": _create_parent_element(event), 589 "signature": event.full_name, 590 } 591 element = _create_base_element( 592 "event", 593 event.name, 594 event.source_mapping.to_json(), 595 type_specific_fields, 596 additional_fields, 597 ) 598 599 self._data["elements"].append(element) 600 601 # endregion 602 ################################################################################### 603 ################################################################################### 604 # region CustomError 605 ################################################################################### 606 ################################################################################### 607 608 def add_custom_error( 609 self, custom_error: CustomError, additional_fields: dict | None = None 610 ) -> None: 611 if additional_fields is None: 612 additional_fields = {} 613 type_specific_fields = { 614 "parent": _create_parent_element(custom_error), 615 "signature": custom_error.full_name, 616 } 617 element = _create_base_element( 618 "custom_error", 619 custom_error.name, 620 custom_error.source_mapping.to_json(), 621 type_specific_fields, 622 additional_fields, 623 ) 624 625 self._data["elements"].append(element) 626 627 # endregion 628 ################################################################################### 629 ################################################################################### 630 # region Nodes 631 ################################################################################### 632 ################################################################################### 633 634 def add_node(self, node: Node, additional_fields: dict | None = None) -> None: 635 if additional_fields is None: 636 additional_fields = {} 637 type_specific_fields = { 638 "parent": _create_parent_element(node), 639 } 640 node_name = str(node.expression) if node.expression else "" 641 element = _create_base_element( 642 "node", 643 node_name, 644 node.source_mapping.to_json(), 645 type_specific_fields, 646 additional_fields, 647 ) 648 self._data["elements"].append(element) 649 650 def add_nodes(self, nodes: list[Node]): 651 for node in sorted(nodes, key=lambda x: x.node_id): 652 self.add_node(node) 653 654 # endregion 655 ################################################################################### 656 ################################################################################### 657 # region Pragma 658 ################################################################################### 659 ################################################################################### 660 661 def add_pragma(self, pragma: Pragma, additional_fields: dict | None = None) -> None: 662 if additional_fields is None: 663 additional_fields = {} 664 type_specific_fields = {"directive": pragma.directive} 665 element = _create_base_element( 666 "pragma", 667 pragma.version, 668 pragma.source_mapping.to_json(), 669 type_specific_fields, 670 additional_fields, 671 ) 672 self._data["elements"].append(element) 673 674 # endregion 675 ################################################################################### 676 ################################################################################### 677 # region File 678 ################################################################################### 679 ################################################################################### 680 681 def add_file(self, filename: str, content: str, additional_fields: dict | None = None): 682 if additional_fields is None: 683 additional_fields = {} 684 type_specific_fields = {"filename": filename, "content": content} 685 element = _create_base_element("file", type_specific_fields, additional_fields) 686 687 self._data["elements"].append(element) 688 689 # endregion 690 ################################################################################### 691 ################################################################################### 692 # region Pretty Table 693 ################################################################################### 694 ################################################################################### 695 696 def add_pretty_table( 697 self, 698 content: MyPrettyTable, 699 name: str, 700 additional_fields: dict | None = None, 701 ): 702 if additional_fields is None: 703 additional_fields = {} 704 type_specific_fields = {"content": content.to_json(), "name": name} 705 element = _create_base_element("pretty_table", type_specific_fields, additional_fields) 706 707 self._data["elements"].append(element) 708 709 # endregion 710 ################################################################################### 711 ################################################################################### 712 # region Others 713 ################################################################################### 714 ################################################################################### 715 716 def add_other( 717 self, 718 name: str, 719 source_mapping: tuple[str, int, int], 720 compilation_unit: "SlitherCompilationUnit", 721 additional_fields: dict | None = None, 722 ) -> None: 723 # If this a tuple with (filename, start, end), convert it to a source mapping. 724 if additional_fields is None: 725 additional_fields = {} 726 if isinstance(source_mapping, tuple): 727 # Parse the source id 728 (filename, start, end) = source_mapping 729 source_id = next( 730 ( 731 source_unit_id 732 for ( 733 source_unit_id, 734 source_unit_filename, 735 ) in compilation_unit.source_units.items() 736 if source_unit_filename == filename 737 ), 738 -1, 739 ) 740 741 # Convert to a source mapping string 742 source_mapping = f"{start}:{end}:{source_id}" 743 744 # If this is a source mapping string, parse it. 745 if isinstance(source_mapping, str): 746 source_mapping_str = source_mapping 747 source_mapping = SourceMapping() 748 source_mapping.set_offset(source_mapping_str, compilation_unit) 749 750 # If this is a source mapping object, get the underlying source mapping dictionary 751 if isinstance(source_mapping, SourceMapping): 752 source_mapping = source_mapping.source_mapping.to_json() 753 754 # Create the underlying element and add it to our resulting json 755 element = _create_base_element("other", name, source_mapping, {}, additional_fields) 756 self._data["elements"].append(element)
logger =
<Logger Slither (WARNING)>
class
OutputConfig:
38class OutputConfig: 39 """Configuration settings for output formatting.""" 40 41 EXCLUDE_LOCATION: bool = False
Configuration settings for output formatting.
def
set_exclude_location(exclude: bool) -> None:
44def set_exclude_location(exclude: bool) -> None: 45 """Set whether to exclude location info from detector messages.""" 46 OutputConfig.EXCLUDE_LOCATION = exclude
Set whether to exclude location info from detector messages.
def
get_exclude_location() -> bool:
49def get_exclude_location() -> bool: 50 """Get whether location info should be excluded from detector messages.""" 51 return OutputConfig.EXCLUDE_LOCATION
Get whether location info should be excluded from detector messages.
def
output_to_json(filename: str | None, error, results: dict) -> None:
61def output_to_json(filename: str | None, error, results: dict) -> None: 62 """ 63 64 :param filename: Filename where the json will be written. If None or "-", write to stdout 65 :param error: Error to report 66 :param results: Results to report 67 :param logger: Logger where to log potential info 68 :return: 69 """ 70 # Create our encapsulated JSON result. 71 json_result = {"success": error is None, "error": error, "results": results} 72 73 if filename == "-": 74 filename = None 75 76 # Determine if we should output to stdout 77 if filename is None: 78 # Write json to console 79 print(json.dumps(json_result)) 80 else: 81 # Write json to file 82 if os.path.isfile(filename): 83 logger.info(yellow(f"{filename} exists already, the overwrite is prevented")) 84 else: 85 with open(filename, "w", encoding="utf8") as f: 86 json.dump(json_result, f, indent=2)
Parameters
- filename: Filename where the json will be written. If None or "-", write to stdout
- error: Error to report
- results: Results to report
- logger: Logger where to log potential info
Returns
def
output_to_sarif( filename: str | None, results: dict, detectors_classes: list[type[slither.detectors.abstract_detector.AbstractDetector]]) -> None:
160def output_to_sarif( 161 filename: str | None, results: dict, detectors_classes: list[type["AbstractDetector"]] 162) -> None: 163 """ 164 165 :param filename: 166 :type filename: 167 :param results: 168 :type results: 169 :return: 170 :rtype: 171 """ 172 173 sarif: dict[str, Any] = { 174 "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", 175 "version": "2.1.0", 176 "runs": [ 177 { 178 "tool": { 179 "driver": { 180 "name": "Slither", 181 "informationUri": "https://github.com/crytic/slither", 182 "version": metadata.version("slither-analyzer"), 183 "rules": [], 184 } 185 }, 186 "results": [], 187 } 188 ], 189 } 190 191 for detector in results.get("detectors", []): 192 _output_result_to_sarif(detector, detectors_classes, sarif) 193 194 if filename == "-": 195 filename = None 196 197 # Determine if we should output to stdout 198 if filename is None: 199 # Write json to console 200 print(json.dumps(sarif)) 201 else: 202 # Write json to file 203 if os.path.isfile(filename): 204 logger.info(yellow(f"{filename} exists already, the overwrite is prevented")) 205 else: 206 with open(filename, "w", encoding="utf8") as f: 207 json.dump(sarif, f, indent=2)
Parameters
- filename:
- results:
Returns
ZIP_TYPES_ACCEPTED =
{'lzma': 14, 'stored': 0, 'deflated': 8, 'bzip2': 12}
def
output_to_zip( filename: str, error: str | None, results: dict, zip_type: str = 'lzma'):
219def output_to_zip(filename: str, error: str | None, results: dict, zip_type: str = "lzma"): 220 """ 221 Output the results to a zip 222 The file in the zip is named slither_results.json 223 Note: the json file will not have indentation, as a result the resulting json file will be smaller 224 :param zip_type: 225 :param filename: 226 :param error: 227 :param results: 228 :return: 229 """ 230 json_result = {"success": error is None, "error": error, "results": results} 231 if os.path.isfile(filename): 232 logger.info(yellow(f"{filename} exists already, the overwrite is prevented")) 233 else: 234 with ZipFile( 235 filename, 236 "w", 237 compression=ZIP_TYPES_ACCEPTED.get(zip_type, zipfile.ZIP_LZMA), 238 ) as file_desc: 239 file_desc.writestr("slither_results.json", json.dumps(json_result).encode("utf8"))
Output the results to a zip The file in the zip is named slither_results.json Note: the json file will not have indentation, as a result the resulting json file will be smaller
Parameters
- zip_type:
- filename:
- error:
- results:
Returns
SupportedOutput =
slither.core.variables.variable.Variable | slither.core.declarations.contract.Contract | slither.core.declarations.function.Function | slither.core.declarations.enum.Enum | slither.core.declarations.event.Event | slither.core.declarations.structure.Structure | slither.core.declarations.pragma_directive.Pragma | slither.core.cfg.node.Node
AllSupportedOutput =
str | slither.core.variables.variable.Variable | slither.core.declarations.contract.Contract | slither.core.declarations.function.Function | slither.core.declarations.enum.Enum | slither.core.declarations.event.Event | slither.core.declarations.structure.Structure | slither.core.declarations.pragma_directive.Pragma | slither.core.cfg.node.Node
class
Output:
399class Output: 400 def __init__( 401 self, 402 info_: str | list[str | SupportedOutput], 403 additional_fields: dict | None = None, 404 markdown_root: str = "", 405 standard_format: bool = True, 406 ) -> None: 407 if additional_fields is None: 408 additional_fields = {} 409 410 # Allow info to be a string to simplify the API 411 info: list[str | SupportedOutput] 412 if isinstance(info_, str): 413 info = [info_] 414 else: 415 info = info_ 416 417 self._data = OrderedDict() 418 self._data["elements"] = [] 419 self._data["description"] = "".join(_convert_to_description(d) for d in info) 420 self._data["markdown"] = "".join(_convert_to_markdown(d, markdown_root) for d in info) 421 self._data["first_markdown_element"] = "" 422 self._markdown_root = markdown_root 423 424 id_txt = "".join(_convert_to_id(d) for d in info) 425 self._data["id"] = hashlib.sha3_256(id_txt.encode("utf8")).hexdigest() 426 427 if standard_format: 428 to_add = [i for i in info if not isinstance(i, str)] 429 430 for add in to_add: 431 self.add(add) 432 433 if additional_fields: 434 self._data["additional_fields"] = additional_fields 435 436 def add(self, add: SupportedOutput, additional_fields: dict | None = None) -> None: 437 if not self._data["first_markdown_element"]: 438 self._data["first_markdown_element"] = add.source_mapping.to_markdown( 439 self._markdown_root 440 ) 441 if isinstance(add, Variable): 442 self.add_variable(add, additional_fields=additional_fields) 443 elif isinstance(add, Contract): 444 self.add_contract(add, additional_fields=additional_fields) 445 elif isinstance(add, Function): 446 self.add_function(add, additional_fields=additional_fields) 447 elif isinstance(add, Enum): 448 self.add_enum(add, additional_fields=additional_fields) 449 elif isinstance(add, Event): 450 self.add_event(add, additional_fields=additional_fields) 451 elif isinstance(add, Structure): 452 self.add_struct(add, additional_fields=additional_fields) 453 elif isinstance(add, CustomError): 454 self.add_custom_error(add, additional_fields=additional_fields) 455 elif isinstance(add, Pragma): 456 self.add_pragma(add, additional_fields=additional_fields) 457 elif isinstance(add, Node): 458 self.add_node(add, additional_fields=additional_fields) 459 else: 460 raise SlitherError(f"Impossible to add {type(add)} to the json") 461 462 @property 463 def data(self) -> dict: 464 return self._data 465 466 @property 467 def elements(self) -> list[dict]: 468 return self._data["elements"] 469 470 # endregion 471 ################################################################################### 472 ################################################################################### 473 # region Variables 474 ################################################################################### 475 ################################################################################### 476 477 def add_variable(self, variable: Variable, additional_fields: dict | None = None) -> None: 478 if additional_fields is None: 479 additional_fields = {} 480 type_specific_fields = {"parent": _create_parent_element(variable)} 481 element = _create_base_element( 482 "variable", 483 variable.name, 484 variable.source_mapping.to_json(), 485 type_specific_fields, 486 additional_fields, 487 ) 488 self._data["elements"].append(element) 489 490 def add_variables(self, variables: list[Variable]): 491 for variable in sorted(variables, key=lambda x: x.name): 492 self.add_variable(variable) 493 494 # endregion 495 ################################################################################### 496 ################################################################################### 497 # region Contract 498 ################################################################################### 499 ################################################################################### 500 501 def add_contract(self, contract: Contract, additional_fields: dict | None = None) -> None: 502 if additional_fields is None: 503 additional_fields = {} 504 element = _create_base_element( 505 "contract", contract.name, contract.source_mapping.to_json(), {}, additional_fields 506 ) 507 self._data["elements"].append(element) 508 509 # endregion 510 ################################################################################### 511 ################################################################################### 512 # region Functions 513 ################################################################################### 514 ################################################################################### 515 516 def add_function(self, function: Function, additional_fields: dict | None = None) -> None: 517 if additional_fields is None: 518 additional_fields = {} 519 type_specific_fields = { 520 "parent": _create_parent_element(function), 521 "signature": function.full_name, 522 } 523 element = _create_base_element( 524 "function", 525 function.name, 526 function.source_mapping.to_json(), 527 type_specific_fields, 528 additional_fields, 529 ) 530 self._data["elements"].append(element) 531 532 def add_functions(self, functions: list[Function], additional_fields: dict | None = None): 533 if additional_fields is None: 534 additional_fields = {} 535 for function in sorted(functions, key=lambda x: x.name): 536 self.add_function(function, additional_fields) 537 538 # endregion 539 ################################################################################### 540 ################################################################################### 541 # region Enum 542 ################################################################################### 543 ################################################################################### 544 545 def add_enum(self, enum: Enum, additional_fields: dict | None = None) -> None: 546 if additional_fields is None: 547 additional_fields = {} 548 type_specific_fields = {"parent": _create_parent_element(enum)} 549 element = _create_base_element( 550 "enum", 551 enum.name, 552 enum.source_mapping.to_json(), 553 type_specific_fields, 554 additional_fields, 555 ) 556 self._data["elements"].append(element) 557 558 # endregion 559 ################################################################################### 560 ################################################################################### 561 # region Structures 562 ################################################################################### 563 ################################################################################### 564 565 def add_struct(self, struct: Structure, additional_fields: dict | None = None) -> None: 566 if additional_fields is None: 567 additional_fields = {} 568 type_specific_fields = {"parent": _create_parent_element(struct)} 569 element = _create_base_element( 570 "struct", 571 struct.name, 572 struct.source_mapping.to_json(), 573 type_specific_fields, 574 additional_fields, 575 ) 576 self._data["elements"].append(element) 577 578 # endregion 579 ################################################################################### 580 ################################################################################### 581 # region Events 582 ################################################################################### 583 ################################################################################### 584 585 def add_event(self, event: Event, additional_fields: dict | None = None) -> None: 586 if additional_fields is None: 587 additional_fields = {} 588 type_specific_fields = { 589 "parent": _create_parent_element(event), 590 "signature": event.full_name, 591 } 592 element = _create_base_element( 593 "event", 594 event.name, 595 event.source_mapping.to_json(), 596 type_specific_fields, 597 additional_fields, 598 ) 599 600 self._data["elements"].append(element) 601 602 # endregion 603 ################################################################################### 604 ################################################################################### 605 # region CustomError 606 ################################################################################### 607 ################################################################################### 608 609 def add_custom_error( 610 self, custom_error: CustomError, additional_fields: dict | None = None 611 ) -> None: 612 if additional_fields is None: 613 additional_fields = {} 614 type_specific_fields = { 615 "parent": _create_parent_element(custom_error), 616 "signature": custom_error.full_name, 617 } 618 element = _create_base_element( 619 "custom_error", 620 custom_error.name, 621 custom_error.source_mapping.to_json(), 622 type_specific_fields, 623 additional_fields, 624 ) 625 626 self._data["elements"].append(element) 627 628 # endregion 629 ################################################################################### 630 ################################################################################### 631 # region Nodes 632 ################################################################################### 633 ################################################################################### 634 635 def add_node(self, node: Node, additional_fields: dict | None = None) -> None: 636 if additional_fields is None: 637 additional_fields = {} 638 type_specific_fields = { 639 "parent": _create_parent_element(node), 640 } 641 node_name = str(node.expression) if node.expression else "" 642 element = _create_base_element( 643 "node", 644 node_name, 645 node.source_mapping.to_json(), 646 type_specific_fields, 647 additional_fields, 648 ) 649 self._data["elements"].append(element) 650 651 def add_nodes(self, nodes: list[Node]): 652 for node in sorted(nodes, key=lambda x: x.node_id): 653 self.add_node(node) 654 655 # endregion 656 ################################################################################### 657 ################################################################################### 658 # region Pragma 659 ################################################################################### 660 ################################################################################### 661 662 def add_pragma(self, pragma: Pragma, additional_fields: dict | None = None) -> None: 663 if additional_fields is None: 664 additional_fields = {} 665 type_specific_fields = {"directive": pragma.directive} 666 element = _create_base_element( 667 "pragma", 668 pragma.version, 669 pragma.source_mapping.to_json(), 670 type_specific_fields, 671 additional_fields, 672 ) 673 self._data["elements"].append(element) 674 675 # endregion 676 ################################################################################### 677 ################################################################################### 678 # region File 679 ################################################################################### 680 ################################################################################### 681 682 def add_file(self, filename: str, content: str, additional_fields: dict | None = None): 683 if additional_fields is None: 684 additional_fields = {} 685 type_specific_fields = {"filename": filename, "content": content} 686 element = _create_base_element("file", type_specific_fields, additional_fields) 687 688 self._data["elements"].append(element) 689 690 # endregion 691 ################################################################################### 692 ################################################################################### 693 # region Pretty Table 694 ################################################################################### 695 ################################################################################### 696 697 def add_pretty_table( 698 self, 699 content: MyPrettyTable, 700 name: str, 701 additional_fields: dict | None = None, 702 ): 703 if additional_fields is None: 704 additional_fields = {} 705 type_specific_fields = {"content": content.to_json(), "name": name} 706 element = _create_base_element("pretty_table", type_specific_fields, additional_fields) 707 708 self._data["elements"].append(element) 709 710 # endregion 711 ################################################################################### 712 ################################################################################### 713 # region Others 714 ################################################################################### 715 ################################################################################### 716 717 def add_other( 718 self, 719 name: str, 720 source_mapping: tuple[str, int, int], 721 compilation_unit: "SlitherCompilationUnit", 722 additional_fields: dict | None = None, 723 ) -> None: 724 # If this a tuple with (filename, start, end), convert it to a source mapping. 725 if additional_fields is None: 726 additional_fields = {} 727 if isinstance(source_mapping, tuple): 728 # Parse the source id 729 (filename, start, end) = source_mapping 730 source_id = next( 731 ( 732 source_unit_id 733 for ( 734 source_unit_id, 735 source_unit_filename, 736 ) in compilation_unit.source_units.items() 737 if source_unit_filename == filename 738 ), 739 -1, 740 ) 741 742 # Convert to a source mapping string 743 source_mapping = f"{start}:{end}:{source_id}" 744 745 # If this is a source mapping string, parse it. 746 if isinstance(source_mapping, str): 747 source_mapping_str = source_mapping 748 source_mapping = SourceMapping() 749 source_mapping.set_offset(source_mapping_str, compilation_unit) 750 751 # If this is a source mapping object, get the underlying source mapping dictionary 752 if isinstance(source_mapping, SourceMapping): 753 source_mapping = source_mapping.source_mapping.to_json() 754 755 # Create the underlying element and add it to our resulting json 756 element = _create_base_element("other", name, source_mapping, {}, additional_fields) 757 self._data["elements"].append(element)
Output( info_: str | list[str | slither.core.variables.variable.Variable | slither.core.declarations.contract.Contract | slither.core.declarations.function.Function | slither.core.declarations.enum.Enum | slither.core.declarations.event.Event | slither.core.declarations.structure.Structure | slither.core.declarations.pragma_directive.Pragma | slither.core.cfg.node.Node], additional_fields: dict | None = None, markdown_root: str = '', standard_format: bool = True)
400 def __init__( 401 self, 402 info_: str | list[str | SupportedOutput], 403 additional_fields: dict | None = None, 404 markdown_root: str = "", 405 standard_format: bool = True, 406 ) -> None: 407 if additional_fields is None: 408 additional_fields = {} 409 410 # Allow info to be a string to simplify the API 411 info: list[str | SupportedOutput] 412 if isinstance(info_, str): 413 info = [info_] 414 else: 415 info = info_ 416 417 self._data = OrderedDict() 418 self._data["elements"] = [] 419 self._data["description"] = "".join(_convert_to_description(d) for d in info) 420 self._data["markdown"] = "".join(_convert_to_markdown(d, markdown_root) for d in info) 421 self._data["first_markdown_element"] = "" 422 self._markdown_root = markdown_root 423 424 id_txt = "".join(_convert_to_id(d) for d in info) 425 self._data["id"] = hashlib.sha3_256(id_txt.encode("utf8")).hexdigest() 426 427 if standard_format: 428 to_add = [i for i in info if not isinstance(i, str)] 429 430 for add in to_add: 431 self.add(add) 432 433 if additional_fields: 434 self._data["additional_fields"] = additional_fields
def
add( self, add: slither.core.variables.variable.Variable | slither.core.declarations.contract.Contract | slither.core.declarations.function.Function | slither.core.declarations.enum.Enum | slither.core.declarations.event.Event | slither.core.declarations.structure.Structure | slither.core.declarations.pragma_directive.Pragma | slither.core.cfg.node.Node, additional_fields: dict | None = None) -> None:
436 def add(self, add: SupportedOutput, additional_fields: dict | None = None) -> None: 437 if not self._data["first_markdown_element"]: 438 self._data["first_markdown_element"] = add.source_mapping.to_markdown( 439 self._markdown_root 440 ) 441 if isinstance(add, Variable): 442 self.add_variable(add, additional_fields=additional_fields) 443 elif isinstance(add, Contract): 444 self.add_contract(add, additional_fields=additional_fields) 445 elif isinstance(add, Function): 446 self.add_function(add, additional_fields=additional_fields) 447 elif isinstance(add, Enum): 448 self.add_enum(add, additional_fields=additional_fields) 449 elif isinstance(add, Event): 450 self.add_event(add, additional_fields=additional_fields) 451 elif isinstance(add, Structure): 452 self.add_struct(add, additional_fields=additional_fields) 453 elif isinstance(add, CustomError): 454 self.add_custom_error(add, additional_fields=additional_fields) 455 elif isinstance(add, Pragma): 456 self.add_pragma(add, additional_fields=additional_fields) 457 elif isinstance(add, Node): 458 self.add_node(add, additional_fields=additional_fields) 459 else: 460 raise SlitherError(f"Impossible to add {type(add)} to the json")
def
add_variable( self, variable: slither.core.variables.variable.Variable, additional_fields: dict | None = None) -> None:
477 def add_variable(self, variable: Variable, additional_fields: dict | None = None) -> None: 478 if additional_fields is None: 479 additional_fields = {} 480 type_specific_fields = {"parent": _create_parent_element(variable)} 481 element = _create_base_element( 482 "variable", 483 variable.name, 484 variable.source_mapping.to_json(), 485 type_specific_fields, 486 additional_fields, 487 ) 488 self._data["elements"].append(element)
def
add_contract( self, contract: slither.core.declarations.contract.Contract, additional_fields: dict | None = None) -> None:
501 def add_contract(self, contract: Contract, additional_fields: dict | None = None) -> None: 502 if additional_fields is None: 503 additional_fields = {} 504 element = _create_base_element( 505 "contract", contract.name, contract.source_mapping.to_json(), {}, additional_fields 506 ) 507 self._data["elements"].append(element)
def
add_function( self, function: slither.core.declarations.function.Function, additional_fields: dict | None = None) -> None:
516 def add_function(self, function: Function, additional_fields: dict | None = None) -> None: 517 if additional_fields is None: 518 additional_fields = {} 519 type_specific_fields = { 520 "parent": _create_parent_element(function), 521 "signature": function.full_name, 522 } 523 element = _create_base_element( 524 "function", 525 function.name, 526 function.source_mapping.to_json(), 527 type_specific_fields, 528 additional_fields, 529 ) 530 self._data["elements"].append(element)
def
add_functions( self, functions: list[slither.core.declarations.function.Function], additional_fields: dict | None = None):
def
add_enum( self, enum: slither.core.declarations.enum.Enum, additional_fields: dict | None = None) -> None:
545 def add_enum(self, enum: Enum, additional_fields: dict | None = None) -> None: 546 if additional_fields is None: 547 additional_fields = {} 548 type_specific_fields = {"parent": _create_parent_element(enum)} 549 element = _create_base_element( 550 "enum", 551 enum.name, 552 enum.source_mapping.to_json(), 553 type_specific_fields, 554 additional_fields, 555 ) 556 self._data["elements"].append(element)
def
add_struct( self, struct: slither.core.declarations.structure.Structure, additional_fields: dict | None = None) -> None:
565 def add_struct(self, struct: Structure, additional_fields: dict | None = None) -> None: 566 if additional_fields is None: 567 additional_fields = {} 568 type_specific_fields = {"parent": _create_parent_element(struct)} 569 element = _create_base_element( 570 "struct", 571 struct.name, 572 struct.source_mapping.to_json(), 573 type_specific_fields, 574 additional_fields, 575 ) 576 self._data["elements"].append(element)
def
add_event( self, event: slither.core.declarations.event.Event, additional_fields: dict | None = None) -> None:
585 def add_event(self, event: Event, additional_fields: dict | None = None) -> None: 586 if additional_fields is None: 587 additional_fields = {} 588 type_specific_fields = { 589 "parent": _create_parent_element(event), 590 "signature": event.full_name, 591 } 592 element = _create_base_element( 593 "event", 594 event.name, 595 event.source_mapping.to_json(), 596 type_specific_fields, 597 additional_fields, 598 ) 599 600 self._data["elements"].append(element)
def
add_custom_error( self, custom_error: slither.core.declarations.custom_error.CustomError, additional_fields: dict | None = None) -> None:
609 def add_custom_error( 610 self, custom_error: CustomError, additional_fields: dict | None = None 611 ) -> None: 612 if additional_fields is None: 613 additional_fields = {} 614 type_specific_fields = { 615 "parent": _create_parent_element(custom_error), 616 "signature": custom_error.full_name, 617 } 618 element = _create_base_element( 619 "custom_error", 620 custom_error.name, 621 custom_error.source_mapping.to_json(), 622 type_specific_fields, 623 additional_fields, 624 ) 625 626 self._data["elements"].append(element)
def
add_node( self, node: slither.core.cfg.node.Node, additional_fields: dict | None = None) -> None:
635 def add_node(self, node: Node, additional_fields: dict | None = None) -> None: 636 if additional_fields is None: 637 additional_fields = {} 638 type_specific_fields = { 639 "parent": _create_parent_element(node), 640 } 641 node_name = str(node.expression) if node.expression else "" 642 element = _create_base_element( 643 "node", 644 node_name, 645 node.source_mapping.to_json(), 646 type_specific_fields, 647 additional_fields, 648 ) 649 self._data["elements"].append(element)
def
add_pragma( self, pragma: slither.core.declarations.pragma_directive.Pragma, additional_fields: dict | None = None) -> None:
662 def add_pragma(self, pragma: Pragma, additional_fields: dict | None = None) -> None: 663 if additional_fields is None: 664 additional_fields = {} 665 type_specific_fields = {"directive": pragma.directive} 666 element = _create_base_element( 667 "pragma", 668 pragma.version, 669 pragma.source_mapping.to_json(), 670 type_specific_fields, 671 additional_fields, 672 ) 673 self._data["elements"].append(element)
def
add_file( self, filename: str, content: str, additional_fields: dict | None = None):
682 def add_file(self, filename: str, content: str, additional_fields: dict | None = None): 683 if additional_fields is None: 684 additional_fields = {} 685 type_specific_fields = {"filename": filename, "content": content} 686 element = _create_base_element("file", type_specific_fields, additional_fields) 687 688 self._data["elements"].append(element)
def
add_pretty_table( self, content: slither.utils.myprettytable.MyPrettyTable, name: str, additional_fields: dict | None = None):
697 def add_pretty_table( 698 self, 699 content: MyPrettyTable, 700 name: str, 701 additional_fields: dict | None = None, 702 ): 703 if additional_fields is None: 704 additional_fields = {} 705 type_specific_fields = {"content": content.to_json(), "name": name} 706 element = _create_base_element("pretty_table", type_specific_fields, additional_fields) 707 708 self._data["elements"].append(element)
def
add_other( self, name: str, source_mapping: tuple[str, int, int], compilation_unit: slither.core.compilation_unit.SlitherCompilationUnit, additional_fields: dict | None = None) -> None:
717 def add_other( 718 self, 719 name: str, 720 source_mapping: tuple[str, int, int], 721 compilation_unit: "SlitherCompilationUnit", 722 additional_fields: dict | None = None, 723 ) -> None: 724 # If this a tuple with (filename, start, end), convert it to a source mapping. 725 if additional_fields is None: 726 additional_fields = {} 727 if isinstance(source_mapping, tuple): 728 # Parse the source id 729 (filename, start, end) = source_mapping 730 source_id = next( 731 ( 732 source_unit_id 733 for ( 734 source_unit_id, 735 source_unit_filename, 736 ) in compilation_unit.source_units.items() 737 if source_unit_filename == filename 738 ), 739 -1, 740 ) 741 742 # Convert to a source mapping string 743 source_mapping = f"{start}:{end}:{source_id}" 744 745 # If this is a source mapping string, parse it. 746 if isinstance(source_mapping, str): 747 source_mapping_str = source_mapping 748 source_mapping = SourceMapping() 749 source_mapping.set_offset(source_mapping_str, compilation_unit) 750 751 # If this is a source mapping object, get the underlying source mapping dictionary 752 if isinstance(source_mapping, SourceMapping): 753 source_mapping = source_mapping.source_mapping.to_json() 754 755 # Create the underlying element and add it to our resulting json 756 element = _create_base_element("other", name, source_mapping, {}, additional_fields) 757 self._data["elements"].append(element)