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

Filtering #2428

Draft
wants to merge 3 commits into
base: dev
Choose a base branch
from
Draft
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
113 changes: 101 additions & 12 deletions slither/__main__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/env python3

import argparse
import cProfile
import glob
Expand All @@ -8,6 +7,7 @@
import logging
import os
import pstats
import re
import sys
import traceback
from importlib import metadata
Expand All @@ -19,6 +19,7 @@
from crytic_compile.platform.etherscan import SUPPORTED_NETWORK
from crytic_compile import compile_all, is_supported

from slither.core.filtering import FilteringRule, FilteringAction
from slither.detectors import all_detectors
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
from slither.printers import all_printers
Expand Down Expand Up @@ -51,6 +52,8 @@
)
from slither.exceptions import SlitherException

# pylint: disable=too-many-lines

logging.basicConfig()
logger = logging.getLogger("Slither")

Expand Down Expand Up @@ -273,13 +276,63 @@ def choose_printers(
# region Command line parsing
###################################################################################
###################################################################################
def parse_filter_paths(args: argparse.Namespace) -> List[FilteringRule]:
"""Parses the include/exclude/filter options from command line."""
regex = re.compile(
r"^(?P<type>[+-])?(?P<path>[^:]+?)(?::(?P<contract>[^.])(?:\.(?P<function>.+)?)?)?$"
)

def parse_option(element: str, option: str) -> FilteringRule:

match = regex.match(element)
if match:
filtering_type = FilteringAction.ALLOW
if match.group("type") == "-" or option == "remove":
filtering_type = FilteringAction.REJECT

try:
return FilteringRule(
type=filtering_type,
path=re.compile(match.group("path")) if match.group("path") else None,
contract=re.compile(match.group("contract"))
if match.group("contract")
else None,
function=re.compile(match.group("function"))
if match.group("function")
else None,
)
except re.error as exc:
raise ValueError(f"Unable to parse option {element}, invalid regex.") from exc

raise ValueError(f"Unable to parse option {element}")

filters = []
if args.include_paths:
logger.info("--include-paths is deprecated, use --include instead.")
filters.extend(
[
FilteringRule(type=FilteringAction.ALLOW, path=re.compile(path))
for path in args.include_paths.split(",")
]
)

elif args.filter_paths:
logger.info("--filter-paths is deprecated, use --remove instead.")
filters.extend(
[
FilteringRule(type=FilteringAction.REJECT, path=re.compile(path))
for path in args.filter_paths.split(",")
]
)

else:
for arg_name in ["include", "remove", "filter"]:
args_value = getattr(args, arg_name)
if not args_value:
continue
filters.extend([parse_option(element, arg_name) for element in args_value.split(",")])

def parse_filter_paths(args: argparse.Namespace, filter_path: bool) -> List[str]:
paths = args.filter_paths if filter_path else args.include_paths
if paths:
return paths.split(",")
return []
return filters


# pylint: disable=too-many-statements
Expand Down Expand Up @@ -315,7 +368,23 @@ def parse_args(
"Checklist (consider using https://github.com/crytic/slither-action)"
)
group_misc = parser.add_argument_group("Additional options")
group_filters = parser.add_mutually_exclusive_group()
group_filters = parser.add_argument_group(
"Filtering",
description="""
The following options allow to control which files will be analyzed by Slither.
While the initial steps (parsing, generating the IR) are done on the full project,
the target for the detectors and/or printers can be restricted using these options.

The filtering allow to to select directories, files, contract and functions. Each part
is compiled as a regex (so A*.sol) will match every file that starts with A and ends with .sol.
Examples :

- `sub1/A.sol:A.constructor` will analyze only the function named constructor in the contract A
found in file A.sol a directory sub1.
- `sub1/.*` will analyze all files found in the directory sub1/
- `.*:A` will analyze all the contract named A
""",
)

group_detector.add_argument(
"--detect",
Expand Down Expand Up @@ -580,22 +649,44 @@ def parse_args(
default=defaults_flag_in_config["no_fail"],
)

# Deprecated
group_filters.add_argument(
"--filter-paths",
help="Regex filter to exclude detector results matching file path e.g. (mocks/|test/)",
help=argparse.SUPPRESS,
action="store",
dest="filter_paths",
default=defaults_flag_in_config["filter_paths"],
)

# Deprecated
group_filters.add_argument(
"--include-paths",
help="Regex filter to include detector results matching file path e.g. (src/|contracts/). Opposite of --filter-paths",
help=argparse.SUPPRESS,
action="store",
dest="include_paths",
default=defaults_flag_in_config["include_paths"],
)

group_filters.add_argument(
"--include",
help="Include directory/files/contract/functions and only run the analysis on the specified elements.",
dest="include",
default=defaults_flag_in_config["include"],
)

group_filters.add_argument(
"--remove",
help="Exclude directory/files/contract/functions and only run the analysis on the specified elements.",
dest="remove",
default=defaults_flag_in_config["remove"],
)
group_filters.add_argument(
"--filter",
help="Include/Exclude directory/files/contract/functions and only run the analysis on the specified elements. Prefix by +_(or nothing) to include, and by - to exclude.",
dest="filter",
default=defaults_flag_in_config["filter"],
)

codex.init_parser(parser)

# debugger command
Expand Down Expand Up @@ -647,9 +738,7 @@ def parse_args(

args = parser.parse_args()
read_config_file(args)

args.filter_paths = parse_filter_paths(args, True)
args.include_paths = parse_filter_paths(args, False)
args.filters = parse_filter_paths(args)

# Verify our json-type output is valid
args.json_types = set(args.json_types.split(",")) # type:ignore
Expand Down
20 changes: 19 additions & 1 deletion slither/core/compilation_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def __init__(self, core: "SlitherCore", crytic_compilation_unit: CompilationUnit
self._language = Language.from_str(crytic_compilation_unit.compiler_version.compiler)

# Top level object
self.contracts: List[Contract] = []
self._contracts: List[Contract] = []
self._structures_top_level: List[StructureTopLevel] = []
self._enums_top_level: List[EnumTopLevel] = []
self._events_top_level: List[EventTopLevel] = []
Expand Down Expand Up @@ -152,6 +152,24 @@ def import_directives(self) -> List[Import]:
###################################################################################
###################################################################################

@property
def contracts(self) -> List[Contract]:
filtered_contracts = [
contract for contract in self._contracts if self.core.filter_contract(contract) is False
]
return filtered_contracts

def add_contract(self, contract: Contract) -> None:
"""Add a contract to the compilation unit.

This method is created, so we don't modify the view only `contracts` property defined above.
"""
self._contracts.append(contract)

@contracts.setter
def contracts(self, contracts: List[Contract]) -> None:
self._contracts = contracts

@property
def contracts_derived(self) -> List[Contract]:
"""list(Contract): List of contracts that are derived and not inherited."""
Expand Down
11 changes: 9 additions & 2 deletions slither/core/declarations/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -643,12 +643,19 @@ def functions(self) -> List["FunctionContract"]:
"""
list(Function): List of the functions
"""
return list(self._functions.values())
functions = [
function
for function in self._functions.values()
if not self.compilation_unit.core.filter_function(function)
]
return functions

def available_functions_as_dict(self) -> Dict[str, "Function"]:
if self._available_functions_as_dict is None:
self._available_functions_as_dict = {
f.full_name: f for f in self._functions.values() if not f.is_shadowed
f.full_name: f
for f in self._functions.values()
if not f.is_shadowed and not self.compilation_unit.core.filter_function(f)
}
return self._available_functions_as_dict

Expand Down
66 changes: 66 additions & 0 deletions slither/core/filtering.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import enum
import re
from dataclasses import dataclass
from typing import TYPE_CHECKING, Union


if TYPE_CHECKING:
from slither.core.declarations import Contract, FunctionContract


class FilteringAction(enum.Enum):
ALLOW = enum.auto()
REJECT = enum.auto()


@dataclass
class FilteringRule:

type: FilteringAction = FilteringAction.ALLOW
path: Union[re.Pattern, None] = None
contract: Union[re.Pattern, None] = None
function: Union[re.Pattern, None] = None

def match_contract(self, contract: "Contract") -> bool:
"""Check with this filter matches the contract.

Verity table is as followed:
path
| | None | True | False |
co |-----------------------------------|
nt | None || default | True | False |
ra | True || True | True | False |
ct | False || False | False | False |

"""

# If we have no constraint, we just follow the default rule
if self.path is None and self.contract is None:
return self.type == FilteringAction.ALLOW

path_match = None
if self.path is not None:
path_match = bool(re.search(self.path, contract.source_mapping.filename.short))

contract_match = None
if self.contract is not None:
contract_match = bool(re.search(self.contract, contract.name))

if path_match is None:
return contract_match

if contract_match is None:
return path_match

if contract_match and path_match:
return True

return False

def match_function(self, function: "FunctionContract") -> bool:
"""Check if this filter apply to this element."""
# If we have no constraint, follow default rule
if self.function is None:
return self.type == FilteringAction.ALLOW

return bool(re.search(self.function, function.name))
20 changes: 20 additions & 0 deletions slither/core/scope/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from slither.core.declarations.structure_top_level import StructureTopLevel
from slither.core.solidity_types import TypeAlias
from slither.core.variables.top_level_variable import TopLevelVariable
from slither.exceptions import SlitherError
from slither.slithir.variables import Constant


Expand Down Expand Up @@ -93,6 +94,25 @@ def add_accessible_scopes(self) -> bool: # pylint: disable=too-many-branches

return learn_something

def get_all_files(self, seen: Set["FileScope"]) -> Set[Filename]:
"""Recursively find all files considered in this FileScope.

The parameter seen here is to prevent circular import from generating an infinite loop.
"""

if self in seen:
return set()

seen.add(self)
if len(seen) > 1_000_000:
raise SlitherError("Unable to analyze all files considered in this FileScope.")

files = {self.filename}
for file_scope in self.accessible_scopes:
files |= file_scope.get_all_files(seen)

return files

def get_contract_from_name(self, name: Union[str, Constant]) -> Optional[Contract]:
if isinstance(name, Constant):
return self.contracts.get(name.name, None)
Expand Down
Loading
Loading