From 9d2f2802115a7520b2d7fd19c51ab54ed2034367 Mon Sep 17 00:00:00 2001 From: Alexis Date: Wed, 17 Apr 2024 15:30:33 +0200 Subject: [PATCH 1/3] Filtering Adds three new options to the Slither CLI to filter results. --- slither/__main__.py | 111 ++++- slither/core/compilation_unit.py | 21 +- slither/core/declarations/contract.py | 11 +- slither/core/filtering.py | 64 +++ slither/core/slither_core.py | 74 ++-- slither/detectors/abstract_detector.py | 6 +- slither/printers/abstract_printer.py | 10 +- slither/slither.py | 16 +- slither/utils/command_line.py | 8 +- .../vyper_parsing/vyper_compilation_unit.py | 2 +- tests/e2e/filtering/__init__.py | 0 .../test_data/test_filtering/README.md | 8 + .../test_data/test_filtering/foundry.toml | 6 + .../test_data/test_filtering/src/sub1/A.sol | 7 + .../test_filtering/src/sub1/sub12/B.sol | 7 + .../test_data/test_filtering/src/sub2/C.sol | 9 + .../test_data/test_filtering/src/sub2/D.sol | 12 + tests/e2e/filtering/test_filtering.py | 382 ++++++++++++++++++ 18 files changed, 691 insertions(+), 63 deletions(-) create mode 100644 slither/core/filtering.py create mode 100644 tests/e2e/filtering/__init__.py create mode 100644 tests/e2e/filtering/test_data/test_filtering/README.md create mode 100644 tests/e2e/filtering/test_data/test_filtering/foundry.toml create mode 100644 tests/e2e/filtering/test_data/test_filtering/src/sub1/A.sol create mode 100644 tests/e2e/filtering/test_data/test_filtering/src/sub1/sub12/B.sol create mode 100644 tests/e2e/filtering/test_data/test_filtering/src/sub2/C.sol create mode 100644 tests/e2e/filtering/test_data/test_filtering/src/sub2/D.sol create mode 100644 tests/e2e/filtering/test_filtering.py diff --git a/slither/__main__.py b/slither/__main__.py index caaef5730b..9a81044dc0 100644 --- a/slither/__main__.py +++ b/slither/__main__.py @@ -8,6 +8,7 @@ import logging import os import pstats +import re import sys import traceback from importlib import metadata @@ -19,6 +20,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 @@ -273,13 +275,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[+-])?(?P[^:]+?)(?::(?P[^.])(?:\.(?P.+)?)?)?$" + ) + 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: + raise ValueError(f"Unable to parse option {element}, invalid regex.") + + 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(",") + ] + ) -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 [] + 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(",")]) + + return filters # pylint: disable=too-many-statements @@ -315,7 +367,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", @@ -580,22 +648,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 @@ -648,8 +738,9 @@ 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.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 diff --git a/slither/core/compilation_unit.py b/slither/core/compilation_unit.py index df652dab0c..4645963884 100644 --- a/slither/core/compilation_unit.py +++ b/slither/core/compilation_unit.py @@ -20,6 +20,7 @@ from slither.core.declarations.function_top_level import FunctionTopLevel from slither.core.declarations.structure_top_level import StructureTopLevel from slither.core.declarations.using_for_top_level import UsingForTopLevel +from slither.core.filtering import FilteringRule from slither.core.scope.scope import FileScope from slither.core.solidity_types.type_alias import TypeAliasTopLevel from slither.core.variables.state_variable import StateVariable @@ -55,7 +56,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] = [] @@ -152,6 +153,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.""" diff --git a/slither/core/declarations/contract.py b/slither/core/declarations/contract.py index fe617b3313..834e604657 100644 --- a/slither/core/declarations/contract.py +++ b/slither/core/declarations/contract.py @@ -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 diff --git a/slither/core/filtering.py b/slither/core/filtering.py new file mode 100644 index 0000000000..3f7188708a --- /dev/null +++ b/slither/core/filtering.py @@ -0,0 +1,64 @@ +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 True if self.type == FilteringAction.ALLOW else False + + 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 + elif contract_match is None: + return path_match + elif contract_match and path_match: + return True + else: + 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 True if self.type == FilteringAction.ALLOW else False + + return bool(re.search(self.function, function.name)) diff --git a/slither/core/slither_core.py b/slither/core/slither_core.py index 8eca260fac..1c735083a5 100644 --- a/slither/core/slither_core.py +++ b/slither/core/slither_core.py @@ -18,6 +18,7 @@ from slither.core.context.context import Context from slither.core.declarations import Contract, FunctionContract from slither.core.declarations.top_level import TopLevel +from slither.core.filtering import FilteringRule, FilteringAction from slither.core.source_mapping.source_mapping import SourceMapping, Source from slither.slithir.variables import Constant from slither.utils.colors import red @@ -64,6 +65,9 @@ def __init__(self) -> None: self._paths_to_filter: Set[str] = set() self._paths_to_include: Set[str] = set() + self.filters: List[FilteringRule] = [] + self.default_action: FilteringAction = FilteringAction.ALLOW + self._crytic_compile: Optional[CryticCompile] = None self._generate_patches = False @@ -332,6 +336,40 @@ def offset_to_definitions(self, filename_str: str, offset: int) -> Set[Source]: # region Filtering results ################################################################################### ################################################################################### + def filter_contract(self, contract: Contract) -> bool: + """Check within the filters if we should exclude the contract. + Returns True if the contract is excluded, False otherwise + """ + action: FilteringAction = self.default_action + for element_filter in self.filters: + if element_filter.match_contract(contract): + action = ( + FilteringAction.ALLOW + if element_filter.type == FilteringAction.ALLOW + else FilteringAction.REJECT + ) + + if action == FilteringAction.ALLOW: + return False + else: + return True + + def filter_function(self, function: "FunctionContract") -> bool: + """Checks within the filter if this function should be excluded.""" + + action: FilteringAction = self.default_action + for element_filter in self.filters: + if element_filter.match_function(function): + action = ( + FilteringAction.ALLOW + if element_filter.type == FilteringAction.ALLOW + else FilteringAction.REJECT + ) + + if action == FilteringAction.ALLOW: + return False + else: + return True def parse_ignore_comments(self, file: str) -> None: # The first time we check a file, find all start/end ignore comments and memoize them. @@ -433,42 +471,6 @@ def valid_result(self, r: Dict) -> bool: return False self._currently_seen_resuts.add(r["id"]) - source_mapping_elements = [ - elem["source_mapping"].get("filename_absolute", "unknown") - for elem in r["elements"] - if "source_mapping" in elem - ] - - # Use POSIX-style paths so that filter_paths|include_paths works across different - # OSes. Convert to a list so elements don't get consumed and are lost - # while evaluating the first pattern - source_mapping_elements = list( - map(lambda x: pathlib.Path(x).resolve().as_posix() if x else x, source_mapping_elements) - ) - (matching, paths, msg_err) = ( - (True, self._paths_to_include, "--include-paths") - if self._paths_to_include - else (False, self._paths_to_filter, "--filter-paths") - ) - - for path in paths: - try: - if any( - bool(re.search(_relative_path_format(path), src_mapping)) - for src_mapping in source_mapping_elements - ): - matching = not matching - break - except re.error: - logger.error( - f"Incorrect regular expression for {msg_err} {path}." - "\nSlither supports the Python re format" - ": https://docs.python.org/3/library/re.html" - ) - - if r["elements"] and matching: - return False - if self._show_ignored_findings: return True if self.has_ignore_comment(r): diff --git a/slither/detectors/abstract_detector.py b/slither/detectors/abstract_detector.py index 8baf9bb3c7..370380ac4b 100644 --- a/slither/detectors/abstract_detector.py +++ b/slither/detectors/abstract_detector.py @@ -88,7 +88,6 @@ def __init__( self, compilation_unit: SlitherCompilationUnit, slither: "Slither", logger: Logger ) -> None: self.compilation_unit: SlitherCompilationUnit = compilation_unit - self.contracts: List[Contract] = compilation_unit.contracts self.slither: "Slither" = slither # self.filename = slither.filename self.logger = logger @@ -171,6 +170,11 @@ def __init__( f"CONFIDENCE is not initialized {self.__class__.__name__}" ) + @property + def contracts(self) -> List[Contract]: + """Direct accessor to the compilation unit contracts.""" + return self.compilation_unit.contracts + def _log(self, info: str) -> None: if self.logger: self.logger.info(self.color(info)) diff --git a/slither/printers/abstract_printer.py b/slither/printers/abstract_printer.py index 166cc55f0a..f10efd6ffc 100644 --- a/slither/printers/abstract_printer.py +++ b/slither/printers/abstract_printer.py @@ -22,7 +22,6 @@ class AbstractPrinter(metaclass=abc.ABCMeta): def __init__(self, slither: "Slither", logger: Optional[Logger]) -> None: self.slither = slither - self.contracts = slither.contracts self.filename = slither.filename self.logger = logger @@ -41,6 +40,15 @@ def __init__(self, slither: "Slither", logger: Optional[Logger]) -> None: f"WIKI is not initialized {self.__class__.__name__}" ) + @property + def contracts(self): + """Direct accessor to the slither contracts. + + We prefer to delay as much as possible the direct access of contracts to leave the possibility + for filtering to be applied. + """ + return self.slither.contracts + def info(self, info: str) -> None: if self.logger: self.logger.info(info) diff --git a/slither/slither.py b/slither/slither.py index 0f22185353..491e051120 100644 --- a/slither/slither.py +++ b/slither/slither.py @@ -5,6 +5,7 @@ # pylint: disable= no-name-in-module from slither.core.compilation_unit import SlitherCompilationUnit +from slither.core.filtering import FilteringRule, FilteringAction from slither.core.slither_core import SlitherCore from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification from slither.exceptions import SlitherError @@ -100,7 +101,7 @@ def __init__(self, target: Union[str, CryticCompile], **kwargs) -> None: disable_solc_warnings (bool): True to disable solc warnings (default false) solc_args (str): solc arguments (default '') ast_format (str): ast format (default '--ast-compact-json') - filter_paths (list(str)): list of path to filter (default []) + filters: list of FilteredElements (default []) triage_mode (bool): if true, switch to triage mode (default false) exclude_dependencies (bool): if true, exclude results that are only related to dependencies generate_patches (bool): if true, patches are generated (json output only) @@ -165,7 +166,7 @@ def __init__(self, target: Union[str, CryticCompile], **kwargs) -> None: # endregion multi-line ) sol_parser._contracts_by_id[contract.id] = contract - sol_parser._compilation_unit.contracts.append(contract) + sol_parser._compilation_unit.add_contract(contract) _update_file_scopes(sol_parser) @@ -177,13 +178,10 @@ def __init__(self, target: Union[str, CryticCompile], **kwargs) -> None: self._detectors = [] self._printers = [] - filter_paths = kwargs.get("filter_paths", []) - for p in filter_paths: - self.add_path_to_filter(p) - - include_paths = kwargs.get("include_paths", []) - for p in include_paths: - self.add_path_to_include(p) + # Initialize the filtering system + self.filters: List[FilteringRule] = kwargs.get("filters", []) + if any(filter.type == FilteringAction.ALLOW for filter in self.filters): + self.default_action = FilteringAction.REJECT self._exclude_dependencies = kwargs.get("exclude_dependencies", False) diff --git a/slither/utils/command_line.py b/slither/utils/command_line.py index a37e859133..0081856d14 100644 --- a/slither/utils/command_line.py +++ b/slither/utils/command_line.py @@ -59,8 +59,12 @@ class FailOnLevel(enum.Enum): "sarif": None, "json-types": ",".join(DEFAULT_JSON_OUTPUT_TYPES), "disable_color": False, - "filter_paths": None, - "include_paths": None, + # Filtering + "filter_paths": None, # deprecated + "include_paths": None, # deprecated + "include": None, + "remove": None, + "filter": None, "generate_patches": False, # debug command "skip_assembly": False, diff --git a/slither/vyper_parsing/vyper_compilation_unit.py b/slither/vyper_parsing/vyper_compilation_unit.py index 2a47d9864d..0ac27bcbba 100644 --- a/slither/vyper_parsing/vyper_compilation_unit.py +++ b/slither/vyper_parsing/vyper_compilation_unit.py @@ -38,7 +38,7 @@ def parse_module(self, data: Module, filename: str): def parse_contracts(self): for contract, contract_parser in self._underlying_contract_to_parser.items(): self._contracts_by_id[contract.id] = contract - self._compilation_unit.contracts.append(contract) + self._compilation_unit.add_contract(contract) contract_parser.parse_enums() contract_parser.parse_structs() diff --git a/tests/e2e/filtering/__init__.py b/tests/e2e/filtering/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e/filtering/test_data/test_filtering/README.md b/tests/e2e/filtering/test_data/test_filtering/README.md new file mode 100644 index 0000000000..4540adac00 --- /dev/null +++ b/tests/e2e/filtering/test_data/test_filtering/README.md @@ -0,0 +1,8 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +This test should be installed using +```shell +forge install --no-commit --no-git foundry-rs/forge-std +``` \ No newline at end of file diff --git a/tests/e2e/filtering/test_data/test_filtering/foundry.toml b/tests/e2e/filtering/test_data/test_filtering/foundry.toml new file mode 100644 index 0000000000..25b918f9c9 --- /dev/null +++ b/tests/e2e/filtering/test_data/test_filtering/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/tests/e2e/filtering/test_data/test_filtering/src/sub1/A.sol b/tests/e2e/filtering/test_data/test_filtering/src/sub1/A.sol new file mode 100644 index 0000000000..b87b8bf9c9 --- /dev/null +++ b/tests/e2e/filtering/test_data/test_filtering/src/sub1/A.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +contract A { + constructor() {} + function a() public view {} +} diff --git a/tests/e2e/filtering/test_data/test_filtering/src/sub1/sub12/B.sol b/tests/e2e/filtering/test_data/test_filtering/src/sub1/sub12/B.sol new file mode 100644 index 0000000000..96cda5adbe --- /dev/null +++ b/tests/e2e/filtering/test_data/test_filtering/src/sub1/sub12/B.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +contract B { + constructor(){} + function b() public view {} +} diff --git a/tests/e2e/filtering/test_data/test_filtering/src/sub2/C.sol b/tests/e2e/filtering/test_data/test_filtering/src/sub2/C.sol new file mode 100644 index 0000000000..bcffaca8bd --- /dev/null +++ b/tests/e2e/filtering/test_data/test_filtering/src/sub2/C.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +contract C { + constructor(){ + + } + function c() public view {} +} diff --git a/tests/e2e/filtering/test_data/test_filtering/src/sub2/D.sol b/tests/e2e/filtering/test_data/test_filtering/src/sub2/D.sol new file mode 100644 index 0000000000..9316bdb5b3 --- /dev/null +++ b/tests/e2e/filtering/test_data/test_filtering/src/sub2/D.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +contract D { + constructor(){ + + } +} + +contract E { + constructor() {} +} \ No newline at end of file diff --git a/tests/e2e/filtering/test_filtering.py b/tests/e2e/filtering/test_filtering.py new file mode 100644 index 0000000000..4bb83fa2bd --- /dev/null +++ b/tests/e2e/filtering/test_filtering.py @@ -0,0 +1,382 @@ +from argparse import Namespace +import logging +import shutil +from pathlib import Path +from typing import List, Dict, Set, Union, OrderedDict +import re + +import pytest + +from slither import Slither +from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification +from slither.printers.abstract_printer import AbstractPrinter +from slither.utils.output import Output +from slither.core.filtering import FilteringRule, FilteringAction +from slither.__main__ import parse_filter_paths + +TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" + + +foundry_available = shutil.which("forge") is not None +project_ready = Path(TEST_DATA_DIR, "test_filtering/lib/forge-std").exists() + + +class DummyPrinter(AbstractPrinter): + ARGUMENT = "dummy_printer" + HELP = ".." + WIKI = ".." + + def output(self, _: str) -> Output: + output = [] + for contract in self.contracts: + for function in contract.functions: + output.append(f"{function.contract_declarer.name}.{function.name}") + + output = self.generate_output(",".join(output)) + return output + + @staticmethod + def analyze_dummy_output(output: OrderedDict) -> Set[str]: + return set(output["description"].split(",")) + + +class DummyDetector(AbstractDetector): + ARGUMENT = "dummy_detector" # run the detector with slither.py --ARGUMENT + HELP = ".." # help information + IMPACT = DetectorClassification.LOW + CONFIDENCE = DetectorClassification.LOW + + WIKI = ".." + + WIKI_TITLE = ".." + WIKI_DESCRIPTION = ".." + WIKI_EXPLOIT_SCENARIO = ".." + WIKI_RECOMMENDATION = ".." + + def _detect(self) -> List[Output]: + results = [] + for contract in self.compilation_unit.contracts_derived: + for function in contract.functions: + results.append(self.generate_result([function])) + + return results + + @staticmethod + def analyze_dummy_output(results: Dict) -> Set[str]: + output = set() + for result in results: + for element in result.get("elements", []): + assert element.get("type") == "function" + contract = element.get("type_specific_fields", {}).get("parent", {}).get("name") + output.add(f"{contract}.{element['name']}") + + return output + + +@pytest.mark.skipif( + not foundry_available or not project_ready, reason="requires Foundry and project setup" +) +def test_filtering(): + def run_detector_for_filtering( + sl: Slither, + filtering: Union[List[FilteringRule], None], + default_action: FilteringAction, + ): + + # First, reset any results + sl._currently_seen_resuts = set() + + # Set up default action + sl.default_action = default_action + + if filtering is not None: + sl.filters = filtering + + return DummyDetector.analyze_dummy_output(sl.run_detectors().pop()) + + slither = Slither(Path(TEST_DATA_DIR, "test_filtering").as_posix()) + slither.register_detector(DummyDetector) + + # First, check that if we deny everything, we don't run on anything + output = run_detector_for_filtering(slither, [], FilteringAction.REJECT) + assert not output + + # Then, if we run on everything, lets check that we get all + output = run_detector_for_filtering(slither, [], FilteringAction.ALLOW) + assert not output ^ { + "A.constructor", + "A.a", + "B.constructor", + "B.b", + "C.constructor", + "C.c", + "D.constructor", + "E.constructor", + } + + # Then, test more closely + # Reject all but a single directory + output = run_detector_for_filtering( + slither, + [FilteringRule(type=FilteringAction.ALLOW, path=re.compile(r"sub1/"))], + FilteringAction.REJECT, + ) + assert not output ^ {"B.b", "A.constructor", "B.constructor", "A.a"} + + # Allow all but deny a directory + output = run_detector_for_filtering( + slither, + [FilteringRule(type=FilteringAction.REJECT, path=re.compile(r"sub1/"))], + FilteringAction.ALLOW, + ) + assert not output ^ {"C.c", "C.constructor", "D.constructor", "E.constructor"} + + # Allow all functions named constructor + output = run_detector_for_filtering( + slither, + [FilteringRule(type=FilteringAction.ALLOW, function=re.compile(r"constructor"))], + FilteringAction.REJECT, + ) + assert not output ^ { + "A.constructor", + "B.constructor", + "C.constructor", + "D.constructor", + "E.constructor", + } + + # Allow only contract C + output = run_detector_for_filtering( + slither, + [FilteringRule(type=FilteringAction.ALLOW, contract=re.compile(r"C"))], + FilteringAction.REJECT, + ) + assert not output ^ {"C.constructor", "C.c"} + + # Allow everything in sub1 but not in sub1/sub12 + output = run_detector_for_filtering( + slither, + [ + FilteringRule(type=FilteringAction.ALLOW, path=re.compile("sub1/")), + FilteringRule(type=FilteringAction.REJECT, path=re.compile("sub12/")), + ], + FilteringAction.REJECT, + ) + assert not output ^ {"A.constructor", "A.a"} + + # Allow everything in D.sol + output = run_detector_for_filtering( + slither, + [ + FilteringRule(type=FilteringAction.ALLOW, path=re.compile("D.sol")), + ], + FilteringAction.REJECT, + ) + assert not output ^ {"D.constructor", "E.constructor"} + + # Allow only E in D.sol + output = run_detector_for_filtering( + slither, + [ + FilteringRule( + type=FilteringAction.ALLOW, path=re.compile("D.sol"), contract=re.compile("E") + ), + ], + FilteringAction.REJECT, + ) + assert not output ^ {"E.constructor"} + + +def get_default_namespace( + include_paths: Union[str, None] = None, + filter_paths: Union[str, None] = None, + include: Union[str, None] = None, + remove: Union[str, None] = None, + filter: Union[str, None] = None, +) -> Namespace: + return Namespace( + include_paths=include_paths, + filter_paths=filter_paths, + include=include, + remove=remove, + filter=filter, + ) + + +def test_filtering_cl_deprecated(caplog): + with caplog.at_level(logging.INFO): + parsed_args = parse_filter_paths(get_default_namespace(include_paths="sub1/")) + + assert parsed_args == [FilteringRule(path=re.compile("sub1/"))] + assert "include-paths is deprecated" in caplog.text + + with caplog.at_level(logging.INFO): + parsed_args = parse_filter_paths(get_default_namespace(filter_paths="sub1/")) + + assert parsed_args == [FilteringRule(type=FilteringAction.REJECT, path=re.compile("sub1/"))] + assert "filter-paths is deprecated" in caplog.text + + +def test_filtering_cl_multiple(): + parsed_args = parse_filter_paths(get_default_namespace(include="sub12/,sub2/")) + + assert parsed_args == [ + FilteringRule(type=FilteringAction.ALLOW, path=re.compile("sub12/")), + FilteringRule(type=FilteringAction.ALLOW, path=re.compile("sub2/")), + ] + + parsed_args = parse_filter_paths(get_default_namespace(remove="sub12/,sub2/")) + + assert parsed_args == [ + FilteringRule(type=FilteringAction.REJECT, path=re.compile("sub12/")), + FilteringRule(type=FilteringAction.REJECT, path=re.compile("sub2/")), + ] + + +def test_filtering_cl_full(): + parsed_args = parse_filter_paths(get_default_namespace(include="sub1/A.sol:A.a")) + assert parsed_args == [ + FilteringRule( + type=FilteringAction.ALLOW, + path=re.compile("sub1/A.sol"), + contract=re.compile("A"), + function=re.compile("a"), + ), + ] + + parsed_args = parse_filter_paths(get_default_namespace(remove="sub1/A.sol:A.a")) + assert parsed_args == [ + FilteringRule( + type=FilteringAction.REJECT, + path=re.compile("sub1/A.sol"), + contract=re.compile("A"), + function=re.compile("a"), + ), + ] + + +def test_filtering_cl_filter(): + parsed_args = parse_filter_paths(get_default_namespace(filter="sub1/,-sub1/sub12/")) + assert parsed_args == [ + FilteringRule( + type=FilteringAction.ALLOW, + path=re.compile("sub1/"), + ), + FilteringRule( + type=FilteringAction.REJECT, + path=re.compile("sub1/sub12/"), + ), + ] + + +def test_invalid_regex(): + with pytest.raises(ValueError): + parse_filter_paths(get_default_namespace(filter="sub1(/")) + + with pytest.raises(ValueError): + parse_filter_paths(get_default_namespace(filter=":not-matching:")) + + +@pytest.mark.skipif( + not foundry_available or not project_ready, reason="requires Foundry and project setup" +) +def test_filtering_printer(): + def run_printer( + sl: Slither, filtering_rules: List[FilteringRule], default_action: FilteringAction + ): + sl.filters = filtering_rules + sl._contracts = [] # Reset the list of contracts so it gets recomputed + sl.default_action = default_action + return DummyPrinter.analyze_dummy_output(sl.run_printers().pop()) + + slither = Slither(Path(TEST_DATA_DIR, "test_filtering").as_posix()) + slither.register_printer(DummyPrinter) + + output = run_printer(slither, [], FilteringAction.ALLOW) + assert not output ^ { + "A.a", + "A.constructor", + "B.b", + "B.constructor", + "C.c", + "C.constructor", + "D.constructor", + "E.constructor", + } + + # First, check that if we deny everything, we don't run on anything + slither.default_action = FilteringAction.REJECT + output = run_printer(slither, [], FilteringAction.REJECT) + assert output == {""} + + # Then, test more closely + # Reject all but a single directory + output = run_printer( + slither, + [FilteringRule(type=FilteringAction.ALLOW, path=re.compile(r"sub1/"))], + FilteringAction.REJECT, + ) + assert not output ^ {"B.b", "A.constructor", "B.constructor", "A.a"} + + # Allow all but deny a directory + output = run_printer( + slither, + [FilteringRule(type=FilteringAction.REJECT, path=re.compile(r"sub1/"))], + FilteringAction.ALLOW, + ) + assert not output ^ {"C.c", "C.constructor", "D.constructor", "E.constructor"} + + # Allow all functions named constructor + output = run_printer( + slither, + [FilteringRule(type=FilteringAction.ALLOW, function=re.compile(r"constructor"))], + FilteringAction.REJECT, + ) + assert not output ^ { + "A.constructor", + "B.constructor", + "C.constructor", + "D.constructor", + "E.constructor", + } + + # Allow only contract C + output = run_printer( + slither, + [FilteringRule(type=FilteringAction.ALLOW, contract=re.compile(r"C"))], + FilteringAction.REJECT, + ) + assert not output ^ {"C.constructor", "C.c"} + + # Allow everything in sub1 but not in sub1/sub12 + output = run_printer( + slither, + [ + FilteringRule(type=FilteringAction.ALLOW, path=re.compile("sub1/")), + FilteringRule(type=FilteringAction.REJECT, path=re.compile("sub12/")), + ], + FilteringAction.REJECT, + ) + assert not output ^ {"A.constructor", "A.a"} + + # Allow everything in D.sol + output = run_printer( + slither, + [ + FilteringRule(type=FilteringAction.ALLOW, path=re.compile("D.sol")), + ], + FilteringAction.REJECT, + ) + assert not output ^ {"D.constructor", "E.constructor"} + + # Allow only E in D.sol + output = run_printer( + slither, + [ + FilteringRule( + type=FilteringAction.ALLOW, path=re.compile("D.sol"), contract=re.compile("E") + ), + ], + FilteringAction.REJECT, + ) + assert not output ^ {"E.constructor"} From 9e223dc221b93a686e003ed6a21ac7299961d54e Mon Sep 17 00:00:00 2001 From: Alexis Date: Wed, 17 Apr 2024 16:25:27 +0200 Subject: [PATCH 2/3] Fix linters issues. --- slither/__main__.py | 12 +++++------- slither/core/compilation_unit.py | 1 - slither/core/filtering.py | 14 ++++++++------ slither/core/slither_core.py | 8 ++++---- .../filtering/test_data/test_filtering/README.md | 2 +- tests/e2e/filtering/test_filtering.py | 12 +++++++----- 6 files changed, 25 insertions(+), 24 deletions(-) diff --git a/slither/__main__.py b/slither/__main__.py index 9a81044dc0..84fbbd134e 100644 --- a/slither/__main__.py +++ b/slither/__main__.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 - import argparse import cProfile import glob @@ -53,6 +52,8 @@ ) from slither.exceptions import SlitherException +# pylint: disable=too-many-lines + logging.basicConfig() logger = logging.getLogger("Slither") @@ -300,8 +301,8 @@ def parse_option(element: str, option: str) -> FilteringRule: if match.group("function") else None, ) - except re.error: - raise ValueError(f"Unable to parse option {element}, invalid regex.") + 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}") @@ -375,7 +376,7 @@ def parse_args( 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. + 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 @@ -737,9 +738,6 @@ 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 diff --git a/slither/core/compilation_unit.py b/slither/core/compilation_unit.py index 4645963884..994dd89461 100644 --- a/slither/core/compilation_unit.py +++ b/slither/core/compilation_unit.py @@ -20,7 +20,6 @@ from slither.core.declarations.function_top_level import FunctionTopLevel from slither.core.declarations.structure_top_level import StructureTopLevel from slither.core.declarations.using_for_top_level import UsingForTopLevel -from slither.core.filtering import FilteringRule from slither.core.scope.scope import FileScope from slither.core.solidity_types.type_alias import TypeAliasTopLevel from slither.core.variables.state_variable import StateVariable diff --git a/slither/core/filtering.py b/slither/core/filtering.py index 3f7188708a..384901e880 100644 --- a/slither/core/filtering.py +++ b/slither/core/filtering.py @@ -36,7 +36,7 @@ def match_contract(self, contract: "Contract") -> bool: # If we have no constraint, we just follow the default rule if self.path is None and self.contract is None: - return True if self.type == FilteringAction.ALLOW else False + return self.type == FilteringAction.ALLOW path_match = None if self.path is not None: @@ -48,17 +48,19 @@ def match_contract(self, contract: "Contract") -> bool: if path_match is None: return contract_match - elif contract_match is None: + + if contract_match is None: return path_match - elif contract_match and path_match: + + if contract_match and path_match: return True - else: - return False + + 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 True if self.type == FilteringAction.ALLOW else False + return self.type == FilteringAction.ALLOW return bool(re.search(self.function, function.name)) diff --git a/slither/core/slither_core.py b/slither/core/slither_core.py index 1c735083a5..292d1f1a14 100644 --- a/slither/core/slither_core.py +++ b/slither/core/slither_core.py @@ -351,8 +351,8 @@ def filter_contract(self, contract: Contract) -> bool: if action == FilteringAction.ALLOW: return False - else: - return True + + return True def filter_function(self, function: "FunctionContract") -> bool: """Checks within the filter if this function should be excluded.""" @@ -368,8 +368,8 @@ def filter_function(self, function: "FunctionContract") -> bool: if action == FilteringAction.ALLOW: return False - else: - return True + + return True def parse_ignore_comments(self, file: str) -> None: # The first time we check a file, find all start/end ignore comments and memoize them. diff --git a/tests/e2e/filtering/test_data/test_filtering/README.md b/tests/e2e/filtering/test_data/test_filtering/README.md index 4540adac00..c729f468a2 100644 --- a/tests/e2e/filtering/test_data/test_filtering/README.md +++ b/tests/e2e/filtering/test_data/test_filtering/README.md @@ -1,4 +1,4 @@ -## Foundry +# Foundry **Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** diff --git a/tests/e2e/filtering/test_filtering.py b/tests/e2e/filtering/test_filtering.py index 4bb83fa2bd..402a46ee0f 100644 --- a/tests/e2e/filtering/test_filtering.py +++ b/tests/e2e/filtering/test_filtering.py @@ -84,6 +84,7 @@ def run_detector_for_filtering( ): # First, reset any results + # pylint: disable=protected-access sl._currently_seen_resuts = set() # Set up default action @@ -192,14 +193,14 @@ def get_default_namespace( filter_paths: Union[str, None] = None, include: Union[str, None] = None, remove: Union[str, None] = None, - filter: Union[str, None] = None, + filter_arg: Union[str, None] = None, ) -> Namespace: return Namespace( include_paths=include_paths, filter_paths=filter_paths, include=include, remove=remove, - filter=filter, + filter=filter_arg, ) @@ -256,7 +257,7 @@ def test_filtering_cl_full(): def test_filtering_cl_filter(): - parsed_args = parse_filter_paths(get_default_namespace(filter="sub1/,-sub1/sub12/")) + parsed_args = parse_filter_paths(get_default_namespace(filter_arg="sub1/,-sub1/sub12/")) assert parsed_args == [ FilteringRule( type=FilteringAction.ALLOW, @@ -271,10 +272,10 @@ def test_filtering_cl_filter(): def test_invalid_regex(): with pytest.raises(ValueError): - parse_filter_paths(get_default_namespace(filter="sub1(/")) + parse_filter_paths(get_default_namespace(filter_arg="sub1(/")) with pytest.raises(ValueError): - parse_filter_paths(get_default_namespace(filter=":not-matching:")) + parse_filter_paths(get_default_namespace(filter_arg=":not-matching:")) @pytest.mark.skipif( @@ -285,6 +286,7 @@ def run_printer( sl: Slither, filtering_rules: List[FilteringRule], default_action: FilteringAction ): sl.filters = filtering_rules + # pylint: disable=protected-access sl._contracts = [] # Reset the list of contracts so it gets recomputed sl.default_action = default_action return DummyPrinter.analyze_dummy_output(sl.run_printers().pop()) From 6c8ee23cce39af80548fb947881a858bfb483766 Mon Sep 17 00:00:00 2001 From: Alexis Date: Tue, 23 Apr 2024 14:51:31 +0200 Subject: [PATCH 3/3] Filter out files before a too deep analysis. --- slither/core/scope/scope.py | 20 ++++++++++ slither/slither.py | 40 ++++++++++++++----- .../test_filtering_analysis/README.md | 8 ++++ .../test_filtering_analysis/foundry.toml | 6 +++ .../test_filtering_analysis/src/sub1/A.sol | 16 ++++++++ .../test_filtering_analysis/src/sub1/C.sol | 13 ++++++ .../test_filtering_analysis/src/sub1/E.sol | 10 +++++ .../test_filtering_analysis/src/sub1/F.sol | 10 +++++ .../test_filtering_analysis/src/sub2/B.sol | 15 +++++++ .../test_filtering_analysis/src/sub2/D.sol | 7 ++++ tests/e2e/filtering/test_filtering.py | 29 ++++++++++++++ 11 files changed, 165 insertions(+), 9 deletions(-) create mode 100644 tests/e2e/filtering/test_data/test_filtering_analysis/README.md create mode 100644 tests/e2e/filtering/test_data/test_filtering_analysis/foundry.toml create mode 100644 tests/e2e/filtering/test_data/test_filtering_analysis/src/sub1/A.sol create mode 100644 tests/e2e/filtering/test_data/test_filtering_analysis/src/sub1/C.sol create mode 100644 tests/e2e/filtering/test_data/test_filtering_analysis/src/sub1/E.sol create mode 100644 tests/e2e/filtering/test_data/test_filtering_analysis/src/sub1/F.sol create mode 100644 tests/e2e/filtering/test_data/test_filtering_analysis/src/sub2/B.sol create mode 100644 tests/e2e/filtering/test_data/test_filtering_analysis/src/sub2/D.sol diff --git a/slither/core/scope/scope.py b/slither/core/scope/scope.py index ee2a98eb3a..b69b6e3955 100644 --- a/slither/core/scope/scope.py +++ b/slither/core/scope/scope.py @@ -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 @@ -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) diff --git a/slither/slither.py b/slither/slither.py index 491e051120..aebfb3c379 100644 --- a/slither/slither.py +++ b/slither/slither.py @@ -1,7 +1,8 @@ import logging -from typing import Union, List, Type, Dict, Optional +from typing import Union, List, Type, Dict, Optional, Set from crytic_compile import CryticCompile, InvalidCompilation +from crytic_compile.utils.naming import Filename # pylint: disable= no-name-in-module from slither.core.compilation_unit import SlitherCompilationUnit @@ -37,13 +38,19 @@ def _check_common_things( def _update_file_scopes( sol_parser: SlitherCompilationUnitSolc, -): # pylint: disable=too-many-branches + analyzed_files: Set[Filename], +): # pylint: disable=too-many-branches, disable=too-many-locals """ Since all definitions in a file are exported by default, including definitions from its (transitive) dependencies, we can identify all top level items that could possibly be referenced within the file from its exportedSymbols. It is not as straightforward for user defined types and functions as well as aliasing. See add_accessible_scopes for more details. """ - candidates = sol_parser.compilation_unit.scopes.values() + # First, lets remove the filtered candidates + candidates = [ + scope + for scope in sol_parser.compilation_unit.scopes.values() + if scope.filename in analyzed_files + ] learned_something = False # Because solc's import allows cycle in the import graph, iterate until we aren't adding new information to the scope. while True: @@ -128,6 +135,11 @@ def __init__(self, target: Union[str, CryticCompile], **kwargs) -> None: self.no_fail = kwargs.get("no_fail", False) + # Initialize the filtering system + self.filters: List[FilteringRule] = kwargs.get("filters", []) + if any(filter.type == FilteringAction.ALLOW for filter in self.filters): + self.default_action = FilteringAction.REJECT + self._parsers: List[SlitherCompilationUnitSolc] = [] try: if isinstance(target, CryticCompile): @@ -157,7 +169,22 @@ def __init__(self, target: Union[str, CryticCompile], **kwargs) -> None: sol_parser.parse_top_level_items(ast, path) self.add_source_code(path) + # Get all the files that are matched by the current set of filters. + # Don't forget considering transitive dependencies + files_to_analyze = set() + for contract in sol_parser._underlying_contract_to_parser: + if self.filter_contract(contract): + continue + + files_to_analyze.add(contract.file_scope.filename) + files_to_analyze |= contract.file_scope.get_all_files(set()) + for contract in sol_parser._underlying_contract_to_parser: + + if contract.file_scope.filename not in files_to_analyze: + logger.debug("Filtering out %s", contract.file_scope.filename) + continue + if contract.name.startswith("SlitherInternalTopLevelContract"): raise SlitherError( # region multi-line-string @@ -168,7 +195,7 @@ def __init__(self, target: Union[str, CryticCompile], **kwargs) -> None: sol_parser._contracts_by_id[contract.id] = contract sol_parser._compilation_unit.add_contract(contract) - _update_file_scopes(sol_parser) + _update_file_scopes(sol_parser, files_to_analyze) if kwargs.get("generate_patches", False): self.generate_patches = True @@ -178,11 +205,6 @@ def __init__(self, target: Union[str, CryticCompile], **kwargs) -> None: self._detectors = [] self._printers = [] - # Initialize the filtering system - self.filters: List[FilteringRule] = kwargs.get("filters", []) - if any(filter.type == FilteringAction.ALLOW for filter in self.filters): - self.default_action = FilteringAction.REJECT - self._exclude_dependencies = kwargs.get("exclude_dependencies", False) triage_mode = kwargs.get("triage_mode", False) diff --git a/tests/e2e/filtering/test_data/test_filtering_analysis/README.md b/tests/e2e/filtering/test_data/test_filtering_analysis/README.md new file mode 100644 index 0000000000..c729f468a2 --- /dev/null +++ b/tests/e2e/filtering/test_data/test_filtering_analysis/README.md @@ -0,0 +1,8 @@ +# Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +This test should be installed using +```shell +forge install --no-commit --no-git foundry-rs/forge-std +``` \ No newline at end of file diff --git a/tests/e2e/filtering/test_data/test_filtering_analysis/foundry.toml b/tests/e2e/filtering/test_data/test_filtering_analysis/foundry.toml new file mode 100644 index 0000000000..25b918f9c9 --- /dev/null +++ b/tests/e2e/filtering/test_data/test_filtering_analysis/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/tests/e2e/filtering/test_data/test_filtering_analysis/src/sub1/A.sol b/tests/e2e/filtering/test_data/test_filtering_analysis/src/sub1/A.sol new file mode 100644 index 0000000000..84db945385 --- /dev/null +++ b/tests/e2e/filtering/test_data/test_filtering_analysis/src/sub1/A.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {B} from "../sub2/B.sol"; +import "./E.sol"; + +contract A is E { + B public b; + + constructor(B b_contract) { + b = b_contract; + } + function a() public view { + b.b(); + } +} diff --git a/tests/e2e/filtering/test_data/test_filtering_analysis/src/sub1/C.sol b/tests/e2e/filtering/test_data/test_filtering_analysis/src/sub1/C.sol new file mode 100644 index 0000000000..ceff61b9ad --- /dev/null +++ b/tests/e2e/filtering/test_data/test_filtering_analysis/src/sub1/C.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {B} from "../sub2/B.sol"; + +contract C { + + B.Status public status; + + constructor(){ + + } +} diff --git a/tests/e2e/filtering/test_data/test_filtering_analysis/src/sub1/E.sol b/tests/e2e/filtering/test_data/test_filtering_analysis/src/sub1/E.sol new file mode 100644 index 0000000000..76e00f10c4 --- /dev/null +++ b/tests/e2e/filtering/test_data/test_filtering_analysis/src/sub1/E.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "./F.sol"; + +contract E is F{ + constructor(){ + + } +} diff --git a/tests/e2e/filtering/test_data/test_filtering_analysis/src/sub1/F.sol b/tests/e2e/filtering/test_data/test_filtering_analysis/src/sub1/F.sol new file mode 100644 index 0000000000..d5f8d12b93 --- /dev/null +++ b/tests/e2e/filtering/test_data/test_filtering_analysis/src/sub1/F.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "./E.sol"; + +contract F { + constructor(){ + + } +} diff --git a/tests/e2e/filtering/test_data/test_filtering_analysis/src/sub2/B.sol b/tests/e2e/filtering/test_data/test_filtering_analysis/src/sub2/B.sol new file mode 100644 index 0000000000..199559e964 --- /dev/null +++ b/tests/e2e/filtering/test_data/test_filtering_analysis/src/sub2/B.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +contract B { + + enum Status { + VALID, + INVALID + } + + constructor(){ + + } + function b() public view {} +} diff --git a/tests/e2e/filtering/test_data/test_filtering_analysis/src/sub2/D.sol b/tests/e2e/filtering/test_data/test_filtering_analysis/src/sub2/D.sol new file mode 100644 index 0000000000..359c525f7a --- /dev/null +++ b/tests/e2e/filtering/test_data/test_filtering_analysis/src/sub2/D.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +contract D { + constructor(){ + } +} diff --git a/tests/e2e/filtering/test_filtering.py b/tests/e2e/filtering/test_filtering.py index 402a46ee0f..03373d628f 100644 --- a/tests/e2e/filtering/test_filtering.py +++ b/tests/e2e/filtering/test_filtering.py @@ -382,3 +382,32 @@ def run_printer( FilteringAction.REJECT, ) assert not output ^ {"E.constructor"} + + +@pytest.mark.skipif( + not foundry_available + or not Path(TEST_DATA_DIR, "test_filtering_analysis/lib/forge-std").exists(), + reason="requires Foundry and project setup", +) +def test_filtering_file_before_parsing(): + slither = Slither( + Path(TEST_DATA_DIR, "test_filtering_analysis").as_posix(), + filters=[ + FilteringRule( + type=FilteringAction.REJECT, + path=re.compile("sub2/"), + ), + ], + ) + + slither.register_printer(DummyPrinter) + printer_output = DummyPrinter.analyze_dummy_output(slither.run_printers().pop()) + + # We want to not get any results in sub2 but still manage to analyze A that depends on B. + assert not printer_output ^ { + "A.a", + "A.constructor", + "C.constructor", + "E.constructor", + "F.constructor", + }