diff --git a/slither/__main__.py b/slither/__main__.py index 6ce890ab0..db2f75191 100644 --- a/slither/__main__.py +++ b/slither/__main__.py @@ -24,7 +24,7 @@ from slither.detectors.abstract_detector import AbstractDetector, DetectorClassi from slither.printers import all_printers from slither.printers.abstract_printer import AbstractPrinter from slither.slither import Slither -from slither.utils.output import output_to_json, output_to_zip, ZIP_TYPES_ACCEPTED +from slither.utils.output import output_to_json, output_to_zip, output_to_sarif, ZIP_TYPES_ACCEPTED from slither.utils.output_capture import StandardOutputCapture from slither.utils.colors import red, blue, set_colorization_enabled from slither.utils.command_line import ( @@ -397,6 +397,13 @@ def parse_args(detector_classes, printer_classes): # pylint: disable=too-many-s default=defaults_flag_in_config["json"], ) + group_misc.add_argument( + "--sarif", + help='Export the results as a SARIF JSON file ("--sarif -" to export to stdout)', + action="store", + default=defaults_flag_in_config["sarif"], + ) + group_misc.add_argument( "--json-types", help="Comma-separated list of result types to output to JSON, defaults to " @@ -645,6 +652,8 @@ def main_impl(all_detector_classes, all_printer_classes): output_error = None outputting_json = args.json is not None outputting_json_stdout = args.json == "-" + outputting_sarif = args.sarif is not None + outputting_sarif_stdout = args.sarif == "-" outputting_zip = args.zip is not None if args.zip_type not in ZIP_TYPES_ACCEPTED.keys(): to_log = f'Zip type not accepted, it must be one of {",".join(ZIP_TYPES_ACCEPTED.keys())}' @@ -652,8 +661,8 @@ def main_impl(all_detector_classes, all_printer_classes): # If we are outputting JSON, capture all standard output. If we are outputting to stdout, we block typical stdout # output. - if outputting_json: - StandardOutputCapture.enable(outputting_json_stdout) + if outputting_json or output_to_sarif: + StandardOutputCapture.enable(outputting_json_stdout or outputting_sarif_stdout ) printer_classes = choose_printers(args, all_printer_classes) detector_classes = choose_detectors(args, all_detector_classes) @@ -732,7 +741,7 @@ def main_impl(all_detector_classes, all_printer_classes): ) = process_all(filename, args, detector_classes, printer_classes) # Determine if we are outputting JSON - if outputting_json or outputting_zip: + if outputting_json or outputting_zip or output_to_sarif: # Add our compilation information to JSON if "compilations" in args.json_types: compilation_results = [] @@ -809,6 +818,10 @@ def main_impl(all_detector_classes, all_printer_classes): StandardOutputCapture.disable() output_to_json(None if outputting_json_stdout else args.json, output_error, json_results) + if outputting_sarif: + StandardOutputCapture.disable() + output_to_sarif(None if outputting_sarif_stdout else args.sarif, json_results) + if outputting_zip: output_to_zip(args.zip, output_error, json_results, args.zip_type) diff --git a/slither/utils/command_line.py b/slither/utils/command_line.py index 79a282b9a..d20ed04a8 100644 --- a/slither/utils/command_line.py +++ b/slither/utils/command_line.py @@ -34,6 +34,7 @@ defaults_flag_in_config = { "exclude_medium": False, "exclude_high": False, "json": None, + "sarif": None, "json-types": ",".join(DEFAULT_JSON_OUTPUT_TYPES), "disable_color": False, "filter_paths": None, diff --git a/slither/utils/output.py b/slither/utils/output.py index 01edfd869..4692bd0bb 100644 --- a/slither/utils/output.py +++ b/slither/utils/output.py @@ -6,6 +6,7 @@ import zipfile from collections import OrderedDict from typing import Optional, Dict, List, Union, Any, TYPE_CHECKING from zipfile import ZipFile +from pkg_resources import require from slither.core.cfg.node import Node from slither.core.declarations import Contract, Function, Enum, Event, Structure, Pragma @@ -56,6 +57,106 @@ def output_to_json(filename: str, error, results: Dict): json.dump(json_result, f, indent=2) +def output_to_sarif(filename: str, results: Dict): + """ + + :param filename: Filename where the SARIF JSON file will be written. If None or "-", write to stdout + :param error: Error to report + :param results: Results to report + :param logger: Logger where to log potential info + :return: + """ + + sarif = { + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "Slither", + "informationUri": "https://github.com/crytic/slither", + "version": require("slither-analyzer")[0].version, + "rules": [], + } + }, + "results": [], + } + ], + } + + for detector in results["detectors"]: + check_id = hashlib.sha3_256(detector["check"].encode("utf-8")).hexdigest() + path = detector["first_markdown_element"].split("#")[0] + lines = detector["first_markdown_element"].split("#")[1].split("-") + + start_line = int(lines[0][1:]) + end_line = start_line + + if len(lines) > 1: + end_line = int(lines[1][1:]) + + confidence = "very-high" + if detector["confidence"] == "Medium": + confidence = "high" + elif detector["confidence"] == "Low": + confidence = "medium" + elif detector["confidence"] == "Informational": + confidence = "low" + + risk = "0.0" + if detector["impact"] == "High": + risk = "8.0" + elif detector["impact"] == "Medium": + risk = "4.0" + elif detector["impact"] == "Low": + risk = "3.0" + + rule = { + "id": check_id, + "name": detector["check"], + "properties": {"precision": confidence, "security-severity": risk}, + } + + # Add the rule if does not exist yet + if ( + len([x for x in sarif["runs"][0]["tool"]["driver"]["rules"] if x["id"] == check_id]) + == 0 + ): sarif["runs"][0]["tool"]["driver"]["rules"].append(rule) + + sarif["runs"][0]["results"].append( + { + "ruleId": check_id, + "message": {"text": detector["description"], "markdown": detector["markdown"]}, + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": {"uri": path}, + "region": {"startLine": start_line, "endLine": end_line}, + } + } + ], + "partialFingerprints": {"id": detector["id"]}, + } + ) + + if filename == "-": + filename = None + + # Determine if we should output to stdout + if filename is None: + # Write json to console + print(json.dumps(sarif)) + else: + # Write json to file + if os.path.isfile(filename): + logger.info(yellow(f"{filename} exists already, the overwrite is prevented")) + else: + with open(filename, "w", encoding="utf8") as f: + json.dump(sarif, f, indent=2) + + # https://docs.python.org/3/library/zipfile.html#zipfile-objects ZIP_TYPES_ACCEPTED = { "lzma": zipfile.ZIP_LZMA,