Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tool to troubleshoot slither errors #1384

Merged
merged 8 commits into from
Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"slither-prop = slither.tools.properties.__main__:main",
"slither-mutate = slither.tools.mutator.__main__:main",
"slither-read-storage = slither.tools.read_storage.__main__:main",
"slither-doctor = slither.tools.doctor.__main__:main",
]
},
)
3 changes: 3 additions & 0 deletions slither/tools/doctor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Slither doctor

Slither doctor is a tool designed to troubleshoot running Slither on a project.
Empty file.
37 changes: 37 additions & 0 deletions slither/tools/doctor/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import argparse

from crytic_compile import cryticparser

from slither.tools.doctor.utils import report_section
from slither.tools.doctor.checks import ALL_CHECKS


def parse_args() -> argparse.Namespace:
"""
Parse the underlying arguments for the program.
:return: Returns the arguments for the program.
"""
parser = argparse.ArgumentParser(
description="Troubleshoot running Slither on your project",
usage="slither-doctor project",
)

parser.add_argument("project", help="The codebase to be tested.")

# Add default arguments from crytic-compile
cryticparser.init(parser)

return parser.parse_args()


def main():
args = parse_args()
kwargs = vars(args)

for check in ALL_CHECKS:
with report_section(check.title):
check.function(**kwargs)


if __name__ == "__main__":
main()
18 changes: 18 additions & 0 deletions slither/tools/doctor/checks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from typing import Callable, List
from dataclasses import dataclass

from slither.tools.doctor.checks.platform import compile_project, detect_platform
from slither.tools.doctor.checks.versions import show_versions


@dataclass
class Check:
title: str
function: Callable[..., None]


ALL_CHECKS: List[Check] = [
Check("Software versions", show_versions),
Check("Project platform", detect_platform),
Check("Project compilation", compile_project),
]
59 changes: 59 additions & 0 deletions slither/tools/doctor/checks/platform.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import logging
from pathlib import Path

from crytic_compile import crytic_compile

from slither.tools.doctor.utils import snip_section
from slither.utils.colors import red, yellow, green


def detect_platform(project: str, **kwargs) -> None:
path = Path(project)
if path.is_file():
print(
yellow(
f"{project!r} is a file. Using it as target will manually compile your code with solc and _not_ use a compilation framework. Is that what you meant to do?"
)
)
return

print(f"Trying to detect project type for {project!r}")

supported_platforms = crytic_compile.get_platforms()
skip_platforms = {"solc", "solc-json", "archive", "standard", "etherscan"}
detected_platforms = {
platform.NAME: platform.is_supported(project, **kwargs)
for platform in supported_platforms
if platform.NAME.lower() not in skip_platforms
}
platform_qty = len([platform for platform, state in detected_platforms.items() if state])

print("Is this project using...")
for platform, state in detected_platforms.items():
print(f" => {platform + '?':<15}{state and green('Yes') or red('No')}")
print()

if platform_qty == 0:
print(red("No platform was detected! This doesn't sound right."))
print(
yellow(
"Are you trying to analyze a folder with standalone solidity files, without using a compilation framework? If that's the case, then this is okay."
)
)
elif platform_qty > 1:
print(red("More than one platform was detected! This doesn't sound right."))
print(
red("Please use `--compile-force-framework` in Slither to force the correct framework.")
)
else:
print(green("A single platform was detected."), yellow("Is it the one you expected?"))


def compile_project(project: str, **kwargs):
print("Invoking crytic-compile on the project, please wait...")

try:
crytic_compile.CryticCompile(project, **kwargs)
except Exception as e: # pylint: disable=broad-except
with snip_section("Project compilation failed :( The following error was generated:"):
logging.exception(e)
59 changes: 59 additions & 0 deletions slither/tools/doctor/checks/versions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from importlib import metadata
import json
from typing import Optional
import urllib

from packaging.version import parse, LegacyVersion, Version

from slither.utils.colors import yellow, green


def get_installed_version(name: str) -> Optional[LegacyVersion | Version]:
try:
return parse(metadata.version(name))
except metadata.PackageNotFoundError:
return None


def get_github_version(name: str) -> Optional[LegacyVersion | Version]:
try:
with urllib.request.urlopen(
f"https://api.github.com/repos/crytic/{name}/releases/latest"
) as response:
text = response.read()
data = json.loads(text)
return parse(data["tag_name"])
except: # pylint: disable=bare-except
return None


def show_versions(**_kwargs) -> None:
versions = {
"Slither": (get_installed_version("slither-analyzer"), get_github_version("slither")),
"crytic-compile": (
get_installed_version("crytic-compile"),
get_github_version("crytic-compile"),
),
"solc-select": (get_installed_version("solc-select"), get_github_version("solc-select")),
}

outdated = {
name
for name, (installed, latest) in versions.items()
if not installed or not latest or latest > installed
}

for name, (installed, latest) in versions.items():
color = yellow if name in outdated else green
print(f"{name + ':':<16}{color(installed or 'N/A'):<16} (latest is {latest or 'Unknown'})")

if len(outdated) > 0:
print()
print(
yellow(
f"Please update {', '.join(outdated)} to the latest release before creating a bug report."
)
)
else:
print()
print(green("Your tools are up to date."))
28 changes: 28 additions & 0 deletions slither/tools/doctor/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from contextlib import contextmanager
import logging
from typing import Optional
from slither.utils.colors import bold, yellow, red


@contextmanager
def snip_section(message: Optional[str]) -> None:
if message:
print(red(message), end="\n\n")

print(yellow("---- snip 8< ----"))
yield
print(yellow("---- >8 snip ----"))


@contextmanager
def report_section(title: str) -> None:
print(bold(f"## {title}"), end="\n\n")
try:
yield
except Exception as e: # pylint: disable=broad-except
with snip_section(
"slither-doctor failed unexpectedly! Please report this on the Slither GitHub issue tracker, and include the output below:"
):
logging.exception(e)
finally:
print(end="\n\n")
2 changes: 2 additions & 0 deletions slither/utils/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class Colors: # pylint: disable=too-few-public-methods
YELLOW = "\033[93m"
BLUE = "\033[94m"
MAGENTA = "\033[95m"
BOLD = "\033[1m"
END = "\033[0m"


Expand Down Expand Up @@ -83,6 +84,7 @@ def set_colorization_enabled(enabled: bool):
red = partial(colorize, Colors.RED)
blue = partial(colorize, Colors.BLUE)
magenta = partial(colorize, Colors.MAGENTA)
bold = partial(colorize, Colors.BOLD)

# We enable colorization by default if the output is a tty
set_colorization_enabled(sys.stdout.isatty())