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.

EXCLUDE_LOCATION: bool = False
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
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)
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
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")
data: dict
462    @property
463    def data(self) -> dict:
464        return self._data
elements: list[dict]
466    @property
467    def elements(self) -> list[dict]:
468        return self._data["elements"]
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_variables(self, variables: list[slither.core.variables.variable.Variable]):
490    def add_variables(self, variables: list[Variable]):
491        for variable in sorted(variables, key=lambda x: x.name):
492            self.add_variable(variable)
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):
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)
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_nodes(self, nodes: list[slither.core.cfg.node.Node]):
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)
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)